AFL++fuzz测试
AFL++fuzz测试
概述
AFL++是AFL的社区维护版本。它拥有更快的fuzzing速度,更好的变异策略、插桩性能和功能,以及支持各类自定义的模块。
AFL简介
AFL(American Fuzzy Lop)是由安全研究员Micha? Zalewski(@lcamtuf)开发的一款基于覆盖引导(Coverage-guided)的模糊测试工具,它通过记录输入样本的代码覆盖率,从而调整输入样本以提高覆盖率,增加发现漏洞的概率。
①从源码编译程序时进行插桩,以记录代码覆盖率(Code Coverage);
②选择一些输入文件,作为初始测试集加入输入队列(queue);
③将队列中的文件按一定的策略进行“突变”;
④如果经过变异文件更新了覆盖范围,则将其保留添加到队列中;
⑤上述过程会一直循环进行,期间触发了crash的文件会被记录下来。
选择和评估测试的目标
开始Fuzzing前,首先要选择一个目标。 AFL的目标通常是接受外部输入的程序或库,输入一般来自文件(后面的文章也会介绍如何Fuzzing一个网络程序)。
1. 用什么语言编写
AFL主要用于C/C++程序的测试,所以这是我们寻找软件的最优先规则。(也有一些基于AFL的JAVA Fuzz程序如kelinci、java-afl等,但并不知道效果如何)
2. 是否开源
AFL既可以对源码进行编译时插桩,也可以使用AFL的QEMU mode
对二进制文件进行插桩,但是前者的效率相对来说要高很多,在Github上很容易就能找到很多合适的项目。
3. 程序版本
目标应该是该软件的最新版本,不然辛辛苦苦找到一个漏洞,却发现早就被上报修复了就尴尬了。
4. 是否有示例程序、测试用例
如果目标有现成的基本代码示例,特别是一些开源的库,可以方便我们调用该库不用自己再写一个程序;如果目标存在测试用例,那后面构建语料库时也省事儿一点。
5.项目规模
某些程序规模很大,会被分为好几个模块,为了提高Fuzz效率,在Fuzzing前,需要定义Fuzzing部分。这里推荐一下源码阅读工具Understand,它treemap
功能,可以直观地看到项目结构和规模。比如下面ImageMagick的源码中,灰框代表一个文件夹,蓝色方块代表了一个文件,其大小和颜色分别反映了行数和文件复杂度。
6. 程序曾出现过漏洞
如果某个程序曾曝出过多次漏洞,那么该程序有仍有很大可能存在未被发现的安全漏洞。如ImageMagick每个月都会发现难以利用的新漏洞,并且每年都会发生一些具有高影响的严重漏洞。
构建语料库
AFL需要一些初始输入数据(也叫种子文件)作为Fuzzing的起点,这些输入甚至可以是毫无意义的数据,AFL可以通过启发式算法自动确定文件格式结构。lcamtuf就在博客中给出了一个有趣的例子——对djpeg进行Fuzzing时,仅用一个字符串”hello”作为输入,最后凭空生成大量jpge图像!
尽管AFL如此强大,但如果要获得更快的Fuzzing速度,那么就有必要生成一个高质量的语料库,这一节就解决如何选择输入文件、从哪里寻找这些文件、如何精简找到的文件三个问题。
1. 选择
(1) 有效的输入
尽管有时候无效输入会产生bug和崩溃,但有效输入可以更快的找到更多执行路径。
(2) 尽量小的体积
较小的文件会不仅可以减少测试和处理的时间,也能节约更多的内存,AFL给出的建议是最好小于1 KB,但其实可以根据自己测试的程序权衡,这在AFL文档的perf_tips.txt
中有具体说明。
2. 寻找
- 使用项目自身提供的测试用例
- 目标程序bug提交页面
- 使用格式转换器,用从现有的文件格式生成一些不容易找到的文件格式:
- afl源码的testcases目录下提供了一些测试用例
- 其他大型的语料库
- afl generated image test sets
- fuzzer-test-suite
- libav samples
- ffmpeg samples
- fuzzdata
- moonshine
3. 修剪
网上找到的一些大型语料库中往往包含大量的文件,这时就需要对其精简,这个工作有个术语叫做——语料库蒸馏(Corpus Distillation)。AFL提供了两个工具来帮助我们完成这部工作——afl-cmin
和afl-tmin
。
(1) 移除执行相同代码的输入文件——AFL-CMIN
afl-cmin
的核心思想是:尝试找到与语料库全集具有相同覆盖范围的最小子集。举个例子:假设有多个文件,都覆盖了相同的代码,那么就丢掉多余的文件。其使用方法如下:
1 | $ afl-cmin -i input_dir -o output_dir -- /path/to/tested/program [params] |
更多的时候,我们需要从文件中获取输入,这时可以使用“@@”代替被测试程序命令行中输入文件名的位置。Fuzzer会将其替换为实际执行的文件:
1 | $$ afl-cmin -i input_dir -o output_dir -- /path/to/tested/program [params] @@ |
下面的例子中,我们将一个有1253个png文件的语料库,精简到只包含60个文件。
(2) 减小单个输入文件的大小——AFL-TMIN
整体的大小得到了改善,接下来还要对每个文件进行更细化的处理。afl-tmin缩减文件体积的原理这里就不深究了,有机会会在后面文章中解释,这里只给出使用方法(其实也很简单,有兴趣的朋友可以自己搜一搜)。
afl-tmin
有两种工作模式,instrumented mode
和crash mode
。默认的工作方式是instrumented mode
,如下所示:
1 | $ afl-tmin -i input_file -o output_file -- /path/to/tested/program [params] @@ |
如果指定了参数-x
,即crash mode
,会把导致程序非正常退出的文件直接剔除。
1 | $ afl-tmin -x -i input_file -o output_file -- /path/to/tested/program [params] @@ |
afl-tmin
接受单个文件输入,所以可以用一条简单的shell脚本批量处理。如果语料库中文件数量特别多,且体积特别大的情况下,这个过程可能花费几天甚至更长的时间!
1 | for i in *; do afl-tmin -i $i -o tmin-$i -- ~/path/to/tested/program [params] @@; done; |
下图是经过两种模式的修剪后,语料库大小的变化:
这时还可以再次使用afl-cmin
,发现又可以过滤掉一些文件了。
搭建AFL++环境
搭建实验环境
获取 & 编译AFL++
本节仅用于在 ubuntu/debian 环境下自行配置AFL++环境
编译方式也可见官网文档 https://github.com/AFLplusplus/AFLplusplus/blob/stable/docs/INSTALL.md
- 获取AFL++
1 | # 下载AFLplusplus源代码 |
- 安装 llvm 和 clang。
AFL++在llvm模式下使用afl-clang-fast具有更多的特性。
运行如下命令,安装llvm、clang和相关依赖
1 | # 11, 12 版本可能会出现一些问题 |
将llvm-config指向已经安装版本::
1 | # 如果是自行安装的其他llvm版本,此处需要对应修改命令末尾的llvm-config-xx为本地的版本 |
可以测试如下命令,如果有版本号输出,则配置完成:
1 | llvm-config --version |
- 编译AFL++
进入第一步下载AFLplusplus的源代码目录cd AFLplusplus
, 而后运行如下编译命令:
1 | # 编译可能需要一些时间,建议拉高虚拟机的内存 |
在AFLplusplus目录中存在afl-fuzz
以及afl-cc
说明基础的编译已经完成;存在afl-clang-fast
说明llvm模式下的AFL++编译已经完成。
1 | $ ls | grep "afl-" |
此外,还可以运行
sudo make install
,将编译后的二进制复制到PATH能查找到的位置,此时就不用再执行接下来的第四步配置了。
- 将
afl-fuzz
、afl-clang-fast
等加入PATH中,方便后续使用:
对于自行配置的环境,如果当前使用的默认shell是bash,则将下一行的命令添加到~/.bashrc
文件的其他位置,如果使用的是zsh,则添加到~/.zshrc
文件中的任意位置。
注意修改命令中
/path/to/AFLplusplus
目录的位置为绝对路径
1 | export PATH=/path/to/AFLplusplus:$PATH |
完成配置后,在新的终端测试afl-fuzz
命令,会有如下类似的输出:
1 | $ afl-fuzz |
编译带有AddressSanitizer的libxml2
这里的libxml2可以改为其他源码
- 获取libxml2的源代码
1 | # fetch libxml2 source code |
- 编译libxml2
1 | cd libxml2-2.9.4 |
- 编译完成后,运行如下命令,如果有类似
T __asan_report_error
的输出,则表示编译出的xmllint二进制文件已经带有ASan的插桩。
1 | nm xmllint | grep __asan_report_error |
Fuzzing libxml2
- 创建一个工作目录
mkdir work
- 为了提高fuzz xml的效率,我们可以使用AFL++预设的xml dictionary:
1 | wget https://raw.githubusercontent.com/AFLplusplus/AFLplusplus/stable/dictionaries/xml.dict |
如果下载失败,也可以使用 https://gitee.com/ret2happy/ssec22_fuzzing_script/raw/master/xml.dict
- 创建初始语料库(可以寻找更好的xml初始种子,此处仅提供样例)
tips: 可以在libxml2源代码中的测试样例中找到更好的测试语料。
1 | git clone https://gitee.com/ret2happy/libxml2_sample.git corpus |
- 创建一个主fuzzer进行fuzz:
配置运行前的系统:
1 | sudo bash -c "echo core >/proc/sys/kernel/core_pattern" |
创建主fuzzer:
1 | afl-fuzz -M master -m none -x xml.dict -i /path/to/corpus -o output -- /path/to/xmllint --valid @@ |
其中
-M master
表示将当前fuzzer尽可能的被指定为master fuzzer。master fuzzer会对各个slave fuzzer进行语料的管理与调度。-m none
ASan开启时需要大量虚拟内存,此处让fuzzer不对内存进行限制。-x xml.dict
指向之前获取的xml.dict,用于提供变异时的候选词。-i /path/to/corpus
指定了初始的输入语料库-o output
指定fuzzer的输出目录,其中包含了已变异待执行的testcase,fuzzer目前的状态、寻找到的crash等各类信息。xmllint --valid @@
指定了被fuzz程序的命令执行方式,其中@@
表示占位的文件名,在运行时AFL会将输入文件的文件名替换@@
,以便被测程序能够读取真实的输入。
tips: 对于master fuzzer,我们可以不使用
-D
的参数,而通过仅在slave fuzzer中加入-D
参数以增加各个fuzzer策略的多样性。这对于寻找crash有很大帮助。
运行后的效果如图所示
1 |
|
- 创建更多的fuzzer:
运行第一个slave fuzzer:
1 | afl-fuzz -S slave1 -D -m none -x xml.dict -i /path/to/corpus -o output -- /path/to/xmllint --valid @@ |
其中,将-M master
改成了-S slave1
。-S
表示当前fuzzer为slave fuzzer,仍然会进行独立的fuzzing,而slave1
的名字可以自定义,作为该fuzzer的名字,各个slave fuzzer之间的名字不能重复。
由此我们可以创建多个fuzzer,充分利用多核进行fuzzing。此外,加入了-D
参数启用deterministic fuzzing。
进行一段时间的fuzzing后,AFL++将发现若干crash,如图所示,可以看到在total crashes
(红字处)显示发现了数个crash:
重现寻找到的crash:
在上述AFL++发现crash后,我们进入output输出目录,在对应的fuzzer文件夹(文件夹名为对应的fuzzer名字)内的crashes
文件夹内可以找到fuzzer保存的用于复现的testcase (README.txt
包含了找到该crash所用的fuzzer命令行参数):
我们可以利用其中的testcase,复现模糊测试得到的内存破坏: