Fuzz 简介

模糊测试 (fuzz testing, fuzzing)是一种软件测试技术。其核心思想是将自动或半自动生成的随机数据输入到一个程序中,并监视程序异常,如崩溃,断言(assertion)失败,以发现可能的程序错误,比如内存泄漏。模糊测试常常用于检测软件或计算机系统的安全漏洞。

Fuzz 的发展历程

  1. 文件Fuzzing技术

  2. 语法模板Fuzzing

  3. 符号执行

    符号执行在学术界中应用得比较多,工业界相对少一些。在Fuzzing中,通过约束求解新路径的条件值,以增加代码覆盖率,可以一定程度上弥补暴力变异的不足。符号执行主要的挑战在于路径爆炸问题,约束求解能力的局限性,以及性能消耗问题,比如内存和时间消耗过大。

  4. 代码覆盖引导技术

    目前业界中基于代码覆盖率的最著名的三大Fuzzer,Google开发的AFLlibfuzzerhonggfuzz,且他们都是开源的,在github上都可以搜索到。

Pros & Cons

Fuzzing 是对于寻找漏洞是非常有效的,但其不是万能的,以下是它的一些优缺点。

Pros

  • (不需要持续交互)Provides results with little effort - once a fuzzer’s up and running, it can be left for hours, days, or months to look for bugs with no interaction
  • Can reveal bugs missed in a manual audit
  • (检测程序整体的稳定性)Provides an overall picture of the robustness of the target software

Cons

  • (不能找到所有的漏洞)Won’t find all bugs - fuzzing may miss bugs that don’t trigger a full program crash, and may be less likely to trigger bugs that are only triggered in highly specific circumstances
  • (crash 样本比较难分析)The crashing test cases that are produced may be difficult to analyze, as the act of fuzzing doesn’t give you much knowledge of how the software operates internally
  • (对于复杂的程序效率会比较低)Programs with complex inputs can require much more work to produce a smart enough fuzzer to get sufficient code coverage

Anatomy of a fuzzer

为了有效fuzz,fuzzer需要执行以下任务

  • 生成测试样本
  • 记录测试用例的信息
  • 把测试样例作为输入文件和目标程序(loader)进行对接
  • 检测并监控 crash

其实上述所说的也就是一个 fuzzer 完整跑一遍的流程的简化。

What should I fuzz? Finding the right software

AFL在C或C ++应用程序上效果最好,因此,在 fuzz 之前一个”有效”的软件的攻击面是更为重要的。

  1. 软件是否有示例代码?
    • 我们要 fuzz 的软件可能是很复杂很庞大的,如果其拥有大量的实例代码,那么我们快速定位到特定的模块,对每个模块单独 fuzz,这样测试的效果会更加优越。
  2. 是否开源?
    • 对比开源项目来讲 afl 的效率会高很多,虽然 afl 也支持使用 qemu 进行黑盒 fuzz,但是效率会大打折扣。
  3. 特殊样本是否容易收集?
    • 我们可能会要 fuzz 不同的文件格式,如果能够快速收集到大量有效的特殊的样本,那么 fuzz 的效率也会随之提高。

AFL简介

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

工作流程

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

afl

程序插桩:
最早是由J.C. Huang 教授提出的,它是在保证被测程序原有逻辑完整性的基础上在程序中插入一些探针(又称为“探测仪”,本质上就是进行信息采集的代码段,可以是赋值语句或采集覆盖信息的函数调用),通过探针的执行并抛出程序运行的特征数据,通过对这些数据的分析,可以获得程序的控制流和数据流信息,进而得到逻辑覆盖等动态信息,从而实现测试目的的方法。

代码覆盖率:
软件测试中的一种度量,描述程式中源代码被测试的比例和程度,所得比例称为代码覆盖率。

环境配置

  • 直接安装:sudo apt install afl
  • 官网下载压缩包,解压后在目录中打开终端输入:sudo make;sudo make install

推荐自己编译安装,apt 安装不支持黑盒测试。

Fuzz目标

AFL主要用于C/C++程序的测试,所以这是我们寻找软件的最优先规则。

目标开源与否不是很重要。

  • 对于开源软件:AFL软件进行编译的同时进行插桩,以方便fuzz(效率更高

  • 对于闭源软件:配合QEMU直接对闭源的二进制代码进行fuzz

构建语料库

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

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

语料库选择

  • 有效的输入:尽管有时候无效输入会产生bug和崩溃,但有效输入可以更快的找到更多执行路径。
  • 尽量小的体积:种子文件最好小于 1KB,较小的文件会不仅可以减少测试和处理的时间,也能节约更多的内存。

主要来源

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

  2. 目标程序bug提交页面

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

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

  5. 其他大型的语料库

修剪

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

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] @@

AFL-TMIN

afl-tmin用来缩减文件体积。

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

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

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

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

AFL实例

开始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
#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[20] = {0};

gets(buf);//存在栈溢出漏洞
printf(buf);//存在格式化字符串漏洞
vuln(buf);
return 0;
}
1
2
3
4
5
6
7
$ mkdir fuzz_test; cd fuzz_test 
$ mkdir fuzz_in fuzz_out
$ afl-gcc -g -o afl_test afl_test.c //插桩编译
$ sudo su
$ echo core >/proc/sys/kernel/core_pattern
$ exit
$ afl-fuzz -i fuzz_in -o fuzz_out ./afl_test

ps:随便在 fuzz_in 放一些种子文件即可。

命令解析:

  • -i:指定测试样本的路径

  • -o:指定输出结果的路径

  • 更详细见:$ afl-fuzz -h

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

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

窗口解读

  • 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利用率

分析crash

因为程序本身很简单,跑了几分钟跑出来 4 个crash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ xxd id:000004,sig:06,src:000000,op:havoc,rep:64

00000000: 4100 7f00 00f1 83ec ff00 7f00 00f1 7fff A...............
00000010: ffff 0c41 007f 0000 f1a2 ffff ff0b fd5f ...A..........._
00000020: 8000 0000 fe0c 5f6b 2000 ff0c 6200 7f0c ......_k ...b...
00000030: 5f6b 00f1 7fec ff00 7f00 00f1 7fff ffff _k..............
00000040: 0c41 007f 0000 f1a2 ffff ff0b fd5f 8000 .A..........._..
00000050: 0000 fe0c 5f6b 2000 ff0c 6200 7f0c 5f6b ...._k ...b..._k
00000060: 0000 00fe 8108 7f00 0000 00fe 405f 6b00 ............@_k.
00000070: 00ff 0000 f17f ecff 007f 0000 f17f ffff ................
00000080: ff0c 4100 7f00 00f1 a2ff ffff 0bfd 5f80 ..A..........._.
00000090: 0000 00fe 0c5f 6b20 0000 00fe 7e0c 4100 ....._k ....~.A.
000000a0: 7f00 00f1 7fff ffff 0bec 5f80 0000 00fe .........._.....
000000b0: 0c5f 6700 0000 fe7e 0800 0000 fe81 087f ._g....~........
000000c0: 0000 0000 fe40 5f6b 0000 00ff 4100 7f00 .....@_k....A...
000000d0: 00f1 a2ff ffff 0bfd 5f80 0000 00fe 0c5f ........_......_
000000e0: 6b20 00ff 0c62 007f 0c5f 6b00 0000 fe72 k ...b..._k....r
000000f0: 087f 0000 0000 fe40 5f6b 0000 ff0c 4100 .......@_k....A.
00000100: 7f00 00f1 7fff ffff 0bec 5f80 0000 00fe .........._.....
00000110: 0c5f 6700 0000 fe7e 0c41 007f 0000 f17f ._g....~.A......
00000120: ffff ff0b ec5f 8000 0000 fe0c 5f67 0000 ....._......_g..
00000130: 00fe 7e08 ..~.

可以看出来是由栈溢出所导致的。

1
2
$ xxd id:000002,sig:06,src:000000,op:havoc,rep:128
00000000: 256e %n

格式化字符串导致。

1
2
xxd id:000000,sig:11,src:000001,op:havoc,rep:4
00000000: 4672 6497 7364 Frd.sd

输入的字符串的首字符为F并且长度为6,则异常退出

黑盒测试

黑盒测试要用到AFL的QEMU模式了。

所以我们需要再额外装一下东西。

1
2
3
4
5
6
$ cd ~
$ sudo apt-get install libini-config-dev libtool-bin automake bison libglib2.0-dev -y
$ cd afl-2.52/qemu_mode
$ ./build_qemu_support.sh
$ cd ..
$ sudo make install

我们接着用刚刚的源文件重新编译然后进行新的黑盒 fuzz。

1
2
$ gcc -g -o afl_test2 afl_test.c
$ afl-fuzz -i fuzz_in -o fuzz_out -Q ./afl_test2

image

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fuzz_out/
├── crashes
│   ├── id:000000,sig:06,src:000000,op:havoc,rep:32
│   ├── id:000001,sig:06,src:000000,op:havoc,rep:32
│   ├── id:000002,sig:11,src:000001,op:havoc,rep:2
│   ├── id:000003,sig:06,src:000002,op:havoc,rep:16
│   ├── id:000004,sig:11,src:000000,op:havoc,rep:128
│   ├── id:000005,sig:11,src:000002,op:havoc,rep:16
│   └── README.txt
├── fuzz_bitmap
├── fuzzer_stats
├── hangs
├── plot_data
└── queue
├── id:000000,orig:test
├── id:000001,src:000000,op:arith8,pos:0,val:-30,+cov
└── id:000002,src:000000,op:arith8,pos:0,val:-35,+cov

3 directories, 13 files

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

参考

https://zh.wikipedia.org/wiki/%E6%A8%A1%E7%B3%8A%E6%B5%8B%E8%AF%95

https://www.f-secure.com/en/consulting/our-thinking/15-minute-guide-to-fuzzing

https://foxglovesecurity.com/2016/03/15/fuzzing-workflows-a-fuzz-job-from-start-to-finish/

https://paper.seebug.org/841/#2_3

https://paper.seebug.org/842/#_1

https://xz.aliyun.com/t/4314#toc-8