logo头像

Hacked By Swing

AFL 浅读

AFL 浅读

AFL 基本思路

AFL 作为一款 fuzz 工具,与直接输入混乱数据相比,主要是加入了对控制流的理解,通过使用共享内存,将控制流信息写入到该共享内存中。通过利用控制流信息,可以更好的引导变异,使得变异更加倾向于使用能够方发现新路径的输入数据,从而提高的 fuzz 的效率。

AFL 大致流程

  1. 加载用户提供的初始输入样本,放入队列中
  2. 从队列中取出下一个输入文件
  3. 尝试缩减输入样本的大小而不影响程序行为
  4. 使用一些方法对文件进行变异
  5. 如果变异结果导致走入新的路径,则记录到队列中
  6. 回到第二步

整个步骤的理解还算比较简单。

一些细节

使用过程

AFL 的使用有两种,一种是有源码情况,一种是无源码情况。

有源码时,使用过程较为简单,基本步骤如下:

  1. 使用提供的编译器 wrapper 进行编译 ( afl-gcc
  2. 使用 afl-fuzz,指定一个目录为工作目录,指定初始样本,开始 fuzz。

无源码时的使用过程:

  1. 编译 afl 自己的 qemu
  2. 使用 afl-fuzz 开始 fuzz,指定 QEMU 模式。

下面我们来具体看看其实现过程。

afl-gcc

程序位于 afl-gcc.c 中,其主要功能是作为一个 wrapper 启动对 gcc 的调用。其基本目的是为使用 gcc 进行编译的程序加入 afl 自己的插桩。

gcc 本身的编译存在几个过程,第一个部分是生成汇编,也就是 gcc 本身的功能,之后 gcc 会调用默认的 as,也就是汇编器,由汇编器来生成可执行文件。而 afl-gcc 的目的就是代替 gcc ,而实现过程则是将传入参数传到 gcc 中,并且启动 gcc 进行编译,不过其通过参数调整,将 afl 自己的 as (汇编器)作为默认汇编器,这样就可以在生成汇编之后对汇编进行修改,从而实现 instrumentation 过程。

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
// 这一部分位于 edit_params 中

// 调用的程序是 g++ 或者 gcc 或者 gcj
if (!strcmp(name, "afl-g++")) {
u8* alt_cxx = getenv("AFL_CXX");
cc_params[0] = alt_cxx ? alt_cxx : (u8*)"g++";
} else if (!strcmp(name, "afl-gcj")) {
u8* alt_cc = getenv("AFL_GCJ");
cc_params[0] = alt_cc ? alt_cc : (u8*)"gcj";
} else {
u8* alt_cc = getenv("AFL_CC");
cc_params[0] = alt_cc ? alt_cc : (u8*)"gcc";
}
// ...

// 循环处理传入 afl-gcc 的参数
while (--argc) {
u8* cur = *(++argv);

// -B 用来输入默认的 as 所在目录,所以这里需要覆盖掉
if (!strncmp(cur, "-B", 2)) {

if (!be_quiet) WARNF("-B is already set, overriding");

if (!cur[2] && argc > 1) { argc--; argv++; }
continue;

}

// 同时也覆盖掉 integrated-as ,这样才可以使用自定义的 as
if (!strcmp(cur, "-integrated-as")) continue;

if (!strcmp(cur, "-pipe")) continue;

#if defined(__FreeBSD__) && defined(__x86_64__)
if (!strcmp(cur, "-m32")) m32_set = 1;
#endif

if (!strcmp(cur, "-fsanitize=address") ||
!strcmp(cur, "-fsanitize=memory")) asan_set = 1;

if (strstr(cur, "FORTIFY_SOURCE")) fortify_set = 1;

cc_params[cc_par_cnt++] = cur;

}

// 将 afl 自己的 as 所在目录传入参数
cc_params[cc_par_cnt++] = "-B";
cc_params[cc_par_cnt++] = as_path;
1
2
3
4
5
6
7
8
9
10
11
12
13
14

// main 中
// 定位自己的 as
find_as(argv[0]);

// 修改传入参数
edit_params(argc, argv);

// 启动 gcc
execvp(cc_params[0], (char**)cc_params);

FATAL("Oops, failed to execute '%s' - check your PATH", cc_params[0]);

return 0;

afl-as

既然 afl-gcc 是通过调用自己的 as 来实现 instrumentation 的,我们就来关注一下其汇编器是如何具体进行 instrumentation 工作的。

汇编器分为两个部分,afl-as.hafl-as.c ,头文件中主要是一些常量的定义,其中包含了插入的汇编内容,.c 文件和之前的 afl-gcc 类似,也是一个 wrapper,最终依然会调用系统的 as 来完成工作,不过在此之前对汇编进行了一些操作,从而完成了 instrumentation。

在这里,其实共享内存已经建立好,共享内存就是一段内存,这一段内存用来保存关于控制流的信息。

我们首先来看 instrumentation 的具体过程:

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
rand_seed = tv.tv_sec ^ tv.tv_usec ^ getpid();

srandom(rand_seed);

edit_params(argc, argv); // 调整参数,同 afl-gcc

if (inst_ratio_str) {

if (sscanf(inst_ratio_str, "%u", &inst_ratio) != 1 || inst_ratio > 100)
FATAL("Bad value of AFL_INST_RATIO (must be between 0 and 100)");

}

if (getenv(AS_LOOP_ENV_VAR))
FATAL("Endless loop when calling 'as' (remove '.' from your PATH)");

setenv(AS_LOOP_ENV_VAR, "1", 1);

/* When compiling with ASAN, we don't have a particularly elegant way to skip
ASAN-specific branches. But we can probabilistically compensate for
that... */

if (getenv("AFL_USE_ASAN") || getenv("AFL_USE_MSAN")) {
sanitizer = 1;
inst_ratio /= 3;
}

if (!just_version) add_instrumentation(); // 进行 instrumentation

if (!(pid = fork())) {

// 启动真实汇编器
execvp(as_params[0], (char**)as_params);
FATAL("Oops, failed to execute '%s' - check your PATH", as_params[0]);

}

核心主要位于 add_instrumentation 中:

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
static void add_instrumentation(void) {

// ...

// 打开汇编文件
if (input_file) {

inf = fopen(input_file, "r");
if (!inf) PFATAL("Unable to read '%s'", input_file);

} else inf = stdin;

// 打开用来输出的文件
outfd = open(modified_file, O_WRONLY | O_EXCL | O_CREAT, 0600);

if (outfd < 0) PFATAL("Unable to write to '%s'", modified_file);

outf = fdopen(outfd, "w");

if (!outf) PFATAL("fdopen() failed");

// 读取每一行
while (fgets(line, MAX_LINE, inf)) {

/* In some cases, we want to defer writing the instrumentation trampoline
until after all the labels, macros, comments, etc. If we're in this
mode, and if the line starts with a tab followed by a character, dump
the trampoline now. */

// 特殊情况需要延迟写入 instrumentation,则咋你这个时候需要写入 trampoline_fmt_*,
// 具体根据架构决定写入内容,该部分内容位于头文件中,是一小段汇编代码
// 其中R(MAP_SIZE) 是生成随机数
if (!pass_thru && !skip_intel && !skip_app && !skip_csect && instr_ok &&
instrument_next && line[0] == '\t' && isalpha(line[1])) {

fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
R(MAP_SIZE));

instrument_next = 0;
ins_lines++;

}

/* Output the actual line, call it a day in pass-thru mode. */

// 写入原文
fputs(line, outf);

if (pass_thru) continue;

/* All right, this is where the actual fun begins. For one, we only want to
instrument the .text section. So, let's keep track of that in processed
files - and let's set instr_ok accordingly. */

// 进行跳转统计的 instrumentation
// 确认只处理 .text 段的内容
if (line[0] == '\t' && line[1] == '.') {

/* OpenBSD puts jump tables directly inline with the code, which is
a bit annoying. They use a specific format of p2align directives
around them, so we use that as a signal. */

if (!clang_mode && instr_ok && !strncmp(line + 2, "p2align ", 8) &&
isdigit(line[10]) && line[11] == '\n') skip_next_label = 1;

if (!strncmp(line + 2, "text\n", 5) ||
!strncmp(line + 2, "section\t.text", 13) ||
!strncmp(line + 2, "section\t__TEXT,__text", 21) ||
!strncmp(line + 2, "section __TEXT,__text", 21)) {
instr_ok = 1;
continue;
}

if (!strncmp(line + 2, "section\t", 8) ||
!strncmp(line + 2, "section ", 8) ||
!strncmp(line + 2, "bss\n", 4) ||
!strncmp(line + 2, "data\n", 5)) {
instr_ok = 0;
continue;
}

}

/* Detect off-flavor assembly (rare, happens in gdb). When this is
encountered, we set skip_csect until the opposite directive is
seen, and we do not instrument. */

if (strstr(line, ".code")) {

if (strstr(line, ".code32")) skip_csect = use_64bit;
if (strstr(line, ".code64")) skip_csect = !use_64bit;

}

// ...

// 需要加入跳转统计的地方:
// main 函数
// .L0 是 gcc 生成的汇编中,跳转目标的 label
// .LBB0_0 clang 的跳转目标 label
// \tjXX 但不是 jmp,即所有条件跳转

/* If we're in the right mood for instrumenting, check for function
names or conditional labels. This is a bit messy, but in essence,
we want to catch:
^main: - function entry point (always instrumented)
^.L0: - GCC branch label
^.LBB0_0: - clang branch label (but only in clang mode)
^\tjnz foo - conditional branches
...but not:
^# BB#0: - clang comments
^ # BB#0: - ditto
^.Ltmp0: - clang non-branch labels
^.LC0 - GCC non-branch labels
^.LBB0_0: - ditto (when in GCC mode)
^\tjmp foo - non-conditional jumps
Additionally, clang and GCC on MacOS X follow a different convention
with no leading dots on labels, hence the weird maze of #ifdefs
later on.
*/

if (skip_intel || skip_app || skip_csect || !instr_ok ||
line[0] == '#' || line[0] == ' ') continue;

/* Conditional branch instruction (jnz, etc). We append the instrumentation
right after the branch (to instrument the not-taken path) and at the
branch destination label (handled later on). */

if (line[0] == '\t') {

// 条件跳转,加入trampoline
if (line[1] == 'j' && line[2] != 'm' && R(100) < inst_ratio) {

fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,
R(MAP_SIZE));

ins_lines++;

}

continue;

}

// ...

if (ins_lines)
fputs(use_64bit ? main_payload_64 : main_payload_32, outf);

if (input_file) fclose(inf);
fclose(outf);

// ...

R 的定义位于 types.h 中:

1
2
3
4
5
#ifdef AFL_LLVM_PASS
# define AFL_R(x) (random() % (x))
#else
# define R(x) (random() % (x)) // 生成随机数,保证在 x 范围内,也就是共享内存大小内
#endif /* ^AFL_LLVM_PASS */

大体来讲,就是在需要加入跳转统计的地方加入 trampoline 。

下面大概来看看 trampoline 在做什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static const u8* trampoline_fmt_64 =

"\n"
"/* --- AFL TRAMPOLINE (64-BIT) --- */\n"
"\n"
".align 4\n"
"\n"
"leaq -(128+24)(%%rsp), %%rsp\n" // 保存插桩用到的寄存器内容
"movq %%rdx, 0(%%rsp)\n"
"movq %%rcx, 8(%%rsp)\n"
"movq %%rax, 16(%%rsp)\n"
"movq $0x%08x, %%rcx\n" // rcx 中存放了一个随机数,即使用 R 宏生成的随机数
"call __afl_maybe_log\n"
"movq 16(%%rsp), %%rax\n" // 恢复寄存器
"movq 8(%%rsp), %%rcx\n"
"movq 0(%%rsp), %%rdx\n"
"leaq (128+24)(%%rsp), %%rsp\n"
"\n"
"/* --- END --- */\n"
"\n";

其实这里的随机数用来唯一标识了这一个跳转。

在 __afl_maybe_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
static const u8* main_payload_64 = 

"\n"
"/* --- AFL MAIN PAYLOAD (64-BIT) --- */\n"
"\n"
".text\n"
".att_syntax\n"
".code64\n"
".align 8\n"
"\n"
"__afl_maybe_log:\n"
"\n"
#if defined(__OpenBSD__) || (defined(__FreeBSD__) && (__FreeBSD__ < 9))
" .byte 0x9f /* lahf */\n"
#else
" lahf\n"
#endif /* ^__OpenBSD__, etc */
" seto %al\n"
"\n"
" /* Check if SHM region is already mapped. */\n"
// 确认共享内存的存在
"\n"
" movq __afl_area_ptr(%rip), %rdx\n"
" testq %rdx, %rdx\n"
" je __afl_setup\n"
"\n"
// 共享内存没有问题,存入控制流信息
"__afl_store:\n"
"\n"
" /* Calculate and store hit for the code location specified in rcx. */\n"
"\n"
#ifndef COVERAGE_ONLY
" xorq __afl_prev_loc(%rip), %rcx\n" // 将跳转过来的位置的随机数与当前随机数 xor
" xorq %rcx, __afl_prev_loc(%rip)\n"
" shrq $1, __afl_prev_loc(%rip)\n" // 右移一位
#endif /* ^!COVERAGE_ONLY */
"\n"
#ifdef SKIP_COUNTS
" orb $1, (%rdx, %rcx, 1)\n"
#else
" incb (%rdx, %rcx, 1)\n" // 写入到共享内存中
// ...

可以理解为:

1
2
3
current = random() % mem_size;
mem[prev ^ current]++;
prev = current >> 1;

也就是说,对跳转位置的记录都是用随机数记录的,而控制流表现为一个 bit ,其所在位置是源到目标的随机数的异或值,然后每一次异或将当前位置随机数右移一位进行表示。右移的目的主要是为了区分源和目标,因为 xor 本身是对称的,这样引入了非对称性。

换句话说,这里可以理解为每一个条件跳转(或是插桩位置,更为准确的话)都用一个共享内存大小范围内的一个随机数标识,然后用右移一位表示源(从他走向其他位置),其原值表示目标(从其他位置走到他),最终共享内存相当于表示了一个表,这个表就是对路径的一个总结。

这个表的形式相当于把一条路径两两拆开,比如 A -> B -> C 拆为 (A, B)(B, C),然后在共享内存里表示这么一个表:

1
2
3
4
5
+-----------+----------+----------+
| 二元路径 | A -> B | B -> C |
+-----------+----------+----------+
| 次数 | | |
+-----------+---------------------+

然后这个表(共享内存中),就可以作为判断是否走向新路径的依据,如果表不一样,就认为走到了新的路径,而表中的 ->,就通过 (id(A) >> 1) ^ (id(B)) 来实现(以此作为索引)。

QEMU mode

我看到的文章似乎都没有介绍 QEMU 是如何进行 instrumentation 的,其实思路接近。

afl 主要使用了 QEMU 的 user mode,在 QEMU 的执行过程中,存在一个翻译过程,其执行流程为逐次翻译基本块,每一次翻译一个基本块,碰到跳转时,找到跳转目标位置,然后将目标位置逐次翻译,直到碰到跳转,将跳转翻译为跳回 QEMU 内容的跳转,这样在下一次执行到该位置的时候,如果碰到跳转,就会进入下一轮翻译。

其实 afl 就是在这个翻译过程中,如果碰到新的块,就加入和 afl-as 一样的内容,进行共享内存赋值,大概内容如下:

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
 typedef struct SyncClocks {
@@ -144,6 +146,8 @@
int tb_exit;
uint8_t *tb_ptr = itb->tc_ptr;

+ AFL_QEMU_CPU_SNIPPET2;
+
qemu_log_mask_and_addr(CPU_LOG_EXEC, itb->pc,
"Trace %p [%d: " TARGET_FMT_lx "] %s\n",
itb->tc_ptr, cpu->cpu_index, itb->pc,
@@ -365,6 +369,7 @@
if (!tb) {
/* if no translated code available, then translate it now */
tb = tb_gen_code(cpu, pc, cs_base, flags, 0);
+ AFL_QEMU_CPU_SNIPPET1;
}

mmap_unlock();

// 其中 两个 SNIPPET 如下


// 一些功能性的处理,主要是对翻译的一些处理,不用很在意
#define AFL_QEMU_CPU_SNIPPET1 do { \
afl_request_tsl(pc, cs_base, flags); \
} while (0)

/* This snippet kicks in when the instruction pointer is positioned at
_start and does the usual forkserver stuff, not very different from
regular instrumentation injected via afl-as.h. */

// 和一段是在实际翻译中,因为翻译后就是执行,所以此时可以进行
// afl-as 中 afl_maybe_log 的功能,对控制流进行记录
#define AFL_QEMU_CPU_SNIPPET2 do { \
if(itb->pc == afl_entry_point) { \
afl_setup(); \
afl_forkserver(cpu); \
} \
afl_maybe_log(itb->pc); \
} while (0)

afl-fuzz

这一部分是 afl fuzz 过程的核心逻辑,其中有几个点需要我们关注,我们将会一个一个来看。

需要解释的内容包括如下几个部分:

  • forkserver:用来加速程序启动过程
  • 如何使用共享内存中的控制流信息

其实还有关于变异的部分,但是变异的部分本身对于使用价值一般,我这里暂时没有关注。

第一个部分是 forkserver。

forkserver 本身思想很简单,在 afl fuzz 的过程中需要启动目标程序很多次,然而程序的启动过程本身是比较复杂的,很多初始化工作需要完成,那我们可以在程序初始化完成后启动一个 server,在 server 中等到消息,如果有消息来临,我们就 fork 一段新程序,这个时候 fork 出来的新程序是已经初始化后的,所以此时就可以节约很多初始化工作。

在 afl-fuzz 对 forkserver 进行初始化的时候,会 fork 出一个新的进程,这个进程就是 forkserver 进程,这个进程和主进程通过管道进行通信。而真正的 forkserver 其实位于进程内,是在插桩的时候放入的,位于 main_payload 中,在之前的 afl-as 解析中我忽略了这一部分,这里拿出来一起看会更加容易。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

// 建立通信管道
if (pipe(st_pipe) || pipe(ctl_pipe)) PFATAL("pipe() failed");

// fork 出 forkserver 的进程
forksrv_pid = fork();

if (forksrv_pid < 0) PFATAL("fork() failed");

if (!forksrv_pid) {

// ... 初始化 forkserver

struct rlimit r;

/* Umpf. On OpenBSD, the default fd limit for root users is set to
soft 128. Let's try to fix that... */

if (!getrlimit(RLIMIT_NOFILE, &r) && r.rlim_cur < FORKSRV_FD + 2) {

r.rlim_cur = FORKSRV_FD + 2;
setrlimit(RLIMIT_NOFILE, &r); /* Ignore errors */

}

在 afl-as 中,也就是插入到目标进程内的地方。

这一部分是汇编编写,阅读起来比较麻烦,大致上就是通过管道进行通讯,获取到信息之后就进行 fork。

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
92
"__afl_forkserver:\n"
"\n"
" /* Enter the fork server mode to avoid the overhead of execve() calls. */\n"
"\n"
" pushl %eax\n"
" pushl %ecx\n"
" pushl %edx\n"
"\n"
" /* Phone home and tell the parent that we're OK. (Note that signals with\n"
" no SA_RESTART will mess it up). If this fails, assume that the fd is\n"
" closed because we were execve()d from an instrumented binary, or because\n"
" the parent doesn't want to use the fork server. */\n"
"\n"
" pushl $4 /* length */\n"
" pushl $__afl_temp /* data */\n"
" pushl $" STRINGIFY((FORKSRV_FD + 1)) " /* file desc */\n"
" call write\n" // hello 信息,确认 forkserver 成功进入
" addl $12, %esp\n"
"\n"
" cmpl $4, %eax\n"
" jne __afl_fork_resume\n"
"\n"
"__afl_fork_wait_loop:\n"
"\n"
" /* Wait for parent by reading from the pipe. Abort if read fails. */\n"
"\n"
" pushl $4 /* length */\n"
" pushl $__afl_temp /* data */\n"
" pushl $" STRINGIFY(FORKSRV_FD) " /* file desc */\n"
" call read\n" // 等待消息
" addl $12, %esp\n"
"\n"
" cmpl $4, %eax\n"
" jne __afl_die\n"
"\n"
" /* Once woken up, create a clone of our process. This is an excellent use\n"
" case for syscall(__NR_clone, 0, CLONE_PARENT), but glibc boneheadedly\n"
" caches getpid() results and offers no way to update the value, breaking\n"
" abort(), raise(), and a bunch of other things :-( */\n"
"\n"
" call fork\n" // 进行 fork,这样就不再需要 afl-fuzz 进行很多次 execve,而是在一个目标程序里进行 fork,减少 execve 加载 ELF 文件等操作带来的 overhead
"\n"
" cmpl $0, %eax\n"
" jl __afl_die\n"
" je __afl_fork_resume\n"
"\n"
" /* In parent process: write PID to pipe, then wait for child. */\n"
"\n"
" movl %eax, __afl_fork_pid\n"
"\n"
" pushl $4 /* length */\n"
" pushl $__afl_fork_pid /* data */\n"
" pushl $" STRINGIFY((FORKSRV_FD + 1)) " /* file desc */\n"
" call write\n"
" addl $12, %esp\n"
"\n"
" pushl $0 /* no flags */\n"
" pushl $__afl_temp /* status */\n"
" pushl __afl_fork_pid /* PID */\n"
" call waitpid\n"
" addl $12, %esp\n"
"\n"
" cmpl $0, %eax\n"
" jle __afl_die\n"
"\n"
" /* Relay wait status to pipe, then loop back. */\n"
"\n"
" pushl $4 /* length */\n"
" pushl $__afl_temp /* data */\n"
" pushl $" STRINGIFY((FORKSRV_FD + 1)) " /* file desc */\n"
" call write\n"
" addl $12, %esp\n"
"\n"
" jmp __afl_fork_wait_loop\n"
"\n"
"__afl_fork_resume:\n"
"\n"
" /* In child process: close fds, resume execution. */\n"
"\n"
" pushl $" STRINGIFY(FORKSRV_FD) "\n"
" call close\n"
"\n"
" pushl $" STRINGIFY((FORKSRV_FD + 1)) "\n"
" call close\n"
"\n"
" addl $8, %esp\n"
"\n"
" popl %edx\n"
" popl %ecx\n"
" popl %eax\n"
" jmp __afl_store\n"
"\n"

第二部分,关于如何使用内存中的控制流信息。

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
// 通过共享内存,来得到一个 hash 值
cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);

// 如果 hash 存在不同,说明和之前的执行流程有变
// 这里得到变动的位置,方便在后续中标出该用例导致了变动(variable)
if (q->exec_cksum != cksum) {

u8 hnb = has_new_bits(virgin_bits);
if (hnb > new_bits) new_bits = hnb;

if (q->exec_cksum) {

u32 i;

for (i = 0; i < MAP_SIZE; i++) {

if (!var_bytes[i] && first_trace[i] != trace_bits[i]) {

var_bytes[i] = 1;
stage_max = CAL_CYCLES_LONG;

}

}

var_detected = 1;

} else {

q->exec_cksum = cksum;
memcpy(first_trace, trace_bits, MAP_SIZE);

}

}

}

stop_us = get_cur_time_us();

total_cal_us += stop_us - start_us;
total_cal_cycles += stage_max;

/* OK, let's collect some stats about the performance of this test case.
This is used for fuzzing air time calculations in calculate_score(). */

q->exec_us = (stop_us - start_us) / stage_max;
q->bitmap_size = count_bytes(trace_bits);
q->handicap = handicap;
q->cal_failed = 0;

total_bitmap_size += q->bitmap_size;
total_bitmap_entries++;

// 对该用例评分,在之后选择中会按照该评分作为用例的优先级
update_bitmap_score(q);

/* If this case didn't result in new output from the instrumentation, tell
parent. This is a non-critical problem, but something to warn the user
about. */

if (!dumb_mode && first_run && !fault && !new_bits) fault = FAULT_NOBITS;

abort_calibration:

if (new_bits == 2 && !q->has_new_cov) {
q->has_new_cov = 1;
queued_with_cov++;
}

/* Mark variable paths. */

if (var_detected) {

var_byte_count = count_bytes(var_bytes);

if (!q->var_behavior) {
mark_as_variable(q);
queued_variable++;
}

总结

这部分内容主要是对 AFL 的大致思路和实现的部分细节做了一些解释,在理解 AFL 上有一定帮助。但是事实上 AFL 的使用还有很多地方需要考虑,不仅仅是了解思路和细节,还需要操作目标程序,找准 fuzz 的点,这些都很重要,之后我也会注重这一方面的学习了解。