最初由 Michal Zalewski [email protected] 开发。
如果没有时间阅读本文件,请参阅 QuickStartGuide.txt。
模糊测试是识别真实世界软件安全问题的最强大且久经考验的策略之一;迄今为止,在安全关键软件中发现的大多数远程代码执行和权限提升漏洞都要归功于它。
遗憾的是,模糊测试也相对浅显;盲目的随机变异使得测试代码中的某些代码路径很难被触及,导致一些漏洞完全超出这种技术的可达范围。
为了解决这个问题,人们进行了大量尝试。早期方法之一——由 Tavis Ormandy 开创——是语料库蒸馏(corpus distillation)。该方法依赖覆盖信号,从大量高质量的候选文件语料库中选出一组有趣的种子,然后用传统方式对其模糊测试。这种方法效果极佳,但要求这样的语料库能够随时获得。此外,块覆盖测量对程序状态的理解非常粗浅,长期来看,对引导模糊测试工作的帮助有限。
其他更高级的研究则聚焦于程序流分析(“混合执行”/concolic execution)、符号执行或静态分析等技术。这些方法在实验环境中都极具前景,但在实际使用中往往会遇到可靠性和性能问题——目前还不能作为“盲目”模糊测试技术的可行替代方案。
American Fuzzy Lop 是一个暴力模糊测试器,结合了一种极其简单但稳如磐石的基于插桩的遗传算法。它使用一种改进形式的边覆盖(edge coverage),轻松捕捉程序控制流中细微的、局部范围的变化。
简单来说,整个算法可以概括为:
将用户提供的初始测试用例加载到队列中,
从队列中取出下一个输入文件,
尝试将测试用例修剪到不改变程序测得行为的最小尺寸,
使用均衡且经过充分研究的传统模糊测试策略组合反复变异该文件,
如果任何生成的变异产生了插桩记录到的新的状态转换,则将变异后的输出作为新条目加入队列。
回到第 2 步。
发现的测试用例还会定期剔除,以淘汰那些已被更新、覆盖更高的发现所取代的用例;并经过若干其他由插桩驱动的精简工作量步骤。
作为模糊测试过程的副产品,该工具会创建一个小型、自包含的有趣测试用例语料库。这些用例对于为其他劳动密集或资源密集的测试方案提供种子非常有用——例如,对浏览器、办公应用、图形套件或闭源工具进行压力测试。
该模糊测试器经过充分测试,开箱即用的性能远超盲目模糊测试或仅基于覆盖率的工具。
当源代码可用时,可以通过一个配套工具注入插桩,该工具可作为 gcc 或 clang 的即插即用替代品,用于任何第三方代码的标准构建过程。
插桩带来的性能影响相当有限;结合 afl-fuzz 实现的其他优化,大多数程序的模糊测试速度可以达到甚至超过传统工具。
重新编译目标程序的正确方式可能因构建过程的具体情况而异,但一个几乎通用的方法是:```shell $ CC=/path/to/afl/afl-gcc ./configure $ make clean all
对于C++程序,你可能还需要设置 `CXX=/path/to/afl/afl-g++`。
clang 包装器(afl-clang 和 afl-clang++)可以以同样的方式使用;
clang 用户也可以选择利用更高性能的插桩模式,
如 llvm_mode/README.llvm 中所述。
在测试库时,你需要找到或编写一个简单的程序,它从
stdin 或文件中读取数据,并将其传递给被测库。在这种情况下,
必须将此可执行文件与插桩库的静态版本链接,或确保
在运行时加载正确的 .so 文件(通常通过设置 `LD_LIBRARY_PATH`)。最简单
的选择是静态构建,
通常可以通过以下方式实现:```shell
$ CC=/path/to/afl/afl-gcc ./configure --disable-shared
在调用 make 时设置 AFL_HARDEN=1 将使 CC 包装器自动启用代码加固选项,从而更容易检测简单的内存错误。Libdislocator 是 AFL 附带的一个辅助库(参见 libdislocator/README.dislocator),也可以帮助发现堆损坏问题。
附注:建议 ASAN 用户查阅 notes_for_asan.txt 文件,其中包含重要的注意事项。
当源代码不可用时,模糊测试器提供了对黑盒二进制进行快速、即时插桩的实验性支持。这是通过运行在鲜为人知的“用户空间模拟”模式下的 QEMU 版本来实现的。
QEMU 是独立于 AFL 的项目,但你可以方便地通过以下方式构建该功能:```shell $ cd qemu_mode $ ./build_qemu_support.sh
有关其他说明和注意事项,请参阅 qemu_mode/README.qemu。
该模式比编译时插桩大约慢 2-5 倍,不太利于并行化,并且可能还有其他一些怪癖。
## 5) 选择初始测试用例
为了正确运行,模糊器需要一个或多个起始文件,其中包含目标应用程序通常预期的输入数据的良好示例。有两条基本规则:
- 保持文件较小。理想情况下小于 1 kB,尽管并非严格要求。有关大小为何重要的讨论,请参阅 [perf_tips.txt](https://github.com/google/afl/blob/master/docs/perf_tips.txt)。
- 只有当多个测试用例在功能上彼此不同时才使用它们。用五十张不同的度假照片去模糊测试一个图像库毫无意义。
你可以在本工具附带的 testcases/ 子目录中找到许多很好的起始文件示例。
附注:如果有大量数据可供筛选,你可能希望使用 afl-cmin 实用程序来识别一组功能不同的文件,这些文件会执行目标二进制中的不同代码路径。
## 6) 模糊测试二进制文件
模糊测试过程本身由 afl-fuzz 实用程序执行。该程序需要一个包含初始测试用例的只读目录、一个单独的位置来存储其发现结果,以及要测试的二进制文件的路径。
对于直接从 stdin 接受输入的目标二进制文件,常用语法为:```shell
$ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program [...params...]
对于从文件读取输入的程序,请使用 '@@' 标记目标命令行中应放置输入文件名的位置。模糊测试器会为你替换该位置:```shell $ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@
你还可以使用 -f 选项将变异后的数据写入到特定
文件。如果程序期望特定的文件扩展名等,这将非常有用。
未插桩的二进制文件可以在 QEMU 模式(在命令行中添加 -Q)下进行模糊测试,
也可以在传统的盲模糊器模式(指定 -n)下进行。
你可以使用 -t 和 -m 来覆盖执行进程的默认超时时间和内存限制;
可能需要调整这些设置的罕见目标示例包括
编译器和视频解码器。
有关优化模糊测试性能的技巧,请参阅 [perf_tips.txt](https://github.com/google/afl/blob/master/docs/perf_tips.txt)。
请注意,afl-fuzz 会先执行一系列确定性模糊测试步骤,
这些步骤可能需要数天时间,但往往能生成整洁的测试用例。如果你想
立即获得快速但粗糙的结果——类似于 zzuf 和其他传统模糊器——
请在命令行中添加 -d 选项。
## 7) 解读输出
有关如何解读显示的状态以及监控进程健康状况的信息,请参阅
[status_screen.txt](https://github.com/google/afl/blob/master/docs/status_screen.txt) 文件。
请务必查阅该文件,尤其是当任何 UI 元素以
红色高亮显示时。
模糊测试过程将持续进行,直到你按下 Ctrl-C。至少,你希望
让模糊器完成一个队列周期,这可能从几个小时
到一周左右不等。
输出目录中会创建三个子目录,并实时
更新:
- queue/ - 针对每条不同执行路径的测试用例,以及用户提供的所有
初始文件。这是第 2 节中提到的合成语料库。
在将此语料库用于任何其他目的之前,你可以使用 afl-cmin 工具将其
缩小到更小的规模。
该工具会找到
一个提供等效边覆盖率的更小子集。
- crashes/ - 导致被测程序接收到致命信号的唯一测试用例
(例如 SIGSEGV、SIGILL、SIGABRT)。条目会按
收到的信号进行分组。
- hangs/ - 导致被测程序超时的唯一测试用例。
默认情况下,在将某个执行判定为挂起之前的超时限制为
1 秒与 -t 参数值中的较大者。
可以通过设置 AFL_HANG_TMOUT 来微调该值,但通常
很少有必要这样做。
如果相关的执行路径涉及以前记录的故障中未出现过的任何状态转换,则崩溃和挂起被视为“独特”。
如果单个 bug 可以通过多种方式触发,过程早期会出现一些计数膨胀,
但这种膨胀在过程初期
应会迅速减少。
崩溃和挂起的文件名与父级、未出错的队列条目
相关联。这应该有助于调试。
当你无法重现 afl-fuzz 发现的崩溃时,最可能的原因是
你没有设置与工具相同的内存限制。请尝试:```shell
$ LIMIT_MB=50
$ ( ulimit -Sv $[LIMIT_MB << 10]; /path/to/tested_binary ... )
将 LIMIT_MB 更改为与传递给 afl-fuzz 的 -m 参数相匹配。在 OpenBSD 上, 还需将 -Sv 更改为 -Sd。
任何现有的输出目录也可以用来恢复中断的任务;试试:```shell $ ./afl-fuzz -i- -o existing_output_dir [...etc...]
如果你安装了 gnuplot,还可以使用 afl-plot 为任意正在进行的模糊测试任务生成一些漂亮的图表。关于效果示例,请参见 [http://lcamtuf.coredump.cx/afl/plot/](http://lcamtuf.coredump.cx/afl/plot/)。
## 8) 并行化模糊测试
每个 afl-fuzz 实例大约占用一个核心。这意味着在多核系统上,必须进行并行化才能充分利用硬件。关于如何在多个核心或多台联网机器上对常见目标进行模糊测试的提示,请参阅 [parallel_fuzzing.txt](https://github.com/google/afl/blob/master/docs/parallel_fuzzing.txt)。
并行模糊测试模式还提供了一种简单的方式,使 AFL 能够与其他模糊测试器、符号执行或混合执行引擎等对接;同样,请参阅 [parallel_fuzzing.txt](https://github.com/google/afl/blob/master/docs/parallel_fuzzing.txt) 的最后一部分获取相关提示。
## 9) 模糊测试字典
默认情况下,afl-fuzz 的变异引擎针对紧凑型数据格式进行了优化——例如图像、多媒体、压缩数据、正则表达式语法或 shell 脚本。它不太适合那些措辞特别冗长且冗余的语言——尤其是 HTML、SQL 或 JavaScript。
为了避免构建语法感知工具的麻烦,afl-fuzz 提供了一种方式,可以用一个可选的字典来为模糊测试过程提供种子,该字典包含与目标数据类型相关的语言关键字、魔法头或其他特殊标记——并利用它随时重建底层语法:
[http://lcamtuf.blogspot.com/2015/01/afl-fuzz-making-up-grammar-with.html](http://lcamtuf.blogspot.com/2015/01/afl-fuzz-making-up-grammar-with.html)
要使用此功能,首先需要按照 dictionaries/README.dictionaries 中讨论的两种格式之一创建字典;然后在命令行中通过 -x 选项将其指定给模糊测试器。
(该子目录中已经提供了一些常用字典。)
目前没有办法提供底层语法更结构化的描述,但模糊测试器很可能会仅凭插桩反馈就能推断出部分信息。这在实际中是有效的,例如:
[http://lcamtuf.blogspot.com/2015/04/finding-bugs-in-sqlite-easy-way.html](http://lcamtuf.blogspot.com/2015/04/finding-bugs-in-sqlite-easy-way.html)
附注:即使没有提供显式字典,afl-fuzz 也会在确定性字节翻转期间密切观察插桩,尝试从输入语料库中提取现有的语法标记。这对某些类型的解析器和语法有效,但远不如 -x 模式。
如果很难获得字典,另一个选择是让 AFL 运行一段时间,然后使用随 AFL 附带的配套工具——令牌捕获库。为此,请参阅 libtokencap/README.tokencap。
## 10) 崩溃分类
基于覆盖率的崩溃分组通常会生成一个小型数据集,可以通过人工方式或使用非常简单的 GDB 或 Valgrind 脚本快速进行分类。每个崩溃还可以追溯到队列中其对应的非崩溃父测试用例,从而更容易诊断故障。
话虽如此,重要的是要认识到,如果不进行大量调试和代码分析工作,某些模糊测试崩溃可能很难快速评估其可利用性。为了协助完成这项任务,afl-fuzz 支持一种非常独特的“崩溃探索”模式,通过 -C 标志启用。
在此模式下,模糊测试器将一个或多个崩溃测试用例作为输入,并使用其反馈驱动的模糊测试策略,在保持程序处于崩溃状态的同时,非常快速地枚举程序可到达的所有代码路径。
未导致崩溃的变异会被拒绝;任何不影响执行路径的更改也同样会被拒绝。
输出是一个小型文件语料库,可以非常快速地检查,以了解攻击者对故障地址的控制程度,或者是否有可能越过初始的越界读取——并查看其背后隐藏着什么。
哦,还有一件事:对于测试用例最小化,可以试试 afl-tmin。该工具的操作方式非常简单:```shell
$ ./afl-tmin -i test_case -o minimized_result -- /path/to/program [...]
该工具同时适用于崩溃与非崩溃的测试用例。在崩溃模式下,它可以愉快地接受已插桩和未插桩的二进制程序。在非崩溃模式下,最小化器依赖标准的 AFL 插桩,在不改变执行路径的前提下简化文件。
最小化器接受 -m、-t、-f 和 @@ 语法,其方式与 afl-fuzz 兼容。
AFL 的另一项近期新增功能是 afl-analyze 工具。它读取一个输入文件,尝试逐字节翻转,并观察被测程序的行为。然后根据哪些部分看起来是关键的、哪些不是,对输入进行颜色编码;虽然并非绝对可靠,但它通常能为复杂文件格式提供快速洞见。关于其操作的更多信息可在 technical_details.txt 末尾附近找到。
模糊测试也是一种奇妙且未被充分利用的技术,可用于发现非崩溃的设计和实现错误。通过修改目标程序,使其在以下情况调用 abort(),已经发现了相当多有趣的 bug,例如:
两个大数库在收到相同的模糊器生成输入时产生不同输出,
一个图像库在连续多次被要求解码同一输入图像时产生不同输出,
一个序列化/反序列化库在反复序列化和反序列化模糊器提供的数据时无法产生稳定的输出,
一个压缩库在被要求压缩然后解压缩特定数据块时,产生的输出与输入文件不一致。
实现这些或类似的健全性检查通常只需很少时间;如果你是某个软件包的维护者,你可以使用 #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION(该标志也与 libfuzzer 共用)或 #ifdef __AFL_COMPILER(后者仅用于 AFL)来使此代码成为条件编译。
请记住,与许多其他计算密集型任务类似,模糊测试可能会对硬件和操作系统造成压力。尤其是:
你的 CPU 会发热,需要足够的冷却。在大多数情况下,如果冷却不足或无法正常工作,CPU 速度会被自动降频。话虽如此,尤其是在不太合适的硬件(笔记本电脑、智能手机等)上进行模糊测试时,某些部件烧毁并非完全不可能。
目标程序可能会异常地占用数 GB 内存,或用垃圾文件填满磁盘空间。AFL 会尝试强制执行基本的内存限制,但无法防止每一种可能的事故。底线是,你不应该在数据丢失风险不可接受的系统上进行模糊测试。
模糊测试涉及对文件系统的数十亿次读写。在现代系统上,这些操作通常会被大量缓存,导致“物理”I/O 相当有限——但有许多因素可能改变这一平衡。你有责任监控潜在问题;在 I/O 非常繁重的情况下,许多 HDD 和 SSD 的寿命可能会缩短。
在 Linux 上监控磁盘 I/O 的一个好方法是 'iostat' 命令:```shell $ iostat -d 3 -x -k [...optional disk ID...]
## 13) 已知限制与改进领域
以下是 AFL 的一些最重要注意事项:
- AFL 通过检查第一个派生进程是否因信号(SIGSEGV、SIGABRT 等)而死亡来检测故障。为这些信号安装了自定义处理程序的项目,可能需要注释掉相关代码。同样地,在被模糊测试目标所派生的子进程中发生的故障,除非你手动添加一些代码来捕获,否则可能会躲过检测。
- 与任何其他暴力破解工具一样,如果使用加密、校验和、加密签名或压缩来整体包装待测试的实际数据格式,模糊测试器的覆盖率将非常有限。
要解决这个问题,你可以注释掉相关的检查(可参考 experimental/libpng_no_checksum/ 以获得启发);如果无法做到,你也可以编写一个后处理器,如 experimental/post_library/ 中所解释的那样。
- 在 ASAN 和 64 位二进制文件方面存在一些令人遗憾的权衡。这并非 afl-fuzz 的特定缺陷;有关提示,请参阅 [notes_for_asan.txt](https://github.com/google/afl/blob/master/docs/notes_for_asan.txt)。
- 目前不直接支持对网络服务、后台守护进程或需要 UI 交互才能运行的交互式应用进行模糊测试。你可能需要做一些简单的代码修改,使它们以更传统的方式运行。Preeny 也可能提供一个相对简单的选择——请参阅:https://github.com/zardus/preeny
还可以在以下网址找到一些修改基于网络的服务的实用技巧:https://www.fastly.com/blog/how-to-fuzz-server-american-fuzzy-lop
- AFL 不会输出人类可读的覆盖率数据。如果你想监控覆盖率,请使用 Michael Rash 的 afl-cov:https://github.com/mrash/afl-cov
- 偶尔,有知觉的机器会起来反抗它们的创造者。如果你遇到这种情况,请查阅 http://lcamtuf.coredump.cx/prep/。
除此之外,有关特定平台的提示,请参阅 INSTALL。
## 14) 特别致谢
如果没有来自以下人士的反馈、错误报告或补丁,afl-fuzz 的许多改进是不可能实现的:```
Jann Horn Hanno Boeck
Felix Groebert Jakub Wilk
Richard W. M. Jones Alexander Cherepanov
Tom Ritter Hovik Manucharyan
Sebastian Roschke Eberhard Mattes
Padraig Brady Ben Laurie
@dronesec Luca Barbato
Tobias Ospelt Thomas Jarosch
Martin Carpenter Mudge Zatko
Joe Zbiciak Ryan Govostes
Michael Rash William Robinet
Jonathan Gray Filipe Cabecinhas
Nico Weber Jodie Cunningham
Andrew Griffiths Parker Thompson
Jonathan Neuschfer Tyler Nighswander
Ben Nagy Samir Aguiar
Aidan Thornton Aleksandar Nikolich
Sam Hakim Laszlo Szekeres
David A. Wheeler Turo Lamminen
Andreas Stieger Richard Godbee
Louis Dassy teor2345
Alex Moneger Dmitry Vyukov
Keegan McAllister Kostya Serebryany
Richo Healey Martijn Bogaard
rc0r Jonathan Foote
Christian Holler Dominique Pelle
Jacek Wielemborek Leo Barnes
Jeremy Barnes Jeff Trull
Guillaume Endignoux ilovezfs
Daniel Godas-Lopez Franjo Ivancic
Austin Seipp Daniel Komaromy
Daniel Binderman Jonathan Metzman
Vegard Nossum Jan Kneschke
Kurt Roeckx Marcel Bohme
Van-Thuan Pham Abhik Roychoudhury
Joshua J. Drake Toby Hutton
Rene Freingruber Sergey Davidoff
Sami Liedes Craig Young
Andrzej Jackowski Daniel Hodson
谢谢!
有问题?有疑虑?需要报告 Bug?请使用 GitHub。
该项目还有一个邮件列表;如需加入,请发送邮件至 [email protected]。或者,如果您希望先浏览 存档,可尝试:https://groups.google.com/group/afl-users。