前言

  在学习了一段时间的pwn后,我个人对漏洞挖掘也是充满了极大的兴趣,但是真实环境中的漏洞挖掘和CTF中的pwn题还是有很大区别的。原因在于,CTF中的pwn题代码量少,实现逻辑并不复杂,存在的漏洞也是比较明显的,一般都是通过代码审计就能发现;而在真实环境中,代码量大,实现逻辑复杂,虽然造成漏洞的代码可能和pwn题相差不大,但是在庞大的代码量下,通过代码审计的方式来发现漏洞并不是一个好的方式。因此,在工业界出现了fuzzing即模糊测试,其原理是变异输入来对程序进行测试,记录可以造成crash的输入,之后再单独分析这些输入。另外,对程序进行fuzzing来发现漏洞的软件叫fuzzer。
  在本篇博客中要介绍的AFL就是fuzzer中的一个里程碑标志,其出现在2013年,并对之后出现的fuzzer产生了重大影响。AFL是基于代码路径覆盖率的,采用二进制插桩实现,可以分为有源码插桩和无源码插桩,下面介绍AFL的使用也是从这两个方向出发。

AFL的安装

  AFL
  安装的话从上面的官方链接点进去,如下截图所示,点击最新源代码打包工具的链接即可获得最新的源代码安装包。下载并解压成功后,进入到该文件夹中,使用make命令进行安装,如果安装过程中出现问题,可以查阅docs/INSTALL获得一些提示,看看是否有帮助,要不就百度一下。

AFL-1

AFL运行界面介绍

  接下来看下AFL工作时的界面情况,如下截图所示,展示了AFL进行fuzzing时的工作界面状态。界面状态中的大部分内容都可以直接根据其中文意思知晓其含义,在本篇博客中将会简单介绍下大部分的状态指示。

AFL-2

  • 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 progressnow 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,之所以要等半个小时,主要看看啥时候跑到蓝色指示轮数。

AFL-3
  之后我们手动ctrl-c终止fuzzing,看看fuzz_out文件下包含哪些内容,如下截图所示:

AFL-4

  • queue – 存放fuzzer生成的所有不同执行路径的测试用例+我们自己一开始构造的测试用例;
  • crashes – 存放造成程序崩溃的测试用例,根据产生的信号不同进行分类;
  • hangs – 存放造成程序超时的测试用例;
  • 剩下的文件记录了fuzzer工作时的一些信息。

  最后我们重点关注的就是crashes下的测试用例,结合我们前面对代码的审计,只有一个栈溢出漏洞,那么这里产生的两个crash是怎么回事?如下截图所示,是两个测试用例的十六进制表示,其实都是栈溢出,只不过第一个用例首字母不是A,所以和第二个用例最终执行的路径不同,因此AFL认定存在两个不同的crash。

AFL-5

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。

AFL-6
  最后看下执行结果,同样也是检测出了两个unique crashes,如下截图所示,虽然两个测试用例的结果长度不相同,但都是检测出的栈溢出漏洞,和前面介绍的一样,第二个用例由于首字母是A,所以执行路径不同,标记为不同的crash。

AFL-7

总结

不忘初心,砥砺前行!

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐