环境配置

  • Ubuntu 22.04
    安装源码:
    showLineNumbers
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # Install git and get this tutorial
    sudo apt-get --yes install git
    git clone https://github.com/google/fuzzing.git fuzzing

    # Get fuzzer-test-suite
    git clone https://github.com/google/fuzzer-test-suite.git FTS

    ./fuzzing/tutorial/libFuzzer/install-deps.sh # Get deps
    ./fuzzing/tutorial/libFuzzer/install-clang.sh # Get fresh clang binaries
    验证:
    1
    2
    clang++ -g -fsanitize=address,fuzzer fuzzing/tutorial/libFuzzer/fuzz_me.cc
    ./a.out 2>&1 | grep ERROR
    结果应该是ASAN检测到内存错误:

入门Fuzzer编写

fuzz target

fuzz target定义:有如下格式定义的对参数进行操作的函数

1
2
3
4
extern "C" int LLVMFuzzerTestOneInput(const uint8_t **Data, size_t Size) {
DoSomethingWithData(Data, Size);
return 0;
}

查看刚刚运行的./fuzz_me.cc源码,当输入长度大于等于3时,会判断前四个元素是否为FUZZ,即存在越界读取问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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;
}

为了编译一个fuzzer二进制程序,需要使用Clang工具编译源码,并添加以下flags:

  • -fsanitize=fuzzer(required):为libFuzzer提供进程内的覆盖率信息,并将其与libFuzzer运行时进行链接;
  • -fsanitize=address(recommended):开启ASAN检测;
  • -fno-omit-frame-pointer:更好看的函数调用栈信息;
  • -O1:编译优化;
  • -g(recommended):开启调试信息,令错误信息更易于阅读。
    例如:
    1
    clang++ -g -fsanitize=address,fuzzer fuzzing/tutorial/libFuzzer/fuzz_me.cc
    然后执行./a.out即可出现上面的结果截图。

输出信息解读

其中有一段信息:INFO: Seed: 4055535767,表明fuzzer由此随机种子开始进行模糊测试。使用-seed=4055535767参数可以指定种子获得相同结果。

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 corpus

默认情况下,libFuzzer假定所有输入都小于等于4096字节。可以使用-max_len=N或者添加种子来修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#2      INITED cov: 3 ft: 3 corp: 1/1b exec/s: 0 rss: 30Mb
#4 NEW cov: 4 ft: 4 corp: 2/4b lim: 4 exec/s: 0 rss: 30Mb L: 3/3 MS: 2 CrossOver-InsertByte-
#1891 NEW cov: 5 ft: 5 corp: 3/25b lim: 21 exec/s: 0 rss: 30Mb L: 21/21 MS: 2 InsertRepeatedBytes-CMP- DE: "F\000"-
#1942 REDUCE cov: 5 ft: 5 corp: 3/24b lim: 21 exec/s: 0 rss: 31Mb L: 20/20 MS: 1 EraseBytes-
#1946 REDUCE cov: 5 ft: 5 corp: 3/16b lim: 21 exec/s: 0 rss: 31Mb L: 12/12 MS: 4 ShuffleBytes-CrossOver-CrossOver-EraseBytes-
#2038 REDUCE cov: 5 ft: 5 corp: 3/13b lim: 21 exec/s: 0 rss: 31Mb L: 9/9 MS: 2 ShuffleBytes-EraseBytes-
#2063 REDUCE cov: 5 ft: 5 corp: 3/9b lim: 21 exec/s: 0 rss: 31Mb L: 5/5 MS: 5 CrossOver-CMP-ChangeBit-ShuffleBytes-EraseBytes- DE: "F\000\000\000"-
#2410 REDUCE cov: 5 ft: 5 corp: 3/8b lim: 21 exec/s: 0 rss: 31Mb L: 4/4 MS: 2 PersAutoDict-EraseBytes- DE: "F\000\000\000"-
#3038 REDUCE cov: 5 ft: 5 corp: 3/7b lim: 25 exec/s: 0 rss: 31Mb L: 3/3 MS: 3 CopyPart-ShuffleBytes-EraseBytes-
#6341 REDUCE cov: 6 ft: 6 corp: 4/11b lim: 53 exec/s: 0 rss: 31Mb L: 4/4 MS: 3 PersAutoDict-InsertByte-CMP- DE: "F\000"-"U\000"-
#6442 REDUCE cov: 6 ft: 6 corp: 4/10b lim: 53 exec/s: 0 rss: 31Mb L: 3/3 MS: 1 EraseBytes-
#28088 REDUCE cov: 7 ft: 7 corp: 5/17b lim: 261 exec/s: 0 rss: 32Mb L: 7/7 MS: 1 CMP- DE: "Z\000\000\000"-
#28250 REDUCE cov: 7 ft: 7 corp: 5/16b lim: 261 exec/s: 0 rss: 32Mb L: 6/6 MS: 2 CopyPart-EraseBytes-
#28351 REDUCE cov: 7 ft: 7 corp: 5/15b lim: 261 exec/s: 0 rss: 32Mb L: 5/5 MS: 1 EraseBytes-
#28392 REDUCE cov: 7 ft: 7 corp: 5/14b lim: 261 exec/s: 0 rss: 32Mb L: 4/4 MS: 1 EraseBytes-

以上信息表示libFuzzer已经尝试了28392次输入,并且发现了5个种子14字节长度,实现覆盖7个点。点可以理解为代码中的基本块(入门可以先这么理解,其实应该叫边吧)。

1
2
3
4
==4197==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6020000b5af3 at pc 0x56907ee52f3c bp 0x7fff01c7e4f0 sp 0x7fff01c7e4e8
READ of size 1 at 0x6020000b5af3 thread T0
#0 0x56907ee52f3b in FuzzMe(unsigned char const*, unsigned long) /home/eutopia/Fuzz_learning/LibFuzzer/fuzzing/tutorial/libFuzzer/fuzz_me.cc:9:7
#1 0x56907ee52fd4 in LLVMFuzzerTestOneInput /home/eutopia/Fuzz_learning/LibFuzzer/fuzzing/tutorial/libFuzzer/fuzz_me.cc:13:3

存在一个输入使ASAN检测到heap-buffer-overflow错误并停止运行。

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

在退出前libFuzzer将引发crash的输入保存为文件,查看文件:

1
2
base ❯ cat crash-0eb8e4ed029b774d80f2b66408203801cb982a60
FUZ

因为FUZ,导致通过了前面的一系列检查,并且对data[3]进行了访问,导致越界。

心脏滴血漏洞实验

fuzzer-test-suite包含了openssl-1.0.1f存在该漏洞
执行以下命令来编译fuzzer

1
2
mkdir -p ~/heartbleed; rm -rf ~/heartbleed/*; cd ~/heartbleed
~/FTS/openssl-1.0.1f/build.sh

编译完成后运行目标fuzzer,过一会后产生crash如下:

种子语料库

设置多个参数指定种子目录时,会递归将所有目录中文件用于种子输入,然后如果模糊测试过程中出现了产生新覆盖率的种子,会将其保存在第一个目录中。

1
2
mkdir MY_CORPUS
./woff2-2016-05-06-fsanitize_fuzzer MY_CORPUS/ seeds/

字典

使用-dict参数设置字典目录:

1
./libxml2-v2.9.2-fsanitize_fuzzer -dict=afl/dictionaries/xml.dict  # Press Ctrl-C in 10-20 seconds

将libFuzzer作为库

如果目标代码必须提供main函数,那么需要将libFuzzer作为库函数调用执行。需要在编译时传递-fsanitize=fuzzer-no-link参数,并指定无main函数的libFuzzer版本,其路径为。

1
/usr/lib/<llvm-version>/lib/clang/<clang-version>/lib/linux/libclang_rt.fuzzer_no_main-<architecture>.a

代码可以执行正常功能,当需要开始模糊测试时,可以调用LLVMFuzzerRunDriver,传入程序参数和回调函数。类似LLVMFuzzerTestOneInput,且声明相同:

1
2
extern "C" int LLVMFuzzerRunDriver(int *argc, char ***argv,
int (*UserCb)(const uint8_t *Data, size_t Size));

参考链接