本文是笔者阅读AFL源码的核心代码部分阅读笔记,主要内容为AFL fuzzer结构和流程等,不包含插桩内容。由于另一部分笔记直接写入代码注释,可能部分内容不是很完善,但笔者自以为将基本所有AFL流程要点记录在笔记中,欢迎各位交流。
AFL-showmap
-
解析各个参数,详见Default说明
-b
隐藏选项,以二进制格式输出结果(类似outdir/queue/fuzz_bitmap
)-t nn+
setstime_out
to 2, tolerate but skip queue entries that time out
-
setup_shm
配置共享内存- 通过环境变量
SHM_ENV_VAR
传递共享内存的地址(十进制数据) atexit
注册共享内存移除函数,将在退出时调用(与fini_array
不是同一个结构,后者是静态写入ELF中,前者是动态注册,但是都在同一套体系中_dl_fini
)
- 通过环境变量
-
setup_signal_handlers
- 通过设置
sigaction
,改写信号捕获处理例程 SIGHUP
SIGINT
SIGTERM
->handle_stop_sig
kill child proc,stop_soon = 1
SIGALRM
->handle_timeout
kill child procchild_timed_out = 1
- 通过设置
-
set_up_environment
ASAN_OPTIONS
MSAN_OPTIONS
- if
AFL_PRELOAD
->LD_PRELOAD
DYLD_INSERT_LIBRARIES
-
find_binary
在 PATH 或当前路径下寻找目标文件- 如果文件路径不含
/
,则被认为是PATH下的文件,会在每一个PATH下搜索 - 当前路径下文件需要加
./
- 如果文件路径不含
-
run_target
Main Routine- Set Memory Barrier
- fork child process
- add memory limit with
setrlimit
- if
-c
option not set, don’t keep core dumps setsid
脱离父进程的进程组、打开的终端、Session IDexecve
execute the target binary- set timer for child proc
classify_counts
预处理trace_bits
,将其分为几个bucket(详见 [Technical Whitepaper](#Coverage measurements))write_results
按照是否为 binary_mode 将trace_bits
输出
AFL-fuzz
-
获取各项参数
-
检查CPU状态(
check_crash_handling
检查核心转储位置等) -
read_testcases
读取测试用例,并将其归入输入pending queue中-
可以通过未清理的文件
out_dir/.state/deterministic_done/testcase_name
,恢复之前完成的确定性测试部分 -
add_to_queue
将 testcase 加入队列q->fname = fname; # origin input testcase name q->len = len; # testcase length q->depth = cur_depth + 1; # queue depth of the testcase q->passed_det = passed_det; # whether passed deterministic fuzzing
- queue 含有
next_100
成员,用于快速跳转(仅有!(index % 100)
含有)
- queue 含有
-
-
load_auto
Load automatically generated extras -
pivot_inputs
Create hard links for input test cases in the output directory-
将输入的testcases映射到输出queue中,并修改为对应标准文件名
-
原输入文件名为
id:%06u,orig:%s
-
-
detect_file_args
将参数中@@
换为输入testcases (out_dir/.cur_input
) -
setup_stdio_file
如果没有设置@@
,则打开新文件out_fd
作为输入 -
check_binary
查找binary,判断是否为shell,判断是否被插桩- 通过查找
__AFL_SHM_ID
(用于forkserver查找shared memory)判断插桩 - 通过查找
__msan_init
判断是否启用Address Sanitizer - 通过查找Persistent Sig (
SIG_AFL_PERSISTENT
)判断是否启用Persistent Mode - 通过查找Defer Sig (
SIG_AFL_DEFERRED_FORKSRV
)判断是否启用Deferred Mode
- 通过查找
-
如果处于qemu mode,则还需要获取qemu参数(参数附加于目标文件名后(–))
-
perform_dry_run
Perform dry run of all test cases to confirm that the app is working as expected-
遍历队列,取出fname,读取文件到内存中
-
calibrate_case
校准testcase- 执行queue中所有testcase,并对对应测试结果进行处理/反馈
- new stage:
calibration
stage_max
: 阶段轮数 3 or 8init_forkserver
- child- 使用两个管道
st_pipe
ctl_pipe
, 传递状态和控制命令- 管道0号为接收,1为发送
- 重定向输出和错误输出到
null
- 如果使用
out_file
,则重定向输入到null
,否则,重定向到out_fd
(见setup_stdio_file
) - set
LD_BIND_NOW
,避免LD_BIND_LAZY
延迟绑定机制降低fork后运行效率 execve
执行目标程序,目标程序会进入fork_server
,完成初始化操作
- 使用两个管道
init_forkserver
- fuzzer- 从
status
读取forkserver状态(hello message),确认forkserver启动
- 从
write_to_testcase
将数据(testcase)由内存写入输入文件,供target使用run_target
同show map (此过程中已完成trace_bit
预处理)- 通过
hash32
计算校验和cksum
,通过校验和判断map是否变化- 如果之前几轮已经计算过
trace_bits
,并且两者的某位不同,则此边为可变边,执行路径不完全与输入相关。并且,延长循环次数,避免遗漏可变边。 - 若第一次执行,则将本次结果更新为
first_trace
- 如果之前几轮已经计算过
- new stage:
- 执行queue中所有testcase,并对对应测试结果进行处理/反馈
-
得到返回值
res
,判断错误类型- 如果没有增加的路径/边,则警告用户,此testcase无用
-
-
has_new_bits
检查当前执行路径是否有新的tuple,更新virgin bits-
采用部分循环展开加速运算
virgin位图会初始化为全1 *current为true且*current & *virgin为true说明当前用例执行到了一些边且在该执行情况在virgin位图中还没有出现过, *virgin &= ~*current;语句将表示当前执行情况的位在virgin位图中标出,下次在出现相同的执行情况时if语句将判断为false,即没有出现新的情况 举个例子,某个边初始为1111 1111,一个用例执行到该边4次,在trace_bits位图中表示为0000 1000。 ~*current为1111 0111,与*virgin进行与操作后为1111 0111,此时*virgin被赋值为1111 0111。 下次再有一个用例执行到该边,执行次数为4,5,6,7时,经过classify_counts函数计数后在trace_bits位图中为0000 1000,与*virgin即1111 0111做与操作为false 因此一个边的某个执行次数被触发过,下一次再有用例触发同一个计数桶中的执行次数时,将不被视为新的情况
-
返回值共有三种情况:
0
: 没有发现任何新边1
: 有已到达边的新触发bucket2
: 含有以前从未到达的边
-
-
如果发现测试中bitmap不同,则出现
variable edge
(不定边,在相同输入下执行情况不同,与随机等有关),标记该bit,并将 -
为后续操作进行提前性能数据统计
-
update_bitmap_score
找到新边后,需要评估当前路径是否为"favorable",即维护一个能触发全部当前总bitmap的最少的边集合,并使开销尽量小- 以上第一个条件并没有被严格执行
- 使用
speed * size
作为fav_factor
,比较选择更小的,作为当前bit的关联path - 后续
cull_queue
对path数量做了缩减,但不是最优解
Main loop
-
cull_queue
see above -
sync_fuzzers
sync between parallel fuzzers -
fuzz_one
主程序,从序列中取出一个testcase (queue_cur
)运行fuzz- 根据
pending_favored
是否有favored testcase正在等待,以及当前状态信息,选择是否跳过当前testcase - 将testcase映射到内存中
- calibration (如果之前的测试失败,就重做)
trime_case
修剪新的testcase,以减少确定性测试时的循环- 使用二分长度,不断缩小删去的长度
- 以删去长度为分隔,将testcase分为几段(不足也算一段),依次遍历删除一段
- 如果删除后对bitmap的checksum没有影响(得到相同checksum),则认为是相同的执行路径,删除有效
- 如果进行过trim,则会标记为
need_write
,在全部trim结束后进行回写,并重新评估分数
calculate_score
计算testcase的性能分数,以执行时间和bitmap大小作为评分因素- 较晚发现的path可以获得
handicap
增益,在初期获得更多运行时间 - 更深的testcase被认为能获得更多发现,所以具有分数增益
- 较晚发现的path可以获得
- 如果已经执行,或者跳过执行确定性测试阶段,则直接跳转到
havoc_stage
,开始非确定性测试 - 每个mutate后(见[下一节](#Mutation Method)),都会进行
common_fuzz_stuff
,运行变异后的testcase,并评估其是否有价值,变异获得的有价值的testcase会入列,等待后续操作
- 根据
Mutation Method
ordered by position in code
前四种过程没有随机性,称为deterministic fuzzing
,只有非dumb mode
,以及main fuzzer使用。
bitflip
,位翻转- 依次翻转每一位;同时翻转连续的2 bit/4 bit;按字节翻转连续的1 byte/2 byte/4 byte
- on 8 bit flap stage,we use effictive map to identify effectless bytes (no effect on exec path even fully flipped),如果一个testcase的90%(
EFF_MAX_PERC
)都是有效的,则认为其全部有效,使其全部进行后续测试,否则将会跳过后续时间更长的deterministic fuzz环节(arithmetic) - 判断语义token:如果翻转一段bytes中的任何一个bit,都会产生同样的结果,则大概率这一段是语义检查的token
- 由经验,在变换最低位时进行检查,效果最好,因为路径不太会产生明显变化
- 检查时,如果发现行为变化,并且前段行为满足extra长度,则尝试加入auto extra(
maybe_add_auto
)- interesting 数据互不相同,extras按照len排序(升序),auto extras 按照use count 降序排列
arithmetic
,算数运算- INC/DEC 对char/short/int依次进行+/-数学运算(在一定范围内)
- 会受到effective map的影响,自动跳过某些字节
- 对大端序和小端序均有计算
interest
,将一些特殊内容写入输入文件中(特殊内容预定义硬编码,与extra不同)- 将每个byte/word/dword替换为interesting关键字
dictionary
,将自动生成或用户提供的token替换/插入到源文件中- 包含over insert两种模式(替换/插入)
- 由于user extra按长度排列,不需要恢复原数据,因为一定会被覆盖
- 会跳过超出最大extra范围的、放不下的、长度内都是effectless的
后两种过程具有随机性,所有fuzzer均会使用:
havoc
“浩劫”,将原文件大量变异- 轮数受到
perf_score
,即testcase执行效率的影响,以及是否完成确定性测试,但通常都很大,在千量级 - 按照随机数随机进行不同的动作(确定性变异+随机参数),通过随机数选择变异操作方式
- 轮数受到
splice
“铰接”,将两个文件拼合为一个文件- 完整一轮执行完后,如果还没有发现,就使用它(来自文档,从逻辑上看,即使有发现,也会运行这一部分)
locate_diffs
比骄傲两个buffer,返回第一个和最后一个不同的字节偏移,用于确定合并位置- 在第一个和最后一个不同的字符之间接入
- 合并后返回havoc
stage_name
中的标号a/b
,表示perform len/step
,即进行操作的位数/迭代步长