Fuzz 简介
模糊测试 (fuzz testing, fuzzing)是一种软件测试技术。其核心思想是将自动或半自动生成的随机数据输入到一个程序中,并监视程序异常,如崩溃,断言(assertion)失败,以发现可能的程序错误,比如内存泄漏。模糊测试常常用于检测软件或计算机系统的安全漏洞。
Fuzz 的发展历程
文件Fuzzing技术
语法模板Fuzzing
符号执行
符号执行在学术界中应用得比较多,工业界相对少一些。在Fuzzing中,通过约束求解新路径的条件值,以增加代码覆盖率,可以一定程度上弥补暴力变异的不足。符号执行主要的挑战在于路径爆炸问题,约束求解能力的局限性,以及性能消耗问题,比如内存和时间消耗过大。
代码覆盖引导技术
目前业界中基于代码覆盖率的最著名的三大Fuzzer,Google开发的
AFL
、libfuzzer
和honggfuzz
,且他们都是开源的,在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 之前一个”有效”的软件的攻击面是更为重要的。
- 软件是否有示例代码?
- 我们要 fuzz 的软件可能是很复杂很庞大的,如果其拥有大量的实例代码,那么我们快速定位到特定的模块,对每个模块单独 fuzz,这样测试的效果会更加优越。
- 是否开源?
- 对比开源项目来讲 afl 的效率会高很多,虽然 afl 也支持使用 qemu 进行黑盒 fuzz,但是效率会大打折扣。
- 特殊样本是否容易收集?
- 我们可能会要 fuzz 不同的文件格式,如果能够快速收集到大量有效的特殊的样本,那么 fuzz 的效率也会随之提高。
AFL简介
AFL(American Fuzzy Lop)是由安全研究员Micha? Zalewski(@lcamtuf)开发的一款基于覆盖引导(Coverage-guided)的模糊测试工具,它通过记录输入样本的代码覆盖率,从而调整输入样本以提高覆盖率,增加发现漏洞的概率。
工作流程
- 从源码编译程序时进行插桩,以记录代码覆盖率(Code Coverage)
- 选择一些输入文件,作为初始测试集加入输入队列(queue)
- 将队列中的文件按一定的策略进行”突变”
- 如果经过变异文件更新了覆盖范围,则将其保留添加到队列中
- 上述过程会一直循环进行,期间触发了crash的文件会被记录下来
程序插桩:
最早是由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,较小的文件会不仅可以减少测试和处理的时间,也能节约更多的内存。
主要来源
使用项目自身提供的测试用例
目标程序bug提交页面
使用格式转换器,用从现有的文件格式生成一些不容易找到的文件格式:
afl源码的testcases目录下提供了一些测试用例
其他大型的语料库
修剪
网上找到的一些大型语料库中往往包含大量的文件,这时就需要对其精简,这个工作有个术语叫做——语料库蒸馏(Corpus Distillation)。AFL提供了两个工具来帮助我们完成这部工作——afl-cmin
和afl-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 mode
和crash 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 |
|
1 | $ mkdir fuzz_test; cd fuzz_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 | $ xxd id:000004,sig:06,src:000000,op:havoc,rep:64 |
可以看出来是由栈溢出所导致的。
1 | $ xxd id:000002,sig:06,src:000000,op:havoc,rep:128 |
格式化字符串导致。
1 | xxd id:000000,sig:11,src:000001,op:havoc,rep:4 |
输入的字符串的首字符为F并且长度为6,则异常退出
黑盒测试
黑盒测试要用到AFL的QEMU模式了。
所以我们需要再额外装一下东西。
1 | $ cd ~ |
我们接着用刚刚的源文件重新编译然后进行新的黑盒 fuzz。
1 | $ gcc -g -o afl_test2 afl_test.c |
输出结果
1 | fuzz_out/ |
queue
:存放所有具有独特执行路径的测试用例。crashes
:导致目标接收致命signal
而崩溃的独特测试用例。crashes/README.txt
:保存了目标执行这些crash
文件的命令行参数。hangs
:导致目标超时的独特测试用例。fuzzer_stats
:afl-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