主要有afl-gcc和afl-fuzz两个程序,分别起到插桩和fuzz的作用。

afl-gcc

在afl-gcc中,先通过find_as找到afl-as程序,这个程序用于对.s文件进行插桩,不断读取行数,面对不同的情况插入汇编指令。

例如这里,检测到汇编时,插入一个随机数log的桩

image-20220719152701993

和检测到条件跳转时

image-20220719152758558

最后再如果插桩成功,将主程序插入

image-20220719152823881

主程序中会开启forkserver通过共享内存与fuzzer通信。

afl-fuzz

主程序

再看到afl-fuzz程序,可以看到8044行中,fuzzer也准备好了共享内存,准备好后

1
2
3
4
5
6
7
setup_post();
setup_shm(); // 准备共享内存
init_count_class16();

setup_dirs_fds();
read_testcases(); // 读取测试样例,并放入queue中
load_auto();

然后是对测试样例进行校验(运行一次),并且选出最优测试用例

1
2
3
perform_dry_run(use_argv); // 校验

cull_queue(); // 择优

其中,对一个testcase的校验通过此函数执行,测试testcase是否有效。

1
2
static u8 calibrate_case(char** argv, struct queue_entry* q, u8* use_mem,
u32 handicap, u8 from_queue)

cull_queue()中,对top_rated的case进行筛选,如果case命中了未被命中的执行路径,则设为favored。

从8091行的循环中,正式开始fuzz。

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
84
85
86
87
88
89
90
91
while (1) {

u8 skipped_fuzz;
// 在每次fuzz前,都会调用cull_queue()
cull_queue();
// 如果当前队列没定义
if (!queue_cur) {

queue_cycle++;
current_entry = 0;
cur_skipped_paths = 0;
queue_cur = queue;

while (seek_to) {
current_entry++;
seek_to--;
queue_cur = queue_cur->next;
}

show_stats();

if (not_on_tty) {
ACTF("Entering queue cycle %llu.", queue_cycle);
fflush(stdout);
}

/* If we had a full queue cycle with no new finds, try
recombination strategies next. */
// queued_paths,就是队列中testcase的数量,如果不变的话,就使用splice
if (queued_paths == prev_queued) {

if (use_splicing) cycles_wo_finds++; else use_splicing = 1;

} else cycles_wo_finds = 0;

prev_queued = queued_paths;

if (sync_id && queue_cycle == 1 && getenv("AFL_IMPORT_FIRST"))
sync_fuzzers(use_argv);

}
// 这里去fuzz一次
skipped_fuzz = fuzz_one(use_argv);
// sync_fuzzer是从其他fuzz找testcase,应该用在多线程fuzz里
if (!stop_soon && sync_id && !skipped_fuzz) {

if (!(sync_interval_cnt++ % SYNC_INTERVAL))
sync_fuzzers(use_argv);

}

if (!stop_soon && exit_1) stop_soon = 2;

if (stop_soon) break;
// 更换下一个case进行fuzz
queue_cur = queue_cur->next;
current_entry++;

}

if (queue_cur) show_stats();

/* If we stopped programmatically, we kill the forkserver and the current runner.
If we stopped manually, this is done by the signal handler. */
if (stop_soon == 2) {
if (child_pid > 0) kill(child_pid, SIGKILL);
if (forksrv_pid > 0) kill(forksrv_pid, SIGKILL);
}
/* Now that we've killed the forkserver, we wait for it to be able to get rusage stats. */
if (waitpid(forksrv_pid, NULL, 0) <= 0) {
WARNF("error waitpid\n");
}

write_bitmap();
write_stats_file(0, 0, 0);
save_auto();

stop_fuzzing:

SAYF(CURSOR_SHOW cLRD "\n\n+++ Testing aborted %s +++\n" cRST,
stop_soon == 2 ? "programmatically" : "by user");

/* Running for more than 30 minutes but still doing first cycle? */

if (queue_cycle == 1 && get_cur_time() - start_time > 30 * 60 * 1000) {

SAYF("\n" cYEL "[!] " cRST
"Stopped during the first cycle, results may be incomplete.\n"
" (For info on resuming, see %s/README.)\n", doc_path);

}

fuzz_one

先会检查case是否fuzz过,有无favored,然后检查case的失败次数,如果在允许范围内,执行一次case,能够执行的话,进入变异阶段。

其中主要是修改out_buf中的内容作为输入,然后通过EXP_ST u8 common_fuzz_stuff(char** argv, u8* out_buf, u32 len)进行fuzz。

在这个函数里,如果出现新的路径,则将case加入到queue中,并且使用calibrate_case校验。

崩溃筛选

在fuzz_one的后半部分,对错误类型进行了判断,主要有两种,一种是超时,如果没有发现新路径就丢弃,反之对其增加超时时间,如果crash了,按照崩溃处理,其他情况丢弃;另一种崩溃的情况里,没有发现新路径就丢弃,反之对crash进行记录。