fuzz入门到入土(二)

LibFuzzer学习(一):轻松找到心脏出血漏洞

和AFL的区别在于LibFuzzer可以直接在进程中尝试输入数据,而AFL多的在于从外部将变异数据传入被测试程序内部。

libFuzzer demo

demo解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdint.h>
#include <stddef.h>

bool FuzzMe(const uint8_t *Data, size_t DataSize) {
return DataSize >= 3 &&
Data[0] == 'L' &&
Data[1] == 'e' &&
Data[2] == '0' &&
Data[3] == 'n' &&
Data[4] == '.';
}

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

这个程序非常简单,首先看一下FuzzMe()函数。接收参数主要有两个,一个是字符串Date,另一个是字符串长度DateSize。如果字符串长度大于3,且Data中数据前5个字节为le0n.则返回1,如果上述中一项不满足则返回0

然后,一般的C++程序都会存在一个main()函数作为入口,但是在这个demo中main()函数被LLVMFuzzerTestOneInput()函数取代了,这是因为libfuzzer需要在进程内为程序提供了输入参数,libfuzzer库内部存在main()函数,这样一来libfuzzer可以自由控制输入,因此在使用libfuzzer时,需要使用LLVMFuzzerTestOneInput()函数来代替main()函数作为执行入口

编译目标程序

目标程序需要使用clang进行编译,libFuzzer的代码覆盖率也是有LLVM内部工具提供的,如果一下载最新版的clang编译器(版本>6.0),就可以直接使用libFuzzer,无需额外安装

目标程序需要使用 clang 进行编译,libFuzzer 的代码覆盖率信息也是由 LLVM 内部工具提供了,如果你已经下载了最新版的 clang 编译器(版本 > 6.0),就可以直接使用 libFuzzer,无需额外安装。

在 clang 编译和链接时,可以使用 -fsanitize=fuzzer 标志启用 libFuzzer,这个参数时必须使用的,为了告知 libFuzzer 库准备进行链接,同时也可以结合 AddressSanitizer (ASAN)、UndefinedBehaviorSanitizer (UBSAN)、MemorySanitizer (MSAN) 一起使用:

1
2
3
4
clang -g -fsanitize=fuzzer demo.c         # 直接构建 fuzz 目标文件
clang -g -fsanitize=fuzzer,address demo.c # 构建 fuzz 目标文件并附加 ASAN 检测(推荐使用)
clang -g -fsanitize=fuzzer,signed-integer-overflow demo.c # 构建 fuzz 目标文件并附加一部分 UBSAN
clang -g -fsanitize=fuzzer,memory demo.c # 构建 fuzz 目标文件并附加 MSAN

那么回到上面的 demo 中,由于它是一个 C++ 程序,所以这里选用 clang++ 进行编译:

1
clang++ -O0 -g -fsanitize=fuzzer,address ./demo.cc -o demo

这里在我的ubuntu22.04上编译时要用O0,不能用O1,如下:

使用 -O0(无优化)

assembly

1
2
3
4
5
6
7
8
9
10
11
# 编译器为每个条件生成独立的基本块
cmp DataSize, 5
jl .Lfalse
mov rax, Data
cmp byte ptr [rax], 'L'
jne .Lfalse
cmp byte ptr [rax+1], 'e'
jne .Lfalse
cmp byte ptr [rax+2], '0'
jne .Lfalse
# ... 更多独立检查

使用 -O1(中等优化)

1
2
3
4
5
6
7
8
# 编译器可能合并条件,减少分支
cmp DataSize, 3
jl .Lfalse
mov rax, Data
mov rcx, [rax] # 一次加载多个字节
and rcx, 0xFFFFFFFF # 掩码操作
cmp rcx, 0x6E30654C # 比较 "Le0n" 的整数值
# ... 更少的基本块

O1会让编译器在编译过程中进行优化(采用&&的短路原理减少内存块的分配)。这样一来就得到了编译好的程序 demo

fuzz过程及日志解析

由于libFuzzer是与程序源码一起进行编译的,所以无需再程序外部做其他的配置,直接运行编译后的程序demo将会看到下面的日志结果:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
le0n:demo1/ $ ./demo_O0
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 3203650924
INFO: Loaded 1 modules (8 inline 8-bit counters): 8 [0x64fd57b29ed0, 0x64fd57b29ed8),
INFO: Loaded 1 PC tables (8 PCs): 8 [0x64fd57b29ed8,0x64fd57b29f58),
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
#12 NEW cov: 4 ft: 4 corp: 2/4b lim: 4 exec/s: 0 rss: 30Mb L: 3/3 MS: 5 CopyPart-ChangeByte-ChangeByte-CrossOver-CrossOver-
#658 NEW cov: 5 ft: 5 corp: 3/7b lim: 8 exec/s: 0 rss: 30Mb L: 3/3 MS: 1 CMP- DE: "L\000"-
#5541 NEW cov: 6 ft: 6 corp: 4/12b lim: 53 exec/s: 0 rss: 31Mb L: 5/5 MS: 3 PersAutoDict-CopyPart-CMP- DE: "L\000"-"e\000"-
#5678 REDUCE cov: 6 ft: 6 corp: 4/11b lim: 53 exec/s: 0 rss: 31Mb L: 4/4 MS: 2 ShuffleBytes-EraseBytes-
#7109 REDUCE cov: 6 ft: 6 corp: 4/10b lim: 63 exec/s: 0 rss: 31Mb L: 3/3 MS: 1 EraseBytes-
=================================================================
==15295==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6020000432d3 at pc 0x64fd57ae6f2e bp 0x7ffc3abb1d20 sp 0x7ffc3abb1d18
READ of size 1 at 0x6020000432d3 thread T0
#0 0x64fd57ae6f2d in FuzzMe(unsigned char const*, unsigned long) /home/le0n/fuzz/libFuzzer/demo1/./demo.cc:9:7
#1 0x64fd57ae7044 in LLVMFuzzerTestOneInput /home/le0n/fuzz/libFuzzer/demo1/./demo.cc:14:3
#2 0x64fd57a0d323 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/home/le0n/fuzz/libFuzzer/demo1/demo_O0+0x3e323) (BuildId: 899e8b30ca0eaa16c1c8eb74df44aaf04c41db09)
#3 0x64fd57a0ca79 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool, bool*) (/home/le0n/fuzz/libFuzzer/demo1/demo_O0+0x3da79) (BuildId: 899e8b30ca0eaa16c1c8eb74df44aaf04c41db09)
#4 0x64fd57a0e269 in fuzzer::Fuzzer::MutateAndTestOne() (/home/le0n/fuzz/libFuzzer/demo1/demo_O0+0x3f269) (BuildId: 899e8b30ca0eaa16c1c8eb74df44aaf04c41db09)
#5 0x64fd57a0ede5 in fuzzer::Fuzzer::Loop(std::vector<fuzzer::SizedFile, std::allocator<fuzzer::SizedFile> >&) (/home/le0n/fuzz/libFuzzer/demo1/demo_O0+0x3fde5) (BuildId: 899e8b30ca0eaa16c1c8eb74df44aaf04c41db09)
#6 0x64fd579fcf22 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/home/le0n/fuzz/libFuzzer/demo1/demo_O0+0x2df22) (BuildId: 899e8b30ca0eaa16c1c8eb74df44aaf04c41db09)
#7 0x64fd57a26c12 in main (/home/le0n/fuzz/libFuzzer/demo1/demo_O0+0x57c12) (BuildId: 899e8b30ca0eaa16c1c8eb74df44aaf04c41db09)
#8 0x7e091ce29d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#9 0x7e091ce29e3f in __libc_start_main csu/../csu/libc-start.c:392:3
#10 0x64fd579f1964 in _start (/home/le0n/fuzz/libFuzzer/demo1/demo_O0+0x22964) (BuildId: 899e8b30ca0eaa16c1c8eb74df44aaf04c41db09)

0x6020000432d3 is located 0 bytes to the right of 3-byte region [0x6020000432d0,0x6020000432d3)
allocated by thread T0 here:
#0 0x64fd57ae485d in operator new[](unsigned long) (/home/le0n/fuzz/libFuzzer/demo1/demo_O0+0x11585d) (BuildId: 899e8b30ca0eaa16c1c8eb74df44aaf04c41db09)
#1 0x64fd57a0d232 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/home/le0n/fuzz/libFuzzer/demo1/demo_O0+0x3e232) (BuildId: 899e8b30ca0eaa16c1c8eb74df44aaf04c41db09)
#2 0x64fd57a0ca79 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool, bool*) (/home/le0n/fuzz/libFuzzer/demo1/demo_O0+0x3da79) (BuildId: 899e8b30ca0eaa16c1c8eb74df44aaf04c41db09)
#3 0x64fd57a0e269 in fuzzer::Fuzzer::MutateAndTestOne() (/home/le0n/fuzz/libFuzzer/demo1/demo_O0+0x3f269) (BuildId: 899e8b30ca0eaa16c1c8eb74df44aaf04c41db09)
#4 0x64fd57a0ede5 in fuzzer::Fuzzer::Loop(std::vector<fuzzer::SizedFile, std::allocator<fuzzer::SizedFile> >&) (/home/le0n/fuzz/libFuzzer/demo1/demo_O0+0x3fde5) (BuildId: 899e8b30ca0eaa16c1c8eb74df44aaf04c41db09)
#5 0x64fd579fcf22 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/home/le0n/fuzz/libFuzzer/demo1/demo_O0+0x2df22) (BuildId: 899e8b30ca0eaa16c1c8eb74df44aaf04c41db09)
#6 0x64fd57a26c12 in main (/home/le0n/fuzz/libFuzzer/demo1/demo_O0+0x57c12) (BuildId: 899e8b30ca0eaa16c1c8eb74df44aaf04c41db09)
#7 0x7e091ce29d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

SUMMARY: AddressSanitizer: heap-buffer-overflow /home/le0n/fuzz/libFuzzer/demo1/./demo.cc:9:7 in FuzzMe(unsigned char const*, unsigned long)
Shadow bytes around the buggy address:
0x0c0480000600: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
0x0c0480000610: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fd
0x0c0480000620: fa fa fd fd fa fa fd fa fa fa fd fa fa fa fd fa
0x0c0480000630: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
0x0c0480000640: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
=>0x0c0480000650: fa fa fd fa fa fa fd fa fa fa[03]fa fa fa fa fa
0x0c0480000660: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0480000670: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0480000680: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0480000690: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c04800006a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==15295==ABORTING
MS: 1 ChangeByte-; base unit: 147f5286f0f0f6e81d00e810d1f49361f173b3f6
0x4c,0x65,0x30,
Le0
artifact_prefix='./'; Test unit written to ./crash-10a45a293f07e88f3b4651bcfc0dd2cf6cb6a162
Base64: TGUw

种子seed

1
INFO: Seed: 3203650924

这里在程序执行时会生成一个seed随机种子,

输入大小

1
2
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 corpu

这里提示没有指定-max_len参数,所以libFuzzer默认4096字节作为最大输入字节,并且提示没有指定一个语料库

覆盖率情况

1
2
3
4
5
6
#2      INITED cov: 3 ft: 3 corp: 1/1b exec/s: 0 rss: 30Mb
#12 NEW cov: 4 ft: 4 corp: 2/4b lim: 4 exec/s: 0 rss: 30Mb L: 3/3 MS: 5 CopyPart-ChangeByte-ChangeByte-CrossOver-CrossOver-
#658 NEW cov: 5 ft: 5 corp: 3/7b lim: 8 exec/s: 0 rss: 30Mb L: 3/3 MS: 1 CMP- DE: "L\000"-
#5541 NEW cov: 6 ft: 6 corp: 4/12b lim: 53 exec/s: 0 rss: 31Mb L: 5/5 MS: 3 PersAutoDict-CopyPart-CMP- DE: "L\000"-"e\000"-
#5678 REDUCE cov: 6 ft: 6 corp: 4/11b lim: 53 exec/s: 0 rss: 31Mb L: 4/4 MS: 2 ShuffleBytes-EraseBytes-
#7109 REDUCE cov: 6 ft: 6 corp: 4/10b lim: 63 exec/s: 0 rss: 31Mb L: 3/3 MS: 1 EraseBytes-

这段信息就是libFuzzer覆盖率情况,这里表示libFuzzer至少运行了7109次(#7109),共发现10个字节中的4给输入字节(4/10b),共覆盖了6个基本块(cov:6)

漏洞类型

后面就是ASan的日志信息了:

1
2
==15295==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6020000432d3 at pc 0x64fd57ae6f2e bp 0x7ffc3abb1d20 sp 0x7ffc3abb1d18
....

在上一篇文章中以及讲述了ASan的结果怎么看,这里就不赘述了。

crash 文件

1
2
artifact_prefix='./'; Test unit written to ./crash-10a45a293f07e88f3b4651bcfc0dd2cf6cb6a162
Base64: TGUw

在日志最后会告知崩溃POC存放在当前目录,名称crash-10a45a293f07e88f3b4651bcfc0dd2cf6cb6a162

演示Heartbleed(心脏滴血)漏洞

小插曲,如何高效的下载github仓库中的一个文件夹

这是最现代、最“根正苗红”的 Git 官方方法。它允许你先克隆一个“空”的仓库结构,然后只拉取你指定的文件夹内容。

优点:

  • 是 Git 的原生功能,功能强大。
  • 后续如果想拉取其他文件夹或者更新,操作也很方便。

步骤:

  1. 克隆一个“空”仓库
    打开终端,运行以下命令。这会克隆仓库的元数据,但不会检出(下载)任何文件。
1
git clone --filter=blob:none --no-checkout https://github.com/google/fuzzer-test-suite
  • --filter=blob:none: 告诉 Git 不要下载任何实际的文件内容 (blobs)。
  • --no-checkout: 克隆后不要将文件检出到工作目录。
  1. 进入仓库目录
1
cd fuzzer-test-suite
  1. 指定你想要的文件夹
    这是最关键的一步。使用 sparse-checkout 命令告诉 Git 你只需要 openssl-1.0.1f
1
git sparse-checkout set openssl-1.0.1f
  1. 检出文件
    现在,Git 会自动从远程仓库只下载并检出你指定的那个文件夹。
    (在新版本的 Git 中,上一步的 set 命令通常会自动触发检出。如果执行完上一步后文件夹没出现,可以手动执行 git checkout。)

现在你的 fuzzer-test-suite 目录里就只会有 openssl-1.0.1f 这一个文件夹了。

这里多此一举了,还是需要把整个仓库clone下来的

1
le0n:demo1/ $ git clone https://github.com/google/fuzzer-test-suite 

Heardbleed 漏洞是在 2014 年发现的 OpenSSL 加密库中的一个严重漏洞,漏洞编号:CVE-2014-0160。该漏洞可以通过模糊测试的方式快速找到,在 Google 提供的 fuzzer-test-suite 项目中可以找到存在 heartbleed 漏洞的 openssl-1.0.1f 版本。

build 和 target 说明

fuzzer-test-suite 项目下载后,首先进入 openssl-1.0.1f 目录查看一下都有什么:

image-20251006134355599

这里主要有两个文件build.shtarget.cc,首先看一下build.sh中的内容

image-20251006135339984

build.sh中主要作用在于从git中拉取OpenSSL_1_0_1f版本源码放在当前路径下的SRC文件夹中,并在当前路径下创建BUILD文件夹,将SRC中的内容复制到BUILD中,接下来编译安装openssl。接下来利用libfuzzer和asan编译target.cc程序。最后将runtime文件夹下的公司钥复制到当前路径下的runtime中,接下来看target.cc:

image-20251006140104131

这里解释一下程序执行流程图,图中进阶去主函数部分,前面为Init()函数中的部分,主要进行openSSL初始化连接公私钥等内容。下面使用libfuzzer的LLVMFuzzerTestOneLnput()函数代替main()函数,看一下内部情况:

  • 28:建立SSL连接
  • 29、30:创建内存型BIO输入参数sinbio和soutbio
  • 31:设置SSL的获取和发送
  • 32:将SSL设置为在服务器模式下工作
  • 33:尝试将Size个字节从Data中写入BIO sinbio(Data内容和Size数量来自libfuzzer编译内容)
  • 34:等待SSl/TLS握手
  • 35:释放内存

编译target文件

这里由于build.sh中已经编辑好了编译流程,所以我在我的demo1目录下创建一个heartbleed目录来直接运行build.sh:

1
2
le0n:demo1/ $ cd heartbleed
le0n:heartbleed/ $ ../fuzzer-test-suite/openssl-1.0.1f/build.sh

运行结束后就可以看到编译好的openssl-1.0.1f-fsanitize_fuzzer

image-20251006142416104

执行fuzz

接下来直接运行程序openssl-1.0.1f-fsanitize_fuzzer,由于libfuzzer在编译的时候就加入了源码,所以直接运行就开始跑了

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
le0n:heartbleed/ $ ./openssl-1.0.1f-fsanitize_fuzzer                                                                       [14:23:40]
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 1074071809
INFO: Loaded 1 modules (35507 inline 8-bit counters): 35507 [0x5aa8345873d0, 0x5aa83458fe83),
INFO: Loaded 1 PC tables (35507 PCs): 35507 [0x5aa83458fe88,0x5aa83461a9b8),
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: 402 ft: 403 corp: 1/1b exec/s: 0 rss: 41Mb
#3 NEW cov: 402 ft: 404 corp: 2/2b lim: 4 exec/s: 0 rss: 41Mb L: 1/1 MS: 1 ShuffleBytes-
......
#99448 NEW cov: 639 ft: 1029 corp: 76/2907b lim: 526 exec/s: 49724 rss: 386Mb L: 18/352 MS: 1 ChangeBinInt-
#99669 REDUCE cov: 639 ft: 1029 corp: 76/2906b lim: 526 exec/s: 49834 rss: 386Mb L: 20/352 MS: 3 ChangeBinInt-CrossOver-EraseBytes-
#100362 REDUCE cov: 639 ft: 1029 corp: 76/2905b lim: 526 exec/s: 50181 rss: 386Mb L: 20/352 MS: 1 EraseBytes-
=================================================================
==12754==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x629000009748 at pc 0x5aa834088de7 bp 0x7fffa8b90770 sp 0x7fffa8b8ff40
READ of size 32832 at 0x629000009748 thread T0
#0 0x5aa834088de6 in __asan_memcpy (/home/le0n/fuzz/libFuzzer/demo1/heartbleed/openssl-1.0.1f-fsanitize_fuzzer+0x2a9de6) (BuildId: e47aecbd85f11ff5cb154f1c3d357a763363f5ff)
#1 0x5aa8340d2d45 in tls1_process_heartbeat /home/le0n/fuzz/libFuzzer/demo1/heartbleed/BUILD/ssl/t1_lib.c:2586:3
......
#15 0x5aa833fd1a84 in _start (/home/le0n/fuzz/libFuzzer/demo1/heartbleed/openssl-1.0.1f-fsanitize_fuzzer+0x1f2a84) (BuildId: e47aecbd85f11ff5cb154f1c3d357a763363f5ff)

0x629000009748 is located 0 bytes to the right of 17736-byte region [0x629000005200,0x629000009748)
allocated by thread T0 here:
#0 0x5aa834089abe in malloc (/home/le0n/fuzz/libFuzzer/demo1/heartbleed/openssl-1.0.1f-fsanitize_fuzzer+0x2aaabe) (BuildId: e47aecbd85f11ff5cb154f1c3d357a763363f5ff)
......
#12 0x7a8d84029d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

SUMMARY: AddressSanitizer: heap-buffer-overflow (/home/le0n/fuzz/libFuzzer/demo1/heartbleed/openssl-1.0.1f-fsanitize_fuzzer+0x2a9de6) (BuildId: e47aecbd85f11ff5cb154f1c3d357a763363f5ff) in __asan_memcpy
Shadow bytes around the buggy address:
0x0c527fff9290: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c527fff92a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c527fff92b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c527fff92c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c527fff92d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c527fff92e0: 00 00 00 00 00 00 00 00 00[fa]fa fa fa fa fa fa
0x0c527fff92f0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c527fff9300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c527fff9310: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c527fff9320: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c527fff9330: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==12754==ABORTING
MS: 2 CMP-ChangeBit- DE: "\377\377\377\000\000\000\000\000"-; base unit: 6c3ded3648f8b5427b1287425de8775381e287ff
0x3d,0x3,0x0,0x0,0x0,0x18,0x3,0x8,0x0,0x8,0x1,0x80,0x40,0x0,0x3,0xff,0xff,0xff,0x0,0x0,0x0,0x0,0x0,0x0,0x16,0x3,
=\003\000\000\000\030\003\010\000\010\001\200@\000\003\377\377\377\000\000\000\000\000\000\026\003
artifact_prefix='./'; Test unit written to ./crash-ea955891f4388f1bf936c9f824c5081c1cd85a03
Base64: PQMAAAAYAwgACAGAQAAD////AAAAAAAAFgM=

可以看到 libfuzzer 在执行 100362 次后,共发现 2905b 中的 76 个输入项,覆盖 639 个基本块。后面 ASan 日志判定漏洞类型为 堆溢出,溢出执行位置pc、bp、sp分别在 0x6290000097480x7fffa8b907700x7fffa8b8ff40。shadow 图中演示数据已经溢出到相邻高地址堆块的头部,fuzz过程中造成的crash已经保存在当前目录crash-ea955891f4388f1bf936c9f824c5081c1cd85a03。后续可以用 gdb 验证一下。

就先这样,下一篇写优化~

gdb验证

使用 GDB 来验证和深入分析 Fuzzing 发现的崩溃是一个非常专业且有效的做法。它能让你从“程序崩溃了”的层面,深入到“程序为什么会以及如何在这里崩溃”的层面。

您看到的 Kali 上的文章,通常是使用现成的漏洞利用(Exploit)脚本,例如专门的 Python 脚本或 Metasploit 模块。这些工具的目的是利用漏洞,即从一个正在运行的、有漏洞的服务中窃取数据。

而我们使用 GDB 的目的不同,是为了调试和分析,即在本地重现崩溃,并观察程序在崩溃前的内存和变量状态,从而从根本上理解漏洞的成因。

下面,我将为您提供一个详细的、分步的 GDB 验证指南。

核心思路

ASan 在发现越界读取的瞬间就中止了程序,这很好,但它也阻止了我们观察“数据被完整拷贝出来”这个后续行为。为了在 GDB 中清晰地观察到数据泄露,我们需要一个没有 ASan带有调试信息的可执行文件。

第一步:重新编译一个用于 GDB 调试的版本

这是最关键的一步。我们需要关闭 ASan,但开启 -g 标志来保留调试符号(函数名、行号等)。

  1. 回到构建目录:

    1
    2
    # 假设您还在 heartbleed 目录下,先回到 fuzzer-test-suite 的根目录
    cd ../fuzzer-test-suite/
  2. 修改构建脚本(临时):
    我们需要编辑 common.sh 文件,去掉 -fsanitize 相关的标志。

    1
    2
    # 打开 common.sh 文件
    nano common.sh

    找到下面这两行(或类似的行):

    1
    2
    export CFLAGS="$CFLAGS -fsanitize=address,fuzzer-no-link"
    export CXXFLAGS="$CXXFLAGS -fsanitize=address,fuzzer-no-link"

    在它们前面加上 #,将它们注释掉。然后添加新的、只包含 -g 的行:

    1
    2
    3
    4
    # export CFLAGS="$CFLAGS -fsanitize=address,fuzzer-no-link"
    # export CXXFLAGS="$CXXFLAGS -fsanitize=address,fuzzer-no-link"
    export CFLAGS="$CFLAGS -g"
    export CXXFLAGS="$CXXFLAGS -g"

    保存并退出 (Ctrl+X, Y, Enter)。

  3. 重新构建:
    现在,我们再次运行 build.sh 来生成一个带调试信息的新版本。

    1
    2
    cd openssl-1.0.1f/
    ./build.sh

    构建完成后,在 fuzzer-test-suite/out/ 目录下会有一个新的 openssl-1.0.1f 可执行文件。这个文件就是我们用来 GDB 调试的目标。

    注意: 调试完成后,记得把 common.sh 文件改回去,以便将来继续进行 Fuzzing。

第二步:使用 GDB 启动并分析

现在,我们有了可执行文件 (out/openssl-1.0.1f) 和导致崩溃的输入文件 (crash-ea955891...)。

  1. 启动 GDB:
    使用 --args 参数来告诉 GDB 程序的命令行参数是什么。

    1
    2
    3
    # 确保你在 fuzzer-test-suite 目录下
    # 把 crash-ea95... 换成你实际的文件名
    gdb --args ./out/openssl-1.0.1f ./openssl-1.0.1f/crash-ea955891f4388f1bf936c9f824c5081c1cd85a03
  2. 设置断点:
    根据之前的 ASan 报告,我们知道漏洞的关键函数是 tls1_process_heartbeat。我们就在这个函数的开头设置一个断点,这样程序一进入这个函数就会停下来。

    1
    2
    (gdb) b tls1_process_heartbeat
    Breakpoint 1 at 0x...: file ssl/t1_lib.c, line 2508.
  3. 运行程序:

    1
    (gdb) run

    程序会开始执行,然后停在我们设置的断点处。

  4. 检查关键变量 (核心步骤):
    心脏滴血漏洞的核心是程序相信了客户端发来的长度字段。在 tls1_process_heartbeat 函数中,有两个关键变量:

    • p: 一个指向心跳请求负载开头的指针 (unsigned char *)。
    • payload: 从请求中读取到的负载长度 (unsigned int)。

    让我们打印出这两个变量的值。

    1
    2
    3
    4
    5
    6
    7
    8
    # 打印 payload 变量的值
    (gdb) p payload
    $1 = 65535 # 你可能会看到一个很大的数字,比如 65535 或其他

    # 打印 p 指向的内存内容(也就是实际的 payload)
    # x/16xb p 的意思是:以 16 进制、字节为单位,显示 p 指针后的 16 个字节
    (gdb) x/16xb p
    0x...: 0x18 0x03 0x01 0x00 0x01 0x01 0x02 0x03 ...

    分析:
    此时你会看到一个巨大的矛盾:payload 的值非常大(比如几万),但 p 指针指向的实际数据非常短。Fuzzer 正是构造了这样一个畸形的数据包!

  5. 观察 memcpy 行为:
    现在,我们单步执行,直到找到那个致命的 memcpy

    • 不断输入 n (next) 来执行下一行代码。
    • 你会找到类似下面这行代码:
      1
      memcpy(bp, pl, payload);
      这里 bp 是目标缓冲区,pl 是源(和我们之前看的 p 差不多),而 payload 就是那个被我们信任了的、巨大的长度。
  6. 见证数据泄露:

    • memcpy 执行前,我们先看一下目标缓冲区 bp 里的内容。它可能是空的,也可能是一些垃圾数据。
      1
      (gdb) x/64xb bp
    • 执行 memcpy: 输入 n,让这行 memcpy 执行完毕。由于没有 ASan,程序现在不会崩溃,而是会忠实地执行这个越界读取!
    • memcpy 执行后,我们再次检查 bp 缓冲区的内容。
      1
      (gdb) x/64xb bp
      你会看到,bp 缓冲区现在不仅包含了原始的那一小段 payload,后面还跟了一大串从其他内存区域复制过来的数据!这些紧跟在原始数据包后面的、本不应该被读取的数据,就是“心脏滴血”泄露出来的信息。
  7. 退出 GDB:

    1
    (gdb) quit

总结

通过 GDB,您亲眼见证了心脏滴血漏洞的完整过程:

  1. 程序接收到一个长度字段与实际数据大小不符的恶意心跳包。
  2. 程序盲目信任了这个长度字段。
  3. tls1_process_heartbeat 函数中,程序调用 memcpy,试图复制这个超长的数据。
  4. memcpy 越过了原始数据包的边界,将相邻内存中的敏感数据一并复制到了用于响应的缓冲区中。
  5. 这些数据最终会被发回给攻击者,造成严重的信息泄露。

这个过程比单纯看 ASan 报告要直观得多,它让你真正理解了为什么一个缓冲区溢出(Buffer Overflow)会导致一个信息泄露(Information Disclosure)漏洞

LibFuzzer学习(二):提高代码覆盖率和速度

前言

前一篇文章中展示了 libfuzzer 的基本用法,在实际的 模糊测试 过程中,仅仅使用基本的用法会消耗非常多的时间,也会因为代码覆盖率不足的原因导致找不到想要的漏洞。这就需要通过一些技巧来提升代码覆盖率和速度。

指定seed corpus提高代码覆盖率

这里我们拿 Google 的 woff2 进行举例,可以在 fuzzer-test-suite 项目中找到 woff2-2016-05-06 项目的 build 文件,该项目主要用于利用 brotli 压缩算法压缩 woff2 格式字体。

本次实验重点在于比较使用 seed corpus 前后代码覆盖率差别,以及提高代码覆盖率带来的好处。

这里首先简单的介绍一下本次实验 woff2-2016-05-06 目录下的两个重要文件,一个是 build.sh,另外一个是 target.cc。首先看 build.sh 文档。

image-20251006153424312

  • 7、8、9 行:将 woff2、brotli、roboto 项目分别下载到当前目录的 SRCBROTYLIseeds 文件夹中。
  • 13-15 行:添加目录包括 BROTLI/deBROTLI/enc 搜索路径,循环换个编译 font.ccnormalize.cctransform.ccwoff2_common.ccwoff2_dec.ccwoff2_enc.ccglyph.cctable_tags.ccvariable_length.ccwoff2_out.cc 文件。
  • 16-18 行:编译 BROTLI/deBROTLI/enc 路径下所有源文件。
  • 26 行:启用 LibFuzzerASAN 编译 target.cc 文件。

接下来看看 target.cc 文件。

image-20251006154652685

target.cc 文件中的内容是比较简单的,使用 LLVMFuzzerTestOneInput 代替程序中原本的 main 函数,借助 LibFuzzer 可以再程序内部提供输入,主函数中关键函数为 woff2::ConvertWOFF2ToTTF(),将 Woff2 格式文件转换成 TTF 格式文件,参数 datasizeLibFuzzer 提供传入。 接下来在创建一个文件夹,并运行 build.sh

1
2
3
4
5
le0n:libFuzzer/ $ mkdir demo2 
le0n:libFuzzer/ $ ls
demo1 demo2 fuzzer-test-suite
le0n:libFuzzer/ $ cd demo2
le0n:demo2/ $ ../fuzzer-test-suite/woff2-2016-05-06/build.sh

运行后,将会看到编译成功的可执行文件 woff2-2016-05-06-fsanitize_fuzzer,以及编译好的各项库文件。

正常使用libfuzzer

正常使用libfuzzer只需要运行一下./woff2-2016-05-06-fsanitize_fuzzer,接下来看libfuzzer和ASan的日志

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
le0n:demo2/ $ ./woff2-2016-05-06-fsanitize_fuzzer
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 2379482884
INFO: Loaded 1 modules (10840 inline 8-bit counters): 10840 [0x5bda00b03070, 0x5bda00b05ac8),
INFO: Loaded 1 PC tables (10840 PCs): 10840 [0x5bda00b05ac8,0x5bda00b30048),
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: 15 ft: 16 corp: 1/1b exec/s: 0 rss: 32Mb
#22 NEW cov: 16 ft: 17 corp: 2/5b lim: 4 exec/s: 0 rss: 32Mb L: 4/4 MS: 5 CopyPart-ChangeBinInt-CrossOver-ChangeBit-CopyPart-
#637 NEW cov: 17 ft: 18 corp: 3/11b lim: 8 exec/s: 0 rss: 32Mb L: 6/6 MS: 5 CopyPart-CMP-InsertByte-ChangeBinInt-ShuffleBytes- DE: "2FOw"-
#678 REDUCE cov: 17 ft: 18 corp: 3/10b lim: 8 exec/s: 0 rss: 32Mb L: 5/5 MS: 1 CrossOver-
#769 REDUCE cov: 18 ft: 19 corp: 4/18b lim: 8 exec/s: 0 rss: 32Mb L: 8/8 MS: 1 CrossOver-
......
#13911997 NEW cov: 622 ft: 1087 corp: 317/21Kb lim: 4096 exec/s: 70619 rss: 392Mb L: 56/289 MS: 1 ChangeBinInt-
#13937934 NEW cov: 624 ft: 1089 corp: 318/21Kb lim: 4096 exec/s: 70393 rss: 392Mb L: 56/289 MS: 2 ChangeBinInt-ShuffleBytes-
==36942== ERROR: libFuzzer: out-of-memory (malloc(2439171686))
To change the out-of-memory limit use -rss_limit_mb=<N>

#0 0x5bda008cfb01 in __sanitizer_print_stack_trace (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x12bb01) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#1 0x5bda00842398 in fuzzer::PrintStackTrace() (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x9e398) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#2 0x5bda00827085 in fuzzer::Fuzzer::HandleMalloc(unsigned long) (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x83085) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#3 0x5bda00826f9a in fuzzer::MallocHook(void const volatile*, unsigned long) (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x82f9a) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#4 0x5bda008d6f87 in __sanitizer::RunMallocHooks(void const*, unsigned long) (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x132f87) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#5 0x5bda00845684 in __asan::Allocator::Allocate(unsigned long, unsigned long, __sanitizer::BufferedStackTrace*, __asan::AllocType, bool) (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0xa1684) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#6 0x5bda00845cd9 in __asan::asan_memalign(unsigned long, unsigned long, __sanitizer::BufferedStackTrace*, __asan::AllocType) (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0xa1cd9) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#7 0x5bda00900832 in operator new(unsigned long) (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x15c832) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#8 0x5bda009ff4fe in allocate /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/ext/new_allocator.h:127:27
#9 0x5bda009ff4fe in allocate /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/alloc_traits.h:464:20
#10 0x5bda009ff4fe in _M_allocate /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:346:20
#11 0x5bda009ff4fe in _M_create_storage /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:361:33
#12 0x5bda009ff4fe in _Vector_base /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:305:9
#13 0x5bda009ff4fe in vector /usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_vector.h:511:9
#14 0x5bda009ff4fe in woff2::ConvertWOFF2ToTTF(unsigned char const*, unsigned long, woff2::WOFF2Out*) /home/le0n/fuzz/libFuzzer/demo2/SRC/src/woff2_dec.cc:1274:24
#15 0x5bda00a166d0 in LLVMFuzzerTestOneInput /home/le0n/fuzz/libFuzzer/demo2/../fuzzer-test-suite/woff2-2016-05-06/target.cc:13:3
#16 0x5bda008293a3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x853a3) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#17 0x5bda00828af9 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool, bool*) (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x84af9) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#18 0x5bda0082a2e9 in fuzzer::Fuzzer::MutateAndTestOne() (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x862e9) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#19 0x5bda0082ae65 in fuzzer::Fuzzer::Loop(std::vector<fuzzer::SizedFile, std::allocator<fuzzer::SizedFile> >&) (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x86e65) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#20 0x5bda00818fa2 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x74fa2) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#21 0x5bda00842c92 in main (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x9ec92) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#22 0x7d38f2629d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#23 0x7d38f2629e3f in __libc_start_main csu/../csu/libc-start.c:392:3
#24 0x5bda0080d9e4 in _start (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x699e4) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)

MS: 4 ShuffleBytes-ChangeByte-ChangeByte-ChangeBinInt-; base unit: d49d0ae17c1bc8ece65328113ba74c187b5cb627
0x77,0x4f,0x46,0x32,0x0,0x0,0xa,0x0,0x0,0x0,0x0,0x38,0x0,0x1,0x36,0x0,0xfc,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xab,0x0,0x2,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x2,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x77,0x0,0x89,0x8b,0x8b,0x9c,0x66,0x6b,0x4,
wOF2\000\000\012\000\000\000\0008\000\0016\000\374\000\000\000\000\000\000\000\000\253\000\002\000\000\000\000\000\000\000\000\000\000\002\000\000\000\000\000\000\000\000w\000\211\213\213\234fk\004
artifact_prefix='./'; Test unit written to ./oom-93c3659d17d213c13cac023c5d0dbf9f538de819
Base64: d09GMgAACgAAAAA4AAE2APwAAAAAAAAAAKsAAgAAAAAAAAAAAAACAAAAAAAAAAB3AImLi5xmawQ=
SUMMARY: libFuzzer: out-of-memory

可以看到 LibFuzzer 尝试执行了 13937934 次,发现了 21kb 个字节中 318 个输入字节,覆盖 624 个代码块,最后检查到的结果仅仅是一个 OOMOOM样例存储在当前目录下 oom-93c3659d17d213c13cac023c5d0dbf9f538de819

这里其实是有一些问题的,一般即使是轻量级运行库,代码覆盖率只达到 624,实际上是有点少的。这是因为启动 LibFuzzer 的时候仅仅通过自带的 seed 进行变化,即使消耗了大量的内存,也不能在规定时间内找到新的 path

指定seed corpus再次执行libfuzzer

在上面build.sh文件中,其实已经下载好了一些seed,存放在当前目录的seeds文件夹中:

1
2
3
le0n:demo2/ $ ls seeds
README.md fonts index.html less package.json roboto.css roboto.css.map roboto.scss sass
le0n:demo2/ $

其中有很多的文件可以作为本次模糊测试的seed corpus,所以系欸笑傲了就可以创建一个文件夹,每次编译后的输入会保存到这个文件夹中。

1
2
le0n:demo2/ $ mkdir out     
le0n:demo2/ $ ./woff2-2016-05-06-fsanitize_fuzzer ./out ./seeds

这样就制定了seed corpus,并将每次编译后的输入保存到out文件夹中。看一下输出日志:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
le0n:demo2/ $ ./woff2-2016-05-06-fsanitize_fuzzer ./out ./seeds                                                                                           [15:55:38]
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 3611030366
INFO: Loaded 1 modules (10840 inline 8-bit counters): 10840 [0x5ad55adec070, 0x5ad55adeeac8),
INFO: Loaded 1 PC tables (10840 PCs): 10840 [0x5ad55adeeac8,0x5ad55ae19048),
INFO: 0 files found in ./out
INFO: 62 files found in ./seeds
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 168276 bytes
INFO: seed corpus: files: 62 min: 14b max: 168276b total: 3896056b rss: 31Mb
#63 INITED cov: 678 ft: 1187 corp: 13/766Kb exec/s: 0 rss: 56Mb
#64 NEW cov: 683 ft: 1258 corp: 14/828Kb lim: 68784 exec/s: 0 rss: 57Mb L: 63752/68784 MS: 1 ChangeByte-NEW_FUNC[1/1]: 0x5ad55ac802f0 in TransformDictionaryWord(unsigned char*, unsigned char const*, int, int) /home/le0n/fuzz/libFuzzer/demo2/BROTLI/dec/./transform.h:261
#70 NEW cov: 702 ft: 1292 corp: 15/896Kb lim: 68784 exec/s: 0 rss: 59Mb L: 68688/68784 MS: 1 ChangeByte-
......
#4941958 NEW cov: 1047 ft: 4011 corp: 905/56Mb lim: 113600 exec/s: 3290 rss: 860Mb L: 67832/68784 MS: 1 CopyPart-
#4968156 NEW cov: 1048 ft: 4012 corp: 906/56Mb lim: 113856 exec/s: 3292 rss: 860Mb L: 67832/68784 MS: 3 ChangeASCIIInt-ShuffleBytes-CrossOver-
=================================================================
==35385==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6290000aeb81 at pc 0x5ad55abaddfa bp 0x7ffc00e14130 sp 0x7ffc00e13900
WRITE of size 18801 at 0x6290000aeb81 thread T0
#0 0x5ad55abaddf9 in __asan_memcpy (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x120df9) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#1 0x5ad55acec381 in Read /home/le0n/fuzz/libFuzzer/demo2/SRC/src/./buffer.h:86:7
#2 0x5ad55acec381 in ReconstructGlyf /home/le0n/fuzz/libFuzzer/demo2/SRC/src/woff2_dec.cc:500:13
#3 0x5ad55acec381 in ReconstructFont /home/le0n/fuzz/libFuzzer/demo2/SRC/src/woff2_dec.cc:917:15
#4 0x5ad55acec381 in woff2::ConvertWOFF2ToTTF(unsigned char const*, unsigned long, woff2::WOFF2Out*) /home/le0n/fuzz/libFuzzer/demo2/SRC/src/woff2_dec.cc:1282:9
#5 0x5ad55acff6d0 in LLVMFuzzerTestOneInput /home/le0n/fuzz/libFuzzer/demo2/../fuzzer-test-suite/woff2-2016-05-06/target.cc:13:3
#6 0x5ad55ab123a3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x853a3) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#7 0x5ad55ab11af9 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool, bool*) (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x84af9) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#8 0x5ad55ab132e9 in fuzzer::Fuzzer::MutateAndTestOne() (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x862e9) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#9 0x5ad55ab13e65 in fuzzer::Fuzzer::Loop(std::vector<fuzzer::SizedFile, std::allocator<fuzzer::SizedFile> >&) (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x86e65) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#10 0x5ad55ab01fa2 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x74fa2) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#11 0x5ad55ab2bc92 in main (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x9ec92) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#12 0x717cb7629d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#13 0x717cb7629e3f in __libc_start_main csu/../csu/libc-start.c:392:3
#14 0x5ad55aaf69e4 in _start (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x699e4) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)

0x6290000aeb81 is located 0 bytes to the right of 18817-byte region [0x6290000aa200,0x6290000aeb81)
allocated by thread T0 here:
#0 0x5ad55abe98dd in operator new[](unsigned long) (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x15c8dd) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#1 0x5ad55acee2b9 in ReconstructGlyf /home/le0n/fuzz/libFuzzer/demo2/SRC/src/woff2_dec.cc:483:25
#2 0x5ad55acee2b9 in ReconstructFont /home/le0n/fuzz/libFuzzer/demo2/SRC/src/woff2_dec.cc:917:15
#3 0x5ad55acee2b9 in woff2::ConvertWOFF2ToTTF(unsigned char const*, unsigned long, woff2::WOFF2Out*) /home/le0n/fuzz/libFuzzer/demo2/SRC/src/woff2_dec.cc:1282:9
#4 0x5ad55acff6d0 in LLVMFuzzerTestOneInput /home/le0n/fuzz/libFuzzer/demo2/../fuzzer-test-suite/woff2-2016-05-06/target.cc:13:3
#5 0x5ad55ab123a3 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x853a3) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#6 0x5ad55ab11af9 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool, bool*) (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x84af9) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#7 0x5ad55ab132e9 in fuzzer::Fuzzer::MutateAndTestOne() (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x862e9) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#8 0x5ad55ab13e65 in fuzzer::Fuzzer::Loop(std::vector<fuzzer::SizedFile, std::allocator<fuzzer::SizedFile> >&) (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x86e65) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#9 0x5ad55ab01fa2 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x74fa2) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#10 0x5ad55ab2bc92 in main (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x9ec92) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37)
#11 0x717cb7629d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

SUMMARY: AddressSanitizer: heap-buffer-overflow (/home/le0n/fuzz/libFuzzer/demo2/woff2-2016-05-06-fsanitize_fuzzer+0x120df9) (BuildId: 396f1a60e878f787ff7093be06563de75c467f37) in __asan_memcpy
Shadow bytes around the buggy address:
0x0c528000dd20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c528000dd30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c528000dd40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c528000dd50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c528000dd60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c528000dd70:[01]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c528000dd80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c528000dd90: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c528000dda0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c528000ddb0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c528000ddc0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==35385==ABORTING
MS: 1 ChangeByte-; base unit: ff41f056b7e38068e1057c2641af6456baa5daa1
artifact_prefix='./'; Test unit written to ./crash-558d58bf06923fd30cba47a6817c123332a79f60

根据日志可以看到本次fuzz载入了seeds目录下的62个文件,共尝试了4968156次共发现56MB字节中的906个输入字节,共覆盖1048个基本快。并且发现了一个堆溢出漏洞。crash结果存放在当前目录的crash-558d58bf06923fd30cba47a6817c123332a79f6

对比未使用seed corpus时的覆盖情况:

image-20251006201826212

image-20251006201839442

可以看到在使用seed corpus后libfuzzer在尝试次数减少的同时,代码覆盖率相比之前提高了424个基本块。这也导致了libfuzzer发现了新路径中的堆溢出路径

也可以在out文件夹中看到每次变异后的数据:

image-20251006202139274

嗯,非常之多

使用多核并行fuzz

和AFL一样,LibFuzzer也提供使用多核并行fuzz。每个LibFuzzer进程都是单进程,但是可以在seed corpus目录中并行开启多个LibFuzzer进程,这样来一个fuzzer进程找到的任何新输入都可以为其他fuzzer进程提供帮助。

可以使用 -jobs=N 参数进行设置,这里需要注意的是 N 并不代表使用的核心,而是设定的 fuzzing job。LibFuzzer在指定-jobs参数后,这些fuzzing job将会由 worker 进行处理,worker数量默认认为是CPU核心数的一半。这是举个例子,-job=40 在16核心上运行,默认会执行8个worker处理40个fuzzing job,当其中一个worker完成已分配的fuzzing job后,将会从剩下的32个fuzzing job中再分配一个job给worker。

还是用woff2进行举例,指定 -jobs=8(服务器 16 核):

1
./woff2-2016-05-06-fsanitize_fuzzer -jobs=8 out/ seeds/

由于我的wsl2可以使用主机的16核,所以LibFuzzer会启用8个worker来处理这8个fuzzing job,每个worker处理的结果日志会分别放在当前目录下 fuzz-0~7.log 文件中。可以看到job 2首先发现了一个崩溃的时间。

在之前未使用并行,但使用seed corpus进行模糊测试,出现崩溃所用的时间大概是 15分钟左右。本次由于使用seed corpus和并行执行,发现一次发现崩溃的时间 不到2分钟

8个worker执行日志及跑出来的crash结果将会在当前路径下看到(我提前中断了):

image-20251006202935797

字典

另外一种提升速度的方式就是使用字典辅助进行模糊测试,可以在被测试程序执行时添加 -dict=dictionaries_path

测试项目构建

这里选择之前使用AFL++测试的libxml2项目进行举例。同时,可以在fuzzer-test-suite项目中找到对应的项目build路径(fuzzer-test-suite/libxml2-v2.9.2/),其中比较重要的文件有两个:build.shtarget.cc。这是先看一下一下 build.sh

image-20251006204547504

这个 build.sh 脚本我做了一些小的改动,下面稍微做一下分析:

  • 13行:将2.9.2版本的libxml2下载到当前目录的SRC文件夹中。
  • 14行:这里做了第一次更改,源脚本中这里下载了一个AFL,目的是为了用AFL项目中的xml.dict字典,因为我的虚拟机里本来就有AFL++,在字典目录中同样有这个字典,所以就把这里给注释掉了。当然如果你的环境中没有AFL或AFL++也可以不把这里注释掉,或者注释掉后单独下载自己习惯用的字典。
  • 15, 7~11行:这里主要是构建libxml2,将项目源代码拷贝到BUILD目录中进行编译。
  • 18, 19行:这里是第二个修改的小地方,原来是将AFL字典目录下的xml.dict拷贝到当前目录,前面也说了,本身环境中AFL++的字典。
  • 21~26行:使用LibFuzzer变量导入target.cc程序。

接下来看看一下 target.cc 内部程序流:

image-20251006204825371

target.cc 内部调用也非常简单,首先将 main() 函数替换为 LibFuzzer 的 LLVMFuzzerTestOneInput() 函数,接下来调用 xmlSetGenericErrorFunc() 函数来进行异常处理,接下来的目标函数 xmlReadMemory() 这个函数是用来解析内存中的 XML 文档并构建树,一参数指向的是 xml 内存数据数组地址,二参数数组长度(官方文档)。由 LLVMFuzzerTestOneInput() 函数随机生成的数据 data 作为 xmlReadMemory() 的一参,size作为二参

构建文档和目标文件解释完了,接下来可以在主目录为本次 libxml2 实验创建一个单独的文件夹,并运行 build.sh 就可以了:

1
le0n:demo3/ $ ../fuzzer-test-suite/libxml2-v2.9.2/build.sh 

运行时由于需要从 git 上获取项目,所以请保持网络畅通,运行结束将会在当前文件夹下看到存放源代码的 SRC 文件夹、存放库文件的 BUILD 文件夹、LibFuzzer 测试程序 libxml2-v2.9.2-fsanitize_fuzzer 和字典 xml.dict

image-20251006205240330

比对代码覆盖率

为了展示使用字典能够提升效率的效果,这里可以做一个小实验:两次执行 LibFuzzer 测试程序,第一次不带字典执行,第二次带字典执行。

在相同的时间内 ctrl+c 中断,两次执行中断时基本块覆盖情况:

第一次不带字典执行,运行 15 秒后 ctrl+c 中断。

1
le0n:demo3/ $ ./libxml2-v2.9.2-fsanitize_fuzzer 

image-20251006205823898

第二次带字典执行,运行 15 秒后中断

1
le0n:demo3/ $ ./libxml2-v2.9.2-fsanitize_fuzzer -dict=xml.dict 

image-20251006210008026

可以看到添加字典后运行覆盖的基本块要比未使用字典运行覆盖的基本块要多841,所以使用字典一定程度上提高fuzz的效率。libxml2就不跑了,在之前的afl++中已经跑过了