afl-fuzz

FUZZ

corpus: 语料库,fuzzer的输入

mutation: 变异

  • deterministic 确定性变异

参数初始化

处理命令行参数

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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
int main(int argc, char** argv) {
// ...

SAYF(cCYA "afl-fuzz " cBRI VERSION cRST " by <lcamtuf@google.com>\n");

doc_path = access(DOC_PATH, F_OK) ? "docs" : DOC_PATH;

gettimeofday(&tv, &tz);
srandom(tv.tv_sec ^ tv.tv_usec ^ getpid());

// 使用getopt函数,处理afl-fuzz的参数
while ((opt = getopt(argc, argv, "+i:o:f:m:b:t:T:dnCB:S:M:x:QV")) > 0)

switch (opt) {
// -i: input dir,包含输入的文件夹, corpus 目录
case 'i': /* input dir */

if (in_dir) FATAL("Multiple -i options not supported");
in_dir = optarg;

if (!strcmp(in_dir, "-")) in_place_resume = 1;

break;

// -o: output dir 将结果输出
case 'o': /* output dir */

if (out_dir) FATAL("Multiple -o options not supported");
out_dir = optarg;
break;

// -M: master 主要的fuzzer
case 'M': { /* master sync ID */

u8* c;

if (sync_id) FATAL("Multiple -S or -M options not supported");
sync_id = ck_strdup(optarg);

if ((c = strchr(sync_id, ':'))) {

*c = 0;

if (sscanf(c + 1, "%u/%u", &master_id, &master_max) != 2 ||
!master_id || !master_max || master_id > master_max ||
master_max > 1000000) FATAL("Bogus master ID passed to -M");

}
// 要执行 deterministic 变异
force_deterministic = 1;

}

break;

// -S: Slave 从fuzzer
case 'S':

if (sync_id) FATAL("Multiple -S or -M options not supported");
sync_id = ck_strdup(optarg);
break;

// -f: 目标程序是从某个固定的文件读入,则可以通过 -f 选项告知 afl-fuzz
case 'f': /* target file */

if (out_file) FATAL("Multiple -f options not supported");
out_file = optarg;
break;

// -x
case 'x': /* dictionary */

if (extras_dir) FATAL("Multiple -x options not supported");
extras_dir = optarg;
break;

// -t: 设置超时时间,ms
case 't': { /* timeout */

u8 suffix = 0;

if (timeout_given) FATAL("Multiple -t options not supported");

if (sscanf(optarg, "%u%c", &exec_tmout, &suffix) < 1 ||
optarg[0] == '-') FATAL("Bad syntax used for -t");

if (exec_tmout < 5) FATAL("Dangerously low value of -t");

if (suffix == '+') timeout_given = 2; else timeout_given = 1;

break;

}

// -m 内存限制
case 'm': { /* mem limit */

u8 suffix = 'M';

if (mem_limit_given) FATAL("Multiple -m options not supported");
mem_limit_given = 1;

// none 代表不限制内存
if (!strcmp(optarg, "none")) {

mem_limit = 0;
break;

}

if (sscanf(optarg, "%llu%c", &mem_limit, &suffix) < 1 ||
optarg[0] == '-') FATAL("Bad syntax used for -m");

// 指定内存限制单位
switch (suffix) {

case 'T': mem_limit *= 1024 * 1024; break;
case 'G': mem_limit *= 1024; break;
case 'k': mem_limit /= 1024; break;
case 'M': break;

default: FATAL("Unsupported suffix or bad syntax for -m");

}

if (mem_limit < 5) FATAL("Dangerously low value of -m");

if (sizeof(rlim_t) == 4 && mem_limit > 2000)
FATAL("Value of -m out of range on 32-bit systems");

}

break;

// -b: bind core 绑定CPU核心
case 'b': { /* bind CPU core */

if (cpu_to_bind_given) FATAL("Multiple -b options not supported");
cpu_to_bind_given = 1;

if (sscanf(optarg, "%u", &cpu_to_bind) < 1 ||
optarg[0] == '-') FATAL("Bad syntax used for -b");

break;

}

// -d :跳过 deterministic 几段
case 'd': /* skip deterministic */

if (skip_deterministic) FATAL("Multiple -d options not supported");
skip_deterministic = 1;
use_splicing = 1;
break;

// 只关心shm某些位置?
case 'B': /* load bitmap */

/* This is a secret undocumented option! It is useful if you find
an interesting test case during a normal fuzzing process, and want
to mutate it without rediscovering any of the test cases already
found during an earlier run.

To use this mode, you need to point -B to the fuzz_bitmap produced
by an earlier run for the exact same binary... and that's it.

I only used this once or twice to get variants of a particular
file, so I'm not making this an official setting. */

if (in_bitmap) FATAL("Multiple -B options not supported");

in_bitmap = optarg;
read_bitmap(in_bitmap);
break;

// 打开 crash exploration 模式
case 'C': /* crash mode */

if (crash_mode) FATAL("Multiple -C options not supported");
crash_mode = FAULT_CRASH;
break;

// 打开 dumb mode,即黑盒模式,不插桩
case 'n': /* dumb mode */

if (dumb_mode) FATAL("Multiple -n options not supported");
if (getenv("AFL_DUMB_FORKSRV")) dumb_mode = 2; else dumb_mode = 1;

break;

// 换个 banner,theme?
case 'T': /* banner */

if (use_banner) FATAL("Multiple -T options not supported");
use_banner = optarg;
break;

// Qemu模式,也就是二进制fuzz
case 'Q': /* QEMU mode */

if (qemu_mode) FATAL("Multiple -Q options not supported");
qemu_mode = 1;

if (!mem_limit_given) mem_limit = MEM_LIMIT_QEMU;

break;

// -V: version
case 'V': /* Show version number */

/* Version number has been printed already, just quit. */
exit(0);

default:

usage(argv[0]);

}

if (optind == argc || !in_dir || !out_dir) usage(argv[0]);
// ...

比如说

1
2
$ echo core | sudo tee /proc/sys/kernel/core_pattern
$ AFL_AUTO afl-fuzz -m none -i samples/ -o fuzzout/ -M master_fuzzer -- /path/to/exe @@

@@ 代表了程序的参数,是个占位符,从samples下的文件读取内容

core_pattern:用于配置核心转储文件(core dump)的命名规则。核心转储文件是在程序发生严重错误(如段错误)时生成的内存转储文件,用于分析程序执行过程中的问题。

setup_signal_handlers

注册信号处理函数

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
/* Set up signal handlers. More complicated that needs to be, because libc on
Solaris doesn't resume interrupted reads(), sets SA_RESETHAND when you call
siginterrupt(), and does other unnecessary things. */

EXP_ST void setup_signal_handlers(void) {

struct sigaction sa;

sa.sa_handler = NULL;
sa.sa_flags = SA_RESTART;
sa.sa_sigaction = NULL;

sigemptyset(&sa.sa_mask);

/* Various ways of saying "stop". */

sa.sa_handler = handle_stop_sig;
sigaction(SIGHUP, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);

/* Exec timeout notifications. */

sa.sa_handler = handle_timeout;
sigaction(SIGALRM, &sa, NULL);

/* Window resize */

sa.sa_handler = handle_resize;
sigaction(SIGWINCH, &sa, NULL);

/* SIGUSR1: skip entry */

sa.sa_handler = handle_skipreq;
sigaction(SIGUSR1, &sa, NULL);

/* Things we don't care about. */

sa.sa_handler = SIG_IGN;
sigaction(SIGTSTP, &sa, NULL);
sigaction(SIGPIPE, &sa, NULL);

}

check_asan_opts

检查ASAN/MSAN相关的参数

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
static void check_asan_opts(void) {
u8* x = getenv("ASAN_OPTIONS");

if (x) {

if (!strstr(x, "abort_on_error=1"))
FATAL("Custom ASAN_OPTIONS set without abort_on_error=1 - please fix!");

if (!strstr(x, "symbolize=0"))
FATAL("Custom ASAN_OPTIONS set without symbolize=0 - please fix!");

}

x = getenv("MSAN_OPTIONS");

if (x) {

if (!strstr(x, "exit_code=" STRINGIFY(MSAN_ERROR)))
FATAL("Custom MSAN_OPTIONS set without exit_code="
STRINGIFY(MSAN_ERROR) " - please fix!");

if (!strstr(x, "symbolize=0"))
FATAL("Custom MSAN_OPTIONS set without symbolize=0 - please fix!");

}

}

fix_up_sync

当使用 Slave fuzzer时,-S syncid,为其初始化output dir。

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
/* Validate and fix up out_dir and sync_dir when using -S. */

static void fix_up_sync(void) {

u8* x = sync_id;

if (dumb_mode)
FATAL("-S / -M and -n are mutually exclusive");

if (skip_deterministic) {

if (force_deterministic)
FATAL("use -S instead of -M -d");
else
FATAL("-S already implies -d");

}

while (*x) {

if (!isalnum(*x) && *x != '_' && *x != '-')
FATAL("Non-alphanumeric fuzzer ID specified via -S or -M");

x++;

}

if (strlen(sync_id) > 32) FATAL("Fuzzer ID too long");

x = alloc_printf("%s/%s", out_dir, sync_id);

sync_dir = out_dir;
out_dir = x;

if (!force_deterministic) {
skip_deterministic = 1;
use_splicing = 1;
}

}

环境变量

环境变量处理

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
// fuzzer input 和 output 不能设置相同的目录
if (!strcmp(in_dir, out_dir))
FATAL("Input and output directories can't be the same");

// dump mode: dump_mode即没有插桩和确定性(deterministic)变异阶段的模式,黑盒测试
// crash mode: 当程序因输入数据而崩溃时,AFL 会将相关信息记录下来,以便后续进行分析。
// qemu mode: qemu 模式
if (dumb_mode) {

if (crash_mode) FATAL("-C and -n are mutually exclusive");
if (qemu_mode) FATAL("-Q and -n are mutually exclusive");

}

if (getenv("AFL_NO_FORKSRV")) no_forkserver = 1;
if (getenv("AFL_NO_CPU_RED")) no_cpu_meter_red = 1;
if (getenv("AFL_NO_ARITH")) no_arith = 1;
if (getenv("AFL_SHUFFLE_QUEUE")) shuffle_queue = 1;
if (getenv("AFL_FAST_CAL")) fast_cal = 1;

// hang 超时时间
if (getenv("AFL_HANG_TMOUT")) {
hang_tmout = atoi(getenv("AFL_HANG_TMOUT"));
if (!hang_tmout) FATAL("Invalid value of AFL_HANG_TMOUT");
}

if (dumb_mode == 2 && no_forkserver)
FATAL("AFL_DUMB_FORKSRV and AFL_NO_FORKSRV are mutually exclusive");

if (getenv("AFL_PRELOAD")) {
setenv("LD_PRELOAD", getenv("AFL_PRELOAD"), 1);
setenv("DYLD_INSERT_LIBRARIES", getenv("AFL_PRELOAD"), 1);
}

if (getenv("AFL_LD_PRELOAD"))
FATAL("Use AFL_PRELOAD instead of AFL_LD_PRELOAD");

如果我们想要给程序添加 LD_PRELOAD,正确方式是设置 AFL_PRELOAD 环境变量

cmdline/banner/checktty

保存命令行参数到一个buffer里

banner:AFL图形化界面

tty

  • 如果AFL_NO_UI环境变量存在,则设置not_on_tty = 1
  • ioctl获取TIOCGWINSZ,如果报错则表示当前不在tty上运行,设置not_on_tty = 1
1
2
3
4
5
save_cmdline(argc, argv);

fix_up_banner(argv[optind]);

check_if_tty();

bind cpu core

绑定一个CPU核心,fuzzer 检查系统的负载,把自己绑定到一个空闲 cpu 核心上。另外,若 cpu 频率可调,则建议用户将其定在最高频率。

1
2
3
4
5
6
7
8
9
  get_core_count();

#ifdef HAVE_AFFINITY
bind_to_free_cpu();
#endif /* HAVE_AFFINITY */
// 确保核心转储不会进入程序 也就是 core pattern 设置
check_crash_handling();
// 检查CPU调节器
check_cpu_governor();

setup post

若有环境变量 AFL_POST_LIBRARY ,则调用 dlopen 挂载这个 lib,将全局变量 post_handler 指向 lib 中的 afl_postprocess函数。

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
  // AFL_POST_LIBRARY
setup_post();

static void setup_post(void) {

void* dh;
u8* fn = getenv("AFL_POST_LIBRARY");
u32 tlen = 6;

if (!fn) return;

ACTF("Loading postprocessor from '%s'...", fn);

dh = dlopen(fn, RTLD_NOW);
if (!dh) FATAL("%s", dlerror());

post_handler = dlsym(dh, "afl_postprocess");
if (!post_handler) FATAL("Symbol 'afl_postprocess' not found.");

/* Do a quick test. It's better to segfault now than later =) */

post_handler("hello", &tlen);

OKF("Postprocessor installed successfully.");

}

在 fuzzer 变异出一个新的用例、即将交给目标程序执行时,这个函数会被调用。所以,假如用户想要对 AFL 变异出的用例进行操作——例如将其记录到数据库中——就可以通过不修改 AFL 源码的方式实现。用户只需写一个 post_handler,编译成动态链接库,通过 AFL_POST_LIBRARY 告知 AFL。

setup shm

shm: AFL根据二元tuple(跳转的源地址和目标地址)来记录分支信息,从而获取target的执行流程和代码覆盖情况。起始阶段 fuzzer 会进行一系列的准备工作,为记录插桩得到的目标程序执行路径,即 tuple 信息。

  • trace_bits 指向共享内存的指针,用于进程间通信
  • virgin_bits 用来记录总的tuple信息;
  • virgin_tmout 记录fuzz过程中出现的所有目标程序的timeout时的tuple信息;
  • virgin_crash 记录fuzz过程中出现的crash时的tuple信息;

这里直接创建一个共享内存,同子进程forkserver中 __afl_global_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
29
30
31
32
33
34
/* Configure shared memory and virgin_bits. This is called at startup. */

EXP_ST void setup_shm(void) {

u8* shm_str;

if (!in_bitmap) memset(virgin_bits, 255, MAP_SIZE);

memset(virgin_tmout, 255, MAP_SIZE);
memset(virgin_crash, 255, MAP_SIZE);

shm_id = shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT | IPC_EXCL | 0600);

if (shm_id < 0) PFATAL("shmget() failed");

// atexit 为程序退出时注册的函数,会在exit前执行
atexit(remove_shm);

shm_str = alloc_printf("%d", shm_id);

/* If somebody is asking us to fuzz instrumented binaries in dumb mode,
we don't want them to detect instrumentation, since we won't be sending
fork server commands. This should be replaced with better auto-detection
later on, perhaps? */
// 环境变量
if (!dumb_mode) setenv(SHM_ENV_VAR, shm_str, 1);

ck_free(shm_str);

trace_bits = shmat(shm_id, NULL, 0);

if (trace_bits == (void *)-1) PFATAL("shmat() failed");

}

trace_bits用一个字节来记录是否到达这个路径,和这个路径被命中了多少次的,而这个次数在0-255之间,但比如一个循环,它循环5次和循环6次可能是完全一样的效果,为了避免被当成不同的路径,或者说尽可能减少因为命中次数导致的区别。

在每次去计算是否发现了新路径之前,先把这个路径命中数进行规整,比如把命中5次和6次都统一认为是命中了8次,一字节宽的变量按如下映射。

1
2
3
4
5
6
7
8
9
10
11
12
// ? 数组还能这样初始化
static const u8 count_class_lookup8[256] = {
[0] = 0, // 00000000
[1] = 1, // 00000001
[2] = 2, // 00000010
[3] = 4, // 00000100
[4 ... 7] = 8, // 00001000
[8 ... 15] = 16,// 00010000
[16 ... 31] = 32,// 00100000
[32 ... 127] = 64,// 01000000
[128 ... 255] = 128// 10000000
};

命中率统计

1
2
3
4
5
6
7
8
9
10
11
EXP_ST void init_count_class16(void) {

u32 b1, b2;

for (b1 = 0; b1 < 256; b1++)
for (b2 = 0; b2 < 256; b2++)
count_class_lookup16[(b1 << 8) + b2] =
(count_class_lookup8[b1] << 8) |
count_class_lookup8[b2];

}

setup dirs

创建输出的目录,存在几个目录

  • queue:输入队列
  • crashes: 记录着所有的crash输入
  • hangs:同上,记录所有的hang输入

read testcases

把初始语料集读进 queue 里

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
/* Read all testcases from the input directory, then queue them for testing.
Called at startup. */

static void read_testcases(void) {

struct dirent **nl;
s32 nl_cnt;
u32 i;
u8* fn;

/* Auto-detect non-in-place resumption attempts. */

fn = alloc_printf("%s/queue", in_dir);
// 首先判断是否存在in_dir/queue
if (!access(fn, F_OK)) in_dir = fn; else ck_free(fn);

ACTF("Scanning '%s'...", in_dir);

/* We use scandir() + alphasort() rather than readdir() because otherwise,
the ordering of test cases would vary somewhat randomly and would be
difficult to control. */
// 不使用readdir是因为测试用例的顺序将随机变化,难以控制。读取后的文件将按字母排序。
nl_cnt = scandir(in_dir, &nl, NULL, alphasort);

if (nl_cnt < 0) {

if (errno == ENOENT || errno == ENOTDIR)

SAYF("\n" cLRD "[-] " cRST
"The input directory does not seem to be valid - try again. The fuzzer needs\n"
" one or more test case to start with - ideally, a small file under 1 kB\n"
" or so. The cases must be stored as regular files directly in the input\n"
" directory.\n");

PFATAL("Unable to open '%s'", in_dir);

}

if (shuffle_queue && nl_cnt > 1) {

ACTF("Shuffling queue...");
shuffle_ptrs((void**)nl, nl_cnt);

}

// 测试用例是否在in_dir/.state/deterministic_done/文件夹中存在
for (i = 0; i < nl_cnt; i++) {

struct stat st;

u8* fn = alloc_printf("%s/%s", in_dir, nl[i]->d_name);
u8* dfn = alloc_printf("%s/.state/deterministic_done/%s", in_dir, nl[i]->d_name);

u8 passed_det = 0;

free(nl[i]); /* not tracked */

if (lstat(fn, &st) || access(fn, R_OK))
PFATAL("Unable to access '%s'", fn);

/* This also takes care of . and .. */

if (!S_ISREG(st.st_mode) || !st.st_size || strstr(fn, "/README.testcases")) {

ck_free(fn);
ck_free(dfn);
continue;

}

if (st.st_size > MAX_FILE)
FATAL("Test case '%s' is too big (%s, limit is %s)", fn,
DMS(st.st_size), DMS(MAX_FILE));

/* Check for metadata that indicates that deterministic fuzzing
is complete for this entry. We don't want to repeat deterministic
fuzzing when resuming aborted scans, because it would be pointless
and probably very time-consuming. */
// 如果存在则判定该测试用例已完成确定性变异,过滤该input
if (!access(dfn, F_OK)) passed_det = 1;
ck_free(dfn);
// 如果存在则判定该测试用例已完成确定性变异,过滤该input
// 如果不存在这加入队列
add_to_queue(fn, st.st_size, passed_det);

}

free(nl); /* not tracked */

if (!queued_paths) {

SAYF("\n" cLRD "[-] " cRST
"Looks like there are no valid test cases in the input directory! The fuzzer\n"
" needs one or more test case to start with - ideally, a small file under\n"
" 1 kB or so. The cases must be stored as regular files directly in the\n"
" input directory.\n");

FATAL("No usable test cases in '%s'", in_dir);

}

last_path_time = 0;
queued_at_start = queued_paths;

}

load

读入自动生成的字典token,从in_dir/auto_extras/auto_%06u处依次读取,调用maybe_add_auto按规则加入字典

pivot_inputs

把初始 corpus 复制到工作目录的 queue 文件夹下

在输出目录中为输入测试用例创建硬链接,其中的mark_as_det_done将一些经过确定性变异的文件放入deterministic_done目录,之后就不会再重复测试。

load_extras

如果用户通过 -x 选项指定了 dictionary,则从那里导入 extra,加载用户自己设定的字典token,从extras_dir读取extras到extras数组里,并按size排序。

detect_file_args

参数是否存在 @@,如果有则替换argv[i] = cwd/out_dir/.cur_input

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
/* Detect @@ in args. */

EXP_ST void detect_file_args(char** argv) {

u32 i = 0;
u8* cwd = getcwd(NULL, 0);

if (!cwd) PFATAL("getcwd() failed");

while (argv[i]) {

u8* aa_loc = strstr(argv[i], "@@");

if (aa_loc) {

u8 *aa_subst, *n_arg;

/* If we don't have a file name chosen yet, use a safe default. */

if (!out_file)
out_file = alloc_printf("%s/.cur_input", out_dir);

/* Be sure that we're always using fully-qualified paths. */

if (out_file[0] == '/') aa_subst = out_file;
else aa_subst = alloc_printf("%s/%s", cwd, out_file);

/* Construct a replacement argv value. */

*aa_loc = 0;
n_arg = alloc_printf("%s%s%s", argv[i], aa_subst, aa_loc + 2);
argv[i] = n_arg;
*aa_loc = '@';

if (out_file[0] != '/') ck_free(aa_subst);

}

i++;

}

free(cwd); /* not tracked */

}

setup stdio -> check binary

1
2
3
4
5
6
// 创建 .cur_input 文件并打开,设为 out_fd。接下来 fuzzer 要把变异出的 input 写进这里,由 child 读取
if (!out_file) setup_stdio_file();

// 检查目标程序,看找不找得到、在不在 /tmp 等。
// 不能为shell脚本,同时检查elf文件头是否合法及程序是否被插桩。
check_binary(argv[optind]);

perform_dry_run

用 queue 中的所有用例跑一遍程序;另外,它使用了 fork server。在代码中,这种 dry run 称为「calibrate(校准)」。

  • 依次读取queue中的内容
  • 调用 calibrate_case 函数校准该测试用例,得到返回值res
  • 根据res判断错误类型

calibrate_case

校准:用例会被运行多次

  • FAULT_NONE:该测试用例不产生crash
  • FAULT_TMOUT:该测试用例产生超时错误。当timeout_given为2时跳过该文件
  • FAULT_CRASH :初始测试用例就引发了崩溃,需要排除mem_limit的问题
  • FAULT_ERROR:目标程序无法执行
  • FAULT_NOINST:没检测到插桩代码,直接退出
  • FAULT_NOBITS:无用测试用例。该用例有路径信息但没新路径

calibrate_case 函数的运行时机至少有两个:一是程序运行之初,用于校准初始 corpus;二是发现了新路径,将有趣的用例加入 queue 时。总结一句:进了 queue 的用例,都要被运行一遍calibrate_case 函数

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

校准过程是多次运行用例(默认是 8 次),统计各次运行的结果。

  • 若 fork server 没有准备好,就调用 init_forkserver() 初始化 fork server
  • 多次调用 run_target() 运行目标程序,观察结果。若没有任何 hit count 命中,则认为程序未插桩,报告错误。
  • 如果发现对某用例多次运行程序,其表现不一致,则将执行次数提升到 40 次,并更新 var_bytes[] (这个全局变量表示 shm 中哪些位置存在不一致性)。另外,将 queue entry 的 var_behavior 标记设为 1
  • 更新 queue entry 信息,例如将 exec_us 字段设为校准过程中的执行时间均值。
  • 给这个用例打分,并更新 top_rated 指针数组。

init forkserver

fuzzer 初始化 forkserver

  1. fork出一个子进程用于执行目标程序,也就是将来的fork server
  2. 重定向文件描述符
    • 文件描述符1和2重定向至dev_null_fd
    • 如果指定了out_file则重定向文件描述符0至out_fd,如果没有执行则重定向至dev_null_fd
  3. 将控制管道和状态管道重定向至FORKSRV_FD和FORKSRV_FD+1,然后关闭用不到的管道
  4. 调用execv,进程替换,子进程执行目标程序作为forkserver,IPC进程间通信
  5. 等待forkserver启动,从状态管道中读取到4字节的数据,判定forkserver启动完毕
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
/* Spin up fork server (instrumented mode only). The idea is explained here:

http://lcamtuf.blogspot.com/2014/10/fuzzing-binaries-without-execve.html

In essence, the instrumentation allows us to skip execve(), and just keep
cloning a stopped child. So, we just execute once, and then send commands
through a pipe. The other part of this logic is in afl-as.h. */

EXP_ST void init_forkserver(char** argv) {

static struct itimerval it;
// status pipe
// control pipe
int st_pipe[2], ctl_pipe[2];
int status;
s32 rlen;

ACTF("Spinning up the fork server...");

if (pipe(st_pipe) || pipe(ctl_pipe)) PFATAL("pipe() failed");

forksrv_pid = fork();

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

// 子进程
if (!forksrv_pid) {

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 */

}

if (mem_limit) {

r.rlim_max = r.rlim_cur = ((rlim_t)mem_limit) << 20;

#ifdef RLIMIT_AS

setrlimit(RLIMIT_AS, &r); /* Ignore errors */

#else

/* This takes care of OpenBSD, which doesn't have RLIMIT_AS, but
according to reliable sources, RLIMIT_DATA covers anonymous
maps - so we should be getting good protection against OOM bugs. */

setrlimit(RLIMIT_DATA, &r); /* Ignore errors */

#endif /* ^RLIMIT_AS */


}

/* Dumping cores is slow and can lead to anomalies if SIGKILL is delivered
before the dump is complete. */

r.rlim_max = r.rlim_cur = 0;

setrlimit(RLIMIT_CORE, &r); /* Ignore errors */

/* Isolate the process and configure standard descriptors. If out_file is
specified, stdin is /dev/null; otherwise, out_fd is cloned instead. */

setsid();

dup2(dev_null_fd, 1);
dup2(dev_null_fd, 2);

if (out_file) {

dup2(dev_null_fd, 0);

} else {

dup2(out_fd, 0);
close(out_fd);

}

/* Set up control and status pipes, close the unneeded original fds. */

if (dup2(ctl_pipe[0], FORKSRV_FD) < 0) PFATAL("dup2() failed");
if (dup2(st_pipe[1], FORKSRV_FD + 1) < 0) PFATAL("dup2() failed");

close(ctl_pipe[0]);
close(ctl_pipe[1]);
close(st_pipe[0]);
close(st_pipe[1]);

close(out_dir_fd);
close(dev_null_fd);
close(dev_urandom_fd);
close(fileno(plot_file));

/* This should improve performance a bit, since it stops the linker from
doing extra work post-fork(). */

if (!getenv("LD_BIND_LAZY")) setenv("LD_BIND_NOW", "1", 0);

/* Set sane defaults for ASAN if nothing else specified. */

setenv("ASAN_OPTIONS", "abort_on_error=1:"
"detect_leaks=0:"
"symbolize=0:"
"allocator_may_return_null=1", 0);

/* MSAN is tricky, because it doesn't support abort_on_error=1 at this
point. So, we do this in a very hacky way. */

setenv("MSAN_OPTIONS", "exit_code=" STRINGIFY(MSAN_ERROR) ":"
"symbolize=0:"
"abort_on_error=1:"
"allocator_may_return_null=1:"
"msan_track_origins=0", 0);

// 子进程调用一次 execv
// 运行 afl-gcc 运行的程序
execv(target_path, argv);

/* Use a distinctive bitmap signature to tell the parent about execv()
falling through. */

*(u32*)trace_bits = EXEC_FAIL_SIG;
exit(0);

}

// 父进程
/* Close the unneeded endpoints. */

close(ctl_pipe[0]);
close(st_pipe[1]);

fsrv_ctl_fd = ctl_pipe[1];
fsrv_st_fd = st_pipe[0];

/* Wait for the fork server to come up, but don't wait too long. */

it.it_value.tv_sec = ((exec_tmout * FORK_WAIT_MULT) / 1000);
it.it_value.tv_usec = ((exec_tmout * FORK_WAIT_MULT) % 1000) * 1000;

setitimer(ITIMER_REAL, &it, NULL);

// 接收到启动的程序的信息
rlen = read(fsrv_st_fd, &status, 4);

it.it_value.tv_sec = 0;
it.it_value.tv_usec = 0;

setitimer(ITIMER_REAL, &it, NULL);

/* If we have a four-byte "hello" message from the server, we're all set.
Otherwise, try to figure out what went wrong. */

if (rlen == 4) {
OKF("All right - fork server is up.");
return;
}

if (child_timed_out)
FATAL("Timeout while initializing fork server (adjusting -t may help)");

// 等待子进程
if (waitpid(forksrv_pid, &status, 0) <= 0)
PFATAL("waitpid() failed");

// 很多的SAYF...
}

cull_queue

精简化队列

  • 创建一个了temp_v数组,该数组的大小与trace_mini一致,为MAP_SIZE >> 3
  • 在初始状态下,数组中的bit全为1,每位代表了路径是否被覆盖,为1时表示未被覆盖
  • temp_v数组用于记录top_rated数组中每个路径的优胜者第一次覆盖的所有路径,即trace_mini数组。
  • temp_v数组存在的目的其实是用于选择第一次覆盖到该路径的优胜者。这里本质上还是使用了贪心算法,AFL贪心的认为对于每个被命中的路径,遍历时第一次遇到的优胜者就是更favorable,所以那个优胜者的favored才会被置1
  • 之后favored未被标记的测试用例将由mark_as_redundant将其q->fs_redundant置1,并放入 out_dir/queue/.state/redundant_edges/ 文件夹中。如果它在后续又被 favored 标记,则从文件夹中删去;反之同理。

while 1: fuzz 主循环

在进行第一轮fuzz后进入fuzz主循环,每一次循环对一个测试用例调用fuzz_one进行测试,对测试用例进行变异,然后通过cull_queue精简队列。AFL会返回执行如下内容,直到停止。

  1. cull_queue
  2. 如果当前queue_cur为空表示所有的queue都被执行完一轮,从queue头开始新一轮fuzz
    • 刷新展示界面
    • 如果执行一轮后的queue中的test_case数量与执行前一样,表示此轮fuzz没有效果,则重新调整变异策略
  3. 调用fuzz_one对queue_cur指定的测试用例进行测试
  4. 移动queue_cur

fuzz one

进行样例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
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
/* Take the current entry from the queue, fuzz it for a while. This
function is a tad too long... returns 0 if fuzzed successfully, 1 if
skipped or bailed out. */

/* 调用fuzz_one对queue_cur进行一次变异测试(fuzz_one并不一定真的执行当前queue_cur,
它有一定的策略;如果不执行,就直接返回1,否则返回0) */
static u8 fuzz_one(char** argv) {

s32 len, fd, temp_len, i, j;
u8 *in_buf, *out_buf, *orig_in, *ex_tmp, *eff_map = 0;
u64 havoc_queued, orig_hit_cnt, new_hit_cnt;
u32 splice_cycle = 0, perf_score = 100, orig_perf, prev_cksum, eff_cnt = 1;

u8 ret_val = 1, doing_det = 0;

u8 a_collect[MAX_AUTO_EXTRA];
u32 a_len = 0;

#ifdef IGNORE_FINDS

/* In IGNORE_FINDS mode, skip any entries that weren't in the
initial data set. */

if (queue_cur->depth > 1)
return 1;

#else

/* 判断pending_favored的值:(这里用到的常量可以在config.h找到)
1.如果为0,对于queue_cur被fuzz过或者不是favored的,有99%的概率不执行,直接返回1
2.如果不为0,并且不是dumb_mode、不是favored的、queued_paths>10:
a.如果queue_cycle大于1,且没有被fuzz过,那么有95%的概率不执行,直接返回1
b.否则,有75%的概率不执行,直接返回1
*/
if (pending_favored) {

/* If we have any favored, non-fuzzed new arrivals in the queue,
possibly skip to them at the expense of already-fuzzed or non-favored
cases. */

if ((queue_cur->was_fuzzed || !queue_cur->favored) &&
UR(100) < SKIP_TO_NEW_PROB)
return 1;

} else if (!dumb_mode && !queue_cur->favored && queued_paths > 10) {

/* Otherwise, still possibly skip non-favored cases, albeit less often.
The odds of skipping stuff are higher for already-fuzzed inputs and
lower for never-fuzzed entries. */

if (queue_cycle > 1 && !queue_cur->was_fuzzed) {
if (UR(100) < SKIP_NFAV_NEW_PROB)
return 1;

} else {
if (UR(100) < SKIP_NFAV_OLD_PROB)
return 1;
}
}

#endif /* ^IGNORE_FINDS */

/* 如果不是tty模式,输出提示信息并刷新stdout缓冲区 */
if (not_on_tty) {
ACTF("Fuzzing test case #%u (%u total, %llu uniq crashes found)...",
current_entry, queued_paths, unique_crashes);
fflush(stdout);
}

/* Map the test case into memory. */

/* 这部分主要将case映射到内存的处理:
1.设置len为queue_cur->len
2.打开case对应的文件,并通过mmap映射到内存里,将地址赋值给in_buf和orig_in
3.分配len大小的内存,并初始化为全0,然后将地址赋值给out_buf
4.将连续超时计数器subseq_tmout清零
5.设置cur_depth为queue_cur->depth
*/
fd = open(queue_cur->fname, O_RDONLY);
if (fd < 0)
PFATAL("Unable to open '%s'", queue_cur->fname);

len = queue_cur->len;

orig_in = in_buf = mmap(0, len, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);

if (orig_in == MAP_FAILED)
PFATAL("Unable to mmap '%s'", queue_cur->fname);
close(fd);

/* We could mmap() out_buf as MAP_PRIVATE, but we end up clobbering every
single byte anyway, so it wouldn't give us any performance or memory usage
benefits. */

out_buf = ck_alloc_nozero(len);
subseq_tmouts = 0;
cur_depth = queue_cur->depth;


/*******************************************
* CALIBRATION (only if failed earlier on) *
*******************************************/

/* 这里开始进入CALIBRATION阶段:
1.假如当前项有校准错误,并且校准错误次数小于3次,那么就调用calibrate_case再次校准
2.如果设置了stop_soon,或者res不等于crash_mode:
a.计数器cur_skipped_paths加1
b.进入abandon_entry作后续处理
*/
if (queue_cur->cal_failed) {

u8 res = FAULT_TMOUT;

if (queue_cur->cal_failed < CAL_CHANCES) {

/* Reset exec_cksum to tell calibrate_case to re-execute the testcase
avoiding the usage of an invalid trace_bits.
For more info: https://github.com/AFLplusplus/AFLplusplus/pull/425 */

queue_cur->exec_cksum = 0;

res = calibrate_case(argv, queue_cur, in_buf, queue_cycle - 1, 0);

if (res == FAULT_ERROR)
FATAL("Unable to execute target application");

}

if (stop_soon || res != crash_mode) {
cur_skipped_paths++;
goto abandon_entry;
}

}


/************
* TRIMMING *
************/

/* 这里开始进入TRIMMING阶段:
1.如果不处于dumb_mode,且当前项没有被裁剪:
a.调用trim_case对queue_cur进行trim
b.设置queue_cur->trim_done的值为1
c.重新用queue_cur->len去设置len的值
2.将in_buf拷贝len个字节到out_buf中(注意in_buf是trim_case的参数,得到了裁剪)
*/
if (!dumb_mode && !queue_cur->trim_done) {

u8 res = trim_case(argv, queue_cur, in_buf);

if (res == FAULT_ERROR)
FATAL("Unable to execute target application");

if (stop_soon) {
cur_skipped_paths++;
goto abandon_entry;
}

/* Don't retry trimming, even if it failed. */

queue_cur->trim_done = 1;

if (len != queue_cur->len)
len = queue_cur->len;
}

memcpy(out_buf, in_buf, len);


/*********************
* PERFORMANCE SCORE *
*********************/

/* 这里开始进入PERFORMANCE SCORE阶段:
1.对当前项调用calculate_score,算出得分并设置orig_perf和perf_score
2.如果设置了skip_deterministic,或者当前项被fuzz过,或者passed_det为1(好像也是被fuzz过)
,那么跳转到havoc_stage去执行
3.如果执行路径校验和,超过此主实例的范围,那么也跳转到havoc_stage去执行
4.若没跳走,设置doing_det的值为1(位于fuzz_one中的一个局部变量)
*/
orig_perf = perf_score = calculate_score(queue_cur);

/* Skip right away if -d is given, if we have done deterministic fuzzing on
this entry ourselves (was_fuzzed), or if it has gone through deterministic
testing in earlier, resumed runs (passed_det). */

if (skip_deterministic || queue_cur->was_fuzzed || queue_cur->passed_det)
goto havoc_stage;

/* Skip deterministic fuzzing if exec path checksum puts this out of scope
for this master instance. */

if (master_max && (queue_cur->exec_cksum % master_max) != master_id - 1)
goto havoc_stage;

doing_det = 1;
// 变异...

mutation

变异的主要类型如下:

  • bitflip,按位翻转,1变为0,0变为1
  • arithmetic,整数加/减算术运算
  • interest,把一些特殊内容替换到原文件中
  • dictionary,把自动生成或用户提供的token替换/插入到原文件中
  • havoc,大破坏,此阶段会对原文件进行大量变异,具体见下文
  • splice,绞接,此阶段会将两个文件拼接起来得到一个新的文件

bitflip, arithmetic, interest, dictionary是非dumb mode(-d)和主fuzzer(-M)会进行的操作,由于其变异方式没有随机性,所以也称为deterministic fuzzing;

havoc和splice则存在随机性,是所有状况的fuzzer(是否dumb mode、主从fuzzer)都会执行的变异。

effector map

如果一个byte完全翻转,都无法带来执行路径的变化,那么这个byte很有可能是属于”data”,而非”metadata”(例如size, flag等),对整个fuzzing的意义不大。所以,在随后的一些变异中,会参考effector map,跳过那些“无效”的byte,从而节省了执行资源。

比如解析elf文件程序,我们将其magic num给改了,就会导致程序结果大不相同

havoc

对于dumb mode或者从fuzzer来说,则是直接从这一阶段开始。

havoc,顾名思义,是充满了各种随机生成的变异,是对原文件的“大破坏”。具体来说,havoc包含了对原文件的多轮变异,每一轮都是将多种方式组合(stacked)而成:

  • 随机选取某个bit进行翻转
  • 随机选取某个byte,将其设置为随机的interesting value
  • 随机选取某个word,并随机选取大、小端序,将其设置为随机的interesting value
  • 随机选取某个dword,并随机选取大、小端序,将其设置为随机的interesting value
  • 随机选取某个byte,对其减去一个随机数
  • 随机选取某个byte,对其加上一个随机数
  • 随机选取某个word,并随机选取大、小端序,对其减去一个随机数
  • 随机选取某个word,并随机选取大、小端序,对其加上一个随机数
  • 随机选取某个dword,并随机选取大、小端序,对其减去一个随机数
  • 随机选取某个dword,并随机选取大、小端序,对其加上一个随机数
  • 随机选取某个byte,将其设置为随机数
  • 随机删除一段bytes
  • 随机选取一个位置,插入一段随机长度的内容,其中75%的概率是插入原文中随机位置的内容,25%的概率是插入一段随机选取的数
  • 随机选取一个位置,替换为一段随机长度的内容,其中75%的概率是替换成原文中随机位置的内容,25%的概率是替换成一段随机选取的数
  • 随机选取一个位置,用随机选取的token(用户提供的或自动生成的)替换
  • 随机选取一个位置,用随机选取的token(用户提供的或自动生成的)插入

覆盖率

共享内存:MAP_SIZE=64K,当然会存在碰撞的问题;但根据AFL文档中的介绍,对于不是很复杂的目标,碰撞概率还是可以接受的:

1
2
3
4
5
6
7
8
 Branch cnt | Colliding tuples | Example targets
------------+------------------+-----------------
1,000 | 0.75% | giflib, lzo
2,000 | 1.5% | zlib, tar, xz
5,000 | 3.5% | libpng, libwebp
10,000 | 7% | libxml
20,000 | 14% | sqlite
50,000 | 30% | -

如果一个目标过于复杂,那么AFL状态面板中的map_density信息就会有相应的提示

众所周知:afl是基于覆盖引导(Coverage-guided)的模糊测试工具,它通过记录输入样本的代码覆盖率,从而调整输入样本以提高覆盖率,增加发现漏洞的概率。但是如果程序存在if语句,并且if语句存在一定的条件,甚至会导致死循环,对覆盖率的影响?

如果用户想提供一个后处理算法,仅需将其写进 afl_postprocess 函数,并编译成动态库,通过 AFL_POST_LIBRARY 环境变量传递给 fuzzer。有点像插件的作用

参考文章