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 + 1
fd向管道写入4个字节,告知afl fuzz
fork server
已经准备好开始了。 - 进入
fuzz loop
- 通过
FORKSRV_FD
fd向管道读入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 + 1
fd向管道写入child_pid
,告知afl fuzz
子进程的pid。 - 等待子进程结束,回收子进程,状态保存在
status
。如果为persistent mode
,那么程序发出SIGSTOP
信号,便结束等待,并且将child_stopped
设置为1。 - 通过
FORKSRV_FD + 1
fd向管道写入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_out
4个字节,表示forkserver
可以开始fork
子进程进行fuzz
。通过
fsrv_st_fd
向状态管道读入4个字节,赋值到child_pid
,表示forkserver
fork
出来的子进程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_EXTRA
a_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_LEN
cksum
通过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_LIMIT
cur_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_tmout
write_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_mode
simplify_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) |