————
概述
LibFuzzer是单进程in-process
,覆盖引导coverage-based
,进化evolutionary
的模糊测试引擎。
LibFuzzer 与被测目标库链接,并通过特定的模糊入口点(又称“目标函数”)将模糊输入提供给目标库。然后,模糊器跟踪到达了代码的哪些区域,并在输入数据的语料库上生成突变,以最大化代码覆盖率。
LibFuzzer的代码覆盖率由LLVM的SanitizerCoverage 工具提供。
Fuzz Target 模糊目标
在库上使用 libFuzzer 的第一步是实现一个Fuzz Target:一个接受字节数组并使用被测 API 对这些字节做一些有趣的事情的函数
// fuzz_target.cc |
实际上,Fuzz Target不以任何方式依赖于libFuzzer,因此也可以与其他Fuzzing engines(如AFL)一起使用
关于Fuzz Target的一些重要事项:
- Fuzzing engines 会在相同程序上使用不同的输入多次执行Fuzz Target
- 必须能接受任何类型的输入(空的、巨大的、格式错误的等)
- 在任何输入上都不能exit
- 可以使用线程,但理想情况下,所有线程都应该在函数结束时加入
- 必须尽可能具有确定性
- 必须很快
- 通常,目标范围越小越好
使用
Clang从6.0开始包括LibFuzzer,无需额外安装
构建fuzzer binary,在编译和链接期间使用-fsanitize=fuzzer标志,也可以与sanitizer结合使用
clang -g -O1 -fsanitize=fuzzer mytarget.c # Builds the fuzz target w/o sanitizers |
示例:
//fuzz_me.cc |
使用libfuzzer和asan编译、运行:
$ clang++-11 -g -fsanitize=address,fuzzer -O0 fuzz_me.cc |
这代表构建了一个fuzzer并发现了一个错误,其中:
INFO: Seed: 4103274391 |
指fuzzer从这个随机种子开始
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes |
默认情况下,libFuzzer假设所有输入都是4096字节或更小,使用参数-max_len=N
更改,或使用非空语料库运行。
#2 INITED cov: 3 ft: 3 corp: 1/1b exec/s: 0 rss: 30Mb |
libFuzzer至少尝试了7046个输入,并发现了4个输入,总共10个字节(corp: 4/10b
) 一共覆盖了6个覆盖点(可以视为基本块)(cov: 6
)
================================================================= |
在其中一个输入上,AddressSanitizer 检测到heap-buffer-overflow
错误并中止了执行
artifact_prefix='./'; Test unit written to ./crash-0eb8e4ed029b774d80f2b66408203801cb982a60 |
在退出进程之前,libFuzzer 在磁盘上创建了一个文件,其中包含触发崩溃的字节。可以对其进行崩溃重现
./a.out crash-0eb8e4ed029b774d80f2b66408203801cb982a60 |
Heartbleed 心脏滴血
Heartbleed(又名 CVE-2014-0160)是OpenSSL 加密库中的一个严重安全漏洞,后来发现,通过模糊测试可以很容易地发现这个错误
下面为openssl-1.0.1f 构建Fuzzer:
$ git clone https://github.com/google/fuzzer-test-suite.git |
这将下载受影响的openssl源码,并为存在错误的特定API构建Fuzzer,参阅openssl-1.0.1f/target.cc
运行Fuzzer:
./openssl-1.0.1f-fsanitize_fuzzer |
几秒钟后可以看到:
#163297 REDUCE cov: 651 ft: 1053 corp: 76/4013b lim: 1021 exec/s: 40824 rss: 397Mb L: 15/381 MS: 1 PersAutoDict- DE: "\x01\x00\x00\x00\x00\x00\x00\x00"- |
corpus语料库
libFuzzer 这样的覆盖引导模糊器依赖于被测代码的样本输入语料库。理想情况,语料库会为被测目标提供各种有效及无效输入。之后Fuzzer会根据当前语料库中的样本生成随机突变。
当然libFuzzer 可以在没有任何初始种子的情况下工作,但可能效率会降低
尝试另一个目标:Woff2
构建:
$ mkdir woff |
运行Fuzzer:
./woff2-2016-05-06-fsanitize_fuzzer |
可能会看到 fuzzer 卡住了——它正在运行数百万个输入,但找不到许多新的代码路径
在这种情况下,需要去找到一些触发足够代码路径的输入——越多越好。woff2 fuzz 目标使用.woff2格式的网络字体,因此可以去找任何此类文件。刚刚执行的构建脚本已经下载了一个包含一些.woff2 文件的项目并将其放入目录./seeds/中。
我们需要将woff2 fuzzer与种子语料库一起使用:
mkdir my_corpus |
当基于 libFuzzer 的 fuzzer 使用一个目录作为参数执行时,它将首先递归地从每个目录中读取文件并在所有目录上执行目标函数。然后,任何触发有新代码路径的输入都将被写回第一个语料库目录(在本例中为MY_CORPUS
)
可以看到,cov覆盖数量大幅提高(相比之前的600多),并且在不断增长
$ ./woff2-2016-05-06-fsanitize_fuzzer my_corpus seeds/ |
libFuzzer 尝试的输入大小现在受到 168276 的限制,这是种子语料库中最大文件的大小。可以用-max_len=N
进行改变
可以随时中断 fuzzer 并使用相同的命令行重新启动它。它将从它停止的地方开始
并行运行
另一种提高模糊测试效率的方法是使用更多的 CPU。如果你用它运行模糊器,-jobs=N
它将产生 N 个独立的作业,但不超过你拥有的核心数量的一半;用于-workers=M
设置允许的并行作业数
./woff2-2016-05-06-fsanitize_fuzzer MY_CORPUS/ seeds/ -jobs=8 |
参考资料
Google官方教程:https://github.com/google/fuzzing/blob/master/tutorial/libFuzzerTutorial.md
LLVM官方文档:https://llvm.org/docs/LibFuzzer.html
Libfuzzer-workshop:https://github.com/Dor1s/libfuzzer-workshop/