目录
  1. 1. 一 前言
  2. 2. 二 AFL简介
  3. 3. 三 选择和评估测试的目标
  4. 4. 四 构建语料库
    1. 4.1. 1. 选择
    2. 4.2. 2. 寻找
    3. 4.3. 3. 修剪
  5. 5. 五 构建被测试程序
    1. 5.1. 1. afl-gcc模式
    2. 5.2. 2. LLVM模式
  6. 6. 六 开始Fuzzing
    1. 6.1. 1. 白盒测试
      1. 6.1.1. (1) 测试插桩程序
      2. 6.1.2. (2) 执行fuzzer
      3. 6.1.3. (3) 使用screen
    2. 6.2. 2. 黑盒测试
  7. 7. 七 结束测试
    1. 7.1. 1.何时结束
    2. 7.2. 2. 输出结果
  8. 8. 八 处理测试结果
AFL漏洞挖掘

转载于 :https://www.freebuf.com/articles/system/191543.html

一 前言

模糊测试(Fuzzing)技术作为漏洞挖掘最有效的手段之一,近年来一直是众多安全研究人员发现漏洞的首选技术。AFL、LibFuzzer、honggfuzz等操作简单友好的工具相继出现,也极大地降低了模糊测试的门槛。

二 AFL简介

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

  • ①从源码编译程序时进行插桩,以记录代码覆盖率(Code Coverage);

  • ②选择一些输入文件,作为初始测试集加入输入队列(queue);

  • ③将队列中的文件按一定的策略进行“突变”;

  • ④如果经过变异文件更新了覆盖范围,则将其保留添加到队列中;

  • ⑤上述过程会一直循环进行,期间触发了crash的文件会被记录下来。

三 选择和评估测试的目标

开始Fuzzing前,首先要选择一个目标。 AFL的目标通常是接受外部输入的程序或库,输入一般来自文件(也可以Fuzzing一个网络程序)。

  1. 用什么语言编写
    AFL主要用于C/C++程序的测试,所以这是我们寻找软件的最优先规则。(也有一些基于AFL的JAVA Fuzz程序如kelinci、java-afl等)

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

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

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

  5. 项目规模
    某些程序规模很大,会被分为好几个模块,为了提高Fuzz效率,在Fuzzing前,需要定义Fuzzing部分。这里推荐一下源码阅读工具Understand,它treemap功能,可以直观地看到项目结构和规模。

  6. 程序曾出现过漏洞
    如果某个程序曾曝出过多次漏洞,那么该程序有仍有很大可能存在未被发现的安全漏洞。

四 构建语料库

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

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

1. 选择

(1) 有效的输入

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

(2) 尽量小的体积

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

2. 寻找

使用项目自身提供的测试用例

目标程序bug提交页面

使用格式转换器,用从现有的文件格式生成一些不容易找到的文件格式:

afl源码的testcases目录下提供了一些测试用例

其他开源的语料库

3. 修剪

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

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

afl-tmin有两种工作模式,instrumented mode和crash mode。默认的工作方式是instrumented mode

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

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

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

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

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

五 构建被测试程序

前面说到,AFL从源码编译程序时进行插桩,以记录代码覆盖率。这个工作需要使用其提供的两种编译器的wrapper编译目标程序,和普通的编译过程没有太大区别

1. afl-gcc模式

afl-gcc/afl-g++作为gcc/g++的wrapper,它们的用法完全一样,前者会将接收到的参数传递给后者,我们编译程序时只需要将编译器设置为afl-gcc/afl-g++就行,如下面演示的那样。如果程序不是用autoconf构建,直接修改Makefile文件中的编译器为afl-gcc/g++也行。

1
$ ./configure CC="afl-gcc" CXX="afl-g++"

在Fuzzing共享库时,可能需要编写一个简单demo,将输入传递给要Fuzzing的库(其实大多数项目中都自带了类似的demo)。这种情况下,可以通过设置LD_LIBRARY_PATH让程序加载经过AFL插桩的.so文件,不过最简单的方法是静态构建,通过以下方式实现:

1
$ ./configure --disable-shared CC="afl-gcc" CXX="afl-g++"

2. LLVM模式

LLVM Mode模式编译程序可以获得更快的Fuzzing速度,进入llvm_mode目录进行编译,之后使用afl-clang-fast构建序程序即可,如下所示:

1
$ cd llvm_mode$ apt-get install clang$ export LLVM_CONFIG=`which llvm-config` && make && cd ..$ ./configure --disable-shared CC="afl-clang-fast" CXX="afl-clang-fast++"

在使用高版本的clang编译时可能会报错,换成clang-3.9后通过编译,如果你的系统默认安装的clang版本过高,可以安装多个版本然后使用update-alternatives切换。

六 开始Fuzzing

1. 白盒测试

(1) 测试插桩程序

编译好程序后,可以选择使用afl-showmap跟踪单个输入的执行路径,并打印程序执行的输出、捕获的元组(tuples),tuple用于获取分支信息,从而衡量衡量程序覆盖情况,下一篇文章中会详细的解释,这里可以先不用管。

1
$ afl-showmap -m none -o /dev/null -- ./build/bin/imagew 23.bmp out.png[*] Executing './build/bin/imagew'...-- Program output begins --23.bmp -> out.pngProcessing: 13x32-- Program output ends --[+] Captured 1012 tuples in '/dev/null'.

使用不同的输入,正常情况下afl-showmap会捕获到不同的tuples,这就说明我们的的插桩是有效的,还有前面提到的afl-cmin就是通过这个工具来去掉重复的输入文件。

(2) 执行fuzzer

在执行afl-fuzz前,如果系统配置为将核心转储文件(core)通知发送到外部程序。 将导致将崩溃信息发送到Fuzzer之间的延迟增大,进而可能将崩溃被误报为超时,所以我们得临时修改core_pattern文件,如下所示:

1
echo core >/proc/sys/kernel/core_pattern

之后就可以执行afl-fuzz了,通常的格式是:

1
$ afl-fuzz -i testcase_dir -o findings_dir /path/to/program [params]

或者使用“@@”替换输入文件,Fuzzer会将其替换为实际执行的文件:

1
$ afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@

如果没有什么错误,Fuzzer就正式开始工作了。首先,对输入队列中的文件进行预处理;然后给出对使用的语料库可警告信息,比如下图中提示有个较大的文件(14.1KB),且输入文件过多;最后,开始Fuzz主循环,显示状态窗口。

测试源代码:

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
27
28
29
30
31
32
33
34
35
#include <stdio.h> 
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>

int vuln(char *str)
{
int len = strlen(str);
if(str[0] == 'A' && len == 66)
{
raise(SIGSEGV);
//如果输入的字符串的首字符为A并且长度为66,则异常退出
}
else if(str[0] == 'F' && len == 6)
{
raise(SIGSEGV);
//如果输入的字符串的首字符为F并且长度为6,则异常退出
}
else
{
printf("it is good!\n");
}
return 0;
}

int main(int argc, char *argv[])
{
char buf[100]={0};
gets(buf);//存在栈溢出漏洞
printf(buf);//存在格式化字符串漏洞
vuln(buf);

return 0;
}

AFL状态窗口

① Process timing:Fuzzer运行时长、以及距离最近发现的路径、崩溃和挂起经过了多长时间。

② Overall results:Fuzzer当前状态的概述。

③ Cycle progress:我们输入队列的距离。

④ Map coverage:目标二进制文件中的插桩代码所观察到覆盖范围的细节。

⑤ Stage progress:Fuzzer现在正在执行的文件变异策略、执行次数和执行速度。

⑥ Findings in depth:有关我们找到的执行路径,异常和挂起数量的信息。

⑦ Fuzzing strategy yields:关于突变策略产生的最新行为和结果的详细信息。

⑧ Path geometry:有关Fuzzer找到的执行路径的信息。

⑨ CPU load:CPU利用率

(3) 使用screen

一次Fuzzing过程通常会持续很长时间,如果这期间运行afl-fuzz实例的终端终端被意外关闭了,那么Fuzzing也会被中断。而通过在screen session中启动每个实例,可以方便的连接和断开。

1
$ screen afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@

也可以为每个session命名,方便重新连接。

1
$ screen -S fuzzer1$ afl-fuzz -i testcase_dir -o findings_dir /path/to/program [params] @@[detached from 6999.fuzzer1]$ screen -r fuzzer1  ...

2. 黑盒测试

所谓黑盒测试,通俗地讲就是对没有源代码的程序进行测试,这时就要用到AFL的QEMU模式了。启用方式和LLVM模式类似,也要先编译。但注意,因为AFL使用的QEMU版本太旧,util/memfd.c中定义的函数memfd_create()会和glibc中的同名函数冲突,在这里可以找到针对QEMU的patch,之后运行脚本build_qemu_support.sh就可以自动下载编译。

1
2
$ sudo apt-get install libini-config-dev libtool-bin automake bison libglib2.0-dev -y$ 
$ cd qemu_mode$ build_qemu_support.sh$ cd .. && make install

现在起,只需添加-Q选项即可使用QEMU模式进行Fuzzing。

1
$ afl-fuzz -Q -i testcase_dir -o findings_dir /path/to/program [params] @@

七 结束测试

1.何时结束

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

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

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

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

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

2. 输出结果

afl-fuzz的输出目录中存在很多文件,有时想要写一个辅助工具可能就要用到其中的文件。下面以多个fuzz实例并行测试时的同步目录为例:

queue:存放所有具有独特执行路径的测试用例。

crashes:导致目标接收致命signal而崩溃的独特测试用例。

crashes/README.txt:保存了目标执行这些crash文件的命令行参数。

hangs:导致目标超时的独特测试用例。

fuzzer_stats:afl-fuzz的运行状态。

plot_data:用于afl-plot绘图。

八 处理测试结果

到了这里,我们可能已经跑出了一大堆的crashes,那么接下来的步骤,自然是确定造成这些crashes的bug是否可以利用,怎么利用?这是另一个重要方面。当然,个人觉得这比前面提到的内容都要困难得多,这需要对常见的二进制漏洞类型、操作系统的安全机制、代码审计和调试等内容都有一定深度的了解。

文章作者: nocbtm
文章链接: https://nocbtm.github.io/2019/11/12/AFL漏洞挖掘/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 nocbtm's Blog
打赏
  • 微信
  • 支付宝