《IMF:Inferred Model-based Fuzzer》论文笔记

一、简介

  • 内核 API 函数之间的调用大多是相互依赖的,即一些 API 的调用需要依赖其他 API 调用所产生的上下文,因此若给定的调用上下文无用,则内核 API 将会始终执行失败,无法进入到更深层次的逻辑中。

  • 这篇论文提出了一种新的内核 fuzz 方式,它利用内核 API 函数之间的依赖(即 API 调用序列的相似性),来推断出依赖模型,进而利用该模型生成出随机并且结构性良好的 API 序列,进行更深层次的 fuzz。

    其中,API 调用的依赖关系包含两种,分别是

    1. 顺序依赖,即 A 函数应该比 B 函数更早被调用。
    2. 数据依赖,函数调用之间存在着数据流传递。
  • Fuzz 的主要目标是 IOKit Lib

  • IMF src - github

需要注意的是,这篇论文是 17 年的论文,实验时所使用的 MacOS 版本为 10.12.3,而本人的机器版本为 MacOS 12.0.1,因此在复现实验是会存在一些困难。

二、架构

该论文所提出的 IMF 架构图如下所示:

image-20220118102357121

其中, IMF 共有三部分组成,分别是

  • Logger:用于记录指定应用程序的 API 调用日志。调用日志中包括了调用函数名,调用传入的参数值等等数据。
  • Inferrer:从 Logger 生成的 API log 中推断出顺序依赖和数据依赖。初始时 Inferrer 会在 Logger 生成的 L 条 log 中,筛选出最大前缀长度的 N (N < L) 个日志;之后这 N 个日志将用于推断依赖关系,生成 API 依赖模型。
  • Fuzzer:使用推断出的 API 依赖模型动态生成出 testcase 并用于测试。

三、例子

论文中给了一个 fuzz 过程的示例。通过这个示例我们可以简单了解一下整个 IMF 的处理过程。

1. 初始

初始时,给定一系列配置文件和 API 函数原型注释文件,其中后者存放着目标 IOKit API 的函数名称、参数类型与个数等等的信息,通常以 JSON 格式保存。

API 函数原型注释文件,主要用于生成 API hook 以及为 API 依赖关系推断。

2. 安装 API hook

  • 开始时,IMF 为目标程序 2048 Game 安装 API hook,这样当目标程序调用 IOKit Lib 时,这些函数调用将会被 hook 并被记录下来。

  • 之后模拟鼠标输入或键盘输入,为目标程序提供输入,这样目标程序就会调用 IOKit 并留下 API Log。

  • 尝试循环执行目标程序 L=1000 次并记录下 L 个日志。

需要注意的是,本人实际复现实验时,可能是受限于 MacOS 版本问题,hook 2048 Game 无法记录下任何 IOKit Log(但是 VSCode 可以,但是日志数量较少)。

因此本人在实验时,所选定的目标程序为 /usr/sbin/ioreg

每一次的日志中都会记录下调用 API 时的 1) 输入参数的类型与值;2) 返回值的类型与值

3. 过滤 API log

直到目前,API hook 已经记录下了 L=1000 个日志,那么接下来就需要对其进行筛选,从中筛出 log 的子集。

这里的例子中,从 L=1000 个日志里,筛选出了 N=2 个最长公共前缀的日志。

需要注意的是,由于 GUI 事件的非确定性,对于同一个 GUI 程序的相同输入,hook 可能不会生成相同的 API 调用序列

但如果使用的目标程序是非 GUI 程序,则生成的 API log 大体相同。

4. 依赖推断

a. 顺序依赖

首先, IMF 假设,应该保留 log 中的 API 调用顺序。但这样可能在模型中包含不必要的顺序依赖关系,导致调用之间的顺序依赖过于近似。不过在实际 fuzz 时会适当放宽这个假设。

此时能获取到的调用顺序如下,其中 $A<B$ 表示函数 A 的调用点在函数 B 的调用点之

$$IOServiceMatching < IOServiceGetMatchingService < IOServiceOpen < IOConnectCallMethod$$

b. 数据依赖

接下来,IMF 将会从 N=2 的 log 子集中,

  1. 检测并识别出常量类型的参数值。先上张图,其中绿色字体表示常量,常量值将被排除在数据流分析之外:

注意,由于先前已经给定了一个 API 函数原型注释文件,因此对于 handler 类型(即诸如io_service_tio_connect_t 的参数,将不会被识别为常量。

image-20220118114142794

  1. 检测数据依赖。若前一个函数调用的返回值,作为了后一个函数调用的参数值,那么可以说这两个函数调用之间存在数据依赖关系,即图中黑色虚线所标识的那样。

    该论文实现了多种启发式数据依赖的检测方式,这里只是简单介绍了一种。

c. 模型生成

该图是根据上图所生成的一个 API Model,模型使用 AST 来表示:

image-20220118120407950

在这个模型中,我们可以很明显的看到每个函数调用都遵循了先前所推断出的顺序依赖,以及函数之间的值依赖关系。

对于指针 outStructCnt 与 API 的关系,IMF 也可以根据先前所给定的 API 函数注释文件来获取到两者之间的内部关系,从而产生诸如第九行这样的代码。

之后 IMF 便可以根据模型来进行变异与生成。

四、具体实现

1. Logger

a. 论文细节

Logger 需要处理两个问题:

  • 目标程序的输入从何而来?
  • 记录 log 时需要记录多少数据?

首先对于第一个问题:由于论文中使用的目标程序大多是 GUI 程序,而 GUI 程序的输入大多是鼠标事件和键盘事件,因此可以使用 PyUserInput 来为目标程序模拟输入事件。

对于第二个问题:记录 log 时,需要保存多少级间接指针的数据?若级别太多,则会占用大量的磁盘空间,加大分析难度。因此在该论文的实验中,只保存了一级间接指针的数据。

b. 技术细节

在论文所提供的代码中, const.py 文件里已经事先记录了目标 IOKit API 的函数原型定义。一个简单的示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# const.py
API_DEFS = [
[
# kern_return_t IOConnectGetService(io_connect_t connect, io_service_t *service);
('kern_return_t', 'IOConnectGetService'),
[
# 第一个参数
('io_connect_t', 'connect', {}),
# 第二个参数。指针参数的第三个字段,即字典中存在一对键值对 IO,用于说明在该函数中,数据是流向指针所指向的内存,还是从该内存中流出;这将用于进一步的数据流分析。
('io_service_t *', 'service', {'IO':'O'})
]
],
......
]

之后,hook.py 文件将根据给定的 IOKit API 函数原型,结合 C 语言 hook 代码的模板,生成诸如以下 C 代码的 hook.c 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <IOKit/IOKitLib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <CoreFoundation/CoreFoundation.h>

#ifndef LOG_PATH
#define LOG_PATH "/tmp/log"
#endif

const char* log_path = LOG_PATH;
// 生成 JSON 格式
void log_CFTypeRef(FILE *f,CFTypeRef target){
CFTypeID ty = CFGetTypeID(target);
if (ty == CFStringGetTypeID()){
fprintf(f,"'%s'",CFStringGetCStringPtr(target,kCFStringEncodingUTF8));
}else if (ty == CFDictionaryGetTypeID()){
fprintf(f,"{");
size_t size = CFDictionaryGetCount(target);
CFTypeRef *keys = (CFTypeRef *) malloc( size * sizeof(CFTypeRef) );
CFTypeRef *vals = (CFTypeRef *) malloc( size * sizeof(CFTypeRef) );
CFDictionaryGetKeysAndValues(target,keys,vals);
for(size_t i=0;i<size; i++){
log_CFTypeRef(f,keys[i]);
fprintf(f,":");
log_CFTypeRef(f,vals[0]);
fprintf(f,",");

}
fprintf(f,"}");
free(keys);
free(vals);
}else if (ty == CFNumberGetTypeID()){
uint64_t n;
CFNumberGetValue(target,CFNumberGetType(target),&n);
fprintf(f,"%d",n);
}else if (ty == CFBooleanGetTypeID()){
fprintf(f,"%s",CFBooleanGetValue(target)?"True":"False");
}else{
fprintf(f,"log_CFTypeRef ERROR");
exit(0);
}
}

// IOCatalogueReset 函数 hook 后的处理操作
kern_return_t fake_IOCatalogueReset(mach_port_t masterPort,uint32_t flag){
FILE *fp = fopen(log_path,"a");
flock(fileno(fp),LOCK_EX);
fprintf(fp,"IN ['IOCatalogueReset',");
if(1) fprintf(fp,"{'name':'masterPort','value': 0x%x,'size' : 0x%lx,'cnt':0x%x, 'data':[",masterPort, sizeof(mach_port_t),1);
else fprintf(fp,"{'name':'masterPort','value': 0x%x, 'size' : 0x%lx,'cnt':'undefined', 'data':[",masterPort,sizeof(mach_port_t));
fprintf(fp,"]},");
if(1) fprintf(fp,"{'name':'flag','value': 0x%x,'size' : 0x%lx,'cnt':0x%x, 'data':[",flag, sizeof(uint32_t),1);
else fprintf(fp,"{'name':'flag','value': 0x%x, 'size' : 0x%lx,'cnt':'undefined', 'data':[",flag,sizeof(uint32_t));
fprintf(fp,"]},");
fprintf(fp,"]\n");
kern_return_t ret = IOCatalogueReset(masterPort,flag);
fprintf(fp,"OUT ['IOCatalogueReset',");
if(1) fprintf(fp,"{'name':'ret','value': 0x%x,'size' : 0x%lx,'cnt':0x%x, 'data':[",ret, sizeof(kern_return_t),1);
else fprintf(fp,"{'name':'ret','value': 0x%x, 'size' : 0x%lx,'cnt':'undefined', 'data':[",ret,sizeof(kern_return_t));
fprintf(fp,"]},");
fprintf(fp,"]\n");
fclose(fp);
return ret;
}
[...]

typedef struct interposer {
void* replacement;
void* original;
} interpose_t;
__attribute__((used)) static const interpose_t interposers[]
__attribute__((section("__DATA, __interpose"))) = {
{
.replacement = (void*) fake_IOCatalogueReset,
.original = (void*) IOCatalogueReset
},
[...]
};

hook.py 将会批量生成 fake_IOXXXX 函数,并填充相应的数据结构至 interposers 数组中。

hook.py hook.c 命令执行完毕,生成出 hook.c 文件后, 执行以下代码将生成待注入的 dylib:

1
clang  -Wall -dynamiclib -framework IOKit -framework CoreFoundation -arch x86_64 hook.c -o hook.dylib

之后执行以下命令:

1
DYLD_INSERT_LIBRARIES=${PWD}/hook.dylib [program path] [program args]

这样,目标程序在使用 IOKit lib 时,对应的 IOKit 函数将会被所注入的动态链接库 hook.dylib 动态 hook,并在 /tmp/log 中记录下日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# kern_return_t IORegistryEntryGetLocationInPlane(
# io_registry_entry_t entry,
# const io_name_t plane,
# io_name_t location );

IN
[
'IORegistryEntryGetLocationInPlane',
{
'name':'entry', # 参数1 变量名
'value': 0x2607, # 参数1 调用时传入的值
'size' : 0x4, # 参数1 所占用的内存大小,即 sizeof(type)
'cnt':0x1, # 参数1 若是指针,则指针所指向的值的个数
'ori':'IOServiceGetMatchingService(
0,IOServiceMatching(
"IOUserServer(com.apple.driverkit.AppleUserHIDDrivers-0x100000419)"))',
'data':[] # 参数1 若是指针,则指针所指向的数组的所有值
},
{
'name':'plane',
'value': '"IOService"',
'size' : 0x80,
'cnt':0x1,
'data':[]
},
{
'name':'location',
'value': '"x&"',
'size' : 0x80,
'cnt':0x1,
'data':[]
},
]

OUT
[
'IORegistryEntryGetLocationInPlane',
{
'name':'ret',
'value': 0xe00002f0,
'size' : 0x4,
'cnt':0x1,
'data':[]
},
]

需要注意的是,对于每一个 IOKit 函数调用,API Hook 都会生成2个条目:

  • 一个是 IN 条目,用于记录传入的参数信息
  • 另一个是 OUT 条目,用于记录函数调用所返回的信息

2. Inferrer

a. Log Filtering

1) 论文细节

由于每次执行目标程序时,不同的环境下会产生不同的日志,因此 IMF 将会对生成的日志进行进一步的过滤与处理。

这里 Log Filtering 的目的是:从给定的日志集中选取N个具有最长公共前缀的日志,并收集这 N 个日志中的公共前缀,以构造出一组具有完全相同的顺序和相同数量的 API 调用序列 S

由于调用序列 S 中在不同环境下所记录的 log 不同,一些参数会有着不同的参数值,因此这种不确定性可以用于更好的确定 API 模型。

2) 技术细节

Filtering 的操作位于 filter.py 中。

  • 初始时,filter 会循环读入每个日志文件,并对每个日志文件中的每个 IN/OUT log 进行哈希。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    def loader(path):
    ret = []
    with open(path, 'rb') as f:
    data = f.read().split('\n')[:-1]
    idx = 0
    while idx < len(data):
    name = parse_name(data[idx])
    selector = parse_selector(data[idx])
    hval = merge(name, selector)
    ret.append(hval)
    idx += 2
    return path, ret

    这里对 log 条目进行哈希时,使用的是函数名 + selector 作为输入源(merge 操作)。其中,selector 只有在函数名为 IOConnectCallXXXXMethod 时才有用到。也就是说,这里的哈希将会对相同的函数名 CallMethod 但不同的 selector 选择子区分开来。

    哈希后的结果是一个数组,数组中有多个元组,每个元组里分别有两个成员,分别是单个 log 文件名,与一个存放着该 log 文件中每个条目哈希的数组:

    1
    2
    3
    4
    5
    6
    7
    8
    [
    'log1.txt', [
    entry1_hash,
    entry2_hash,
    ....
    ],
    ....
    ]
  • 上面步骤所输出的内容,称为一个 group。接下来 filter 将会执行 categorize 函数,遍历 groups 中某个 index 所对应的 log entry hash。这样做的目的是为了进行最长公共子序列筛选。

    每次筛选后,相同 idx 但不同的 hash 的 log entry 将会被单独拆开并合并至新的 group 中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    def categorize(groups, idx):
    ret = []
    for group in groups:
    tmp = {}
    for fn, hvals in group:
    hval = get(hvals, idx)
    if hval not in tmp:
    tmp[hval] = []
    tmp[hval].append((fn, hvals))
    for hval in tmp:
    if hval != None :
    ret.append(tmp[hval])
    return ret
  • 每次筛选并合并成新的 groups 后,都会尝试执行一次 pick_best 的操作,遍历每个 groups 中的 group,并获取数量大于等于 N 的 group 中的 log entry。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def find_best(groups, n):
    before = None
    idx = 0
    while len(groups) != 0:
    before = groups
    groups = categorize(groups, idx)
    if pick_best(groups, n) == None:
    return pick_best(before, n), idx
    idx += 1
    utils.error('find_best error')

    如果可以获取,则说明筛选还没有详尽,因此 idx++,继续筛选;若无法获取,则回退返回上一次筛选的内容,并从中选择 log entry 大于等于 N 的 group,同时指定当前所分析到的 idx 长度。(注意单个 group 中会有多个 log 文件)

  • 这样,根据上面的步骤,filter 便可以筛选并继续保存序列长度为 idx(注意这 idx 个长度的序列为公共子序列) 的多个 log 文件。

    1
    2
    3
    4
    5
    6
    7
    8
    def save_best(path, best_group, idx):
    for fn, _ in best_group:
    name = fn.split('/')[-1]
    with open(fn, 'rb') as f:
    data = f.read().split('\n')[:-1]
    with open(os.path.join(path, name), 'wb') as f:
    for x in data[:idx*2]:
    f.write(x+'\n')

b. API Model Inference

1) 论文细节

论文中对于 API 的顺序依赖并没有进行特殊的处理,乐观的认为 API 函数之间的调用关系,应该会遵循筛选后的调用序列 S 中的某个相同序列。

而对于 API 的数据依赖,论文中将数据依赖的检测方式分为两步:

  1. 识别出所有的常量
  2. 识别出一对函数之间的数据流关系

首先是常量识别。对于调用序列的某个函数调用,其常量参数在其他调用序列(即过滤出的 N 个调用序列)中也一定是相同的。例如下面这个例子,

1
2
3
4
5
6
7
8
9
// 序列1
[...]
/* 第i个调用 */ A(变量1, 12);
[...]

// 序列2
[...]
/* 第i个调用 */ A(变量2, 12);
[...]

可以看到,对于不同序列中的第 i 个调用,其参数2的值相同,始终为 12,因此可以认为函数 A 的参数2 是一个常量值。

即,假设 $S^k_{i, j}$为第$k$个调用序列中的第$j$个函数调用第$i$个参数,若满足 $S^1_{i, j}=S^2_{i, j}=…=S^N_{i, j}$,则说明 $S_{i,j}$ 是一个常量参数

需要注意的是,在进行常量识别时,需要忽视掉句柄类型。因为对于这种类型的变量来说,即便值相同,但它们依然不是常量。

接下来是数据流识别IMF 并没有识别参数与参数之间的数据流传递关系(和 syzkaller 不同),它只是简单的识别函数之间那种 函数1返回值 -> 函数2参数值 的数据流关系:

  • 对于某个指定函数调用点的输入参数值,若该调用点前有任何一个函数的返回值与输入参数值相同,则说明这之中存在数据流依赖关系。
  • 如果有多个函数的返回值与输入参数值相同,则始终选择最近的那个函数。

需要注意的是,为了提高精度,IMF 会取每个调用序列每个函数的数据流依赖交集

而 inferrer 的最终输出是一个 C 语言的代码片段,即 AST 格式。其中,inferrrer 会根据顺序依赖来生成一系列的函数调用语句。对于每个函数调用,其函数参数将会根据类型来进行不同的填充:

  • 常量参数:使用调用序列里的常量值
  • 非常量参数
    • 若与其他函数存在数据依赖,则声明一个变量,将输入参数与存在数据依赖的函数相连接
    • 若不存在数据依赖,则随机选择一个该输入参数在日志中出现的值
2) 技术细节

执行 inferrer 时,初始时,程序会先实例化 ApiFuzz 类,在该类的构造函数中执行 const.load_apis 函数,将先前准备好的 IOKit API 函数原型定义 读入内存,并以 Api 类的结构保存。单个 Api 类的结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* 
以该例子为例
[('kern_return_t', 'IOMasterPort'),
[('mach_port_t', 'bootstrapPort', {}), ('mach_port_t *', 'masterPort', {'IO': 'O'})]]
*/
IOMasterPort_Api_class = {
rtype:'kern_return_t',
rval: Arg_class {
type: 'kern_return_t',
name: 'ret',
opt: {}
},
name:'IOMasterPort',
args : [
Arg {
type: 'mach_port_t',
name: 'bootstrapPort',
opt: {}
},
Arg {
type: 'mach_port_t *',
name: 'masterPort',
opt: {'IO': 'O'}
}
]
}

之后,程序会在 ApiFuzz 类的 make_model 成员函数中,以多进程方式执行 load_apilog 成员,将先前 hook 生成的 API log 读入内存。

注意到 API log 中每两个条目(即一对 IN/OUT 条目)对应的是一个 IOKit 函数调用的参数输入与函数返回,因此在 load_apilog 函数中,程序同样会以一对条目为单位读入 ApiLog 类中。每一个 ApiLog 的结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
io_registry_entry_t IORegistryGetRootEntry (mach_port_t masterPort)
IN ['IORegistryGetRootEntry',{'name':'masterPort','value': 0x0,'size' : 0x4,'cnt':0x1, 'data':[]},]
OUT ['IORegistryGetRootEntry',{'name':'ret','value': 0x2903,'size' : 0x4,'cnt':0x1, 'data':[]},]
*/
ApiLog(派生自API) = {
// 以下四个是 API 类中的字段
args: xxxxx,
name: 'IORegistryGetRootEntry',
rtype: xxxxx,
rval: xxxxx,

api : Api(IORegistryGetRootEntry),
args_dict: {
'masterPort': Arg(IORegistryGetRootEntry_arg0)
},
hval: None,
il: {
'masterPort': ArgLog(派生自 Arg) {
// 以下三个是 Arg 类的字段
type: 'mach_port_t',
name: 'masterPort',
opt: {},

arg: Arg(内部内容和上面三个字段一样),
log: {'name':'masterPort','value': 0x0,'size' : 0x4,'cnt':0x1, 'data':[]}
is_input : True,
}
},
ol: {},
rval_log: ArgLog {
// 以下三个是 Arg 类的字段
type: 'io_registry_entry_t',
name: 'ret',
opt: {},

arg: Arg(内部内容和上面三个字段一样),
log: {'name':'ret','value': 0x2903,'size' : 0x4,'cnt':0x1, 'data':[]}
is_input : False,
}
}

之后,程序将所有读入的 API log 均存入 ApiFuzz 类中的 apisets 数组,并使用该数组创建 Model 类进行建模。有意思的是,在建模时,只会使用一个 log 文件。

1
2
3
4
5
6
7
8
class Model:
def __init__(self, apisets):
self.mapis = []
for idx in range(len(apisets[0])):
apilog = apisets[0][idx]
self.mapis.append(Mapi(apilog, idx))
self.check_const(apisets)
self.add_dataflow(apisets)

Model 类在初始化时,会将每个 apilog 都转换成 Mapi 类型的结构。 该结构的布局和 Api 类型有点类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Mapi = {
api : Api(arglog.api)
idx : xx,
il : {
'masterPort': Marg(派生自 Arg) {
arg : arglog.arg,
value : Mval {
value : xxx,
const : xxx,
dataflow = xxx,
raw : xxx,
ori : xxx,
ty : xxx,
ptr : xxx,
name : xxx,
},
is_in_flag : 数据流是否是流进
name : arg的名称

array_flag : 表示该arg是否是一个指向数组的指针
data : 如果当前arg 是数组,则这里存放数组中的内容
cnt : 表示当 arg 是数组类型时的长度
}
}
ol : {
.....
}
}

转换完成后,立即执行 check const 操作,尝试分辨出是否是常量值。若参数类型是指针类型,则程序会单独对指针所指向数组中的每一个元素进程 check const 操作;若参数是非指针类型,则对该参数的数值进行 check const 检查。

check const 检查操作相当的简单:如果第 i 个函数调用的第 j 个参数,在筛选出的 api log 中互不相同,则说明这是一个变量值。

check const 检查完成后,下一步操作是 add dataflow。

1
2
3
4
5
6
7
8
def add_dataflow(self, apisets):
for apiset in apisets:
before = {}
for idx in range(len(apiset)):
apilog = apiset[idx]
mapi =self.mapis[idx]
mapi.add_dataflow(before, apilog)
update_before(before, apilog, mapi, idx)

初始时,add dataflow 函数声明了一个 before 字段,该字段表示过去函数调用所生成的 value 值。之后将每个 Mapi 中 Marg 的参数值加入至 Mval 类型中的 raw 数组中,最后调用 get_xxx_df 函数来更新 Mval 类中的 dataflow 字段,指定该 Mval 的数据流来源。

这样,通过多次遍历 apilog,程序可以对一些 Mval 设置其数据流的单项关系,为接下来代码生成做准备。

3. Fuzzer

a. Fuzz 配置

fuzz 的配置主要有以下几种:

  1. T : 超时时间
  2. I : 迭代次数
  3. P : 变异概率
  4. F : 固定位数,用于变异
  5. R : 随机数种子。

实际开源的代码模板如下所示,注意到这里并没有关于超时时间的设置,这可能是因为这部分代码没有开源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void parse_args(int argc, char **argv){
int opt;
while ((opt = getopt(argc, argv, "f:s:b:r:l:")) != -1){
switch(opt){
case 'f':
log_file = optarg;
break;
case 's':
seed = parse_uint(optarg);
set_seed = 1;
break;
case 'b':
bitlen = parse_uint(optarg);
break;
case 'r':
rate = parse_uint(optarg);
break;
case 'l':
max_loop = parse_uint(optarg);
break;
default :
help();
}

}
if(log_file == NULL && set_seed == 0){
help();
}
}

b. 变异策略

变异策略较为简略,只有参数值变异:对其进行数据上的变异。

这些变异代码都是预先写死在 python 文件中,作为代码模板的一部分,以下是简单的代码模板示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
uint16_t mut_short(uint16_t v){
uint16_t r ;
if( MAYBE ){
r= get_rand();
if(bitlen <16){
return v ^ (r & ((1 << (16-bitlen))-1) );
}else{
return v ^ (r & 1) ;
}
}
return v;
}

uint32_t mut_int(uint32_t v){
uint32_t r =0;
if( MAYBE ){
r = (r<<16) | (uint32_t) get_rand();
r = (r<<16) | (uint32_t) get_rand();
if(bitlen <32){
return v ^ (r & ((1 << (32-bitlen))-1) );
}else{
return v ^ (r & 1) ;
}

}
return v;
}

五、评估

  • IMF 在 macOS 中找到了相当多的 kernel panic 样例。其中大部分是 DoS,有一些可以尝试进行利用。

  • 对于不同类型的目标程序,其能起到 fuzz 的效果是不同的。这是因为不同类型的目标程序,所调用的 IOKit 函数侧重点也不相同。

    image-20220121184714950

    通过该图我们可以看到,Game 类型的目标程序所产生的 Api Log,被 IMF 读入并用于 fuzz macos 所触发的 kernel panic 最多,但该程序类型却并不是触发内核覆盖率最广的类型。这也可以看到 IMF 极度依赖于执行目标程序所收集到的Api Log。

  • IMF 精度会受到 N 的影响。对于不同 N ,fuzz 的精度会产生一些波动:

    image-20220121185131190

六、不足之处

  • IMF 的工作建立在那些参数类型非常明确的 syscall API,更侧重于以黑盒方式对参数进行变异,而不会了解每个参数的有效内存范围。
  • IMF 的前提是了解每个系统调用规范的定义,但这对于驱动程序来说并不适合。因为对于驱动程序来说,其参数多以 void* 传递,IMF 无法根据该无类型指针建立显式数据流依赖关系。
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2022 Kiprey
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~