AFL (American fuzzy lop) 二进制程序模糊测试工具学习
AFL前言AFL的安装AFL运行界面介绍fuzzing -- 有源码的程序fuzzing -- 无源码的程序总结前言 在学习了一段时间的pwn后,我个人对漏洞挖掘也是充满了极大的兴趣,但是真实环境中的漏洞挖掘和CTF中的pwn题还是有很大区别的。原因在于,CTF中的pwn题代码量少,实现逻辑并不复杂,存在的漏洞也是比较明显的,一般都是通过代码审计就能发现;而在真实环境中,代码量大,实现逻辑复杂,
前言
在学习了一段时间的pwn后,我个人对漏洞挖掘也是充满了极大的兴趣,但是真实环境中的漏洞挖掘和CTF中的pwn题还是有很大区别的。原因在于,CTF中的pwn题代码量少,实现逻辑并不复杂,存在的漏洞也是比较明显的,一般都是通过代码审计就能发现;而在真实环境中,代码量大,实现逻辑复杂,虽然造成漏洞的代码可能和pwn题相差不大,但是在庞大的代码量下,通过代码审计的方式来发现漏洞并不是一个好的方式。因此,在工业界出现了fuzzing即模糊测试,其原理是变异输入来对程序进行测试,记录可以造成crash的输入,之后再单独分析这些输入。另外,对程序进行fuzzing来发现漏洞的软件叫fuzzer。
在本篇博客中要介绍的AFL就是fuzzer中的一个里程碑标志,其出现在2013年,并对之后出现的fuzzer产生了重大影响。AFL是基于代码路径覆盖率的,采用二进制插桩实现,可以分为有源码插桩和无源码插桩,下面介绍AFL的使用也是从这两个方向出发。
AFL的安装
AFL
安装的话从上面的官方链接点进去,如下截图所示,点击最新源代码打包工具的链接即可获得最新的源代码安装包。下载并解压成功后,进入到该文件夹中,使用make
命令进行安装,如果安装过程中出现问题,可以查阅docs/INSTALL
获得一些提示,看看是否有帮助,要不就百度一下。
AFL运行界面介绍
接下来看下AFL工作时的界面情况,如下截图所示,展示了AFL进行fuzzing时的工作界面状态。界面状态中的大部分内容都可以直接根据其中文意思知晓其含义,在本篇博客中将会简单介绍下大部分的状态指示。
- process timing – 指示了fuzzing测试的时间消耗。
一般说来一个中等程序的测试会需要数天或者数周的时间。其下面的四个状态栏信息按照中文翻译理解就行,值得说明的是,第二个状态指示last new path
,如果在程序开始测试的几分钟内没有变化,说明要么是程序引入不正确,或者是测试用例输入不正确,亦或者设置的内存太小,总之fuzzing没有正确启动。当然AFL会在last new path
长时间没有变化时给出警告。当然其实也有可能程序确实过于简单,比如我们自己写的测试程序,没有分支语句也会造成上诉情况。 - overall results – 汇总了fuzzing测试的执行结果。
第一个状态栏cycles done
表明fuzzing的轮数,其颜色值得说明一下,最开始是用品红色表示其处于the first pass
(关于the first pass
下一个小节会解释),正如截图中所示。如果在the first pass
后有新的发现,进入子过程,颜色会变成黄色,所有子过程完成后将会变成蓝色,最后变成绿色的话表明已经长时间没有新的动作了,此时也提示我们应该手动ctrl-c去关闭fuzzing。 - cycle progress – 展示了当前队列中fuzzer的位置,以及放弃了的超时输入。
- map coverage – 展示了程序的覆盖率。
- stage progress – 进一步展示了fuzzer的执行过程细节。
now trying
指明当前所用的变异输入的方法,exec speed
指明测试用例的执行速度,正常情况下是超过500 exec/sec,长时间低于100的话,建议最好查看perf_tips.txt
来寻求优化。 - findings in depth – 展示了一些路径、crash的信息。
- fuzzing strategy yields – 进一步展示了AFL所做的工作,采用的策略情况。
- path geometry – 汇总了路径测试的相关信息。
levels
表示测试等级,pending
表示还没有经过fuzzing的输入数量,pend fav
表明fuzzer感兴趣的输入数量,own finds
表示在fuzzing过程中新找到的,或者是并行测试从另一个实例导入的,imported
中的n/a
表明不可用,即没有导入。最后说明一下stability
,表明相同输入是否产生了相同的行为,一般结果都是100%,如果低于100%并且变红,则需要查阅官方文档寻找解决步骤。
AFL执行阶段介绍
这里补充一下AFL各阶段的工作任务,也就是对应运行界面中stage progress
的now trying
一栏。
-
calibration – 在fuzzing测试之前的阶段,主要是检查执行路径检测异常,建立基线执行速度。
-
trim L/S – 同样也是在fuzzing测试之前的阶段,修建测试用例使其更短,但保证裁剪后仍能达到相同的执行路径。L表示length长度,S表示stepover步距,其值与文件大小是相关的。
-
bitflip L/S – 确定性的比特位翻转。以S为增量,L长度的bit数被翻转。有以下几种变型模式:1/1, 2/1, 4/1, 8/8, 16/8, 32/8。
-
arith L/8 – 确定性的算术运算。AFL会尝试去减去或者加上一些整数使其为8bit/16bit/32bit的值,步距永远是8bits。
-
interest L/8 – 确定性的值覆盖。AFL自身保留了一些Interesting的8bit/16bit/32bit的值,用这些值去覆盖原有的测试用例,步距永远是8bits。
-
extras – 确定性的字典注入。这里AFL自身有一个字典,当然也可以使用
-x
选项来指明使用用户提供的字典。 -
havoc – 固定长度的堆叠随机扭曲。该阶段会尝试位翻转,用随机数或者Interesting的整数去覆盖,块删除,块复制,以及字典的相关操作。
-
splice – 最后一种策略。在上述策略都执行完后将会执行该策略,它和havoc差不多,不过它会首先将队列中的两个随机输入先拼接在一起。
-
sync – 这个是并行执行的策略选项,通过
-M
或者-S
选项进行指定。该策略并不会涉及到真正的fuzzing,会导入从另一个fuzzer得到的输出和测试用例。
以上所有策略执行一遍也就是前文提到的the first pass
。
fuzzing – 有源码的程序
接下来介绍AFL如何fuzzing有源码的程序。对于有源码的程序,先使用afl的编译器对源码进行插桩编译,编译c程序使用afl-gcc
,编译c++程序使用afl-g++
。
$ afl-gcc test.c -o test
$ afl-g++ test.cpp -o test
然后使用afl-fuzz
进行测试,命令行如下,-i
指明测试用例的目录,-o
指明测试结果的存放目录。对于直接从终端获取输入的程序来说,我们需要在testcase_dir
目录下新建一个文件,文件的内容就是程序的输入,文件命名不唯一,后面会给一个示例。
$ ./afl-fuzz -i testcase_dir -o findings_dir /path/to/program [...params...]
对于以文件作为输入的程序,可以使用下面的命令,直接在程序参数位置处使用@@
,AFL将会自动将其替换为测试用例中的文件名。
$ /afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@
以afl-2.52b
版本为例,用下面的代码作为示例,可以看到代码中存在一个明显的栈溢出漏洞,另外之所以要写个if判断,主要是防止程序太过于简单,如果程序没有多条路径分支,last new path
状态栏就不会更新,提示我们进行检查。
// test.c
#include <stdio.h>
int main() {
char buf[100] = {0};
gets(buf); // stack overflow
if (buf[0] == 'A')
printf("hello\n");
else
printf("NO A\n");
return 0;
}
按照下面的命令操作进行,fuzz_out文件夹会在fuzzing时自动生成。
$ mkdir fuzz_in
$ echo "hello" > fuzz_in/testcase
$ afl-gcc test.c -o test
$ afl-fuzz -i fuzz_in -o fuzz_out ./test
如下截图所示,展示了fuzzing测试的结果,由于程序比较简单,在fuzzing几分钟后就已经发现了2个unique crashes,之所以要等半个小时,主要看看啥时候跑到蓝色指示轮数。
之后我们手动ctrl-c终止fuzzing,看看fuzz_out文件下包含哪些内容,如下截图所示:
- queue – 存放fuzzer生成的所有不同执行路径的测试用例+我们自己一开始构造的测试用例;
- crashes – 存放造成程序崩溃的测试用例,根据产生的信号不同进行分类;
- hangs – 存放造成程序超时的测试用例;
- 剩下的文件记录了fuzzer工作时的一些信息。
最后我们重点关注的就是crashes下的测试用例,结合我们前面对代码的审计,只有一个栈溢出漏洞,那么这里产生的两个crash是怎么回事?如下截图所示,是两个测试用例的十六进制表示,其实都是栈溢出,只不过第一个用例首字母不是A
,所以和第二个用例最终执行的路径不同,因此AFL认定存在两个不同的crash。
fuzzing – 无源码的程序
接下来看看如何fuzzing无源码的程序,AFL依赖QEMU实现了这个功能,qemu是一个仿真器。下载AFL的源码后可以很方便的通过其自带脚本完成安装,如下命令所示,执行该命令后会去下载指定版本的qemu然后安装:
$ cd qemu_mode
$ ./build_qemu_support.sh
在执行该脚本的过程中可能会遇到一些问题,读者可以百度或者到stackoverflow上查找相关解决方案,或者在评论区留言,大家可以一起讨论。成功安装后,我们可以执行如下命令来测试无源码的程序。这里和前面的操作大同小异,也是先建立测试文件夹和测试用例,源代码文件也是和前面一样,只不过编译时我们用gcc
,这样就有了无源码的测试程序,然后测试时指定-Q
参数。
$ mkdir fuzz_in
$ echo "hello" > fuzz_in/testcase
$ gcc test.c -o test
$ afl-fuzz -i fuzz_in -o fuzz_out -Q ./test
如下截图所示,是无源码的程序fuzzing运行截图,一般有源码插桩的程序测试速度比无源码的快2-5倍,从截图中我们也能看到exec speed只有1000/sec,而上面有源码插桩的接近5000/sec。
最后看下执行结果,同样也是检测出了两个unique crashes,如下截图所示,虽然两个测试用例的结果长度不相同,但都是检测出的栈溢出漏洞,和前面介绍的一样,第二个用例由于首字母是A
,所以执行路径不同,标记为不同的crash。
总结
不忘初心,砥砺前行!
更多推荐
所有评论(0)