libfuzzer 文档

简介

libfuzzer是进程内的,覆盖率指导的,进化的fuzzing引擎。

就是变异,覆盖率那些都给你做好了,你只需要定义LLVMFuzzerTestOneInput,将编译的数据喂给要fuzz的目标函数就行

libfuzzer还在不断开发完善中,所以可能需要当前版本的clang或者没那么古老的clang进行编译

clang6.0开始就默认在里面包含了libfuzzer

使用——需要实现一个fuzz target

所谓fuzz target,就是去实现LLVMFuzzerTestOneInput,下面是文档给出的示例,你需要将

1
2
3
4
5
// fuzz_target.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
DoSomethingInterestingWithMyAPI(Data, Size);
return 0; // Non-zero return values are reserved for future use.
}

有以下注意事项:
1、fuzzing引擎会在同一个进程用不同的输入执行LLVMFuzzerTestOneInput很多次
2、LLVMFuzzerTestOneInput比如容忍任何形式的输入(空,很长的数据,格式错误的数据等)
3、任何输入都不能调用exit退出(毕竟还要继续fuzz啊)
4、可以使用线程,应该线程也应在函数结束前结束,不知道对不对,后面是原文(It may use threads but ideally all threads should be joined at the end of the function.)
5、必须具有确定性,因为不确定性降低fuzz的效率(比如不会根据输入去随机选择路径)
6、必须快,避免立方以上的复杂性,进行日志记录或者过多的内存消耗
7、理想的情况下,不要修改任何的全局状态
8、通常目标约窄越好,比如目标可以解析多种数据格式,就写成多个目标,每个格式一个

使用libfuzzer

Clang6.0开始包含了libfuzzer,

使用就是在编译时使用-fsanitize=fuzzer即可,当然也可以加上 AddressSanitizer (ASAN),UndefinedBehaviorSanitizer (UBSAN),以及MemorySanitizer (MSAN),下面是例子

1
2
3
4
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

当然也可以单独编译libfuzzer,之后链接起来就可以了(这是以前文档的做法了,当然假如你想使用最新的libfuzzer也就只能像下面那样了)

1
clang -fsanitize-coverage=trace-pc-guard -fsanitize=address your_lib.cc fuzz_target.cc libFuzzer.a -o my_fuzzer

语料库

libfuzzer可以在没有任何初始种子的情况下工作,但是如果被测库接受复杂的结构化输入,效率将会降低。

所以对于一些结构化的输入最好提供语料库(原始样本),这样可以提高效率

如果语料库很大,可以先将其最小化,同事保留完成的覆盖率,使用-merge=1即可

1
2
mkdir NEW_CORPUS_DIR  # Store minimized corpus here.
./my_fuzzer -merge=1 NEW_CORPUS_DIR FULL_CORPUS_DIR

假如想将其他的语料库加到现在的,只讲有新覆盖率的加入

1
./my_fuzzer -merge=1 CURRENT_CORPUS_DIR NEW_POTENTIALLY_INTERESTING_INPUTS_DIR

运行

最好创建一个目录,里面包含初始的“种子”样本输入

1
2
mkdir CORPUS_DIR
cp /some/input/samples/* CORPUS_DIR

之后运行就行,当然可以加一些参数

1
./my_fuzzer CORPUS_DIR  # -max_len=1000 -jobs=20 ...

一旦fuzzer发现了有趣的testcase,就是能够触发新的路径的testcase,就会添加到CORPUS_DIR

默认情况,fuzzer会持续运行,直到发现一个bug,任何的crash或者sanitizer的报错,都会停止fuzzing,之后保存触发crash的样本会默认会保存到当前目录,通常命名为crash-<sha1>, leak-<sha1>, 或者 timeout-<sha1>

并行fuzz

每个libfuzzer进程都是单线程,除非这个库自己起了多线程。

但是可以多个libfuzzer共享一个语料库目录,那么一个模糊器进程找到的任何新输入将对其他模糊器进程可用(除非你是用-reload=0关闭了这个reload预料库的功能)

我们可以通过-jobs=N来控制,N个fuzzing jobs必须完成它的使命(就是找到bug或者time/iteration达到我们限制的上限了),jobs是在worker进程中运行的,默认使用一半可用的cpu核心;当然我们可以用参数-workers=N指定worker的个数。比如,在一个12核的机器使用-jobs=30 ,就默认运行6个worker,最好的情况每个worker可能5个bug

fork模式

这还是一个实验的模式,-fork=N表示并行的jobs的数量,这个应该跟上面的jobs不是同一个

这个模式在每个process中开启了oom-, timeout-, and crash-resistant

最上层的libfuzzer不会做任何fuzzing,会产生最多N个并发的子进程,并为子进程提供corpus的小的随机子集。子进程退出后,最上层的libfuzzer会将其产生的corpus合并到主corpus

下面是相关的一些flags:

1
2
3
4
5
6
-ignore_ooms
True by default. If an OOM happens during fuzzing in one of the child processes, the reproducer is saved on disk, and fuzzing continues.(默认为true,如果子进程发生Out of memory,会保存输入,但fuzzing继续)
-ignore_timeouts
True by default, same as -ignore_ooms, but for timeouts.(忽略超时)
-ignore_crashes
False by default, same as -ignore_ooms, but for all other crashes.(默认是false,开启会忽略所有错误)

这个其实是想最终将-jobs = N-workers = N替换为-fork = N

merge恢复

有时候合并较大的语料库,比较耗时,kill掉之后也可以恢复,需要使用-merge_control_file(指定用于合并过程的控制文件)

推荐使用killall -SIGUSR1 /path/to/fuzzer/binary来优雅地kill

下面是官方例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
% rm -f SomeLocalPath
% ./my_fuzzer CORPUS1 CORPUS2 -merge=1 -merge_control_file=SomeLocalPath
...
MERGE-INNER: using the control file 'SomeLocalPath'
...
# While this is running, do `killall -SIGUSR1 my_fuzzer` in another console
==9015== INFO: libFuzzer: exiting as requested

# This will leave the file SomeLocalPath with the partial state of the merge.
# Now, you can continue the merge by executing the same command. The merge
# will continue from where it has been interrupted.
% ./my_fuzzer CORPUS1 CORPUS2 -merge=1 -merge_control_file=SomeLocalPath
...
MERGE-OUTER: non-empty control file provided: 'SomeLocalPath'
MERGE-OUTER: control file ok, 32 files total, first not processed file 20
...

运行选项

可以传递零个或多个语料库目录作为命令行参数。模糊器将从这些语料库目录中的每一个中读取测试输入,并且所生成的任何新测试输入将被写回到第一个语料库目录中。

1
./fuzzer [-flag1=val1 [-flag2=val2 ...] ] [dir1 [dir2 ...] ]

如果给的是文件列表不是目录,那就是相当于回归测试,漏洞复现了

文档中已经对-flag列得比较详细了,我就不复制了,你用-help=1得到的信息启示更详细

但是列一些比较有趣的/常用的

1
2
3
4
5
6
7
8
9
10
11
-max_len:最大长度
-timeout:超时时间
-merge:这个用来合并语料库的
-minimize_crash:将提供的crash input最小化,可以与`-runs=N or -max_total_time=N `一起使用
-jobs:jobs的数量
-workers:指定workers的数量
-dict:指定字典
-reduce_inputs:尝试减少输入的大小,同时保留其全部功能集;默认就为1了。
-only_ascii:默认是0,就是只生成ASCII中可打印的以及空格,应该tab也算
-artifact_prefix:指定crash的路径前缀
-exact_artifact_path:指定crash文件的名字,并行fuzz的时候就不要使用了

输出解读

文档说信息是输出到stderr,比如下面的例子

1
2
3
4
5
6
7
8
9
10
11
INFO: Seed: 1523017872
INFO: Loaded 1 modules (16 guards): [0x744e60, 0x744ea0),
INFO: -max_len is not provided, using 64
INFO: A corpus is not provided, starting from an empty corpus
#0 READ units: 1
#1 INITED cov: 3 ft: 2 corp: 1/1b exec/s: 0 rss: 24Mb
#3811 NEW cov: 4 ft: 3 corp: 2/2b exec/s: 0 rss: 25Mb L: 1 MS: 5 ChangeBit-ChangeByte-ChangeBit-ShuffleBytes-ChangeByte-
#3827 NEW cov: 5 ft: 4 corp: 3/4b exec/s: 0 rss: 25Mb L: 2 MS: 1 CopyPart-
#3963 NEW cov: 6 ft: 5 corp: 4/6b exec/s: 0 rss: 25Mb L: 2 MS: 2 ShuffleBytes-ChangeBit-
#4167 NEW cov: 7 ft: 6 corp: 5/9b exec/s: 0 rss: 25Mb L: 3 MS: 1 InsertByte-
...

首先是输出有关fuzzer的选项和配置的信息,包括当前的初始种子,当然你可以通过-seed=N来指定

接下来就是输出事件,还有统计信息,

事件有下面的

1
2
3
4
5
6
7
READ:fuzzer已经从语料库目录读取了所有输入样本
INITED:模糊器已完成初始化,包括运行语料库中的所有样本
NEW:发现新路径,并将输入保存到语料库目录
REDUCE:找到一个更好,一般指更小的输入触发以前的路径(设置-reduce_inputs=0,可以禁用)
pulse:fuzzer已经生成2^n的输入(主要是定期生成让用户知道模糊器仍在工作)
DONE:模糊测试完成,达到了迭代限制(-runs)或者时间限制(-max_total_time)
RELOAD:fuzzer定期从语料库重新加载输入,这样,就可以发现其他模糊测试工具发现的输入

每一行的统计信息:

1
2
3
4
5
6
cov:当前语料库所衣服挂的代码块或者边数
ft:libfuzzer用不同的signals来评估代码覆盖率:边覆盖率,边的数量,value profiles,间接调用和被调用等,将他们合并在一起,就是features (ft)
corp:当前内存中测试的语料库的数量还有大小(单位为bytes)
lim:当前限制的输入长度,随着时间推移,会增加到-max_len
exec/s:每秒的迭代次数
rss:当前内存的消耗

对于NEW和REDUCE事件,还有有关产生新输入的变异操作的信息:

1
2
3
L:新输入的大小

MS: <n> <operations> 用于生成输入的变异操作的计数和变异方法。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cat << EOF > test_fuzzer.cc
#include <stdint.h>
#include <stddef.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (size > 0 && data[0] == 'H')
if (size > 1 && data[1] == 'I')
if (size > 2 && data[2] == '!')
__builtin_trap();
return 0;
}
EOF
# Build test_fuzzer.cc with asan and link against libFuzzer.
clang++ -fsanitize=address,fuzzer test_fuzzer.cc
# Run the fuzzer with no corpus.
./a.out

更多例子可以在这里看到:

https://github.com/google/fuzzing/blob/master/tutorial/libFuzzerTutorial.md

进阶功能

字典

使用-dict=DICTIONARY_FILE指定,这样应该可以快速绕过magic values

使用字典可能会大大提高搜索速度。 字典语法类似于AFL的-x选项所使用的语法。

1
2
3
4
5
6
7
8
9
10
# Lines starting with '#' and empty lines are ignored.

# Adds "blah" (w/o quotes) to the dictionary.
kw1="blah"
# Use \\ for backslash and \" for quotes.
kw2="\"ac\\dc\""
# Use \xAB for hex values
kw3="\xF7\xF8"
# the name of the keyword followed by '=' may be omitted:
"foo\x0Abar"

Tracing CMP instructions

跟踪CMP指令

编译的时候加上:-fsanitize-coverage=trace-cmp

libFuzzer将拦截CMP指令并根据截获的CMP指令的参数去引导突变。 这可能减缓模糊的速度,但是很有可能会改善结果。

Value Profile

必须开启上面的编译标志,运行的时候加上-use_value_profile=1,之后fuzzer就会收集比较指令的参数的value profiles,并将一些新的values作为新的覆盖。

实现的操作如下:
1、编译器通过对所有CMP指令的插桩,获取两个参数
2、获取两个参数后计算 (caller_pc&4095) | (popcnt(Arg1 ^ Arg2) << 12),使用这个值,在bitset中设置一个bit
3、在 bitset 中设置了新的bit说明找到新路径了

此功能可能会发现许多有趣的输入,但是有两个缺点。 首先,可能会导致速度降低2倍。 其次,语料库可能增长数倍。

Fuzzer-friendly build mode

1、一些软件使用伪随机数生成器,导致可能同一个输入,但是导致路径不一样
2、或者png会有校验和

我们可以用FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION这个宏来使代码更加友好,下面是例子

1
2
3
4
5
6
7
8
void MyInitPRNG() {
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
// In fuzzing mode the behavior of the code should be deterministic.
srand(0);
#else
srand(time(0));
#endif
}

与afl一起fuzz

libfuzzer可以使用afl的发现的语料库,比如下面的例子

1
2
./afl-fuzz -i testcase_dir -o findings_dir /path/to/program @@
./llvm-fuzz testcase_dir findings_dir # Will write new tests to testcase_dir

当然afl也可以fuzz,LLVMFuzzerTestOneInput写的目标,具体可以看这:https://github.com/llvm/llvm-project/tree/master/compiler-rt/lib/fuzzer/afl

我的fuzzer是否优秀?

这个可以通过查看代码覆盖率来衡量,看看我们的语料库或者LLVMFuzzerTestOneInput函数是否有改进的空间

https://clang.llvm.org/docs/SourceBasedCodeCoverage.html

具体例子可以看这个

https://github.com/google/fuzzing/blob/master/tutorial/libFuzzerTutorial.md#visualizing-coverage

自定义的变异

我们可以自定义变异,可以参考: https://github.com/google/fuzzing/blob/master/docs/structure-aware-fuzzing.md

启动初始化

如果这个库启动的时候需要初始化,有几种方式,

比如在LLVMFuzzerTestOneInput里面,(或者有时候是全局变量 ,直接在全局方位初始化得了)

1
2
3
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
static bool Initialized = DoInitialization();
...

此外,我们还可以定义一个LLVMFuzzerInitialize,建议你确实需要argc和argv的时候才使用

1
2
3
4
extern "C" int LLVMFuzzerInitialize(int *argc, char ***argv) {
ReadAndMaybeModify(argc, argv);
return 0;
}

泄露

使用AddressSanitizer 或者 LeakSanitizer 构建的程序会在程序关闭后检查内存泄露

但是进程内的模糊测试是不方便的,因为一旦发现导致内存泄露的突变立即去报告这个内存泄露,每次变异后都去检测代价太大

默认情况,libfuzzer在每次变异时会计算malloc和free调用的次数,如果次数不匹配(但是并不是意味着内存泄露),libfuzzer会给到LeakSanitizer过一下,如果确实泄露,才会复制报告,进程退出

如果目标存在大量内存泄露,而且禁止了泄露检查,那么会耗尽内存

开发libfuzzer

在MacOS和Linux上,libfuzzer是LLVM项目的一部分。

其他系统可使用-DLIBFUZZER_ENABLE=YES来请求编译libfuzzer

编译tests可以使用DLIBFUZZER_ENABLE_TESTS=ON

参考

https://github.com/Dor1s/libfuzzer-workshop
https://docs.google.com/presentation/d/1pbbXRL7HaNSjyCHWgGkbpNotJuiC4O7L_PDZoGqDf5Q/edit#slide=id.p4
https://github.com/google/fuzzing/blob/master/tutorial/libFuzzerTutorial.md
https://chromium.googlesource.com/chromium/src/testing/libfuzzer/+/HEAD/
https://chromium.googlesource.com/chromium/src/testing/libfuzzer/+/HEAD/getting_started.md

自愿打赏专区