LibFuzzer 基础使用

————

概述

LibFuzzer是单进程in-process,覆盖引导coverage-based,进化evolutionary的模糊测试引擎。

LibFuzzer 与被测目标库链接,并通过特定的模糊入口点(又称“目标函数”)将模糊输入提供给目标库。然后,模糊器跟踪到达了代码的哪些区域,并在输入数据的语料库上生成突变,以最大化代码覆盖率。

LibFuzzer的代码覆盖率由LLVM的SanitizerCoverage 工具提供。

Fuzz Target 模糊目标

在库上使用 libFuzzer 的第一步是实现一个Fuzz Target:一个接受字节数组并使用被测 API 对这些字节做一些有趣的事情的函数

// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
DoSomethingInterestingWithMyAPI(Data, Size);
return 0; // Values other than 0 and -1 are reserved for future use.
}

实际上,Fuzz Target不以任何方式依赖于libFuzzer,因此也可以与其他Fuzzing engines(如AFL)一起使用

关于Fuzz Target的一些重要事项:

  1. Fuzzing engines 会在相同程序上使用不同的输入多次执行Fuzz Target
  2. 必须能接受任何类型的输入(空的、巨大的、格式错误的等)
  3. 在任何输入上都不能exit
  4. 可以使用线程,但理想情况下,所有线程都应该在函数结束时加入
  5. 必须尽可能具有确定性
  6. 必须很快
  7. 通常,目标范围越小越好

使用

Clang从6.0开始包括LibFuzzer,无需额外安装

构建fuzzer binary,在编译和链接期间使用-fsanitize=fuzzer标志,也可以与sanitizer结合使用

clang -g -O1 -fsanitize=fuzzer                         mytarget.c # Builds the fuzz target w/o sanitizers
clang -g -O1 -fsanitize=fuzzer,address mytarget.c # Builds the fuzz target with ASAN
clang -g -O1 -fsanitize=fuzzer,signed-integer-overflow mytarget.c # Builds the fuzz target with a part of UBSAN
clang -g -O1 -fsanitize=fuzzer,memory mytarget.c # Builds the fuzz target with MSAN

示例:

//fuzz_me.cc
#include <stdint.h>
#include <stddef.h>

bool FuzzMe(const uint8_t *Data, size_t DataSize) {
return DataSize >= 3 &&
Data[0] == 'F' &&
Data[1] == 'U' &&
Data[2] == 'Z' &&
Data[3] == 'Z'; // :‑<
}

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
FuzzMe(Data, Size);
return 0;
}

使用libfuzzer和asan编译、运行:

$ clang++-11 -g -fsanitize=address,fuzzer -O0 fuzz_me.cc
$ ./a.out
INFO: Seed: 4103274391
INFO: Loaded 1 modules (7 inline 8-bit counters): 7 [0x5a8ed0, 0x5a8ed7),
INFO: Loaded 1 PC tables (7 PCs): 7 [0x56c840,0x56c8b0),
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
#2 INITED cov: 3 ft: 3 corp: 1/1b exec/s: 0 rss: 30Mb
#5 NEW cov: 4 ft: 4 corp: 2/4b lim: 4 exec/s: 0 rss: 31Mb L: 3/3 MS: 3 CopyPart-InsertByte-InsertByte-
#904 NEW cov: 5 ft: 5 corp: 3/7b lim: 11 exec/s: 0 rss: 31Mb L: 3/3 MS: 4 ChangeByte-ShuffleBytes-ShuffleBytes-CMP- DE: "F\x00"-
#7046 NEW cov: 6 ft: 6 corp: 4/10b lim: 68 exec/s: 0 rss: 31Mb L: 3/3 MS: 2 ChangeBit-ChangeByte-
=================================================================
==133103==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200002bf33 at pc 0x000000551af3 bp 0x7ffc8ed21370 sp 0x7ffc8ed21368
......
......
artifact_prefix='./'; Test unit written to ./crash-0eb8e4ed029b774d80f2b66408203801cb982a60
......

这代表构建了一个fuzzer并发现了一个错误,其中:

INFO: Seed: 4103274391

指fuzzer从这个随机种子开始

INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus

默认情况下,libFuzzer假设所有输入都是4096字节或更小,使用参数-max_len=N更改,或使用非空语料库运行。

#2	INITED cov: 3 ft: 3 corp: 1/1b exec/s: 0 rss: 30Mb
#5 NEW cov: 4 ft: 4 corp: 2/4b lim: 4 exec/s: 0 rss: 31Mb L: 3/3 MS: 3 CopyPart-InsertByte-InsertByte-
#904 NEW cov: 5 ft: 5 corp: 3/7b lim: 11 exec/s: 0 rss: 31Mb L: 3/3 MS: 4 ChangeByte-ShuffleBytes-ShuffleBytes-CMP- DE: "F\x00"-
#7046 NEW cov: 6 ft: 6 corp: 4/10b lim: 68 exec/s: 0 rss: 31Mb L: 3/3 MS: 2 ChangeBit-ChangeByte-

libFuzzer至少尝试了7046个输入,并发现了4个输入,总共10个字节(corp: 4/10b) 一共覆盖了6个覆盖点(可以视为基本块)(cov: 6

=================================================================
==133103==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200002bf33 at pc 0x000000551af3 bp 0x7ffc8ed21370 sp 0x7ffc8ed21368

在其中一个输入上,AddressSanitizer 检测到heap-buffer-overflow错误并中止了执行

artifact_prefix='./'; Test unit written to ./crash-0eb8e4ed029b774d80f2b66408203801cb982a60

在退出进程之前,libFuzzer 在磁盘上创建了一个文件,其中包含触发崩溃的字节。可以对其进行崩溃重现

./a.out crash-0eb8e4ed029b774d80f2b66408203801cb982a60

Heartbleed 心脏滴血

Heartbleed(又名 CVE-2014-0160)是OpenSSL 加密库中的一个严重安全漏洞,后来发现,通过模糊测试可以很容易地发现这个错误

使用https://github.com/google/fuzzer-test-suite提供的例子,包含了现成的脚本来为各种目标构建Fuzzer,包括存在“heartbleed”错误的openssl-1.0.1f

下面为openssl-1.0.1f 构建Fuzzer:

$ git clone https://github.com/google/fuzzer-test-suite.git
$ cd fuzzer-test-suite
$ mkdir ./heartbleed
$ cd ./heartbleed
$ ../openssl-1.0.1f/build.sh

这将下载受影响的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"-
#163917 REDUCE cov: 651 ft: 1053 corp: 76/4011b lim: 1021 exec/s: 40979 rss: 397Mb L: 27/381 MS: 1 EraseBytes-
#165332 REDUCE cov: 651 ft: 1053 corp: 76/4009b lim: 1030 exec/s: 41333 rss: 397Mb L: 13/381 MS: 4 ShuffleBytes-InsertByte-ChangeByte-EraseBytes-
#167837 REDUCE cov: 651 ft: 1053 corp: 76/4008b lim: 1050 exec/s: 41959 rss: 397Mb L: 14/381 MS: 4 CopyPart-ChangeByte-ChangeByte-EraseBytes-
#168004 REDUCE cov: 651 ft: 1053 corp: 76/4005b lim: 1050 exec/s: 42001 rss: 397Mb L: 11/381 MS: 1 EraseBytes-
=================================================================
==153177==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x629000009748 at pc 0x00000051f8d7 bp 0x7ffed58fa250 sp 0x7ffed58f9a18
READ of size 65292 at 0x629000009748 thread T0
#0 0x51f8d6 in __asan_memcpy (/root/Desktop/fuzzer-test-suite/heartbleed/openssl-1.0.1f-fsanitize_fuzzer+0x51f8d6)
#1 0x55e863 in tls1_process_heartbeat /root/Desktop/fuzzer-test-suite/heartbleed/BUILD/ssl/t1_lib.c:2586:3
#2 0x5c9c9a in ssl3_read_bytes /root/Desktop/fuzzer-test-suite/heartbleed/BUILD/ssl/s3_pkt.c:1092:4
#3 0x5ce0a3 in ssl3_get_message /root/Desktop/fuzzer-test-suite/heartbleed/BUILD/ssl/s3_both.c:457:7

corpus语料库

libFuzzer 这样的覆盖引导模糊器依赖于被测代码的样本输入语料库。理想情况,语料库会为被测目标提供各种有效及无效输入。之后Fuzzer会根据当前语料库中的样本生成随机突变。

当然libFuzzer 可以在没有任何初始种子的情况下工作,但可能效率会降低

尝试另一个目标:Woff2

构建:

$ mkdir woff
$ cd woff
$ ../woff2-2016-05-06/build.sh

运行Fuzzer:

./woff2-2016-05-06-fsanitize_fuzzer

可能会看到 fuzzer 卡住了——它正在运行数百万个输入,但找不到许多新的代码路径

在这种情况下,需要去找到一些触发足够代码路径的输入——越多越好。woff2 fuzz 目标使用.woff2格式的网络字体,因此可以去找任何此类文件。刚刚执行的构建脚本已经下载了一个包含一些.woff2 文件的项目并将其放入目录./seeds/中。

我们需要将woff2 fuzzer与种子语料库一起使用:

mkdir my_corpus
./woff2-2016-05-06-fsanitize_fuzzer my_corpus/ seeds/

当基于 libFuzzer 的 fuzzer 使用一个目录作为参数执行时,它将首先递归地从每个目录中读取文件并在所有目录上执行目标函数。然后,任何触发有新代码路径的输入都将被写回第一个语料库目录(在本例中为MY_CORPUS

可以看到,cov覆盖数量大幅提高(相比之前的600多),并且在不断增长

$ ./woff2-2016-05-06-fsanitize_fuzzer my_corpus  seeds/               
INFO: Seed: 2513206469
INFO: Loaded 1 modules (11107 inline 8-bit counters): 11107 [0x799050, 0x79bbb3),
INFO: Loaded 1 PC tables (11107 PCs): 11107 [0x723120,0x74e750),
INFO: 0 files found in my_corpus
INFO: 677 files found in seeds/
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 168276 bytes
INFO: seed corpus: files: 677 min: 1b max: 168276b total: 43813862b rss: 32Mb
#678 INITED cov: 991 ft: 3436 corp: 448/27Mb exec/s: 678 rss: 377Mb
#1024 pulse cov: 991 ft: 3436 corp: 448/27Mb lim: 68784 exec/s: 512 rss: 391Mb
#1364 NEW cov: 991 ft: 3438 corp: 449/27Mb lim: 68784 exec/s: 682 rss: 391Mb L: 67832/68784 MS: 1 ChangeByte-

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/

文章作者: HotSpurzzZ
文章链接: http://example.com/2022/09/10/LibFuzzer 学习/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 HotSpurzzZ