afl-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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
/* Main entry point */

int main(int argc, char** argv) {

s32 opt;
u64 prev_queued = 0;
u32 sync_interval_cnt = 0, seek_to;
u8 *extras_dir = 0;
u8 mem_limit_given = 0;
u8 exit_1 = !!getenv("AFL_BENCH_JUST_ONE");
char** use_argv;

struct timeval tv;
struct timezone tz;

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());

while ((opt = getopt(argc, argv, "+i:o:f:m:b:t:T:dnCB:S:M:x:QV")) > 0)

switch (opt) {

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;

case 'o': /* output dir */

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

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");

}

force_deterministic = 1;

}

break;

case 'S':

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

case 'f': /* target file */

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

case 'x': /* dictionary */

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

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;

}

case 'm': { /* mem limit */

u8 suffix = 'M';

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

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;

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;

}

case 'd': /* skip deterministic */

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

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;

case 'C': /* crash mode */

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

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;

case 'T': /* banner */

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

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;

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]);

setup_signal_handlers();
check_asan_opts();

if (sync_id) fix_up_sync();

if (!strcmp(in_dir, out_dir))
FATAL("Input and output directories can't be the same");

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;

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");

save_cmdline(argc, argv);

fix_up_banner(argv[optind]);

check_if_tty();

get_core_count();

#ifdef HAVE_AFFINITY
bind_to_free_cpu();
#endif /* HAVE_AFFINITY */

check_crash_handling();
check_cpu_governor();

setup_post();
setup_shm();
init_count_class16();

setup_dirs_fds();
read_testcases();
load_auto();

pivot_inputs();

if (extras_dir) load_extras(extras_dir);

if (!timeout_given) find_timeout();

detect_file_args(argv + optind + 1);

if (!out_file) setup_stdio_file();

check_binary(argv[optind]);

start_time = get_cur_time();

if (qemu_mode)
use_argv = get_qemu_argv(argv[0], argv + optind, argc - optind);
else
use_argv = argv + optind;

perform_dry_run(use_argv);

cull_queue();

show_init_stats();

seek_to = find_start_position();

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

if (stop_soon) goto stop_fuzzing;

/* Woop woop woop */

if (!not_on_tty) {
sleep(4);
start_time += 4000;
if (stop_soon) goto stop_fuzzing;
}

while (1) {

u8 skipped_fuzz;

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

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);

}

skipped_fuzz = fuzz_one(use_argv);

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;

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);

}

fclose(plot_file);
destroy_queue();
destroy_extras();
ck_free(target_path);
ck_free(sync_id);

alloc_report();

OKF("We're done here. Have a nice day!\n");

exit(0);

}

#endif /* !AFL_LIB *

因为是整个AFL的核心区域,函数长度有点长,我们从以下几个部分分析

主要数据结构、变量和初始化

1
2
3
4
5
6
7
8
9
10
11
s32 opt;                        // getopt选项解析
u64 prev_queued = 0; // 上一轮队列中的测试用例数
u32 sync_interval_cnt = 0, // 同步间隔计数器
seek_to; // 队列中的起始位置
u8 *extras_dir = 0; // 额外字典目录
u8 mem_limit_given = 0; // 内存限制是否设置
u8 exit_1 = !!getenv("AFL_BENCH_JUST_ONE"); // 基准测试模式
char** use_argv; // 实际使用的命令行参数

struct timeval tv; // 时间结构
struct timezone tz; // 时区结构
1
2
3
4
5
6
7
8
9
// 1. 显示版本信息
SAYF(cCYA "afl-fuzz " cBRI VERSION cRST " by <lcamtuf@google.com>\n");

// 2. 设置文档路径
doc_path = access(DOC_PATH, F_OK) ? "docs" : DOC_PATH;

// 3. 初始化随机数生成器
gettimeofday(&tv, &tz);
srandom(tv.tv_sec ^ tv.tv_usec ^ getpid());

跟afl-as使用相同的策略,确保整个AFL工具的随机性一致

命令行解析

1
2
3
4
while ((opt = getopt(argc, argv, "+i:o:f:m:b:t:T:dnCB:S:M:x:QV")) > 0)
switch (opt) {
// 各种选项处理
}
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
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;

case 'o': /* output dir */

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

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");

}

force_deterministic = 1;

}

break;

case 'S':

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

case 'f': /* target file */

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

case 'x': /* dictionary */

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

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;

}

case 'm': { /* mem limit */

u8 suffix = 'M';

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

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;

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;

}

case 'd': /* skip deterministic */

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

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;

case 'C': /* crash mode */

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

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;

case 'T': /* banner */

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

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;

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

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

default:

usage(argv[0]);

支持的主要选项:

选项 参数 功能 重要性
-i 目录 输入测试用例目录 必需
-o 目录 输出结果目录 必需
-f 文件 目标程序输入文件 可选
-m 内存 内存限制 重要
-t 时间 执行超时 重要
-M/-S ID 主/从同步模式 并行
-x 目录 字典目录 优化
-Q - QEMU模式 特殊
-C - 崩溃模式 调试
-n - 哑模式 特殊
-B bitmap 基于已有覆盖率

extern char *optarg;

optarg定义在<unistd.h>头文件中,由 getopt() 函数维护,当 getopt() 解析到一个需要参数的选项时,会将该参数的地址赋值给 optarg,例如此时有-i /tmp/test这个选项,如果getopt的第三个参数有i:的话,那么此时*optarg="/tmp/test"😎

“M” & “S”选项

可以看到是主/从同步模式,afl-fuzz的单个fuzzer只占用一个进程,因此只能占用一个核心,而在多核系统中可以同时运行多个fuzzer实例

1
2
3
4
5
6
# 启动主实例
./afl-fuzz -i input -o sync_dir -M fuzzer01 ./target @@

# 启动从实例
./afl-fuzz -i input -o sync_dir -S fuzzer02 ./target @@
./afl-fuzz -i input -o sync_dir -S fuzzer03 ./target @@

实例与实例之间的共享方式是通过将信息输入到同一个文件夹下,每个fuzzer周期性的check文件夹下其他fuzzer生成的用例,将有用的用例加入到自己的用例集中,这里可以详细分析一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sync_dir/
├── fuzzer01/ # 主实例 (-M)
│ ├── queue/ # 测试用例队列
│ ├── crashes/ # 崩溃样本
│ ├── hangs/ # 挂起样本
│ └── .synced/ # 同步状态记录
│ ├── fuzzer02 # 记录从fuzzer02同步到的位置
│ └── fuzzer03 # 记录从fuzzer03同步到的位置
├── fuzzer02/ # 从实例 (-S)
│ ├── queue/
│ └── .synced/
└── fuzzer03/ # 从实例 (-S)
├── queue/
└── .synced/

目录结构大致会是这样的

1
2
3
4
5
6
// 主循环中的同步调用
if (!stop_soon && sync_id && !skipped_fuzz) {
if (!(sync_interval_cnt++ % SYNC_INTERVAL))
sync_fuzzers(use_argv);
}
#define SYNC_INTERVAL 5 // 每5次fuzz_one()调用后同步一次

可以看到同步时机是5次

sync_fuzzers
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
static void sync_fuzzers(char** argv) {
DIR* sd;
struct dirent* sd_ent;
u32 sync_cnt = 0;

// 1. 打开同步目录
sd = opendir(sync_dir);
if (!sd) PFATAL("Unable to open '%s'", sync_dir);

// 2. 遍历同步目录中的所有fuzzer实例
while ((sd_ent = readdir(sd))) {
// 跳过自己和隐藏文件
if (sd_ent->d_name[0] == '.' || !strcmp(sync_id, sd_ent->d_name))
continue;

// 3. 检查是否有queue子目录
qd_path = alloc_printf("%s/%s/queue", sync_dir, sd_ent->d_name);
if (!(qd = opendir(qd_path))) {
ck_free(qd_path);
continue;
}

// 4. 读取同步状态
qd_synced_path = alloc_printf("%s/.synced/%s", out_dir, sd_ent->d_name);
id_fd = open(qd_synced_path, O_RDWR | O_CREAT, 0600);

// 读取上次同步到的测试用例ID
if (read(id_fd, &min_accept, sizeof(u32)) > 0)
lseek(id_fd, 0, SEEK_SET);
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
// 遍历其他fuzzer的队列文件
while ((qd_ent = readdir(qd))) {
// 解析文件名,提取用例ID
if (qd_ent->d_name[0] == '.' ||
sscanf(qd_ent->d_name, CASE_PREFIX "%06u", &syncing_case) != 1 ||
syncing_case < min_accept) continue;

// 只处理新的测试用例
if (syncing_case >= next_min_accept)
next_min_accept = syncing_case + 1;

path = alloc_printf("%s/%s", qd_path, qd_ent->d_name);
fd = open(path, O_RDONLY);

if (fd < 0) {
ck_free(path);
continue;
}

// 读取测试用例内容
if (fstat(fd, &st)) PFATAL("fstat() failed");

if (st.st_size && st.st_size <= MAX_FILE) {
u8* mem = mmap(0, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);

// 执行测试用例
write_to_testcase(mem, st.st_size);
fault = run_target(argv, exec_tmout);

// 检查是否有趣并保存
syncing_party = sd_ent->d_name;
queued_imported += save_if_interesting(argv, mem, st.st_size, fault);
syncing_party = 0;

munmap(mem, st.st_size);
}
}

// 更新同步状态
ck_write(id_fd, &next_min_accept, sizeof(u32), qd_synced_path);

可以看到,保留用例来自于save_if_interesting函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static u8 save_if_interesting(char** argv, void* mem, u32 len, u8 fault) {
u8 hnb;
u8 keeping = 0, res;

if (fault == crash_mode) {
// 关键:检查是否有新的覆盖率位
if (!(hnb = has_new_bits(virgin_bits))) {
if (crash_mode) total_crashes++;
return 0; // 没有新覆盖率,不保存
}

// 有新覆盖率,保存到队列
add_to_queue(fn, len, 0);

if (hnb == 2) {
queue_top->has_new_cov = 1; // 标记为有新覆盖率
queued_with_cov++;
}

keeping = 1;
}

return keeping;
}
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
static inline u8 has_new_bits(u8* virgin_map) {
u64* current = (u64*)trace_bits; // 当前执行的覆盖率
u64* virgin = (u64*)virgin_map; // 全局virgin位图
u32 i = (MAP_SIZE >> 3);
u8 ret = 0;

while (i--) {
// 检查当前执行是否触及了virgin位图中的新位
if (unlikely(*current) && unlikely(*current & *virgin)) {
if (likely(ret < 2)) {
u8* cur = (u8*)current;
u8* vir = (u8*)virgin;

// 检查是否有完全新的路径 (virgin位为0xff)
if ((cur[0] && vir[0] == 0xff) || (cur[1] && vir[1] == 0xff) ||
(cur[2] && vir[2] == 0xff) || (cur[3] && vir[3] == 0xff) ||
(cur[4] && vir[4] == 0xff) || (cur[5] && vir[5] == 0xff) ||
(cur[6] && vir[6] == 0xff) || (cur[7] && vir[7] == 0xff))
ret = 2; // 发现新路径
else
ret = 1; // 只是命中次数变化
}

// 更新virgin位图
*virgin &= ~*current;
}
current++;
virgin++;
}

return ret;
}

ret=0的话代表没有新覆盖率,ret=1代表发现已知路径的新的命中次数,ret=2代表发现全新的执行路径

bitmap

1
2
3
4
5
6
#define MAP_SIZE_POW2       16          // 2^16 = 65536
#define MAP_SIZE (1 << MAP_SIZE_POW2) // 65536字节

EXP_ST u8 virgin_bits[MAP_SIZE], // 未触及的覆盖率区域
virgin_tmout[MAP_SIZE], // 超时情况下的virgin位图
virgin_crash[MAP_SIZE]; // 崩溃情况下的virgin位图
1
2
3
4
5
6
7
8
9
EXP_ST void setup_shm(void) {
// 关键:如果没有加载bitmap (-B选项),则初始化为全1
if (!in_bitmap) memset(virgin_bits, 255, MAP_SIZE);

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

// ... 共享内存设置
}
1
2
3
4
5
6
7
8
9
10
// Virgin位图的值含义:
// 0xFF (11111111) - 完全未触及的路径
// 0xFE (11111110) - 触及过1次
// 0xFC (11111100) - 触及过1-2次
// 0xF8 (11111000) - 触及过1-4次
// 0xF0 (11110000) - 触及过1-8次
// 0xE0 (11100000) - 触及过1-16次
// 0xC0 (11000000) - 触及过1-32次
// 0x80 (10000000) - 触及过1-128次
// 0x00 (00000000) - 触及过128+次

Bitmap文件的十六进制示例

bash

Apply to afl-fuzz.c

Run

1
2
3
4
5
6
7
8
9
10
11
\# 查看bitmap文件内容(前32字节)

hexdump -C output_dir/fuzz_bitmap | head -5

00000000 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|

00000010 ff ff ff fe ff ff fc ff ff f8 ff ff f0 ff ff e0 |................|

00000020 ff ff c0 ff ff 80 ff ff 00 ff ff ff ff ff ff ff |................|

00000030 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|

解读:

  • 0x0000-0x000F: 全部为0xFF,表示这些路径完全未被触及

  • 0x0010: 0xFE,表示这个路径被触及过1次

  • 0x0012: 0xFC,表示这个路径被触及过2-3次

  • 0x0014: 0xF8,表示这个路径被触及过4-7次

  • 以此类推…

接下来就是一些参数的验证和环境的检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 必需参数检查
if (optind == argc || !in_dir || !out_dir) usage(argv[0]);

// 2. 信号处理器设置
setup_signal_handlers();

// 3. ASAN选项检查
check_asan_opts();

// 4. 同步设置
if (sync_id) fix_up_sync();

// 5. 目录冲突检查
if (!strcmp(in_dir, out_dir))
FATAL("Input and output directories can't be the same");
1
2
3
4
5
6
if (dumb_mode) {

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

}

同时dubm模式与crash和qemu模式不兼容

1
2
3
4
5
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;

重要环境变量:

变量 功能 影响
AFL_NO_FORKSRV 禁用fork server 性能下降
AFL_NO_CPU_RED 禁用CPU红色警告 界面优化
AFL_NO_ARITH 禁用算术变异 策略简化
AFL_SHUFFLE_QUEUE 随机化队列顺序 探索策略
AFL_FAST_CAL 快速校准 启动优化

系统环境准备和初始化

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
// 1. 必需参数检查
if (optind == argc || !in_dir || !out_dir) usage(argv[0]);

// 2. 信号处理器设置
setup_signal_handlers();

// 3. ASAN选项检查
check_asan_opts();

// 4. 同步设置
if (sync_id) fix_up_sync();

// 5. 目录冲突检查
if (!strcmp(in_dir, out_dir))
FATAL("Input and output directories can't be the same");

save_cmdline(argc, argv); // 保存命令行
fix_up_banner(argv[optind]); // 设置横幅
check_if_tty(); // 检查终端
get_core_count(); // 获取CPU核心数

#ifdef HAVE_AFFINITY
bind_to_free_cpu(); // 绑定到空闲CPU
#endif

check_crash_handling(); // 检查崩溃处理
check_cpu_governor(); // 检查CPU调速器
setup_post(); // 设置后处理
setup_shm(); // 设置共享内存
init_count_class16(); // 初始化计数分类

setup_dirs_fds(); // 设置目录和文件描述符
read_testcases(); // 读取测试用例
load_auto(); // 加载自动令牌

pivot_inputs(); // 转换输入格式
if (extras_dir) load_extras(extras_dir); // 加载字典
if (!timeout_given) find_timeout(); // 自动检测超时

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
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);

/* 各种停止信号的处理 */
sa.sa_handler = handle_stop_sig;
sigaction(SIGHUP, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);

/* 执行超时通知 */
sa.sa_handler = handle_timeout;
sigaction(SIGALRM, &sa, NULL);

/* 窗口大小改变 */
sa.sa_handler = handle_resize;
sigaction(SIGWINCH, &sa, NULL);

/* SIGUSR1: 跳过当前条目 */
sa.sa_handler = handle_skipreq;
sigaction(SIGUSR1, &sa, NULL);

/* 我们不关心的信号 */
sa.sa_handler = SIG_IGN;
sigaction(SIGTSTP, &sa, NULL);
sigaction(SIGPIPE, &sa, NULL);
}
handle_stop_sig
1
2
3
4
5
static void handle_stop_sig(int sig) {
stop_soon = 1;
if (child_pid > 0) kill(child_pid, SIGKILL);
if (forksrv_pid > 0) kill(forksrv_pid, SIGKILL);
}
  • 设置stop_soon标志为1,表示AFL应该开始优雅退出

  • 终止所有子进程和fork服务器进程

  • 响应SIGHUP(终端断开)、SIGINT(Ctrl+C)和SIGTERM(终止请求)信号

handle_timeout
1
2
3
4
5
6
7
8
9
static void handle_timeout(int sig) {
if (child_pid > 0) {
child_timed_out = 1;
kill(child_pid, SIGKILL);
} else if (child_pid == -1 && forksrv_pid > 0) {
child_timed_out = 1;
kill(forksrv_pid, SIGKILL);
}
}
  • 设置child_timed_out标志,表示目标程序执行超时

  • 强制终止超时的子进程

  • 响应SIGALRM信号,用于执行时间限制控制

handle_skipreq
1
2
3
static void handle_skipreq(int sig) {
skip_requested = 1;
}
  • 设置skip_requested标志,允许跳过当前测试用例

  • 响应SIGUSR1信号,提供手动干预模糊测试过程的能力

忽略的信号
  • SIGTSTP(Ctrl+Z):防止AFL被挂起

  • SIGPIPE(管道破裂):防止在写入已关闭的管道时程序终止

check_asan_opts

用于验证内存安全工具的环境变量配置是否符合要求。这个函数主要检查ASAN(AddressSanitizer)和MSAN(MemorySanitizer)的配置选项,确保它们以最适合模糊测试的方式设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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!");
}
}

AFL要求ASAN(AddressSanitizer)配置包含以下选项:

abort_on_error=1

  • 功能:当ASAN检测到内存错误时立即终止程序

  • 重要性:确保AFL能够捕获到崩溃,而不是让程序继续执行

  • 如果缺少:AFL会显示致命错误并终止

symbolize=0

  • 功能:禁用ASAN的符号化错误报告

  • 重要性:防止ASAN生成详细的错误报告,这会干扰AFL的崩溃检测机制

  • 如果缺少:AFL会显示致命错误并终止

对于MSAN(MemorySanitizer),AFL要求:

exit_code=MSAN_ERROR

  • 功能:设置MSAN检测到错误时的退出代码为特定值(MSAN_ERROR)

  • 重要性:允许AFL准确识别由未初始化内存使用导致的崩溃

  • 如果缺少:AFL会显示致命错误并终止

symbolize=0

  • 功能:与ASAN相同,禁用符号化错误报告

  • 重要性:确保错误报告不会干扰AFL的崩溃检测

  • 如果缺少:AFL会显示致命错误并终止

当使用AFL与ASAN/MSAN一起进行模糊测试时,应该设置以下环境变量:

1
2
export ASAN_OPTIONS=abort_on_error=1:symbolize=0:detect_leaks=0
export MSAN_OPTIONS=exit_code=86:symbolize=0

fix_up_sync

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
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;
}
}

主-从模式:

  • 主节点(-M):执行确定性测试,系统地探索输入空间

  • 从节点(-S):专注于随机变异,提高覆盖率和发现率

共享发现:

  • 所有节点共享一个顶级同步目录

  • 每个节点定期检查其他节点的发现

  • 有趣的测试用例会被所有节点采纳

工作分配:

  • 主节点处理计算密集型的确定性阶段

  • 从节点可以更快地进入随机变异阶段

  • 整体提高了模糊测试的效率

save_cmdline(argc, argv);

该函数将命令行参数数组(argc/argv)转换为单个连续的字符串,并将其存储在全局变量orig_cmdline中,以便在程序的其他部分使用。

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
/* Make a copy of the current command line. */

static void save_cmdline(u32 argc, char** argv) {

u32 len = 1, i;
u8* buf;

for (i = 0; i < argc; i++)
len += strlen(argv[i]) + 1;

buf = orig_cmdline = ck_alloc(len);

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

u32 l = strlen(argv[i]);

memcpy(buf, argv[i], l);
buf += l;

if (i != argc - 1) *(buf++) = ' ';

}

*buf = 0;

}

fix_up_banner(argv[optind]);

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
static void fix_up_banner(u8* name) {

if (!use_banner) {

if (sync_id) {

use_banner = sync_id;

} else {

u8* trim = strrchr(name, '/');
if (!trim) use_banner = name; else use_banner = trim + 1;

}

}

if (strlen(use_banner) > 40) {

u8* tmp = ck_alloc(44);
sprintf(tmp, "%.40s...", use_banner);
use_banner = tmp;

}
}

就是afl用来设置横幅(banner)的函数

check_if_tty()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* Check if we're on TTY. */

static void check_if_tty(void) {

struct winsize ws;

if (getenv("AFL_NO_UI")) {
OKF("Disabling the UI because AFL_NO_UI is set.");
not_on_tty = 1;
return;
}

if (ioctl(1, TIOCGWINSZ, &ws)) {

if (errno == ENOTTY) {
OKF("Looks like we're not running on a tty, so I'll be a bit less verbose.");
not_on_tty = 1;
}

return;
}

}

检查是否是在tty即终端下运行的函数

剩下的就挑几个有点意思的分析吧

read_testcases

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
/* 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);
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. */

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);

}

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

if (!access(dfn, F_OK)) passed_det = 1;
ck_free(dfn);

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;

}

读取输入目录中的所有测试用例并将其排队等待测试,此函数在启动时调用。

1
2
3
4
/* Auto-detect non-in-place resumption attempts. */

fn = alloc_printf("%s/queue", in_dir);
if (!access(fn, F_OK)) in_dir = fn; else ck_free(fn);

检查是否存在<in-dir>/queue目录,存在的话设置为新的in_dir,为了支持恢复之前的模糊测试会话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* We use scandir() + alphasort() rather than readdir() because otherwise,
the ordering of test cases would vary somewhat randomly and would be
difficult to control. */

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);

}

使用scandir()函数遍历in_dir,通过alphasort()确保测试用例按字母顺序排序

1
2
3
4
5
6
if (shuffle_queue && nl_cnt > 1) {

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

}

如果启用了shuffle_queue且同时存在多个测试用例,使用shuffle_ptrs函数随机排列测试用例

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

if (!access(dfn, F_OK)) passed_det = 1;
ck_free(dfn);

add_to_queue(fn, st.st_size, passed_det);

}

过滤非常规文件、空文件和README文件;将有效的测试用例添加到队列

load_auto

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
static void load_auto(void) {

u32 i;

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

u8 tmp[MAX_AUTO_EXTRA + 1];
u8* fn = alloc_printf("%s/.state/auto_extras/auto_%06u", in_dir, i);
s32 fd, len;

fd = open(fn, O_RDONLY, 0600);

if (fd < 0) {

if (errno != ENOENT) PFATAL("Unable to open '%s'", fn);
ck_free(fn);
break;

}

/* We read one byte more to cheaply detect tokens that are too
long (and skip them). */

len = read(fd, tmp, MAX_AUTO_EXTRA + 1);

if (len < 0) PFATAL("Unable to read from '%s'", fn);

if (len >= MIN_AUTO_EXTRA && len <= MAX_AUTO_EXTRA)
maybe_add_auto(tmp, len);

close(fd);
ck_free(fn);

}

if (i) OKF("Loaded %u auto-discovered dictionary tokens.", i);
else OKF("No auto-generated dictionary tokens to reuse.");

}

将之前模糊测试会话中发现的有价值的字节序列(token)加入字典中,用于新的模糊测试会话

会发现上述涉及两个概念:token 和 字典

字典里存储着afl-fuzz认定的有效token,存在必要性

例如:png文件的文件头是IHDR,当afl-fuzz变异到I字节后,因为发生了错误,所以走到了一个不同的执行路径,然后继续H D R的变异,会发现与字节I变异走到了同一个执行路径,都发生了同一个错误,那么afl-fuzz会认定IHDR是一个有效的、不可分割的字节单元,将其加入字典后,再次用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
pivot_inputs();

if (extras_dir) load_extras(extras_dir);

if (!timeout_given) find_timeout();

detect_file_args(argv + optind + 1);

if (!out_file) setup_stdio_file();

check_binary(argv[optind]);

start_time = get_cur_time();

if (qemu_mode)
use_argv = get_qemu_argv(argv[0], argv + optind, argc - optind);
else
use_argv = argv + optind;

perform_dry_run(use_argv);

cull_queue();

show_init_stats();

seek_to = find_start_position();

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

pivot_inputs:规范命名,优先使用硬链接而非复制,记录测试用例之间的派生关系

load_extras:可以通过设置-x选项加载任意一个dict文件(字典)或者目录

find_timeout:设置超时时间

detect_file_args&&setup_stdio_file:检测末尾是否有@@

  • 如果有@@,则afl-fuzz会从文件读取,无论是默认的<out_dir>/.cur_input还是用户指定的(-f)。
  • 如果没有@@但有-f,则表明afl会从用户指定的文件读取
  • 如果没有@@且没有-f,则表明从标准输入读取。

check_binary:检查二进制文件的合法性

get_cur_time:获取当前的准确时间

get_qemu_argv:解析qemu的参数

例如:./afl-fuzz -Q -i testcases/input -o findings -- ./target_binary @@运行这个命令

那么在该函数中就会将其解析成:./afl-qemu-trace -- ./target_binary @@