fuzzy
afl-as
add_instrumentation()
在实际调用as前进行add instrumentation,也就是插桩。
通过识别如下前导标签后,紧接着进行插桩 (只在.text段进行插桩):
main:- function entry point (always instrumented).L0:- GCC branch label.LBB0_0:- clang branch label (only in clang mode)\tjnz foo- conditional branches
如下为x64、x86桩代码:
1 | static const u8* trampoline_fmt_64 = |
1 | static const u8* trampoline_fmt_32 = |
最后再插入main_payload_32或main_payload_64,实际上就是写入__afl_maybe_log等一系列正式fuzz需要使用的函数和变量的汇编,,之后会详细解读。
main()
设置随机种子值为tv.tv_sec ^ tv.tv_usec ^ getpid(),再设置好真正as的参数。当开启ASAN或MSAN时,插桩密度inst_ratio设置为%33左右,初始为%100。然后插桩,fork子进程用来实现真正的as,等待子进程结束后,unlink掉插桩完的文件,然后退出。
afl-clang-fast
afl-llvm-pass
llvm是用来进行代码优化,提供了IR,通过对IR操作,进行优化。对IR操作其实也就是插入桩代码,只不过afl-as是对汇编进行操作,而llvm是对IR操作。llvm提供模块化、标准化的一系列IR API,将程序分为Module-Function-BasicBlock-Instruction-Operand&Opcode,层次结构清晰,可以直接遍历每个基本块方便地插桩。
插入的IR Instruction效果如下:
1 | cur_location = <COMPILE_TIME_RANDOM>; |
afl-llvm-rt
LLVM_MODE有额外三种模式分别是deffered instrumentation、persistent mode、trace-pc-guard
deferred instrumentation
推迟forkserver的启动,启动时机为程序初始化工作后,但是在main函数之前。这样减少了初始化的开销或者程序解析fuzz data的开销
选好合适的位置,插入下面的代码,即可启用该模式
1 |
|
__AFL_INIT调用__afl_manual_init
1 | void __afl_manual_init(void) { |
__afl_map_shm函数读取环境变量 "__AFL_SHM_ID"来获得shm_id,然后调用shmat获取共享内存赋值给_afl_area_ptr。
__afl_start_forkserver逻辑如下:
- 初始化
child_stopped为0,通过FORKSRV_FD + 1fd向管道写入4个字节,告知afl fuzzfork server已经准备好开始了。 - 进入
fuzz loop- 通过
FORKSRV_FDfd向管道读入4个字节赋值给was_killed - 如果
child_stopped和was_killed都不为空值,说明子进程停下了并且发出SIGKILL信号,那么通过waitpid函数回收子进程 - 如果
child_stopped为空值,那么fork一个子进程,并且关闭子进程与管道的通信,然后子进程退出__afl_start_forkserver函数,执行程序 - 如果
child_stopped不为空值,通过kill(child_pid, SIGCONT)重启子进程,并设置child_stopped为0。该分支仅针对persistent mode,因为只有该模式不fork完成fuzz,而是复用同一个进程。 - 再通过
FORKSRV_FD + 1fd向管道写入child_pid,告知afl fuzz子进程的pid。 - 等待子进程结束,回收子进程,状态保存在
status。如果为persistent mode,那么程序发出SIGSTOP信号,便结束等待,并且将child_stopped设置为1。 - 通过
FORKSRV_FD + 1fd向管道写入status,告知afl fuzz子进程的状态,并结束此次fuzz。
- 通过
persistent mode
当程序的某些API是无状态的或者读入不同的输入数据时,API的状态会自动重置,那么这个程序就可以长期存活复用,减少了fork的开销。
通过向程序插入如下代码:
1 | while (__AFL_LOOP(1000)) { |
__AFL_LOOP调用__afl_persistent_loop,逻辑如下:
- 第一次
loop时,如果is_persistent为1,清空__afl_area_pptr,将__afl_area_ptr[0]设置为1,__afl_prev_loc设置为0,cyclic_cnt设置为max_cnt,也就是循环次数。 - 不是第一次的
loop,--cyclic_cnt,发出信号SIGSTOP,程序暂停,直到程序被重启,将__afl_area_ptr[0]设置为1,__afl_prev_loc设置为0。- 当
cyclic_cnt为0时,也就是结束了所有fuzz时,将__afl_area_ptr指向__afl_area_initial,也就是无用数组,不再记录覆盖率等数据。
- 当
trace-pc-guard
该模式不是通过基本块,而是边来插桩。要使用这个模式需要重新编译LLVM_MODE,通过
1 | AFL_TRACE_PC=1 make clean all |
从而定义了DUSE_TRACE_PC宏,从而在执行afl-clang-fast传入 -fsanitize-coverage=trace-pc-guard参数
1 | ifdef AFL_TRACE_PC |
__sanitizer_cov_trace_pc_guard
这个函数在经过每条edge都会调用
1 | void __sanitizer_cov_trace_pc_guard(uint32_t* guard) { |
通过guard指向的值确定共享内存对应的索引
__sanitizer_cov_trace_pc_guard_init
该函数来初始化guard指向的值
1 | void __sanitizer_cov_trace_pc_guard_init(uint32_t* start, uint32_t* stop) { |
afl-fuzz
setup_shm
- 如果
in_bitmap为空,则将virgin_bits初始化每bit都为1。 - 初始化
virgin_tmout、virgin_crash每bit为1。 - 通过
shmget开辟一块读写共享内存,返回该内存块对应的共享标识符到shm_id。注册atexit handler为remove_shm,用来当程序结束时,删除共享内存块。 - 如果
dumb_mode为空,则设置SHM_ENV_VAR为shm_id。 - 通过
shmat获取对该共享内存的访问,并将共享内存地址赋值给trace_bits
maybe_add_auto
如果设置
MAX_AUTO_EXTRAS或者USE_AUTO_EXTRAS为0,则直接返回for i = 1 to len,如果
mem[0]和mem[i]不同,则跳出循环。如果
i等于len,也就是mem里的字节值都和第一个字节值相等,则直接返回。如果
len等于2,就和interesting_16数组里的元素比较,有相同就直接返回。如果
len等于4,就和interesting_32数组里的元素比较,有相同就直接返回。for
i= 0 toextra_cnt,如果extras[i].len≥len就跳出循环,因为extras数组按照长度排序。继续遍历
i,如果i<extras_cnt并且extras[i].len==len,用memcmp_nocase(extras[i].data, mem, len),如果这两个内容是一样的,则直接返回。auto_changed置为1。for
i= 0 toa_extras_cnt- 如果
a_extras[i].len等于len并且a_extras[i].data和mem内容相同a_extras[i].hit_cnt++,跳转到sort_a_extras
- 如果
如果
a_extras_cnt小于MAX_AUTO_EXTRAS,说明a_extras数组没被填满,则用mem、len构造新的元素放入这个数组。否则,从
a_extras数组后半段随机挑选个元素,将它的data替换为ck_memdup(mem, len),长度替换为len,hit_cnt置为0。sort_a_extras- 先用
qsort(a_extras, a_extras_cnt, sizeof(struct extra_data), compare_extras_use_d);,按照hit_cnt递减排序。 qsort(a_extras, MIN(USE_AUTO_EXTRAS, a_extras_cnt),sizeof(struct extra_data), compare_extras_len);,按照len递增排序。
- 先用
perform_dry_run
执行所有测试用例,检查是否正常工作
- 打开
q->fname,并读取内容到use_mem res = calibrate_case(argv, q, use_mem, 0, 1)校准测试用例- 如果
res为crash_mode或FAULT_NOBITS,则输出SAYF(cGRA " len = %u, map size = %u, execspeed = %llu us\n" cRST, q->len, q->bitmap_size, q->exec_us); - 根据
res判断错误结果:FAULT_NONE:- 如果
q是第一个测试用例,则调用check_map_coverage,评估map coverage:- 如果
trace_bits路径数小于100,则直接返回 trace_bits数组中后半段有值,则直接返回
- 如果
- 如果
FAULT_TMOUT:- 如果指定了
-t参数,则timeout_given参数设置为2,则警告Test case results in a timeout (skipping),q->cal_failed设置为CAL_CHANCES,cal_failures计数++。
- 如果指定了
FAULT_CRASH:- 抛出异常
FATAL("Test case '%s' results in a crash", fn);。
- 抛出异常
FAULT_ERROR:- 抛出异常
FATAL("Unable to execute target application ('%s')", argv[0]);。
- 抛出异常
FAULT_NOINST:- 抛出异常
FATAL("No instrumentation detected");。
- 抛出异常
FAULT_NOBITS:- 没有出现路径信息,说明这个测试样例是无用的。
useless_at_start++。
- 没有出现路径信息,说明这个测试样例是无用的。
- 如果
q->var_behavior不为空,则说明该测试用例在相同条件下,多次执行,有不同的路径记录,抛出警告WARNF("Instrumentation output varies across runs."); q = q->next,执行下个测试样例。
calibrate_case
如果
q->exec_cksum == 0说明这个case是第一次运行,设置first_run为1。q->cal-failed++设置
stage_name为calibration,并且如果fast_cal不为0,则设置stage_max为3否则为8。stage_max是最大校验次数。当
dumb_mode != 1 && !no_forkserver && !forksrv_pid,调用init_forserver(argv)。也就是不处于dumb_mode并且no_forkserver为空,还未启动forkserver,就初始化forkserver。如果这个
queue_entry不来自input文件夹,而是评估新case,则此时q->exec_cksum不为0,拷贝trace_bits到first_trace,并计算has_new_bits,如果这个值比new_bits大,则更新new_bits为这个值。执行
calibrate stage,共stage_max轮- 如果不是第一次执行,就调用
show_status。 - 调用
run_target(argv, use_tmout)将结果赋值给fault。 - 如果
stop_soon不为空或者fault != crash_mode,就跳到abort_calibration标签处。 - 如果不处于
dumb_mode并且处于第一次循环,并且count_bytes(trace_bits)为0,也就是没有路径记录,则设置fault为FAULT_NOINST,跳到abort_calibration标签处。 - 根据当前的
trace_bits通过hash32(trace_bits, MAP_SIZE, HASH_CONST)函数计算赋值给cksum。 - 如果
q->exec_cksum != cksum说明这个case第一次执行或者这次执行和上次执行的路径不同,则调用has_new_bits(virgin_bits)计算,计算的值如果大于new_bits,则更新new_bits。- 如果
q->exec_cksum不为0,说明不是第一次执行,说明执行触发的路径可变,则遍历整个shm,如果!var_bytes[i] && first_trace[i] != trace_bits[i],则说明当前索引的路径可变,设置var_bytes[i]为1,将stage_max更新为40。将var_detected设置为1。 - 否则就是第一次执行,将
q->exec_cksum设置为cksum,将trace_bits当前的路径记录拷贝给first_trace。
- 如果
- 如果不是第一次执行,就调用
将执行
calibrate stage的总时间加到total_cal_us。将执行总次数
stage_max加到total_cal_cycles。q->exec_us更新为执行一次的平均时间。q->bitmap_size更新为覆盖的路径数。q->cal_failed设置为0。total_bitmap_size加上q->bitmap_size。total_bitmap_entries++。update_bitmap_score(q)如果
!dumb_mode && first_run && !fault && !new_bits,则设置fault为FAULT_NOBITS。abort_calibration:- 如果
new_bits == 2 && !q->has_new_cov,说明当前queue发现了新路径,设置q->has_new_cov为1,并且queue_with_cov++。 - 如果
var_detected不为0,var_byte_count赋值为count_bytes(var_bytes),如果q->var_behavior为0,调用mark_as_variable(q),将当前的queue和out_dir/queue/.state/variable_behavior/filename创建符号链接,并且设置q->var_behavior为1。queued_variable++。
- 如果
返回
fault。
init_forkserver
通过
pipe函数创建st_pipe状态管道、ctl_pipe控制管道。fork进程赋值给forksrv_pid当前进程为子进程
重定向标准输出、标准错误到
dev_null_ld。如果设置了out_file则重定向标准输入到dev_null_fd,否则重定向到out_fd,并且关闭out_fd。重定向
FORKSRV_FD到ctl_pipe[0],即通过FORKSRV_FD来读取控制管道。重定向
FORKSRV_FD + 1到st_pipe[1],即通过FORKSRV_FD + 1来写入状态管道。关闭不必要的文件描述符。
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));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
- 如果`LD_BIND_LAZY`环境变量为空,则设置`LD_BIND_NOW`环境变量为1。
- 设置`ASAN_OPTIONS`环境变量为`abort_on_error=1:` `detect_leaks=0:` `symbolize=0:` `allocator_may_return_null=1`,同理设置`MSAN_OPTIONS`。
- 调用`execv(target_path, argv)`,替换进程空间,执行`target_path`。
- 如果调用失败,则将`*(u32 *)tarce_bits`设置为`0xfee1dead`,并退出。
- 当前进程为父进程
- 关闭`ctl_pipe[0]` `st_pipe[1]`。
- 设置`fsrv_ctl_fd`为`ctl_pipe[1]`,设置`fsrv_st_fd`为`st_pipe[0]`。
- 等待`forkserver`启动,从`fsrv_st_fd`读取4个字节到`status`。
- 如果读取长度为4个字节,说明启动成功,返回。
## has_new_bits
- 将`trace_bits`的首`u64`元素地址赋值给`current`,`virgin_map`的首`u64`地址赋值给`virgin`。
- 将`i`赋值为`MAP_SIZE >> 3`,因为**每8个字节遍历`MAP`**。
- 进入while循环遍历map
- 如果`*current`不为0且`*current & *virgin`不为0,说明发现覆盖了新路径或者某条路径的执行次数发生了变化
- 如果`ret`小于2
- `cur`赋值为`(u8 *)current`,`vir`赋值为`(u8 *)virgin`。
- 在这8个字节中,如果有一个字节`*cur`不为0且`*vir == 0xff`,即发现覆盖了新路径,设置`ret`为2,否则为1。
- `*virgin &= ~*current`
- 如果`ret`不为0且`virgin_map == virgin_bits`,那么`bitmap_changed`设置为1。
- 返回`ret`。
## count_bytes
- 设置`ptr = (u32 *)mem`。
- `i == (MAP_SIZE >> 2)`,`ret = 0`。
- 进入while循环,每四个字节遍历
- `v = *(ptr)`
- 如果`v`为0,即当前4个字节没有路径覆盖,直接`continue`。
- 否则每个字节如果不为0,都会`ret++`。
- 返回`ret`。
## run_target
- 初始化`trace_bits`,全部置为0。
- 如果`dumb_mode == 1`或者`no_forkserver`不为0
- `fork`进程赋值给`child_pid`。
- 当前进程为子进程:
- 重定向标准输出、标准错误到`dev_null_fd`。
- 如果`out_file`不为空,则重定向标准输入到`dev_null_fd`。否则,重定向标准输入到`out_fd`,关闭`out_fd`。
- 关闭不需要的`fd`:
- ```c++
close(dev_null_fd);
close(out_dir_fd);
close(dev_urandom_fd);
close(fileno(plot_file));设置
ASAN_OPTIONS、MSAN_OPTIONS。execv替换进程空间,执行target_path。替换失败,则
*trace_bits = 0xfee1dead,exit(0)退出。
否则就是处于有
forkserver的模式,且forkserver已经初始化完毕。通过
fsrv_ctl_fd向控制管道写入prev_timed_out4个字节,表示forkserver可以开始fork子进程进行fuzz。通过
fsrv_st_fd向状态管道读入4个字节,赋值到child_pid,表示forkserverfork出来的子进程pid。如果
dumb_mode == 1或者no_forkserver不为0- 则
waitpid等待子进程结束,状态变量保存在status。
- 则
否则,通过
fsrv_st_fd向状态管道读取4个字节,赋值到status,表示子进程执行完最后的状态。计算
exec_ms,即target执行时间,并将total_execs++。将
trace_bits首4个字节的值赋值给tb4。调用
classify_counts((u64 *)trace_bits)。child_timed_out赋值给prev_timed_out。如果
WIFSIGNALED(status)不为0且!stop_soon,也就是target执行完返回的是异常状态- 通过
WTERMSIG(status)获得异常状态信号值,赋值给kill_signal。 - 如果
child_timed_out不为0且kill_signal等于SIGKILL,说明target执行超时了,返回FAULT_TMOUT。 - 否则返回
FAULT_CRASH。
- 通过
如果
uses_asan不为0且WEXITSTATUS(status) == MSAN_ERROR,也就是target退出状态是MSAN_ERROR,返回FAULT_CRASH。如果(
dumb_mode为1或者no_forkserver不为0)且tb4等于0xfee1dead,也就是execv调用失败,返回FAULT_ERROR。如果
exec_tmout大于等于timeout且exec_ms大于slowest_exec_ms,将slowest_exec_ms赋值给exec_ms。返回
FAULT_NONE。
classify_counts
i赋值为MAP_SIZE >> 3,即8个字节遍历。进入while循环遍历
如果
*mem不为0:mem16 = (u16 *)mem,每2个字节赋值。每2个字节通过
count_class_lookup16[*mem16]取到对应的值。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]; }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- ```c++
static 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
};count_class_lookup16是为加快分类速度,count_class_lookup8是使不同的执行次数更有区分度、稀疏化,也减小了微小执行次数变化的干扰。
update_bitmap_score
update top_rated[]
当我们碰到新路径时,我们调用update_bitmap_score来看看这个路径是否比其他已有的更favorable,这个favorable目的在于能够有能触发bitmap所有bit的最小路径集合,专注于fuzz这些路径,而忽略其他的。
用
q->exec_us * q->len赋值给fav_factor,即将当前queue的执行时间和queue的长度的乘积作为fav因子,赋值给fav_factor。按字节for循环遍历
map- 如果当前字节
trace_bits不为0- 如果当前字节
top_rated不为0- 如果
fav_factor大于当前字节top_rated的fav因子,则直接continue --top_rated[i]->tc_ref,减少当前字节top_rated的引用计数,如果减到0时,则释放掉top_rated[i]->trace_mini,将top_rated[i]->trace_mini置为0,即丢弃它记录的trace_mini。
- 如果
- 将
top_rated[i]赋值为当前queue,q->tc_ref++。 - 如果
q->trace_mini为空- 则申请
MAP >> 3的空间,将地址赋值给q->trace_mini,调用minimize_bits(q->trace_mini, trace_bits)。
- 则申请
score_changed置为1。
- 如果当前字节
- 如果当前字节
minimize_bits
将trace_bits压缩到更小的bitmap,这个bitmap的大小是MAPSIZE >> 3,这两个bitmap都是按照字节操作的,所以要把原来8个字节压缩到1个字节,且抛弃掉计数信息。
1 | static void minimize_bits(u8 *dst, u8 *src) |
原来map字节保存的索引通过>> 3实现,因为8个字节要映射到1个字节,原来的 路径存在信息 保存在字节里,现在保存在对应的bit上,通过|= 1 << (i & 7)实现
cull_queue
从top_rated[]选出favored。
- 如果
dumb_mode不为0或者score_changed等于0,则直接返回。 score_changed赋值为0。- 初始化
temp_v每bit上都为1,temp_v为压缩bitmap。 - 初始化
queued_favored和pending_favored为0,遍历queue中每个entry,将favored都置为0。 - 按字节for循环遍历
map- 如果当前字节
top_rated不为0且temp_v对应的索引上的值不为0- 则遍历
top_rated[i]->trace_mini每个字节,通过temp_v[j] &= ~top_rated[i]->trace_mini[j],清除掉temp_v对应的路径信息。 top_rated[i]->favored置为1,标记为favored。queued_favored++。- 如果
top_rated[i]->was_fuzzed为空,也就是entry还没被fuzz过,pending_favored++。
- 则遍历
- 如果当前字节
- 遍历
queue的每个entry,调用mark_as_redundant(q, !q->favored),标记是否是redundant。
mark_as_redundant
- 如果
state等于q->fs_redundant,则直接返回。 q->fs_redundant赋值为state。- 如果
state等于1- 则创建
out_file/queue/.state/redundant_edges/fname。
- 则创建
- 否则
- 删除
out_file/queue/.state/redundant_edges/fname。
- 删除
fuzz执行
主循环
精简队列
cull_queue如果
queue_cur为空,即遍历完队列所有的case或者还未进行过fuzzqueue_cycle++,current_entry置为0,cur_skipped_paths置为0,queue_cur重新置为queue,代表要开始新的一轮fuzz。- 如果
seek_to不为空,就从seek_to指定的case开始fuzz。 show_stats刷新展示界面。- 如果
queued_paths等于prev_queued,也就是完成一次完整fuzz后,都没发现新的路径。- 如果
use_splicing不为0,则cycles_wo_finds++,否则设置use_spicing为1。
- 如果
- 否则,
cycles_wo_finds置为0。 - 更新
prev_queued为queued_paths。 - 如果
sync_id不为0且queue_cycle等于1且环境变量AFL_IMPORT_FIRST不为空,则调用sync_fuzzers(use_argv)。
调用
fuzz_one()对queue_cur进行一次fuzz,将结果返回给skipped_fuzz。如果
stop_soon为0且sync_id不为空且skipped_fuzz为0sync_interval_cnt++,当它为5的倍数时,sync_fuzzers(use_argv)同步fuzzer。
queue_cur = queue_cur->next; current_entry++,开始fuzz下一个case。
fuzz_one
- 如果
pending_favored不为0- 如果
queue_cur->was_fuzzed不为0或者queue_cur->favored为0,即当前的case已经被fuzz过或者它不是favored,就有99%的概率直接返回1。
- 如果
- 否则,如果不处于
dumb_mode且queue_cur不是favored且queued_paths超过10,即队列里的case数超过10- 如果
queue_cycle大于1且queue_cur没有被fuzz过,就有75%的概率直接返回1。否则被fuzz过,就有95%概率直接返回1。
- 如果
- 打开该case对应的文件,将内容通过
mmap映射到内存,将地址返回给orig_in和in_buf。 - 申请
queue_cur->len大小的内存,将地址返回给out_buf。 cur_depth赋值为queue_cur->depth。
CALIBRATION
- 如果当前
queue_cur出现过校验错误- 赋值
res为FAULT_TMOUT。 - 如果
queue_cur->cal_failed小于3queue_cur->exec_cksum置为0,调用calibrate_case(argv, queue_cur, in_buf, queue_cycle - 1, 0)返回值赋给res。- 如果
res等于FAULT_ERROR,则抛出错误FATAL("Unable to execute target application")。
- 赋值
TRIMMING
- 如果不处于
dumb_mode且queue_cur->trim_done为0- 调用
trim_case(argv, queue_cur, in_buf)返回给res。 queue_cur->trim_done置为1。- 修正
len赋值为queue_cur->len。 - 将
in_buf内容拷贝给out_buf。
- 调用
PERFORMANCE SCORE
- 调用
calculate_score(queue_cur)结果返回给perf_score和orig_pref。 - 如果
skip_deterministic不为0或者queue_cur被fuzz过或者queue_cur->passed_det不为0,就跳转到havoc_stage。 - 如果
master_max不为0并且queue_cur->exec_cksum % master_max不等于master_id,则跳转到havoc_stage。 doing_det置为1。
SIMPLE BITFLIP (+dictionary construction)
1 |
stage_short赋值为flip1,stage_max赋值为len << 3,stage_name赋值为bitflip 1/1。orig_hit_cnt赋值为queued_paths + unique_crashes。prev_cksum赋值为queue_cur->exec_cksum。- for循环
stage_cur = 0; stage_cur < stage_max; stage_cur++- 总共次数为
len << 3,其实就是每次循环对某个字节的某个bit都进行翻转,循环结束刚好对每个字节的每个bit进行翻转。 stage_cur_byte赋值为stage_cur >> 3,即当前翻转的字节索引值。FLIP_BIT(out_buf, stage_cur),进行bit翻转。- 调用
common_fuzz_stuff(argv, out_buf, len),如果返回值不为0,则跳转到abandon_entry。 FLIP_BIT(out_buf, stage_cur),再翻转回来。- 如果不处于
dumb_mode并且(stage_cur & 7) == 7,即刚好完成一个字节的翻转- 调用
hash32(trace_bits, MAP_SIZE, HASH_CONST)将结果赋值给cksum - 如果
stage_cur等于stage_max - 1且cksum等于prev_cksum- 如果
a_len小于MAX_AUTO_EXTRA,则a_collect[a_len]赋值为out_buf[stage_cur >> 3]。 a_len++- 如果
a_len在MIN_AUTO_EXTRA和MAX_AUTO_EXTRA之间,就调用maybe_add_auto(a_collect, a_len)。
- 如果
- 否则,如果
cksum不等于prev_cksum- 如果
a_len大于等于MIN_AUTO_EXTRA且小于等于MAX_AUTO_EXTRA,则调用maybe_add_auto(a_collect, a_len); a_len置为0。prev_cksum赋值为cksum。
- 如果
- 如果
cksum不等于queue_cur->exec_cksum - 如果
a_len小于MAX_AUTO_EXTRAa_collect[a_len]赋值为out_buf[stage_cur >> 3]。
a_len++。
- 调用
- 总共次数为
new_hit_cnt赋值为queued_paths + unique_crashes。stage_finds[STAGE_FLIP1]加上new_hit_cnt - orig_hit_cnt,即新发现的路径或者crash。stage_cycles[STAGE_FLIP1]加上stage_max。- 设置
stage_name为bitflip 2/1,和上述过程同理,但是现在一次循环翻转2个bit,步长为1个bit。 - 同理
bitflip 4/1。 - 生成 effector map
1 |
- 申请
EFF_ALEN(len)大小的内存,将地址赋值给eff_map,eff_map也是压缩存储的bitmap,总是标记第一字节和最后一字节的值为1。
1 | eff_map[0] = 1; |
bitflip 8/8阶段,通过每个字节异或0xFF进行翻转,调用common_fuzz_stuff(argv, out_buf, len)。- 如果
eff_map[EFF_APOS(stage_cur)]为0- 如果不处于
dumb_mode或者len大于等于EFF_MIN_LENcksum通过hash32(trace_bits, MAP_SIZE, HASH_CONST)计算,否则,直接按照~queue_cur->exec_cksum计算。- 如果
cksum不等于queue_cur->exec_cksum,则将eff_map[EFF_APOS(stage_cur)]置为1,并eff_cnt++,即当case的长度够长时,才会通过翻转字节后会不会改变覆盖的路径,来判断需不需标记这个字节是meatdata,将这个字节对应的位置的eff_map标记为1。也就是这个字节是关键字时,翻转它才会改变路径,否则它是plain data的话,翻转它不会造成路径变化。在后续的bitflip中,会跳过eff_map对应位置为0的字节。但是当case的长度太短了,则会直接标记为1。 - 再异或
0xFF翻转回来。
- 如果不处于
- 如果
如果
eff_cnt不等于EFF_ALEN(len)并且eff_cnt * 100 / EFF_ALEN(len)大于EFF_MAX_PERC- 则直接把
eff_map所有字节标记为1,即认为所有字节都是有必要进行之后的fuzz。block_eff_select加上EFF_ALEN(len)。
- 则直接把
否则
block_eff_select加上eff_cnt。block_eff_total加上EFF_ALEN(len)。bitflip 16/8同理,但是eff_map[EFF_APOS(i)]和eff_map[EFF_APOS(i + 1)]都为0,就会stage_max--并跳过此次字节翻转。bitfilp 32/8同理,但是是四个字节对应的eff_map值都为0,跳过此次字节翻转。
ARITHMETIC INC/DEC
arith 8/8、arith 16/8、arith 32/8总过这三个小阶段,和bitflip原理类似,只不过变成加上或减去值,由于ARITH_MAX的值是35,所以变化的范围是[-35, 35],同时也会考虑大小端序。- 通过
eff_map和加上或减去某些值判断是不是和bitflip一样,来跳过某些mutate。
INTERESTING VALUES
interest 8/8、interest 16/8、interest 32/8,对对应长度的值替换成interesting value。
1 |
DICTIONARY STUFF
user extras (over):在对应的偏移处覆盖成extras。- 如果
extras_cnt > MAX_DET_EXTRAS并且UR(extras_cnt) >= MAX_DET_EXTRAS并且剩下的长度小于extras[j].len或者原来的值和要插入的extras一样或者eff_map对应位置值为0,跳过此次mutate。
- 如果
user extras (insert):在对应偏移处插入extras。auto extras (over):和第一小阶段类似,但是使用的是自动生成的extras。此时完成了所有的
deterministic mutate,如果queue_cur->passed_det为空,则调用mark_as_det_done(queue_cur)标记。
RANDOM HAVOC
- 总的来说,就是在随机次数下采取随机种
mutate组合,并且mutate的执行也会是随机的。如果在这个阶段发现了新路径,执行次数也会增加。
SPLICING
- 只有在遍历整个
queue都没有新发现的情况下才会启用splicing。 - 随机选取一个
queue_entry,寻找合适的位置和当前case拼接,然后跳转到havoc stage。
trim_case
- 如果
q->len小于5,则直接返回 0。 bytes_trim_in加上q->len。len_p2赋值为next_p2(q->len),也就是赋值为第一个大于等于len的2的幂数。remove_len赋值为MAX(len_p2 / TRIM_START_STEPS, TRIM_MIN_BYTES)。while(remove_len >= MAX(len_p2 / TRIM_END_STEPS, TRIM_MIN_BYTES))remove_pos赋值为remove_len。stage_cur置为0,stage_max赋值为q->len / remove_len。while(remove_pos < q->len)trim_avail赋值为MIN(remove_len, q->len - remove_pos)。- 调用
write_with_gap(in_buf, q->len, remove_pos, trim_avail)将out_file从remove_pos处剪掉trim_avail的长度。 - 调用
run_target(argv, exec_tmout),将结果赋值给fault。 trim_execs++。- 如果
stop_soon不为0或者fault等于FAULT_ERROR。跳转到abort_trimming。 cksum赋值为hash32(trace_bits, MAP_SIZE, HASH_CONST)。- 如果
cksum等于q->exec_cksum,即剪掉这部分对case没有影响move_tail赋值为q->len - remove_pos - trim_avail。q->len减掉trim_avail。len_p2赋值为next_p2(q->len)。memmove(in_buf + remove_pos, in_buf + remove_pos + trim_avail, move_tail);,实现trim- 如果
needs_write为0needs_write置为1。- 将
trace_bits拷贝到clean_trace。
- 否则,
remove_pos加上remove_len。 show_status(),并且stage_cur++。
remove右移一位。
- 如果
needs_write不为0unlink(q->fname),打开新的q->fname,将文件描述符赋值给fd。- 将
trimmed in_buf写进q->fname。 - 将
clean_trace拷贝给trace_bits。 - 调用
update_bitmap_score(q)。
abort_trimming标签bytes_trim_out加上q->len。- 返回
fault。
sync_fuzzers
读取其他fuzzer的queue,保存到自己的queue
- 通过
opendir(sync_dir)打开sync_dir,将返回值给sd。 - while循环读取该目录下的文件
(sd_ent = readdir(sd))- 跳过
.开头的文件和自己的输出文件夹。 - 通过
opendir("sync_dir/sd_ent->d_name/queue")打开queue目录,将返回值给qd,跳过没有queue子目录的目录。 - 打开
out_dir/.synced/sd_ent->d_name将返回值给id_fd。 - 从
id_fd读取四个字节赋值给min_accept。minaccept赋值给next_min_accept。min_accept是上次最后一个读取的case的下一个case的id。 - while循环读取
queue下的case(qd_ent = readdir(qd))- 跳过
.目录和id比min_accept小的case,将读取的id赋值给syncing_case。 - 如果
syncing_case大于等于next_min_accept,则next_min_accept等于syncing_case + 1。 - 打开
dq_path/qd_ent->d_name,即打开这个case,将返回值给fd。 - 通过
fd将内容映射到mem。 - 通过
write_to_testcase(mem, st.st_size),将case的内容写入到out_file。 run_target(argv, exec_tmout)将返回值赋给fault。queued_imported加上save_if_interesting(argv, mem, st.st_size, fault)。
- 跳过
- 通过
id_fd写入next_min_accept。
- 跳过
common_fuzz_stuff
写入mutated case,run_target()执行,通过save_if_interesting()保存interesting case。
write_to_case(out_buf, len),将mutate过的case写入out_file。run_target(out_buf, len),将返回值给fault。- 如果
fault等于FAULT_TMOUT- 如果
subseq_tmouts++大于TMOUT_LIMITcur_skipped_paths++。- 返回1。
- 如果
- 否则
subseq_tmouts等于0。 queued_discovered加上save_if_interesting(argv, out_buf, len, fault)。show_stats()。- 返回0。
save_if_interesting
- 如果
fault等于crash_mode- 调用
has_new_bits(virgin_bits)返回值给hnb,如果为0- 如果
crash_mode不为0total_crashes++。- 返回0。
- 如果
fn赋值为alloc_printf("%s/queue/id:%06u,%s", out_dir, queued_paths,describe_op(hnb));add_to_queue(fn, len, 0);添加到queue。- 如果
hnb等于2,即覆盖了新路径queue_top->has_new_cov赋值为1。queued_with_cov++。
queue_top->exec_cksum赋值为hash32(trace_bits, MAP_SIZE, HASH_CONST)。- 调用
calibrate_case(argv, queue_top, mem, queue_cycle - 1, 0),将结果给res。 - 如果
res等于FAULT_ERROR抛出异常退出。 - 打开case
open(fn),将结果赋值给fd。 - 通过
fd写入mem,ck_write(fd, mem, len, fn);。 keeping赋值为1。
- 调用
Switch(fault)FAULT_TMOUT:total_tmouts++- 如果
unique_hangs大于等于KEEP_UNIQUE_HANG,返回keeping。 - 如果不处于
dumb_mode- 调用
simplify_trace((u64 *)trace_bits) - 如果
has_new_bits(virgin_tmout)为0,返回keeping。
- 调用
unique_tmouts++。- 如果
exec_tmout小于hang_tmoutwrite_to_testcase写入到out_file。- 再次调用
run_target(argv, hang_tmout),将结果赋值给new_fault。 - 如果
stop_soon为0且new_fault等于FAULT_CRASH,跳转到keep_as_crash。 - 如果
stop_soon不为0或者new_fault不等于FAULT_TMOUT,返回keeping。 fn赋值为alloc_printf("%s/hangs/id:%06llu,%s", out_dir,unique_hangs, describe_op(0));unique_hangs++。last_hang_time等于get_cur_time。
FAULT_CRASH(keep_as_crash标签):total_crashes++- 如果
unique_crashes大于等于KEEP_UNIQUE_CRASH,则直接返回keeping。 - 如果不处于
dumb_modesimplify_trace((u64 *)trace_bits)- 如果
has_new_bits(virgin_crash)为0,则直接返回keeping。 fn赋值为fn = alloc_printf("%s/crashes/id:%06llu,sig:%02u,%s", out_dir,unique_crashes, kill_signal, describe_op(0));unique_crashes++。last_crash_time赋值为get_cur_time()。last_crash_execs赋值为total_execs。
FAULT_ERROR:- 抛出异常退出。
default:- 返回
keeping
- 返回
open(fn),将mem写入。- 返回
keeping。
simplify_trace
1 | static const u8 simplify_lookup[256] = { |
i赋值为MAP_SIZE >> 3。while(i--),每8个字节遍历- 如果
mem不为0u8 *mem8 = (u8 *)mem,遍历每个字节,只要不为0,就会置为128,否则置为1。
- 否则,直接置为
0x0101010101010101ULL,也就是字节值为1代表没有路径覆盖,值为128代表有。
- 如果
calculate_score
计算perf_score用来判断后续havoc持续的时间
total_cal_us / total_cal_cycles计算平均执行时间avg_exec_us。total_bitmap_size / total_bitmap_entries计算平均bitmap大小avg_bitmap_size。handicap值越大,说明这个case对应的新路径越难发现,越有mutate的价值depth值越大,和handicap类似。
1 | if (q->exec_us * 0.1 > avg_exec_us) |
插桩代码
关键变量
__afl_area_ptr:存储共享内存的地址。__afl_prev_loc:上一个基本块的id,即上一次R(MAP_SIZE)生成的随机数。__afl_fork_pid:fork进程的pid。__afl_temp:存储临时变量。
__afl_maybe_log
如果__afl_area_ptr为0
- 如果
__afl_setup_failure不为0,则直接返回。 - 如果
_afl_global_area_ptr不为0,则直接__afl_area_ptr赋值为__afl_global_area_ptr。 - 否则读取环境变量
__AFL_SHM_ID,再通过shmat获取共享内存的地址,如果获取失败则__afl_setup_failure++,直接返回。 - 将共享内存地址给
__afl_area_ptr和__afl_global_area_ptr。 - 向状态管道写入4个字节,表示
forkserver启动成功。如果写入4个字节成功,进入while循环- 从控制管道读取4个字节到
__afl_temp,如果读取失败,则退出。 fork进程,如果为子进程则跳转到__afl_fork_resume- 如果为父进程,则将子进程
pid保存到__afl_fork_pid,再通过状态管道写入__afl_fork_pid。 waitpid等待子进程结束,将子进程结束状态保存到__afl_temp。
- 从控制管道读取4个字节到
__afl_fork_resume标签:- 关闭状态管道的写入和控制管道的读取
- 如下代码为信息记录
1 | cur_location = <COMPILE_TIME_RANDOM>; |
完整代码
1 | char __fastcall _afl_maybe_log(__int64 a1, __int64 a2, __int64 a3, __int64 rnd) |