AFL实例
原文链接:https://foxglovesecurity.com/2016/03/15/fuzzing-workflows-a-fuzz-job-from-start-to-finish/
寻找合适的软件进行测试
AFL主要是针对C与C++应用,所以在我们寻找软件进行测试的时候,这应该是一个标准。同时还有一下几个问题需要考虑:
1.是否有可用的源码?
如果测试一个有源码的项目,当这个项目过于庞大时,我们可以为了测试的需要而对其进行修剪,这将会使我们的模糊测试更为简单。
2.是否可以自己对源码进行编译?
当你能够从源代码构建软件时,AFL的效果最好。虽然AFL支持与QEMU一起对二进制文件进行黑盒测试,但是这样做往往表现不太好。我认为理想状况下,我应该能够使用afl-clang-fast与afl-clang-fast++对项目进行编译。
3.所测项目是否有合适的测试用例?
在测试的时候,可能需要对文件格式进行fuzzing,如果有一些作为种子的独特和有趣的测试用例,对于我们来说将会是一个很好的开始。如果项目有一些单元测试和测试用例,这也将是一个巨大的好处。
如果你刚刚开始进行测试,思考这些问题将会节省很多时间。
yaml-cpp项目
我们应该如何寻找满足上述要求的软件?Github是一个最好的地方,在Github上可以很轻易的搜索到最近更新的,用C,C++写的项目。例如,在Github上搜索超过200星的C++项目带领我们找到一个具有很大希望的项目:yaml-cpp(https://github.com/jbeder/yaml-cpp/)。看看这个项目是否满足我们之前提到的3个问题:
1.是否能自己编译?
yaml-cpp使用cmake来构建其系统。所以我们可以定义我们要使用的编译器,afl-clang-fast++将是个很好的选择。在yaml-cpp的README中有一个有趣的注释是,它默认构建一个静态库,这对我们来说是完美的,因为我们希望对AFL一个静态编译和检测的二进制文件进行模糊。
2.是否有可用的源码?
在yaml-cpp项目的util文件夹中,有几个cpp文件,用来展示yaml-cpp库的几个特定的功能。我们主要对parse.cpp这个文件比较感兴趣。因为parse.cpp文件可以从命令行获取输入,我们可以很容易的使用AFL的持久模式对它进行模糊测试。
3.所测项目是否有合适的测试用例?
在yaml-cpp项目的test文件夹中,有一个specexamples.h的文件,其中包含了需过yaml的测试用例,每一个测试用例似乎都对应ymal-cpp库的一段代码。
这些条件都已经满足了,下面,我们开始对yaml-cpp项目进行测试。
开始fuzz
安装afl,就不多说了。
安装llvm环境:
sudo apt-get install clang llvm-dev llvm
sudo apt-get install clang-3.4
打开afl文件夹中的llvm_mode文件夹,执行make命令,编译afl-clang-fast and afl-clang-fast++。
下载yaml-cpp。使用afl对其进行编译:
git clone https://github.com/jbeder/yaml-cpp.git
cd yaml-cpp
mkdir build
cd build
cmake -DCMAKE_CXX_COMPILER=afl-clang-fast++ ..
make
编译成功后,对代码进行一点修改,这样可以提高afl的速度。在yaml-cpp/util/parse.cpp文件中,我们可以使用持续模式的AFL技巧来更新main()函数。
if (argc > 1) {
std::ifstream fin;
fin.open(argv[1])
parse(fin);
} else {
parse(std::cin);
}(修改前的main()函数)
对于这个简单的main()函数,我们可以修改它的if,else语句,增加一个while循环,以及一个叫做__AFL_LOOP()的AFL函数,我们将__AFL_LOOP()函数的参数设置为1000,意思是告诉AFL在一个进程中fuzz1000个测试用例之后,再新开一个进程执行相同的工作。修改完成后加入build目录重新编译,执行make命令。
if (argc > 1) {
std::ifstream fin;
fin.open(argv[1]);
parse(fin);
} else {
while (__AFL_LOOP(1000)) {
parse(std::cin);
}
}(修改后的main()函数)
测试二进制
随着二进制编译完成,我们可以使用AFL的afl-showmap工具来进行测试。afl-showmap工具将会运行一个给定的二进制给定的插桩的二进制程序(通过stdin接收的任意输入通过stdin传递给插桩好的二进制程序),并打印在程序执行期间它看到的反馈的报告。
通过将输入更改为应该执行新代码路径的内容,您应该可以看到在报告结束时报告的元组数量增加或减少。
可以看到,发送一个简单的YAML key(hi)只表示1787个元组的反馈,但是具有value的YAML key(hi:blah)表示2268个元组的反馈。 我们应该很好地去使用插桩的二进制程序,现在我们需要测试用例来生成我们fuzzing需要的testcases。
用高品质测试用例进行播种
你所给定的初始的testcases是非常重要的,这将关系到我们在fuzz是否能获取一些好的crashes。就像之前提到过的,yaml-cpp项目中specexamples.h中包含了一些很好的测试用例,为对它们进行了些整理,使之更方便我们测试,链接如下:https://github.com/brandonprry/yaml-fuzz/tree/master/raw_testcases
AFL中有两个工具用来确保:
1.测试库中的文件应保证唯一性。
2,每一个测试文件都应尽可能高效的保证其唯一的代码路径。
这两个工具,就是afl-cmin和afl-tmin。afl-cmin工具需要给定一个包含测试用例的文件夹,然后会运行每一个测试用例,并将收到的反馈进行对比,找到其中能最高效的保证其唯一代码路径的测试用例,并保存到一个新的目录中去。(对测试用例去重)
afl-tmin工具,则只能作用在特定的文件上。当我们进行fuzzing时,不想浪费CPU来处理一些相对于测试用例所表示的代码路径来说没有用bit和byte。为了使每一个test case达到表示与原始测试用例相同的代码路径所需的最小值,afl-tmin遍历测试用例的实际字节,逐步删除很小的数据块,直到删除任意字节都会影响到代码路径表示。(压缩测试用例的大小)
下面看一个实例,在之前给的测试用例中,我们对2这个文件进行操作:
./afl-tmin -i 2 -o 2.min -- /home/nana/yaml-cpp/build/util/parse
cat 2
cat 2.min
这是一个很好的例子,说明了AFL的强大。AFL不知道YAML是什么,但是它能有效地清除所有不是用来表示键值对的特殊YAML字符的所有字符。它通过确定改变这些特定字符将会改变从插桩二进制程序得到的反馈,来保留它们。它也从原始文件中删除4个不影响代码路径表示的字节,节省CPU的浪费。
为了快速最小化启动测试语料库,我通常使用快速for循环将每个文件最小化为一个具有.min特殊文件扩展名的新文件。
for i in *; do afl-tmin -i $i -o $i.min -- ~/parse; done;
mkdir ~/testcases && cp *.min ~/testcases
这个for循环将会遍历该目录下的每一个文件,并使用afl-tmin来使它达到最小化为一个名字相同,多了.min扩展名的文件。这样我可以把*.min复制到我用来作为AFL的种子的文件夹。
开始fuzzers
大部分fuzzing演示都是到这里就结束了,但是在我这里,真正的内容才刚刚开始。现在我们已经有了高质量的测试集,让我们开始吧!
AFL有两种类型的fuzzing策略,一种是确定性的,一种是随机的、混乱的。当开始afl-fuzz实例时,你可以指定fuzz实例要遵循策略的类型。一般来说,你只需要一个确定性的(主)fuzzer,但是你可以有很多随机(从)fuzzer。如果你之前使用过AFL,并且不知道这是在说什么,那你之前可能只运行了一个afl-fuzz实例。如果没有指定fuzzing策略,afl-fuzz实例将会在每个策略间来回切换。
screen afl-fuzz -i testcases/ -o syncdir/ -M fuzzer1 -- ./parse
screen afl-fuzz -i testcases/ -o syncdir/ -S fuzzer2 -- ./parse
首先,请注意我们如何在screen(linux命令)会话中启动每个实例。这允许我们连接和断开连接到运行fuzzer的screen会话,所以我们不会意外关闭运行afl-fuzz实例的终端!还要注意在每个相应的命令中使用的参数-M和-S。通过传递-M fuzzer1参数给afl-fuzz,我告诉它是一个master
fuzzer(使用确定性策略),并且fuzz实例的名称是fuzzer1。另一方面,传递给第二个命令的-S fuzzer2参数,说明要使用随机,混乱的策略运行实例,名称为fuzzer2。 这两个模糊器将相互工作,当新的代码路径被找到时,来回传递新的测试用例。
什么时候停止测试与修剪用例
一般我们等到master fuzzer完成了一次循环之后,选择停止,这时候Slave fuzzer一般已经运行了许多循环了。在fuzzing过程中,AFL会创建一个包含新测试用例的巨大的语料库,其中可能仍然存在bugs。我们一个尽可能的缩小这个语料库,然后重新设置测试用例,再继续运行。
观察上图发现,fuzzer1已经运行了一个周期。停止afl-fuzz实例,合并和最小化每个实例的队列,再重新启动fuzzing。当使用多个fuzzing实例运行时,AFL将在根目录的syncdir目录里,根据传给afl-fuzz的参数(fuzzer的名称),为每个fuzzer维护一个独立的、同步目录。每个单独的fuzzer,syncdir目录都包含一个队列queue目录,其中包含AFL能够生成的所有导致新的代码路径被检测出来的测试用例。
我们需要合并每个fuzz实例的队列目录,但是因为其中会有很多重叠,需要最小化这个新的测试数据集。
cd syncdir
mkdir queue_all
afl-cmin -i queue_all/ -o queue_cmin -- ~/parse
一旦我们通过afl-cmin运行生成的队列,我们需要最小化每个结果文件,以使我们不在我们不需要的字节上浪费CPU周期。然而,现在我们有比最小化初始test cases多一些的文件。一个用于最小化数千个文件的简单for循环很可能需要几天。随着时间的推移,我写了一个小bash脚本,称为afl-ptmin,它将afl-tmin并行化到一定数量的进程中,并证明在最小化过程中显著地提升了速度。
#!/bin/bash
cores=$1
inputdir=$2
outputdir=$3
pids=""
total=`ls $inputdir | wc -l`
for k in `seq 1 $cores $total`
do
for i in `seq 0 $(expr $cores - 1)`
do
file=`ls -Sr $inputdir | sed $(expr $i + $k)"q;d"`
echo $file
afl-tmin -i $inputdir/$file -o $outputdir/$file -- ~/parse &
done
wait
done
它的用法很简单,只需要三个参数:启动进程的数量,要最小化test cases的目录,以及写入最小化test cases的输出目录。
./afl-ptmin 8 ./queue_cmin/ ./queue/
完成这些操作后,我们即可以使用最小的最小化队列queue进行fuzzing。
筛选崩溃信息
在fuzzing的生命周期里,另一个很无聊的工作就是对crasher进行筛选。
有一个由@rantyben编写的工具crashwalk(https://github.com/bnagy/crashwalk)。它会自己调用gdb和一个特殊的gdg插件来快速确定那些崩溃可能导致漏洞。虽然不能完全依赖与它,但在应该首先关注那些crasher上给能开了个好头。安装比较直接,但需要一些依赖库:
apt-get install gdb golang
mkdir src
cd src
git clone https://github.com/jfoote/exploitable.git
cd && mkdir go
export GOPATH=~/go
go get -u github.com/bnagy/crashwalk/cmd/…
crashwalk安装在~/go/bin/中,我们可以自动分析文件,看它们是否能导致可利用的bugs。
~/go/bin/cwtriage -root syncdir/fuzzer1/crashes/ -match id -~/parse @@
确定有效性和代码覆盖率
寻找crasher是一项有趣的事,但是不能对二进制中的代码路径进行量化,能就像在黑暗中拍照一样,什么也做不了,只能希望有个好结果。通过确定那些代码中你没有到达的地方,你可以调整你的测试用例从而达到那些代码。
一个很好的工具是afl-cov(https://github.com/mrash/afl-cov),可以很好的帮助你解决这个确切的问题。当你找到新的路径,它会观察你的fuzz目录,并立即运行testcase来寻找任何新的代码库覆盖你可能已经击中。 它使用lcov实现这一点,所以我们必须使用一些特殊的选项重新编译parse二进制文件,然后继续。
cd ~/yaml-cpp/build/
rm -rf ./*
cmake -DCMAKE_CXX_FLAGS="-O0 -fprofile-arcs -ftest-coverage" \
-DCMAKE_EXE_LINKER_FLAGS="-fprofile-arcs -ftest-coverage" ..
make
cp util/parse ~/parse_cov
# screen afl-cov/afl-cov -d ~/syncdir/ --live --coverage-cmd "~/parse_cov AFL_FILE" --code-dir ~/yaml-cpp/
一旦完成,afl-cov在syncdir目录下的名为cov的目录中生成报告信息。 其中包括可以在Web浏览器中轻松查看的HTML文件,详细说明命中了哪些函数和哪行代码,以及未命中的函数和代码行。