American Fuzzy Lop 模糊测试器 README 翻译,AFL 是目前最广泛使用的模糊测试器之一,README 可以让我们较好的了解它的特性与限制。希望可以更出下一篇2333.
guided fuzzing 的挑战
模糊测试,对于真实世界软件而言,是最强大和可靠的安全问题定位策略之一。它可以发现主要的远程命令执行和权限扩散问题。
不幸的是,模糊测试由于盲目地随机变异,很难走完待测代码中特定的某条代码执行路径,不能触达的代码路径上的脆弱性会被遗漏。
为了解决这一问题,研究人员做出了很多尝试。一种由 Tavis Ormandy 率先提出的早期方法是 corpus distillation。该方法依赖于覆盖率信号来选择高质量的随机种子的子集,然后按传统方法进行模糊测试。该方法的性能非常好,但需要 corpus 在 fuzz 时可获得。此外,块覆盖率仅仅提供了对程序状态最简单的理解,对于长距离的关联就作用很小了。
更复杂的研究则聚焦于程序流分析、符号执行或静态分析。所有这些方法在实验环境下都有不错的表现,但在实际使用中,还存在一些可靠性的问题,不能替代比较简单的方法。
afl-fuzz 方法
American Fuzzy Lop 是一个暴力模糊测试器与一个极其简单但可靠的插桩引导的遗传算法。它使用一种修改过形式的边覆盖率,以便用更少的努力本地地改变程序控制流。
总体上,afl 所用的算法可以总结如下:
- 加载用户提供的初始测试用例到队列中
- 从队列中获取下一个输入文件
- 尝试裁减测试用例到不改变测量到的测试结果的最小输入大小
- 按照传统 fuzz 策略,重复变异输入文件
- 如果任何新生成的变异导致插桩代码记录到新的状态迁移,添加变异后的输出到队列
- 返回 2,直到队列为空
发现的测试用例会被周期性的被更新的更高的覆盖率的测试用例清除。通过这种方式和其它插桩信息驱动的方法,可以帮助 afl 更快地提高覆盖率。
作为模糊测试的附带结果,afl 会构建一个不大的、自包含的关于有趣的测试用例的语料库。这些语料对其它模糊测试,以及其它重人工/资源测试范式也有相当的帮助。
AFL 配套的插桩程序
当源代码可以获得时,插桩代码可以通过配套工具注入,插桩工具可以如同 gcc 或 clang,替代它们在任何标准流程中工作。
插桩代码对性能有一定的影响。与 afl-fuzz 中实现的各种优化协同,大多数程序可以达到与传统工具等同或更快地模糊测试速度。
正确的重编译方式与具体的构建过程有关,但一个几乎通用的方式是:
1 | $ CC=/path/to/afl/afl-gcc ./configure |
对于 C++ 程序,需要设置 CXX=/path/to/afl/afl-g++
。对于使用clang
和clang++
对应的 wrapper:afl-clang
和 afl-clang++
。clang
用户也许会选择利用一个高性能插桩 mode,描述在llvm_mode/README.llvm
中。
当测试库时,你需要找或写一个简单的程序来从 stdin 或文件读输入,并传递给被测试的库。在这种情况下,不管是动态还是静态链接这个库时,要注意是否是插桩后的版本。在动态链接时,要配置LD_LIBRARY_PATH
,确保链接时加载了正确的.so
文件。最简单的选择是静态编译,通常可以是用如下命令:
1 | $ CC=/path/to/afl/afl-gcc ./configure --disable-shared |
设置AFL_HARDEN=1
当运行make
时将造成 CC wrapper 自动开启代码加固选项,让它可以更好的检测出简单的内存问题。libdislocator
是一个 AFL 的辅助库,可以帮助发现堆破坏问题,描述在libdislocator/README.dislocator
。
插桩仅有二进制的程序
当源码不可获取时,通过 QEMU 运行在用户态执行模式,AFL 对快速的黑盒二进制动态插桩提供试验性的支持。
QEMU 项目独立于 AFL,但可以方便的使用 AFL 提供的脚本加入动态插桩特性。
1 | $ cd qemu_mode |
详情可以阅读README.qemu
在 qemu 动态插桩模式下,会比编译时插桩慢 2 ~ 5 倍。这种方式相对难以并行,且可能存在一些其它问题。
选择初始测试用例
AFL 的正确使用有赖于一个或多个初始文件,初始文件中要包含一个好的输入数据样例,格式符合目标应用期待的格式。
对输入文件的要求可以总结为如下两条:
- 保持文件比较小。小于 1kB 是比较理想的大小,虽然并不是严格限制。
- 对于逐 bit 变异的枚举算法,如果输入中只有特定一位触发 bug,而翻转其它位只会产生不合法输入,如果测试用例只有 100 bytes 长,有 71% 的概率在前 1000 次尝试中触发 bug,而扩展到 1kB 长,同样 1000 次尝试触发的概率下降到 11%,而 10kB 长则下降到 1%。
- 使用简单的目标,对于图片等输入资源较大的软件,最好通过简单例子调用辅助库生成符合格式要求的输入,从而提高变异效率
- 使用 llvm 插桩,llvm mode 可以提供 2 倍的性能提升,但限定于构建流程依赖于 clang 的对象,而非 gcc。
- 使用多种测试用例当且仅当这些用例有功能上的不同时。
在testcases/
子文件夹下,找到许多好的样例。
P.S. 如果测试语料库比较大,可以使用afl-cmin
工具来确定一个覆盖目标二进制文件不同功能的语料子集。
对二进制的模糊测试
模糊测试过程由afl-fuzz
完成。afl-fuzz
需要一个含有初始测试用例的只读目录,一个单独的位置来存放 findings,以及待测二进制的路径
1 | $ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program [...params...] |
对于通过文件获取输入的程序,用 ‘@@’ 来标记在目标命令中输入文件名的位置。fuzzer 会自动替换为生成的文件名。
1 | $ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@ |
也可以使用 -f 选项将随机变异的数据写入到特定文件中,尤其是当目标程序需要特定文件后缀的输入时。
为插桩的二进制可以在 QEMU 模式下被 fuzz(添加 -Q 参数)或者使用传统的黑盒模糊测试模式(添加 -n 参数)。
使用 -t 和 -m 来覆盖执行过程中默认的超时和内存的限制。一般只有编译器和视频解码器这样对输入长度要求比较长的目标才需要修改这两项设置。
其它的优化模糊测试性能的部分在附带的perf_tips.txt
中讨论。
注意:afl-fuzz 开始时会执行一系列确定的模糊测试步骤,可能会持续数天的时间,但可以产出比较规整的测试用例。如果想快速获得测试用例,不在乎结果的质量,传统的模糊测试工具如 zzuf 可能是不错的选择。
解释输出
解释输出的详细解释在status_screen.txt
文件中,包括输出的状态,和如何监视 fuzz 过程的健康程度。有状态标红时,请查看该文件。
模糊测试进程在被 Ctrl-C 中断前都不会停止。在终止前,应至少允许 fuzzer 完成一个队列循环,虽然一个循环可能持续数小时到一周。
fuzzer 会在输出文件夹中创建三个子文件夹并实时更新
- queue/ - 测试用例对每个不同的执行路径,和所有用户给出的初始用例。所有这些文件合起来就是前述的测试语料库。在使用这个语料库用于其它目的前,可以使用
afl-cmin
工具压缩该语料库。该工具可以找出一个能提供同等边覆盖率的文件子集。 - crashes/ - 造成被测程序收到提前终止 signal(如 SIGSEGV,SEGILL,SIGABRT)的特殊测试用例,在 crashes 中这些测试用例按触发的 signal 类别分类存放。
- hangs/ - 造成被测程序超时的特殊测试用例。默认的时间限制是 1 秒,也可以由 -t 参数修改为其它值。这个值可以通过设置 AFL_HANG_TMOUT 来微调,但在大多数情况下不需要。
crashes 和 hangs 中特殊(unique)测试用例的标准是,是否相应的执行路径触发的错误在之前记录的错误中没有重复。如果一个 bug 可以通过多种执行路径触达,在开始时会导致计数膨胀,但随着更多路径触达该 bug,应该可以减少这样的重复计数。
crashes 和 hangs 的文件名与产生它的无错误测试用例对应的队列表项入口相关。这会帮助后续的调试。
如果你不能重现一个afl-fuzz
找到的 crash,很可能是内存限制不同。可以尝试如下指令添加内存限制:
1 | $ LIMIT_MB=50 |
改变 LIMIT_MB 使得内存限制和afl-fuzz
的 -m 参数一致.在 OpenBSD 系统下需要修改 -Sv 为 -Sd。
已有的输出文件夹可以用来继续afl-fuzz
中断的工作
1 | $ ./afl-fuzz -i- -o existing_output_dir [...etc...] |
如果安装了 gnuplot,可以通过afl-plot
对正在执行的模糊测试认证绘制漂亮的概览图,图片样例可以在 http://lcamtuf.coredump.cx/afl/plot/ 处查看。
并行化模糊测试
每个 afl-fuzz 实例仅使用单核,这意味着在多核系统上,想要完全利用硬件性能,并行化是必要的。关于并行模糊测试统一目标的具体说明可以参考parallel_fuzzing.txt
。
并行模糊测试模式也提供了一个简单的方式为 AFL 提供接口的其它 fuzzer 和符号执行引擎,来帮助 AFL 达到更好的性能。
模糊测试字典
默认设置下,afl-fuzz
的随机变异引擎为紧凑的数据格式(如图片、多媒体、压缩包、正则表达式、shell 脚本)优化过。对于相对繁复冗余的文件格式,如 HTML、SQL 和 Javascript。
为了避免构建语法相关的工具,afl-fuzz
提供了一种方式,用字典提供特定格式的语言关键字、magic head 或其它和格式相关的特殊 byte —— 实际操作参考:http://lcamtuf.blogspot.com/2015/01/afl-fuzz-making-up-grammar-with.html。
使用这个特性,需要首先创建符合 README.dictrionaries 描述支持的两种格式之一,并在运行afl-fuzz
时,用 -x 选项指向它。
字典的语法并不能提供更多的结构信息,fuzzer 会自行通过插桩反馈找到部分结构化信息。这在实践中是可行的:http://lcamtuf.blogspot.com/2015/04/finding-bugs-in-sqlite-easy-way.html
PS. 即使没有显式地提供字典,afl-fuzz
会通过观察确定的翻转输入byte对插桩结果的影响尝试提取输入中的语法词。这种方式虽然对一些语法解析工具有用,但不如 -x 模式。
如果一个字典很难构建,还可以使用 token 捕捉库 libtokencap,详见 libtokencap/README.libtokencap
crash 分类
基于覆盖率分组 crash 通常产生一组小的数据集可以快速地手工分类或使用简单的 gdb 或 Valgrind(一种内存管理和线程缺陷检测工具)脚本分类。每个 crash 可以追溯到自身的变异来源(没有 crash 的源样本),以帮助测试人员定位错误。
应当承认的是,一些模糊测试得到的 crash 很难快速确认可利用性,而是需要进一步的调试和代码分析工作。为了帮助完成这个工作,afl-fuzz
支持 “crash exploration” mode 来提供辅助信息,需要 -C 选项打开该功能。
该模式下,fuzzer 选择一个或多个 crash 测试用例作为输入,并使用它的反馈驱动的模糊测试策略,在保持 crash 发生的前提下,快速枚举所有可以触达的代码路径。因此,对 crash 状态造成影响和没有触发新的执行路径的随机变异结果会被拒绝。
crash exploration mode 的输出是一组输入文件,可以快速测试得出攻击者可以从出错处得到的控制程度。
为了最小化测试用例,afl-tmin
值得一试,该工具的使用很简单
1 | $ ./afl-tmin -i test_case -o minimized_result -- /path/to/program [...] |
该工具可以用相似的方式处理 carsh 的和 non-crash 测试用例。在 crash 模式下,它可以接受已插桩或未插桩的二进制文件。在 non-crash 模式下,最小化依赖于标准的 AFL 插桩,在保证执行路径覆盖率的前提下测试。
afl-tmin
接受 -m,-t,@@ 语法可以和在 afl-fuzz
工具中一样被使用。
另一个最近添加的工具是afl-analyze
工具。它对一个输入文件逐个尝试翻转 bytes 并观察被测试程序的行为,然后对输入上色表示哪些部分的输入比较关键,而哪些并不重要。虽然该工具并不严格,但提供了一个快速观察复杂文件格式的视角。
超越 crash
模糊测试是一个美好且尚未被充分利用的技术来发现无 crash 和实现错误的设计。在下列情形时,一些有趣的漏洞可以通过调用 abort() 来找到:
- 对同样的模糊测试器生成的输入,两个大整数库产生不同的输出
- 一个图形库在解码同一个输入图片数次解码产生不同的输出
- 一个压缩库在压缩并解压后产生的结果不一致
实现这些或相似的完整性检测通常仅需很少的时间。如果你是特定包的维护者,你可以使这个#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
(libfuzzer 也可以使用这个 flag)或者#ifdef __AFL_COMPILER
(只有 AFL 可以使用这个 flag)
已知风险
- 你的 CPU 会跑得比较热需要充分的散热,所以在笔记本或者智能手机等散热不好的设备上,并不是没有可能因为散热不足导致问题。
- 目标程序可能不规律地结束,如 OOM 或者硬盘被填满了。虽然 AFL 尝试限制基本的内存使用,但并不能阻止所有错误情况。因此不要在不能允许数据丢失发生的系统上使用 AFL。
- 模糊测试引入百万级的读写文件系统,这使得 HDD 和 SSD 寿命有所减少,注意数据备份。
在 Linux 推荐的硬盘 I/O 监控方式是 iostat 指令:
1 | $ iostat -d 3 -x -k [...optional disk ID...] |
已知的限制和需要继续改进的领域
- AFL 通过检测第一个由于收到错误信号(SIGSEGV,SIGABRT等)发生错误的进程发现错误。程序中如果有处理这些错误信号的代码应当注释掉。在待测程序的子进程中发生的错误,除非手动添加捕获代码,否则会逃过检测。
- 和其它暴力工具一样,AFL 对有加密、校验和、密码学签名或压缩包裹的测试数据格式,只能提供很有限的覆盖率。为了绕过这个缺陷(如 experimental/libpng_no_checksum/ for inspiration),可以注释掉相关的校验代码。或者实现一个后处理器(如 experimental/post_library/)。
- 使用 address sanitizer 和 AFL 需要一些权衡,详见 notes_for_asan.txt
- AFL 不直接支持对网络服务,守护程序或通过 UI 交互的应用进行模糊测试。需要使用者修改这些程序,使它们以更传统的方式运行。Preeny 可以帮助用户完成这种转换,具体过程可以参考 https://www.fastly.com/blog/how-to-fuzz-server-american-fuzzy-lop。
- AFL 不支持人类可读的覆盖率数据,如果希望监测覆盖率,可以使用
afl-cov
工具 https://github.com/mrash/afl-cov。