文章目录
- 前言
- 环境搭建
- 基础使用
- 编写 fuzz target
- 编译链接
- demo 测试 && 输出日志分析
- 心脏滴血漏洞测试
- 提高代码覆盖率和测试速度
- 指定种子语料库
- 多核并行 Fuzz
- 使用字典
- 参考
前言
相较于 AFL
来说,LibFuzzer
在单个进程内完成模糊测试,以此来避免的反复启动进程的开销,所以理论上其比 AFL
的效率更高
对于 LibFuzzer
官方给出如下定义
LibFuzzer is in-process, coverage-guided, evolutionary fuzzing engine.
LibFuzzer is linked with the library under test, and feeds fuzzed inputs to the library via a specific fuzzing entrypoint (aka “target function”);
the fuzzer then tracks which areas of the code are reached, and generates mutations on the corpus of input data in order to maximize the code coverage.
The code coverage information for libFuzzer is provided by LLVM’s SanitizerCoverage instrumentation.
按照官方定义,libFuzzer
是一个 in-process
(进程内的),coverage-guided
(以覆盖率为引导的),evolutionary
(进化的) 的 fuzz
引擎,是 LLVM
项目的一部分
-
in-process
(进程内的):LibFuzzer
并没有为每一个测试用例都开启一个新进程,而是直接在内存中变异数据 -
coverage-guided
(以覆盖率为引导的):LibFuzzer
对每一个输入都进行代码覆盖率的计算,并且不断积累这些测试用例以使代码覆盖率最大化 -
evolutionary
(进化的):fuzz
按照类型分为3类,这是最后一种- 第一类是基于生成的
Generation Based
。通过对目标协议或文件格式建模的方法,从零开始产生测试用例,没有先前的状态 - 第二类是基于突变的
Evolutionary
。基于一些规则,从已有的数据样本或存在的状态变异而来 - 第三类是基于进化的
Evolutionary
。包含了上述两种,同时会根据代码覆盖率的回馈进行变异
- 第一类是基于生成的
LibFuzzer
和要被测试的库链接在一起,通过一个特殊的模糊测试进入点(目标函数),用测试用例 feed
(喂)要被测试的库。fuzzer
会跟踪哪些代码区域已经测试过,然后在输入数据的语料库上产生变异,来最大化代码覆盖。其中代码覆盖的信息由 LLVM
的 SanitizerCoverage
插桩提供
环境搭建
参考 官方 进行搭建即可
git clone https://github.com/Dor1s/libfuzzer-workshop
cd libfuzzer-workshop/libFuzzer
./Fuzzer/build.sh
编译成功后就获得了一个静态库文件 libFuzzer.a
LibFuzzer
是LLVM
项目的一部分,所以如果你安装了最新版的clang
,其是默认带有LibFuzzer
库的,所以你可以不需要自己编译
基础使用
编写 fuzz target
LibFuzzer
已经提供了数据样本生成模块和异常检查模块,所以我们需要做的事情就是将 LibFuzzer
生成的数据样本提供给被测试目标,即实现模糊测试入口点,官方提供的编写模板如下:
// 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.
}
注:函数名称、参数、返回值类型都不能动,并且注意参数中传来的字节数组 Data
是通过底层const
修饰了的,也就是不允许修改其中数据
Data
是LibFuzzer
生成的测试数据,size
是数据的长度DoSomethingInterestingWithMyAPI
函数就是我们实际要进行fuzz
的函数(被测试目标)
官方文档中对被测试目标有如下要求:
- 函数会在同一进程中多次执行,即被循环调用
- 必须接受所有格式的输入
- 不允许主动退出(即运行在内部直接调用
exit
),前面说了是循环调用,退出了就没法循环了 - 可以开线程,但返回之前必须结束它,原因还是那个——循环调用,自己的线程自己关
- 其执行必须结果必须是具有确定性的,两次的
Data
如果一致,则两次执行的结果也必须一致 - 不允许修改全局变量,因为在同一个进程里,修改全局变量会导致下一次运行时读取的是修改后的结果,可能会违反前面说的确定性原则
- 尽量窄范围测试,如果测试处理多种数据格式的目标,还是分割成多个子目标为好。这既是处于速度考量,也是出于模糊测试数据变异的效果考量
编译链接
clang++ -g -O1 -fsanitize=fuzzer,address fuzz_target.cc /path/libFuzzer.a -o fuzzer_target
这里也可以不指定
libFuzzer.a
库,直接
clang++ -g -O1 -fsanitize=fuzzer,address fuzz_target.cc -o fuzzer_target
-g
和-O1
是gcc/clang
的通用选项,这两个选项不是必须的-fsanitize=fuzzer
才是关键,通过这个选项启用libFuzzer
,向libFuzzer
提供进程中的覆盖率信息,并与libFuzzer
运行时链接。- 还可以附加其他
sanitize
选项也可以加进来,如-fsanitize=fuzzer,address
同时启用了地址检查
常用内存错误检测工具:
AddressSanitizer(ASAN)
: 检测uaf
, 缓冲区溢出,stack-use-after-return, container-overflow
等内存访问错误,使用-fsanitize = address
MemorySanitizer(MSAN)
: 检测未初始化内存的访问,使用-fsanitize = memory
。MSAN
不能与其他sanitize
结合使用,应单独使用
UndefinedBehaviorSanitizer(UBSAN)
: 检测一些其他的漏洞,整数溢出,类型混淆等,检测到C/C++
的各种功能的使用,这些功能已明确列出来导致未定义的行为。使用-fsanitize = undefined
,也可以将ASAN
和UBSAN
合并到一个版本中
关于地址 sanitize
详细作用可以查看 llvm
的官方文档 AddressSanitizer
demo 测试 && 输出日志分析
这里给出一个 demo
进行测试:
#include <stdint.h>
#include <stddef.h>
bool FuzzMe(const uint8_t *Data, size_t DataSize) {
return DataSize >= 3 &&
Data[0] == 'X' &&
Data[1] == 'X' &&
Data[2] == 'O' &&
Data[3] == 'L' &&
Data[4] == 'M';
}
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
FuzzMe(Data, Size);
return 0;
}
进行编译:
clang++ -g -O1 -fsanitize=fuzzer,address demo.cpp -o demo
编译完成后,生成的 demo
是一个可接受用户参数的命令行程序,格式如下:
./target -flag1=val1 -flag2=val2 ... dir1 dir2 ...
flags
代表控制测试过程的选项参数,可以提供零到任意个,但必须是严格的 -flag=value
形式
- 选项前导用单横线,即使选项是一个词而非单个字符
- 选项必须要提供对应的值,即使只是一个开关选项如
-help
,必须要写作-help=1
,且选项与值中间只能用等号,不能用空格
dirs
表示语料库目录,它们的内容都会被读取作为初始语料库,但测试过程中生成的新输入只会被保存到第一个目录下
然后就可以直接运行 demo
开始 fuzz
了
这里简单看下输出日志:
Seed
:随机数种子,可以使用-seed=xx
进行指定-max_len
:测试输入的最大长度,不指定的话其会自行进行推测corpus
:初始语料库,可以通过demo ./corpus
提供- 以
#
开头的信息表示在fuzz
的过程中覆盖的路径信息 INITED
:表示fuzzer
已完成初始化,其中包括被测代码运行每个初始输入样本READ
:表示fuzzer
已从语料库目录中读取了所有提供的输入样本NEW
: 表示fuzzer
创建的一个测试输入涵盖了被测代码的新区域。此输入将保存到主要语料库目录pulse
:表示fuzzer
已生成2^n
个输入(定期生成以使用户确信fuzzer
仍在工作)REDUCE
:表示fuzzer
发现了一个更好(更小)的输入,可以触发先前发现的特征(设置-reduce_inputs=0
以禁用)cov: num
:表示执行当前语料库所覆盖的代码块或边的总数ft: num
:表示LibFuzzer
使用不同的信号来评估代码覆盖率:边缘覆盖率,边缘计数器,值配置文件,间接调用方/被调用方对等corp
: 当前内存中测试语料库中的条目数及其大小(以字节为单位)exec/s
: 每秒模糊器迭代的次数rss
: 当前的内存消耗
这里我们可以添加一些参数选项:
./target -option=val
心脏滴血漏洞测试
fuzzer-test-suite 是谷歌提供的一个用于练习 fuzz
的项目。心脏滴血(HeartBleed
)是 OpenSSL
加密库的一个堆溢出漏洞,fuzzer-test-suite
项目提供了 openssl-1.0.1f
版本
拉取上述项目让进入 openssl-1.0.1f
目录可以看到如下目录树:
里面有两个重要的文件 build.sh
和 target.cc
,其中 build.sh
就是用来搭建 openssl
环境的,并且会编译 target.cc
#!/bin/bash
# Copyright 2016 Google Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
. $(dirname $0)/../custom-build.sh $1 $2
. $(dirname $0)/../common.sh
build_lib() {
rm -rf BUILD
cp -rf SRC BUILD
# This version of openssl has unstable parallel make => Don't use `make -j `.
(cd BUILD && CC="$CC $CFLAGS" ./config && make clean && make)
}
get_git_tag https://github.com/openssl/openssl.git OpenSSL_1_0_1f SRC
build_lib
build_fuzzer
if [[ $FUZZING_ENGINE == "hooks" ]]; then
# Link ASan runtime so we can hook memcmp et al.
LIB_FUZZING_ENGINE="$LIB_FUZZING_ENGINE -fsanitize=address"
fi
$CXX $CXXFLAGS $SCRIPT_DIR/target.cc -DCERT_PATH=\"$SCRIPT_DIR/\" BUILD/libssl.a BUILD/libcrypto.a $LIB_FUZZING_ENGINE -I BUILD/include -o $EXECUTABLE_NAME_BASE
rm -rf runtime
cp -rf $SCRIPT_DIR/runtime .
来看下 target.cc
:
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <assert.h>
#include <stdint.h>
#include <stddef.h>
SSL_CTX *Init() {
SSL_library_init();
SSL_load_error_strings();
ERR_load_BIO_strings();
OpenSSL_add_all_algorithms();
SSL_CTX *sctx;
assert (sctx = SSL_CTX_new(TLSv1_method()));
/* These two file were created with this command:
openssl req -x509 -newkey rsa:512 -keyout server.key \
-out server.pem -days 9999 -nodes -subj /CN=a/
*/
assert(SSL_CTX_use_certificate_file(sctx, "runtime/server.pem",
SSL_FILETYPE_PEM));
assert(SSL_CTX_use_PrivateKey_file(sctx, "runtime/server.key",
SSL_FILETYPE_PEM));
return sctx;
}
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
static SSL_CTX *sctx = Init();
SSL *server = SSL_new(sctx);
BIO *sinbio = BIO_new(BIO_s_mem());
BIO *soutbio = BIO_new(BIO_s_mem());
SSL_set_bio(server, sinbio, soutbio);
SSL_set_accept_state(server);
BIO_write(sinbio, Data, Size);
SSL_do_handshake(server);
SSL_free(server);
return 0;
}
在 target
中启动了 openssl
,然后往里面喂 libfuzzer
提供的数据
利用 build.sh
搭建好环境后,其会生成一个 openssl-1.0.1f-fsanitize_fuzzer
文件,运行即可开始 fuzz
,ASAN
很快就检测到了堆溢出
提高代码覆盖率和测试速度
其实影响 LibFuzzer
效果的因素有很多,种子语料库的选择、模糊测试接口函数的编写、编译选项等等,这里仅仅看下几个比较简单的提高效率的方法
指定种子语料库
语料库的使用比较简单:
./target corpus_dir1 corpus_dir2 corpus_dir3
后面的文件夹都会被当作语料库目录,但是变异生成的输入都会保存在第一个文件夹中,所以一般情况下,我们会让 corpus_dir1
是一个空文件夹,这样就可以获取一个比较好的种子语料库
然后变异过程中可能产生很多重复的输入,这就使得保存的输入种子存在大量重复(这里重复指的是效果一样,而不是说内容完全一样),这时我们可以使用 -merge=1
标志去精简语料库
这里以 fuzzer-test-suite
项目的 woff2-2016-05-06
为例来看看设置语料库和不设置语料库代码的覆盖率,环境构建:
mkdir woff2
cd woff2
../woff2-2016-05-06/build.sh
环境构建成功后可以在目录下看到 seeds
文件夹,其是官方提供的 fuzz
语料库
不指定语料库 fuzz
这里我们直接执行 woff2-2016-05-06-fsanitize_fuzzer
,效果如下:
笔者没有跑出 crash
,共执行了大概 4 百万次,代码覆盖基本块也只有 800 多个,整体的效果比较差
指定语料库 fuzz
先对 seeds
进行下精简,然后在进行 fuzz
:
./woff2-2016-05-06-fsanitize_fuzzer -merge=1 corpus_min/ seeds/
./woff2-2016-05-06-fsanitize_fuzzer corpus/ corpus_min/
可以看到这里执行了 10 多次,代码覆盖基本块就到达了 900 多,所以提供一些种子语料库能够有效的加快 Fuzz
的速度
这里笔者继续跑着,看看能不能跑出
crash
最后差不多跑了 10 多分钟吧,跑了个堆溢出
但是可以看到这里总共执行了 4 百多万次,但是覆盖基本块有接近 1000,而且 corp
为 628/40Mb
多核并行 Fuzz
这里跟 AFL
类似,不同 Fuzzer
之间可以共享语料库,在 LibFuzzer
中使用 -jobs=N
指定 fuzzing job
的数量。然后这些 fuzzing job
会由 worker
进行处理,worker
的数量默认为 CPU
数量的一半。如果 -job=20
运行在 16 核的机器上,则默认执行 8 个 worker
处理这 20 个 fuzzing lab
由于笔者虚拟机是 4 核的机器,所以只有两个 worker
,所以其实跟直接运行没啥区别,但是如果的你 CPU
核心比较多的话,速度肯定是会大大提升的
使用字典
首先需要明确的是为什么要使用字典。其实这里跟 AFL
中类似,对于有些数据其是有特定格式的,比如 png
图片有特定的 png
头,xml
文件有特定格式,而变异过程是随机不确定的,这时就有极大的可能导致数据的特定格式被破坏,从而导致 target
一直拒绝。
在 LibFuzzer
中使用字典只需要设置 -dict=dict_dir
即可,例子后面做 libfuzzer lab
时就有了
参考
https://github.com/google/fuzzing/blob/master/tutorial/libFuzzerTutorial.md
libFuzzer使用总结教程
入门libFuzzer——编译链接
afl-fuzz: making up grammar with a dictionary in hand