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程序如kelincijava-afl等,但并不知道效果如何)

2. 是否开源

AFL既可以对源码进行编译时插桩,也可以使用AFL的QEMU mode对二进制文件进行插桩,但是前者的效率相对来说要高很多,在Github上很容易就能找到很多合适的项目。

3. 程序版本

目标应该是该软件的最新版本,不然辛辛苦苦找到一个漏洞,却发现早就被上报修复了就尴尬了。

4. 是否有示例程序、测试用例

如果目标有现成的基本代码示例,特别是一些开源的库,可以方便我们调用该库不用自己再写一个程序;如果目标存在测试用例,那后面构建语料库时也省事儿一点。

5.项目规模

某些程序规模很大,会被分为好几个模块,为了提高Fuzz效率,在Fuzzing前,需要定义Fuzzing部分。这里推荐一下源码阅读工具Understand,它treemap功能,可以直观地看到项目结构和规模。比如下面ImageMagick的源码中,灰框代表一个文件夹,蓝色方块代表了一个文件,其大小和颜色分别反映了行数和文件复杂度。

img

6. 程序曾出现过漏洞

如果某个程序曾曝出过多次漏洞,那么该程序有仍有很大可能存在未被发现的安全漏洞。如ImageMagick每个月都会发现难以利用的新漏洞,并且每年都会发生一些具有高影响的严重漏洞。

构建语料库

AFL需要一些初始输入数据(也叫种子文件)作为Fuzzing的起点,这些输入甚至可以是毫无意义的数据,AFL可以通过启发式算法自动确定文件格式结构。lcamtuf就在博客中给出了一个有趣的例子——对djpeg进行Fuzzing时,仅用一个字符串”hello”作为输入,最后凭空生成大量jpge图像!

尽管AFL如此强大,但如果要获得更快的Fuzzing速度,那么就有必要生成一个高质量的语料库,这一节就解决如何选择输入文件、从哪里寻找这些文件、如何精简找到的文件三个问题。

1. 选择

(1) 有效的输入

尽管有时候无效输入会产生bug和崩溃,但有效输入可以更快的找到更多执行路径。

(2) 尽量小的体积

较小的文件会不仅可以减少测试和处理的时间,也能节约更多的内存,AFL给出的建议是最好小于1 KB,但其实可以根据自己测试的程序权衡,这在AFL文档的perf_tips.txt中有具体说明。

2. 寻找

  1. 使用项目自身提供的测试用例
  2. 目标程序bug提交页面
  3. 使用格式转换器,用从现有的文件格式生成一些不容易找到的文件格式:
  4. afl源码的testcases目录下提供了一些测试用例
  5. 其他大型的语料库
  6. afl generated image test sets
  7. fuzzer-test-suite
  8. libav samples
  9. ffmpeg samples
  10. fuzzdata
  11. moonshine

3. 修剪

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

img

(2) 减小单个输入文件的大小——AFL-TMIN

整体的大小得到了改善,接下来还要对每个文件进行更细化的处理。afl-tmin缩减文件体积的原理这里就不深究了,有机会会在后面文章中解释,这里只给出使用方法(其实也很简单,有兴趣的朋友可以自己搜一搜)。

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

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

img

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

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

img

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

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

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

img

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

img

搭建AFL++环境

搭建实验环境

获取 & 编译AFL++

本节仅用于在 ubuntu/debian 环境下自行配置AFL++环境

编译方式也可见官网文档 https://github.com/AFLplusplus/AFLplusplus/blob/stable/docs/INSTALL.md

  1. 获取AFL++
1
2
# 下载AFLplusplus源代码
git clone https://github.com/AFLplusplus/AFLplusplus.git --depth 1
  1. 安装 llvm 和 clang。

AFL++在llvm模式下使用afl-clang-fast具有更多的特性。

运行如下命令,安装llvm、clang和相关依赖

1
2
3
4
5
6
# 11, 12 版本可能会出现一些问题
# https://github.com/llvm/llvm-project/issues/55785
sudo apt install -y llvm-13 clang-13 llvm-13-dev

# 安装依赖包
sudo apt-get install -y build-essential python3-dev flex bison libglib2.0-dev libpixman-1-dev python3-setuptools

将llvm-config指向已经安装版本::

1
2
# 如果是自行安装的其他llvm版本,此处需要对应修改命令末尾的llvm-config-xx为本地的版本
sudo update-alternatives --install /usr/bin/llvm-config llvm-config /usr/bin/llvm-config-13 13

可以测试如下命令,如果有版本号输出,则配置完成:

1
llvm-config --version
  1. 编译AFL++

进入第一步下载AFLplusplus的源代码目录cd AFLplusplus, 而后运行如下编译命令:

1
2
3
4
# 编译可能需要一些时间,建议拉高虚拟机的内存
make afl-fuzz
make afl-showmap
make llvm

在AFLplusplus目录中存在afl-fuzz以及afl-cc说明基础的编译已经完成;存在afl-clang-fast说明llvm模式下的AFL++编译已经完成。

1
2
3
4
5
6
7
$ ls | grep "afl-"
afl-cc # <-
...
afl-clang-fast # <-
...
afl-fuzz
...

此外,还可以运行sudo make install,将编译后的二进制复制到PATH能查找到的位置,此时就不用再执行接下来的第四步配置了。

  1. afl-fuzzafl-clang-fast等加入PATH中,方便后续使用:

对于自行配置的环境,如果当前使用的默认shell是bash,则将下一行的命令添加到~/.bashrc文件的其他位置,如果使用的是zsh,则添加到~/.zshrc文件中的任意位置。

注意修改命令中/path/to/AFLplusplus目录的位置为绝对路径

1
export PATH=/path/to/AFLplusplus:$PATH

完成配置后,在新的终端测试afl-fuzz命令,会有如下类似的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ afl-fuzz
afl-fuzz++4.06c based on afl by Michal Zalewski and a large online community

afl-fuzz [ options ] -- /path/to/fuzzed_app [ ... ]

Required parameters:
-i dir - input directory with test cases
-o dir - output directory for fuzzer findings

Execution control settings:
-p schedule - power schedules compute a seed's performance score:
fast(default), explore, exploit, seek, rare, mmopt, coe, lin
quad -- see docs/FAQ.md for more information
-f file - location read by the fuzzed program (default: stdin or @@)
-t msec - timeout for each run (auto-scaled, default 1000 ms). Add a '+'
...

编译带有AddressSanitizer的libxml2

这里的libxml2可以改为其他源码

  1. 获取libxml2的源代码
1
2
3
# fetch libxml2 source code
wget http://xmlsoft.org/download/libxml2-2.9.4.tar.gz
tar -xf libxml2-2.9.4.tar.gz
  1. 编译libxml2
1
2
3
4
5
6
7
8
9
10
cd libxml2-2.9.4

# install dependency
sudo apt install automake

# config it
CC=afl-clang-fast CXX=afl-clang-fast++ ./configure --with-debug --disable-shared -without-ftp --without-http --without-legacy --without-python

# build it
AFL_USE_ASAN=1 make -j `nproc`
  1. 编译完成后,运行如下命令,如果有类似T __asan_report_error的输出,则表示编译出的xmllint二进制文件已经带有ASan的插桩。
1
nm xmllint | grep __asan_report_error

Fuzzing libxml2

  1. 创建一个工作目录 mkdir work
  2. 为了提高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

  1. 创建初始语料库(可以寻找更好的xml初始种子,此处仅提供样例)

tips: 可以在libxml2源代码中的测试样例中找到更好的测试语料。

1
git clone https://gitee.com/ret2happy/libxml2_sample.git corpus
  1. 创建一个主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 noneASan开启时需要大量虚拟内存,此处让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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

american fuzzy lop ++4.07c {slave1} (../libxml2-2.9.4/xmllint) [fast]
┌─ process timing ────────────────────────────────────┬─ overall results ────┐
│ run time : 0 days, 0 hrs, 27 min, 47 sec │ cycles done : 0 │
│ last new find : 0 days, 0 hrs, 0 min, 23 sec │ corpus count : 1965 │
│last saved crash : none seen yet │saved crashes : 0 │
│ last saved hang : none seen yet │ saved hangs : 0 │
├─ cycle progress ─────────────────────┬─ map coverage┴──────────────────────┤
│ now processing : 912.1 (46.4%) │ map density : 1.66% / 7.89% │
│ runs timed out : 0 (0.00%) │ count coverage : 2.56 bits/tuple │
├─ stage progress ─────────────────────┼─ findings in depth ─────────────────┤
│ now trying : user extras (over) │ favored items : 545 (27.74%) │
│ stage execs : 2172/9000 (24.13%) │ new edges on : 797 (40.56%) │
│ total execs : 1.34M │ total crashes : 0 (0 saved) │
exec speed : 870.1/sec │ total tmouts : 3 (0 saved) │
├─ fuzzing strategy yields ────────────┴─────────────┬─ item geometry ───────┤
│ bit flips : 277/21.3k, 69/21.3k, 37/21.2k │ levels : 5 │
│ byte flips : 1/2665, 14/2632, 12/2568 │ pending : 1931 │
│ arithmetics : 137/148k, 2/8133, 0/729 │ pend fav : 534 │
│ known ints : 23/15.0k, 24/72.7k, 18/112k │ own finds : 1962 │
│ dictionary : 211/140k, 108/152k, 4/11.2k, 1/11.9k │ imported : 0 │
│havoc/splice : 910/564k, 114/12.6k │ stability : 99.91% │
│py/custom/rq : unused, unused, unused, unused ├───────────────────────┘
│ trim/eff : 9.16%/1079, 0.00% │ [cpu000: 25%]
└────────────────────────────────────────────────────┘

  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,复现模糊测试得到的内存破坏: