AFLplusplus 源码分析——同步机制
最近遇到 AFL++ 同步 interesting corpus 失败的问题,不得不嗑一嗑 AFL++ 的源码了,该来的躲不掉,虽然 C 代码让我有点头大。
一、同步机制
常规情况下 AFL++ 是以单实例运行的。三个臭皮匠顶个诸葛亮,多开几个 AFL++ 实例一起跑是可行的,AFL++ 和其他的 Fuzz 工具(LibFuzzer、HonggFuzz)一起跑也是可以的。
通过 afl-fuzz -h
可以看到 AFL++ 提供了相关的命令参数:
Other stuff:
-M/-S id - distributed mode (-M sets -Z and disables trimming)
see docs/fuzzing_in_depth.md#c-using-multiple-cores
for effective recommendations for parallel fuzzing.
-F path - sync to a foreign fuzzer queue directory (requires -M, can
be specified up to 32 times)
简而言之,AFL++ 同步相关的几个参数如下:
- -M:指定 AFL++ 主实例。主实例会从 -S 指定的从实例、-F 指定的其他 Fuzz 中同步有趣的测试用例。主实例只有一个,负责管理所有的测试用例。
- -S:指定 AFL++ 从实例。从实例产生的有趣测试用例将被同步到 -M 指定的主实例中,从实例也会从 -M 指定的主实例中同步有趣的测试用例。
- -F:指定其他 Fuzz 实例的 corpus 目录。由其他 Fuzz 产生的有趣的测试用例,将会被同步到 -M 指定的主实例中。
可以看到 主实例位于多实例运行模式的核心地位,它负责汇总和分发 AFL++ 的测试用例。
1 | +---------+ +---------+ |
实际使用时,执行命令参考如下:
1 | afl-fuzz -i inputs -o outputs -M master -F /path/to/foreign_fuzzer1/queue -F /path/to/foreign_fuzzer2/queue -- ./target |
二、源码分析
AFL++ 的源码入口为afl_fuzz.c
的main
函数。AFL++ 从其他 fuzzer 中同步 corpus,是通过 sync_fuzzers
函数来实现的。
在 main
函数中,可以看到对 sync_fuzzers
函数的调用。一路跟踪下来,代码调用关系参考如下时序图:
sequenceDiagram participant main participant sync_fuzzers participant read_foreign_testcases participant update_sync_time participant scandir participant fuzz_run_target participant save_if_interesting participant has_new_bits participant describe_op participant ck_write participant add_to_queue main->>sync_fuzzers: 开始同步 loop 遍历 AFL++ 的 out 目录中的子目录 d_name sync_fuzzers->>update_sync_time: 更新同步时间 sync_fuzzers->>scandir: 读取 d_name/queue 目录中的 corpus 列表 loop 遍历新增的 corpus(通过 corpus 的 id 号来识别) sync_fuzzers->>fuzz_run_target: 用 corpus 作为输入运行一次 target fuzz_run_target-->>sync_fuzzers: 返回 target 运行结果 sync_fuzzers->>save_if_interesting: 检查并保存 corpus save_if_interesting->>has_new_bits: 检查 corpus 是否为 interesting has_new_bits-->>save_if_interesting: 返回检查结果 alt 如果 corpus 是 interesting save_if_interesting->>describe_op: 为 corpus 创建文件名 describe_op-->>save_if_interesting: 返回文件名 save_if_interesting-->>ck_write: 将 interesting 的 corpus 保存到 AFL++ 的 queue 目录中 save_if_interesting->>add_to_queue: 将 interesting 的 corpus 添加到 AFL++ 的队列中 end end end alt 如果指定了其他 Fuzz 的 corpus 目录 sync_fuzzers->>read_foreign_testcases: 开始同步其他 Fuzz 的 corpus loop 遍历其他 Fuzz 的 corpus 目录 read_foreign_testcases->>scandir: 读取其他 Fuzz 的 corpus 列表 loop 遍历其他 Fuzz 的 corpus read_foreign_testcases->>fuzz_run_target: 用 corpus 作为输入运行一次 target fuzz_run_target-->>read_foreign_testcases: 返回 target 运行结果 read_foreign_testcases->>save_if_interesting: 检查 corpus 是否为 interesting save_if_interesting->>has_new_bits: 检查 corpus 是否为 interesting has_new_bits-->>save_if_interesting: 返回检查结果 alt 如果 corpus 是 interesting save_if_interesting->>describe_op: 为 corpus 创建文件名 describe_op-->>save_if_interesting: 返回文件名 save_if_interesting-->>ck_write: 将 interesting 的 corpus 保存到 AFL++ 的 queue 目录中 save_if_interesting->>add_to_queue: 将 interesting 的 corpus 添加到 AFL++ 的队列中 end end end read_foreign_testcases-->>sync_fuzzers: 同步其他 Fuzz 的 corpus 结束 end sync_fuzzers-->>main: 同步完毕
简而言之,主实例同步从实例过程是这样的:
- AFL++ 会检查 out 目录下的文件夹(排除 AFL++ 本身的 default 文件夹)
- 如果文件夹中没有 queue 子目录,转 1
- 如果文件夹中有 queue,则遍历 queue 下的 corpus
- 如果 corpus 是处理过的(根据文件名 id 判断),转 4
- 如果 corpus 是没有处理过的,那就用这个 corpus 作为输入,执行一次 target
- 分析 target 执行结果,判断 corpus 是否为 interesting,如果不是 interesting,转 3
- 如果 corpus 是 interesting,则将 corpus 保存为 out/default/queue 目录下的文件,并将 corpus 添加到 AFL++ 的内存队列中,再转 3
主实例同步其他 Fuzz 的过程与之类似,只是每次同步时,并不会根据 corpus 文件名的 id 编号过滤已经分析过的 corpus,因此从外部 Fuzz 同步 corpus 的性能较差。
三、实践应用
如果某一款工具能生成 corpus,但是希望以从示例的方式被主实例同步,需要保证这款工具产生的 corpus 的命名符合以下要求:
- 以
id:
开头,然后是高位补 0 的六位数字 - 第一个 corpus 的 id 从 0 开始,随后的 corpus 依次以 1 递增
其原因读一遍 sync_fuzzers
的源码就明白了,源码面前,了无秘密。