AFL工具探索


前言

学习一下模糊测试,其中AFL是一个非常经典的Fuzzing工具,以此入门,希望对未来的学习有所帮助。

简介

AFL(American Fuzzy Lop)是由安全研究员Michał Zalewski(@lcamtuf)开发的一款基于覆盖引导(Coverage-guided)的模糊测试工具,它通过记录输入样本的代码覆盖率,从而调整输入样本以提高覆盖率,增加发现漏洞的概率。其工作流程大致如下:

  • 从源码编译程序时进行插桩,以记录代码覆盖率(Code Coverage);
  • 选择一些输入文件,作为初始测试集加入输入队列(queue);
  • 将队列中的文件按一定的策略进行“突变”;
  • 如果经过变异文件更新了覆盖范围,则将其保留添加到队列中;
  • 上述过程会一直循环进行,期间触发了crash的文件会被记录下来。

1.jpg

Fuzzing处理

测试目标

  • 模糊测试的目标通常是接受外部输入的程序或库,输入一般来自控制台或文件

  • 目标范围

    AFL主要用于C/C++程序的测试。

    也有一些基于AFL的JAVA Fuzz程序如kelincijava-afl等,但并不知道效果如何

  • 目标要求

    可以编译源码时插装,也可以通过QEMU mode对二进制文件动态插桩,前者测试速度比后者快很多。

构建语料库

AFL需要一些初始输入数据(也叫种子文件)作为Fuzzing的起点,这些输入甚至可以是毫无意义的数据,AFL可以通过启发式算法自动确定文件格式结构。

尽管AFL如此强大,但如果要获得更快的Fuzzing速度,那么就有必要生成一个高质量的语料库

主要围绕三个问题

  • 如何选择输入文件

    • 有效的输入,覆盖更多路径。
    • 尽量小的体积,减少测试和处理的时间。
  • 从哪里寻找这些文件

  • 如何精简找到的文件三个问题

    网上找到的一些大型语料库中往往包含大量的文件,这时就需要对其精简,这个工作有个术语叫做——语料库蒸馏(Corpus Distillation)。AFL提供了两个工具来帮助我们完成这部工作——afl-cminafl-tmin

    (1) 移除执行相同代码的输入文件——afl-cmin

    afl-cmin的核心思想是:尝试找到与语料库全集具有相同覆盖范围的最小子集。举个例子:假设有多个文件,都覆盖了相同的代码,那么就丢掉多余的文件。其使用方法如下:

    $ afl-cmin -i input_dir -o output_dir -- /path/to/tested/program [params]

    更多的时候,我们需要从文件中获取输入,这时可以使用“@@”代替被测试程序命令行中输入文件名的位置。Fuzzer会将其替换为实际执行的文件:

    $ afl-cmin -i input_dir -o output_dir -- /path/to/tested/program [params] @@

    下面的例子中,我们将一个有1253个png文件的语料库,精简到只包含60个文件。
    4.jpg

    (2) 减小单个输入文件的大小——afl-tmin

    整体的大小得到了改善,接下来还要对每个文件进行更细化的处理,afl-tmin缩减文件体积的方法如下。

    afl-tmin有两种工作模式,instrumented modecrash mode。默认的工作方式是instrumented mode,如下所示:

    $ afl-tmin -i input_file -o output_file -- /path/to/tested/program [params] @@

    5.jpg如果指定了参数-x,即crash mode,会把导致程序非正常退出的文件直接剔除。

    $ afl-tmin -x -i input_file -o output_file -- /path/to/tested/program [params] @@

    6.jpg

    afl-tmin接受单个文件输入,所以可以用一条简单的shell脚本批量处理。如果语料库中文件数量特别多,且体积特别大的情况下,这个过程可能花费几天甚至更长的时间!

    for i in *; do afl-tmin -i $i -o tmin-$i -- ~/path/to/tested/program [params] @@; done;

    下图是经过两种模式的修剪后,语料库大小的变化:
    7.jpg

    这时还可以再次使用afl-cmin,发现又可以过滤掉一些文件了。
    8.jpg

构建测试程序

AFL从源码编译程序时进行插桩,来记录代码覆盖率。

有两种模式

  • afl-gcc

    通过GCC构建

  • afl-clang

    通过LLVM构建 Fuzzing速度更快

开始Fuzzing

afl-fuzz是主要测试程序

对那些可以直接从stdin读取输入的目标程序来说,语法如下:
./afl-fuzz -i testcase_dir -o findings_dir /path/to/program […params…]
对从文件读取输入的目标程序来说,要用“@@”,语法如下:
./afl-fuzz -i testcase_dir -o findings_dir /path/to/program […params…] @@

  • 白盒测试

    编译完程序后,可以选择使用afl-showmap跟踪单个输入的执行路径,并打印执行的输出,捕获的元组tuple等。

    tuple用于获取分支信息,从而计算程序覆盖率。

  • 黑盒测试

    针对二进制程序,需要增加-Q选项。

Fuzzing结果分析

Fuzzing之后我们还需要考虑以下问题

  • 何时结束Fuzzing工作
  • afl-fuzz生成了哪些文件
  • 如何对产生的crash进行验证和分类
  • 用什么来评估Fuzzing的结果
  • 代码覆盖率及相关概念
  • AFL是如何记录代码覆盖率的

Fuzzing工作状态

因为afl-fuzz永远不会停止,所以何时停止测试很多时候就是依靠afl-fuzz提供的状态来决定的。除了前面提到过的通过状态窗口、afl-whatsup查看afl-fuzz状态外,这里再补充几种方法。

  • afl-stat

    afl-stat是afl-utils这套工具AFL辅助工具中的一个(这套工具中还有其他更好用的程序,后面用到时会做介绍),该工具类似于afl-whatsup的输出结果。

    使用前需要一个配置文件,设置每个afl-fuzz实例的输出目录:

    {
        "fuzz_dirs": [
            "/root/syncdir/SESSION000",
            "/root/syncdir/SESSION001",
            ...
            "/root/syncdir/SESSION00x"
        ]
    }

    然后指定配置文件运行即可:

    $ afl-stats -c afl-stats.conf
    [SESSION000 on fuzzer1]
     Alive:   1/1
     Execs:   64 m
     Speed:   0.3 x/s
     Pend:    6588/249
     Crashes: 101
    [SESSION001 on fuzzer1]
     Alive:   1/1
     Execs:   105 m
     Speed:   576.6 x/s
     Pend:    417/0
     Crashes: 291
    ...
  • afl-whatsup

    afl-whatsup是依靠读afl-fuzz输出目录中的fuzzer_stats文件来显示状态的,每次查看都要需要手动执行,十分麻烦。因此可以对其进行修改,让其实时显示fuzzer的状态。方法也很简答,基本思路就是在所有代码外面加个循环就好,还可以根据自己的喜好做些调整:

  • afl-plot

    可视化结果

  • pythia

    一个拓展项目

结束测试

检查afl-fuzz工作状态的目的是为何时停止测试提供依据,通常来说符合下面几种情况时就可以停掉了。

(1)状态窗口中”cycles done”字段颜色变为绿色该字段的颜色可以作为何时停止测试的参考,随着周期数不断增大,其颜色也会由洋红色,逐步变为黄色、蓝色、绿色。当其变为绿色时,继续Fuzzing下去也很难有新的发现了,这时便可以通过Ctrl-C停止afl-fuzz。

6.jpg

(2)距上一次发现新路径(或者崩溃)已经过去很长时间了,至于具体多少时间还是需要自己把握,比如长达一个星期或者更久估计大家也都没啥耐心了吧。

7.jpg

(3)目标程序的代码几乎被测试用例完全覆盖,这种情况好像很少见,但是对于某些小型程序应该还是可能的,至于如何计算覆盖率将在下面介绍。

(4)上面提到的pythia提供的各种数据中,一旦path covera达到99%(通常来说不太可能),如果不期望再跑出更多crash的话就可以中止fuzz了,因为很多crash可能是因为相同的原因导致的;还有一点就是correctness的值达到1e-08,根据pythia开发者的说法,这时从上次发现path/uniq crash到下一次发现之间大约需要1亿次执行,这一点也可以作为衡量依据。

输出结果

afl-fuzz的输出目录中存在很多文件

  • queue:存放所有具有独特执行路径的测试用例。
  • crashes:导致目标接收致命signal而崩溃的独特测试用例。
  • crashes/README.txt:保存了目标执行这些crash文件的命令行参数。
  • hangs:导致目标超时的独特测试用例。
  • fuzzer_stats:afl-fuzz的运行状态。
  • plot_data:用于afl-plot绘图。

处理测试结果

我们跑出了一大堆的crashes,那么接下来的步骤,自然是确定造成这些crashes的bug是否可以利用,怎么利用?这是另一个重要方面。当然,个人觉得这比前面提到的内容都要困难得多,这需要对常见的二进制漏洞类型、操作系统的安全机制、代码审计和调试等内容都有一定深度的了解。但如果只是对crash做简单的分析和分类,那么下面介绍的几种方法都可以给我们提供一些帮助。

  • crash exploration mode

    通过观察crash情况产生类似样例,将一个导致crash测试用例作为afl-fuzz的输入,使用-C选项开启crash exploration模式后,可以快速地产生很多和输入crash相关、但稍有些不同的crashes,从而判断能够控制某块内存地址的长度。

  • triage_crashes

    AFL源码的experimental目录中有一个名为triage_crashes.sh的脚本,可以帮助我们触发收集到的crashes。例如下面的例子中,11代表了SIGSEGV信号,有可能是因为缓冲区溢出导致进程引用了无效的内存;06代表了SIGABRT信号,可能是执行了abort\assert函数或double free导致,这些结果可以作为简单的参考。

  • crashwalk

    当然上面的两种方式都过于鸡肋了,如果你想得到更细致的crashes分类结果,以及导致crashes的具体原因,那么crashwalk就是不错的选择之一。这个工具基于gdb的exploitable插件,安装也相对简单,在ubuntu上,只需要如下几步即可:

    $ apt-get install gdb golang
    $ mkdir tools
    $ cd tools
    $ git clone https://github.com/jfoote/exploitable.git
    $ mkdir go
    $ export GOPATH=~/tools/go
    $ export CW_EXPLOITABLE=~/tools/exploitable/exploitable/exploitable.py
    $ go get -u github.com/bnagy/crashwalk/cmd/...

    crashwalk支持AFL/Manual两种模式。前者通过读取crashes/README.txt文件获得目标的执行命令(前面第三节中提到的),后者则可以手动指定一些参数。两种使用方式如下:

    #Manual Mode
    $ ~/tools/go/bin/cwtriage -root syncdir/fuzzer1/crashes/ -match id -- ~/parse @@
    #AFL Mode
    $ ~/tools/go/bin/cwtriage -root syncdir -afl

    11.jpg

    两种模式的输出结果都一样,如上图所示。这个工具比前面几种方法要详细多了,但当有大量crashes时结果显得还是十分混乱。

  • afl-collect

    最后重磅推荐的工具便是afl-collect,它也是afl-utils套件中的一个工具,同样也是基于exploitable来检查crashes的可利用性。它可以自动删除无效的crash样本、删除重复样本以及自动化样本分类。使用起来命令稍微长一点,如下所示:

    $ afl-collect -j 8 -d crashes.db -e gdb_script ./afl_sync_dir ./collection_dir --  /path/to/target --target-opts

    但是结果就像下面这样非常直观:

    11-1.jpg

代码覆盖率及相关概念

代码覆盖率是模糊测试中一个极其重要的概念,使用代码覆盖率可以评估和改进测试过程,执行到的代码越多,找到bug的可能性就越大,毕竟,在覆盖的代码中并不能100%发现bug,在未覆盖的代码中却是100%找不到任何bug的,所以本节中就将详细介绍代码覆盖率的相关概念。

  • 代码覆盖率(Code Coverage)

    是一种度量代码的覆盖程度的方式,也就是指源代码中的某行代码是否已执行;对二进制程序,还可将此概念理解为汇编代码中的某条指令是否已执行。其计量方式很多,但无论是GCC的GCOV还是LLVM的SanitizerCoverage,都提供函数(function)、基本块(basic-block)、边界(edge)三种级别的覆盖率检测,更具体的细节可以参考LLVM的官方文档

  • 基本块(Basic Block)

    简称BB,指一组顺序执行的指令,BB中第一条指令被执行后,后续的指令也会被全部执行,每个BB中所有指令的执行次数是相同的。

    13.jpg

  • 边界(edge)

    AFL的技术白皮书中提到fuzzer通过插桩代码捕获边(edge)覆盖率。那么什么是edge呢?我们可以将程序看成一个控制流图(CFG),图的每个节点表示一个基本块,而edge就被用来表示在基本块之间的转跳。知道了每个基本块和跳转的执行次数,就可以知道程序中的每个语句和分支的执行次数,从而获得比记录BB更细粒度的覆盖率信息。

    15.jpg

  • 元组(tuple)

    AFL中用二元组记录当前基本块+前一基本块的信息。从而获取目标的执行流程和代码覆盖情况,伪代码如下:

    cur_location = <COMPILE_TIME_RANDOM>;           //用一个随机数标记当前基本块
    shared_mem[cur_location ^ prev_location]++;     //将当前块和前一块异或保存到shared_mem[]
    prev_location = cur_location >> 1;              //cur_location右移1位区分从当前块到当前块的转跳

    实际插入的汇编代码,如下图所示,首先保存各种寄存器的值并设置ecx/rcx,然后调用__afl_maybe_log,这个方法的内容相当复杂,这里就不展开讲了,但其主要功能就和上面的伪代码相似,用于记录覆盖率,放入一块共享内存中。

    16.jpg

计算代码覆盖率

这里需要用到的工具之一是GCOV,它随gcc一起发布,所以不需要再单独安装,和afl-gcc插桩编译的原理一样,gcc编译时生成插桩的程序,用于在执行时生成代码覆盖率信息。

另外一个工具是LCOV,它是GCOV的图形前端,可以收集多个源文件的gcov数据,并创建包含使用覆盖率信息注释的源代码HTML页面。

最后一个工具是afl-cov,可以快速帮助我们调用前面两个工具处理来自afl-fuzz测试用例的代码覆盖率结果。在ubuntu中可以使用apt-get install afl-cov安装afl-cov,但这个版本似乎不支持分支覆盖率统计,所以还是从Github下载最新版本为好,下载完无需安装直接运行目录中的Python脚本即可使用:

$ apt-get install lcov
$ git clone https://github.com/mrash/afl-cov.git
$ ./afl-cov/afl-cov -V
afl-cov-0.6.2

第一步,使用gcov重新编译源码,在CFLAGS中添加"-fprofile-arcs""-ftest-coverage"选项,可以在--prefix中重新指定一个新的目录以免覆盖之前alf插桩的二进制文件。

$ make clean
$ ./configure --prefix=/root/tiff-4.0.10/build-cov CC="gcc" CXX="g++" CFLAGS="-fprofile-arcs -ftest-coverage" --disable-shared
$ make
$ make install

第二步,执行afl-cov。其中-d选项指定afl-fuzz输出目录;—live用于处理一个还在实时更新的AFL目录,当afl-fuzz停止时,afl-cov将退出;–enable-branch-coverage用于开启边缘覆盖率(分支覆盖率)统计;-c用于指定源码目录;最后一个-e选项用来设置要执行的程序和参数,其中的AFL_FILE和afl中的”@@”类似,会被替换为测试用例,LD_LIBRARY_PATH则用来指定程序的库文件。

$ cd ~/tiff-4.0.10
$ afl-cov -d ~/syncdir --live --enable-branch-coverage -c . -e "cat AFL_FILE | LD_LIBRARY_PATH=./build-cov/lib ./build-cov/bin/tiff2pdf AFL_FILE"

成功执行的结果如下所示:

14.jpg

我们可以通过—live选择,在fuzzer运行的同时计算覆盖率,也可以在测试结束以后再进行计算,最后会得到一个像下面这样的html文件。它既提供了概述页面,显示各个目录的覆盖率;也可以在点击进入某个目录查看某个具体文件的覆盖率。

17.jpg

点击进入每个文件,还有更详细的数据。每行代码前的数字代表这行代码被执行的次数,没有执行过的代码会被红色标注出来。

18.jpg

参考资料


Author: Luluting
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Luluting !
  TOC