winafl中基于插桩的覆盖率反馈原理
最近winafl
增加支持对Intel PT
的支持的,但是只支持x64
,且覆盖率计算不全,比如条件跳转等,所以它现在还是不如直接用插桩去hook的方式来得准确完整,这里主要想分析也是基于 DynamoRIO
插桩的覆盖率反馈原理。
之前曾有人在《初识 Fuzzing 工具 WinAFL》(https://paper.seebug.org/323/#32)中“3.2.2 插桩模块”一节中简单分析过其插桩原理,但没有找到我想要的答案,因此只好自动动手分析下源码。
比如,我想知道:
通过循环调用fuzzing的目标函数来提高速度,但
DynamoRIO
的覆盖率信息是如何同步给fuzzer主进程的?具体是如何实现寄存器环境的记录与恢复,从而实现目标函数的不断循环?
覆盖率信息是如何记录与分析的?
覆盖率信息记录与分析原理
第3个问题发现已经有人分析过afl
,可以参见这里《AFL内部实现细节小记》(http://rk700.github.io/2017/12/28/afl-internals/),简单总结下:
AFL在编译源码时,为每个代码生成一个随机数,代表位置地址;
在二元组中记录分支跳转的源地址与目标地址,将两者异或的结果为该分支的key,保存每个分支的执行次数,用1字节来储存;
保存分支的执行次数实际上是一张大小为64K的哈希表,位于共享内存中,方便target进程与fuzzer进程之间共享,对应的伪代码如下:
1
2
3cur_location = <COMPILE_TIME_RANDOM>;
shared_mem[cur_location ^ prev_location]++;
prev_location = cur_location >> 1;fuzzer进程通过buckets哈希桶来归类这些分支执行次数,如下结构定义,左边为执行次数,右边为记录值trace_bits:
1
2
3
4
5
6
7
8
9
10
11static const u8 count_class_lookup8[256] = {
[0] = 0,
[1] = 1,
[2] = 2,
[3] = 4,
[4 ... 7] = 8,
[8 ... 15] = 16,
[16 ... 31] = 32,
[32 ... 127] = 64,
[128 ... 255] = 128
};对于是否触发新路径,主要通过计算各分支的trace_bits的hash值(算法:
u32 cksum **=** hash32(trace_bits, MAP_SIZE常量, HASH_CONST常量);
)是否发生变化来实现的
覆盖信息的传递原理
先在fuzzer进程中先创建命名管道,其中fuzzer_id为随机值:
1
2
3
4
5
6
7
8
9
10
11
12
13//afl-fuzz.c
pipe_name = (char *)alloc_printf("\\\\.\\pipe\\afl_pipe_%s", fuzzer_id);
pipe_handle = CreateNamedPipe(
pipe_name, // pipe name
PIPE_ACCESS_DUPLEX | // read/write access
FILE_FLAG_OVERLAPPED, // overlapped mode
0,
1, // max. instances
512, // output buffer size
512, // input buffer size
20000, // client time-out
NULL); // default security attribute创建drrun进程去运行目标程序并Hook,在childpid_(%fuzzer_id%).txt的文件中记录子进程id,即目标进程ID,然后等待管道连接,并通过读取上述txt文件以获取目标进程id,主要用来后面超时中断进程的:
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//afl-fuzz.c
pidfile = alloc_printf("childpid_%s.txt", fuzzer_id);
if (persist_dr_cache) {
cmd = alloc_printf(
"%s\\drrun.exe -pidfile %s -no_follow_children -persist -persist_dir \"%s\\drcache\" -c winafl.dll %s -fuzzer_id %s -drpersist -- %s",
dynamorio_dir, pidfile, out_dir, client_params, fuzzer_id, target_cmd);
} else {
cmd = alloc_printf(
"%s\\drrun.exe -pidfile %s -no_follow_children -c winafl.dll %s -fuzzer_id %s -- %s",
dynamorio_dir, pidfile, client_params, fuzzer_id, target_cmd);
}
......
if(!CreateProcess(NULL, cmd, NULL, NULL, inherit_handles, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
FATAL("CreateProcess failed, GLE=%d.\n", GetLastError());
}
......
if(!OverlappedConnectNamedPipe(pipe_handle, &pipe_overlapped)) {
FATAL("ConnectNamedPipe failed, GLE=%d.\n", GetLastError());
}
watchdog_enabled = 0;
if(drioless == 0) {
//by the time pipe has connected the pidfile must have been created
fp = fopen(pidfile, "rb");
if(!fp) {
FATAL("Error opening pidfile.txt");
}
fseek(fp,0,SEEK_END);
pidsize = ftell(fp);
fseek(fp,0,SEEK_SET);
buf = (char *)malloc(pidsize+1);
fread(buf, pidsize, 1, fp);
buf[pidsize] = 0;
fclose(fp);
remove(pidfile);
child_pid = atoi(buf);
free(buf);
ck_free(pidfile);
}
else {
child_pid = pi.dwProcessId;
}在插桩模块winafl.dll中打开前面创建的命名管道,然后通过管道与fuzzer主进程进行交互:
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//winafl.c
static void
setup_pipe() {
pipe = CreateFile(
options.pipe_name, // pipe name
GENERIC_READ | // read and write access
GENERIC_WRITE,
0, // no sharing
NULL, // default security attributes
OPEN_EXISTING, // opens existing pipe
0, // default attributes
NULL); // no template file
if (pipe == INVALID_HANDLE_VALUE) DR_ASSERT_MSG(false, "error connecting to pipe");
}
......
char ReadCommandFromPipe()
{
DWORD num_read;
char result;
ReadFile(pipe, &result, 1, &num_read, NULL);
return result;
}
void WriteCommandToPipe(char cmd)
{
DWORD num_written;
WriteFile(pipe, &cmd, 1, &num_written, NULL);
}当插桩模块winafl.dll监测到程序首次运行至目标函数入口时,
pre_fuzz_handler
函数会被执行,然后通过管道写入’P’命令,代表开始进入目标函数,afl-fuzz.exe进程收到命令后,会向目标进程写入管道命令’F’,并监测超时时间和循环调用次数。afl-fuzz.exe与目标进程正是通过读写管道命令来交互的,主要有’F’(退出目标函数)、’P’(进入目标函数)、’K’(超时中断进程)、’C’(崩溃)、’Q’(退出进程)。覆盖信息通过文件映射方法(内存共享)写入winafl_data.afl_area
: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//winafl.c
pre_fuzz_handler(void *wrapcxt, INOUT void **user_data)
{
......
if(!options.debug_mode) {
WriteCommandToPipe('P');
command = ReadCommandFromPipe();
if(command != 'F') {
if(command == 'Q') {
dr_exit_process(0);
} else {
DR_ASSERT_MSG(false, "unrecognized command received over pipe");
}
}
} else {
debug_data.pre_hanlder_called++;
dr_fprintf(winafl_data.log, "In pre_fuzz_handler\n");
}
......
memset(winafl_data.afl_area, 0, MAP_SIZE); // 用于存储覆盖率信息
if(options.coverage_kind == COVERAGE_EDGE || options.thread_coverage) {
void **thread_data = (void **)drmgr_get_tls_field(drcontext, winafl_tls_field);
thread_data[0] = 0;
thread_data[1] = winafl_data.afl_area; //如果开启-thread_coverage选项,则会将覆盖率信息写入线程TLS中
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//winafl.c
static void
setup_shmem() {
HANDLE map_file;
map_file = OpenFileMapping(
FILE_MAP_ALL_ACCESS, // read/write access
FALSE, // do not inherit the name
options.shm_name); // name of mapping object
if (map_file == NULL) DR_ASSERT_MSG(false, "error accesing shared memory");
winafl_data.afl_area = (unsigned char *) MapViewOfFile(map_file, // handle to map object
FILE_MAP_ALL_ACCESS, // read/write permission
0,
0,
MAP_SIZE);
if (winafl_data.afl_area == NULL) DR_ASSERT_MSG(false, "error accesing shared memory");
}
篡改目标函数循环调用的原理
此步的关键就在于进入目标函数前调用的pre_fuzz_handler
函数,以及函数退出后调用的post_fuzz_handler
函数。
进入pre_fuzz_handler
函数时,winafl.dll会先获取以下信息
1 | app_pc target_to_fuzz = drwrap_get_func(wrapcxt); //获取目标函数地址 |
其中内存上下文信息支持各平台的寄存器记录:
1 | typedef struct _dr_mcontext_t { |
接下来就是获取和设置fuzzed的目标函数参数:
1 | //save or restore arguments |
当目标函数退出后,执行post_fuzz_handler
函数,会恢复栈顶指针和pc地址,以此实现目标函数的循环调用:
1 | static void |
总结
总结下整个winafl
执行流程:
- afl-fuzz.exe通过创建命名管道与内存映射来实现与目标进程交互,其中管道用来发送和接收命令相互操作对方进程,内存映射主要用来记录覆盖率信息;
- 覆盖率记录主要通过
drmgr_register_bb_instrumentation_event
去设置BB执行的回调函数,通过instrument_bb_coverage
或者instrument_edge_coverage
来记录覆盖率情况,如果发现新的执行路径,就将样本放入队列目录中,用于后续文件变异,以提高代码覆盖率; - 目标进程执行到目标函数后,会调用
pre_fuzz_handler
来存储上下文信息,包括寄存器和运行参数; - 目标函数退出后,会调用
post_fuzz_handler
函数,记录恢复上下文信息,以执行回原目标函数,又回到第2步; - 目录函数运行次数达到指定循环调用次数时,会中断进程退出。