AFL++ 代码详解 – overview
AFL++ 是一个开源的模糊测试工具,基于Google的AFL,改进了性能,并新增了一些特性,是当前 SOTA(state-of-the-art) 的模糊测试工具之一。
本篇为 AFL++ README 的翻译,为了了解并学习 AFL++ 的代码实现,本人将开启专栏持续研读 AFL++ 的代码,并整理为笔记。
本人学识有限,如有错漏不准确之处,欢迎讨论交流。
目录
文章目录
概览
AFL++ Fuzz 过程可以分为 4 个阶段:插桩阶段,准备阶段,执行阶段、验证阶段。对应图中 4 个阶段 (该图来自源代码仓库) 。
实线框是必经步骤,虚线框是可选步骤。
-
插桩阶段:
- 选择编译器
- 选择编译时选项
- 选择 Sanitizer (如果你不了解什么是 Sanitizer,没关系后续会详细说明,简单来说就是可以检测出特定类型漏洞的插桩代码)
- 修改目标程序
- 编译目标源码
- 撰写编译 harness (harness的含义是测试用具,即为了能够测试目标需要额外使用的部分,例如:需要测试某些函数时,必须撰写程序调用这些函数才能测试,撰写的程序就是harness)
-
准备阶段
- 收集输入
- 输入集去重
- 最小化输入文件
-
执行阶段
- 执行
afl-fuzz
- 使用多核心执行
- 使用多机器执行
- 执行
-
验证阶段
- 监控程序状态
- 检查覆盖率
- 触发 crash
插桩阶段
插桩阶段涉及的内容比较多,因为灰盒模糊测试多数还是依靠对程序的简易插桩来获取一些编译时和运行时的信息,指导模糊测试的执行。但是如果插桩变得过于复杂,那么速度会变得很慢,Fuzz 快速这个优势就不复存在了。
插桩阶段各个步骤展开如图所示
1. 选择编译器
AFL++ 支持多种编译器,以及对应编译器特殊的编译模式。
-
LTO 模式
LTO 全称为 Link Time Optimization 链接时优化。其实 GCC 和 LLVM 都支持 LTO,但是想要插桩那只能采用 LLVM 11 版本以上。
// TODO: 暂未使用过,后续再补充 -
LLVM 模式
LLVM 架构支持编写 Pass (可以理解为插件),可以在前端转为中间语言 (IR) 的时候,对代码进行修改。使用 LLVM 模式的好处是插桩的时机是在中间语言层,插桩的效果好过 GCC/CLANG 模式的汇编代码插桩,可读性也优于汇编 (其实是我菜)。 -
GCC_PLUGIN 模式
// TODO: 应该是AFL++新增的,暂不了解,需要看看 -
GCC/CLANG 模式
最原始的编译模式,插桩方式为在编译过程中插入汇编代码。
2. 选择选项
选择的选项是与编译器相关的,特定的选项需要特定的编译器,甚至特定版本才能支持。
-
COMPCOV
个人理解应该是程序中所有比较指令的覆盖率,就比如汇编中CMP
类指令。只能在 LTO 模式下 LLVM 编译 -
CMPLOG
-
选择性插桩
3. 选择 Sanitizer
Sanitizer 是 Google 开源的工具集,是一系列用于检测内存问题的工具,原理也是在源码中插桩,对内存中的值进行校验,以判断是否有可能出现内存问题。Sanitizer 原是集成在 LLVM 项目中,后来 GCC 也支持。
常见的 Sanitizer 包括但不限于 AddressSanitizer(ASAN)、Control Flow Integrity Sanitizer (CFISAN)、LeakSanitizer(LSAN)、MemorySanitizer(MSAN)、ThreadSanitizer(TSAN)、UndefinedBehaviorSanitizer(UBSAN)。故名思意,这些 Sanitizer 就是用于检测它们名称中显示的问题的。
4. 修改目标程序
这里指的是做一些插桩,以及为了能够运行而写一些必要的 harness
5. 编译目标源码
编译大型项目通常需要一些编译的工具,例如 configure、CMake、Meson 等。由于对一些大型开源项目进行 fuzz 通常需要添加一些编译选项,通常是需要对编译配置文件进行修改的,了解这些编译工具的使用也非常必要。
准备阶段
准备阶段的主要内容是为待运行的程序准备初始输入,以及提高一下这些输入的质量,包括精简输入集和减小输入文件的大小。
1. 收集输入
由于待测试的程序可能会对输入有一定的格式要求,例如音频处理程序需要输入音频文件,图片处理程序需要输入图片文件,所以需要收集特定的输入,至少 1 个输入文件。
2. 精简输入集
有时候输入集可能文件很多,但不是每个文件都有用,例如有两个文件结构类似,处理这两个文件时程序的执行过程一致,那么这两个文件只有一个文件有用,另一个则不必执行了。
AFL++ 提供一个名为 afl-cmin
的工具,可以帮助精简输入集,其原理为:根据覆盖率对输入集进行精简,如果当前执行的输入提升了程序的覆盖率,则保留,否则去掉。
3. 最小化输入文件
除了精简输入集外,还可以缩小单个输入文件的大小来提升输入的质量。输入文件大则大概率会导致程序执行时间变长,影响执行效率。在尽量保证输入对程序的影响相同的情况下,将过长的输入文件截短,可以提高程序执行效率。
AFL++ 同样也提供了一个名为 afl-tmin
的工具,其原理为:设定一个最小单位,将文件截短最小单位的长度,并交给程序执行,如果在截短前后程序执行输入的覆盖率没有变化,则可以认为截短该文件最小单位长度对程序执行没有影响,重复执行该操作直到覆盖率有差异为止。
执行阶段
执行阶段主要就是将输入集的文件逐个喂给程序运行。
1. 执行 afl-fuzz
AFL++ 最关键的代码主要,其一主要在编译,另外就是在 afl-fuzz
文件里了,但这里的步骤不包含监控运行时信息的部分,只是将程序执行起来,仅此而已。
在执行 afl-fuzz
时,也可以自定义一些参数,例如设置内存大小等。
2. 使用多核心
AFL++ 的每一个实例都是单线程,仅绑定一个 CPU 核心运行。一核有难,八核围观未免有些残忍,闲置的核心也可以利用起来,但是如果两个实例同时运行,信息不能同步的话,也会导致资源的浪费。所以 AFL++ 有一个多实例的模式:主从模式。
主从模式为一个主实例 (Master),多个从实例 (Slave),在运行afl-fuzz
时,可以加入 -M
+ 主实例名
参数表示该实例为主实例,一台机器上仅能运行一个主实例,但从实例可以有多个,运行时加入 -S
+ 从实例名
参数即可
3. 使用多机器执行
既然可以使用多核心,那么使用多台机器可以整合更多资源来进行模糊测试。AFL++ 还可以支持多机器同步执行,与主从模式类似,在每个机器上都可以执行一个主实例,参数为 -M name-$HOSTNAME
,参数上多加一个主机名即可,从实例运行参数与多核心一致:-S
+ 从实例名
。AFL++ 会依靠参数中的主机名在多个机器中同步信息。
验证阶段
将输入喂给待测程序执行后,通过插桩插入到程序中的代码会动态提取特定信息并反馈,需要接收这些信息并处理,为后续模糊测试的方向提供指导。
1. 监控状态
如果想了解 AFL++ 的执行状态,可以使用 afl-whatsup
来查看 Fuzz 每个阶段的状态。afl-plot
可以产生一个有图片的 HTML 报告来显示 Fuzz 随时间的结果图。
2. 检查覆盖率
afl-showmap
可以直接看到 AFL++ 内部用来记录覆盖率的 bitmap,afl-cov
可以生成 HTML 报告显示覆盖率结果。
3. 触发 crash
afl-fuzz
有一个探索模式,参数为 -C
,基于能够触发 crash 的输入,能够变异出更多能够触发 crash 的覆盖率不同的输入。
除此之外,还能够使用 gdb
或者其他第三方工具来分析这个 crash,看看这个 crash 是不是真的漏洞,是的话用这个能够触发 crash 的输入作为 PoC
就可以报 CVE 了。
4. 可选阶段
如果当你发现,长时间的 fuzz 已经不能找到新的漏洞或者覆盖率已经没有提升了,那么此时可以考虑停止 fuzz 了。或者尝试用不同的参数和模式运行,或者对于输入的文件有新的想法,也可以挑选一些不同的输入来 fuzz。