名称: libfuzzer 类型: 模糊测试器 描述: > 内置在LLVM中的覆盖率引导模糊测试器,用于C/C++项目。用于模糊测试 可以用Clang编译的C/C++代码。
libFuzzer
libFuzzer 是一个进程内、覆盖率引导的模糊测试器,是LLVM项目的一部分。由于其简单性和与LLVM工具链的集成,它是模糊测试C/C++项目的推荐起点。尽管libFuzzer自2022年底以来一直处于仅维护模式,但它比其替代品更容易安装和使用,有广泛的支持,并将在可预见的未来继续维护。
何时使用
| 模糊测试器 | 最适合于 | 复杂度 |
|---|---|---|
| libFuzzer | 快速设置、单项目模糊测试 | 低 |
| AFL++ | 多核模糊测试、多样化突变 | 中 |
| LibAFL | 自定义模糊测试器、研究项目 | 高 |
| Honggfuzz | 基于硬件的覆盖率 | 中 |
选择 libFuzzer 当:
- 您需要为C/C++代码进行简单、快速的设置
- 项目使用Clang进行编译
- 单核模糊测试最初足够
- 后续过渡到AFL++是一个选项(harness兼容)
注意: 为libFuzzer编写的模糊测试harness与AFL++兼容,如果需要更高级的功能如更好的多核支持,可以轻松过渡。
快速开始
#include <stdint.h>
#include <stddef.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// 如果需要,验证输入
if (size < 1) return 0;
// 使用模糊测试器提供的数据调用目标函数
my_target_function(data, size);
return 0;
}
编译和运行:
clang++ -fsanitize=fuzzer,address -g -O2 harness.cc target.cc -o fuzz
mkdir corpus/
./fuzz corpus/
安装
先决条件
- LLVM/Clang编译器(包含libFuzzer)
- 用于覆盖率分析的LLVM工具(可选)
Linux (Ubuntu/Debian)
apt install clang llvm
对于最新LLVM版本:
# 从apt.llvm.org添加LLVM仓库
# 然后安装特定版本,例如:
apt install clang-18 llvm-18
macOS
# 使用Homebrew
brew install llvm
# 或使用Nix
nix-env -i clang
Windows
通过Visual Studio安装Clang。参考Microsoft的文档进行设置。
推荐: 如果可能,在本地x86_64虚拟机或租用DigitalOcean、AWS或Hetzner上的虚拟机上进行模糊测试。Linux为libFuzzer提供最佳支持。
验证
clang++ --version
# 应显示LLVM版本信息
编写Harness
Harness结构
Harness是模糊测试器的入口点。libFuzzer反复调用LLVMFuzzerTestOneInput函数,提供不同的输入。
#include <stdint.h>
#include <stddef.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// 1. 可选:验证输入大小
if (size < MIN_REQUIRED_SIZE) {
return 0; // 拒绝太小的输入
}
// 2. 可选:将原始字节转换为结构化数据
// 示例:从字节数组中解析两个整数
if (size >= 2 * sizeof(uint32_t)) {
uint32_t a = *(uint32_t*)(data);
uint32_t b = *(uint32_t*)(data + sizeof(uint32_t));
my_function(a, b);
}
// 3. 调用目标函数
target_function(data, size);
// 4. 始终返回0(非零保留供将来使用)
return 0;
}
Harness规则
| 做 | 不做 |
|---|---|
| 处理所有输入类型(空、巨大、畸形) | 调用exit() - 停止模糊测试过程 |
| 在返回前加入所有线程 | 让线程运行 |
| 保持harness快速和简单 | 添加过多日志或复杂性 |
| 保持确定性 | 使用随机数生成器或读取/dev/random |
| 每次运行之间重置全局状态 | 依赖先前执行的状态 |
| 使用狭窄、专注的目标 | 在一个harness中混合不相关的数据格式(PNG + TCP) |
原理:
- 速度重要: 目标每个核心每秒执行100-1000次
- 可重现性: 崩溃必须在模糊测试完成后可重现
- 隔离: 每次执行应独立
使用FuzzedDataProvider处理复杂输入
对于复杂输入(字符串、多个参数),使用FuzzedDataProvider助手:
#include <stdint.h>
#include <stddef.h>
#include "FuzzedDataProvider.h" // 来自LLVM项目
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
FuzzedDataProvider fuzzed_data(data, size);
// 提取结构化数据
size_t allocation_size = fuzzed_data.ConsumeIntegral<size_t>();
std::vector<char> str1 = fuzzed_data.ConsumeBytesWithTerminator<char>(32, 0xFF);
std::vector<char> str2 = fuzzed_data.ConsumeBytesWithTerminator<char>(32, 0xFF);
// 使用提取的数据调用目标
char* result = concat(&str1[0], str1.size(), &str2[0], str2.size(), allocation_size);
if (result != NULL) {
free(result);
}
return 0;
}
从LLVM仓库下载FuzzedDataProvider.h。
交错模糊测试
使用单个harness测试多个相关函数:
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (size < 1 + 2 * sizeof(int32_t)) {
return 0;
}
uint8_t mode = data[0];
int32_t numbers[2];
memcpy(numbers, data + 1, 2 * sizeof(int32_t));
// 根据第一个字节选择函数
switch (mode % 4) {
case 0: add(numbers[0], numbers[1]); break;
case 1: subtract(numbers[0], numbers[1]); break;
case 2: multiply(numbers[0], numbers[1]); break;
case 3: divide(numbers[0], numbers[1]); break;
}
return 0;
}
另见: 关于详细harness编写技术、处理复杂输入的模式、 结构感知模糊测试和基于protobuf的模糊测试,请参见fuzz-harness-writing技术技能。
编译
基本编译
关键标志是-fsanitize=fuzzer,它:
- 链接libFuzzer运行时(提供
main函数) - 启用SanitizerCoverage插桩用于覆盖率跟踪
- 禁用内置函数如
memcmp
clang++ -fsanitize=fuzzer -g -O2 harness.cc target.cc -o fuzz
标志解释:
-fsanitize=fuzzer: 启用libFuzzer-g: 添加调试符号(有助于崩溃分析)-O2: 生产级优化(推荐用于模糊测试)-DNO_MAIN: 如果代码有main函数,定义宏
使用消毒剂
地址消毒剂(推荐):
clang++ -fsanitize=fuzzer,address -g -O2 -U_FORTIFY_SOURCE harness.cc target.cc -o fuzz
多个消毒剂:
clang++ -fsanitize=fuzzer,address,undefined -g -O2 harness.cc target.cc -o fuzz
另见: 关于详细消毒剂配置、常见问题、ASAN_OPTIONS标志、 和高级消毒剂使用,请参见address-sanitizer和undefined-behavior-sanitizer技术技能。
构建标志
| 标志 | 目的 |
|---|---|
-fsanitize=fuzzer |
启用libFuzzer运行时和插桩 |
-fsanitize=address |
启用地址消毒剂(内存错误检测) |
-fsanitize=undefined |
启用未定义行为消毒剂 |
-fsanitize=fuzzer-no-link |
插桩而不链接模糊测试器(用于库) |
-g |
包含调试符号 |
-O2 |
生产优化级别 |
-U_FORTIFY_SOURCE |
禁用强化(可能干扰ASan) |
构建静态库
对于产生静态库的项目:
- 使用模糊测试插桩构建库:
export CC=clang CFLAGS="-fsanitize=fuzzer-no-link -fsanitize=address"
export CXX=clang++ CXXFLAGS="$CFLAGS"
./configure --enable-shared=no
make
- 将静态库与harness链接:
clang++ -fsanitize=fuzzer -fsanitize=address harness.cc libmylib.a -o fuzz
CMake集成
project(FuzzTarget)
cmake_minimum_required(VERSION 3.0)
add_executable(fuzz main.cc harness.cc)
target_compile_definitions(fuzz PRIVATE NO_MAIN=1)
target_compile_options(fuzz PRIVATE -g -O2 -fsanitize=fuzzer -fsanitize=address)
target_link_libraries(fuzz -fsanitize=fuzzer -fsanitize=address)
构建:
cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ .
cmake --build .
语料库管理
创建初始语料库
为语料库创建目录(可以开始时为空):
mkdir corpus/
可选但推荐: 提供种子输入(有效示例文件):
# 对于PNG解析器:
cp examples/*.png corpus/
# 对于协议解析器:
cp test_packets/*.bin corpus/
种子输入的好处:
- 模糊测试器不从零开始
- 更快到达有效代码路径
- 显著提高效果
语料库结构
语料库目录包含:
- 触发唯一代码路径的输入文件
- 最小化版本(libFuzzer自动最小化)
- 按内容哈希命名(例如,
a9993e364706816aba3e25717850c26c9cd0d89d)
语料库最小化
libFuzzer在模糊测试期间自动最小化语料库条目。要显式最小化:
mkdir minimized_corpus/
./fuzz -merge=1 minimized_corpus/ corpus/
这会在minimized_corpus/中创建去重、最小化的语料库。
另见: 关于语料库创建策略、种子选择、格式特定语料库构建、 和语料库维护,请参见fuzzing-corpus技术技能。
运行活动
基本运行
./fuzz corpus/
这运行直到找到崩溃或停止(Ctrl+C)。
推荐: 崩溃后继续
./fuzz -fork=1 -ignore_crashes=1 corpus/
-fork和-ignore_crashes标志(实验性但广泛使用)允许在找到崩溃后继续模糊测试。
常用选项
控制输入大小:
./fuzz -max_len=4000 corpus/
经验法则: 最小现实输入大小的2倍。
设置超时:
./fuzz -timeout=2 corpus/
中止运行超过2秒的测试用例。
使用字典:
./fuzz -dict=./format.dict corpus/
关闭stdout/stderr(加速模糊测试):
./fuzz -close_fd_mask=3 corpus/
查看所有选项:
./fuzz -help=1
多核模糊测试
选项1: 作业和工作器(推荐):
./fuzz -jobs=4 -workers=4 -fork=1 -ignore_crashes=1 corpus/
-jobs=4: 运行4个顺序活动-workers=4: 使用4个进程并行处理作业- 测试用例在作业之间共享
选项2: 分支模式:
./fuzz -fork=4 -ignore_crashes=1 corpus/
注意: 对于严肃的多核模糊测试,考虑切换到AFL++、Honggfuzz或LibAFL。
重新执行测试用例
重新运行单个崩溃:
./fuzz ./crash-a9993e364706816aba3e25717850c26c9cd0d89d
无模糊测试地测试目录中所有输入:
./fuzz -runs=0 corpus/
解释输出
当模糊测试运行时,您会看到统计信息如:
INFO: Seed: 3517090860
INFO: Loaded 1 modules (9 inline 8-bit counters)
#2 INITED cov: 3 ft: 4 corp: 1/1b exec/s: 0 rss: 26Mb
#57 NEW cov: 4 ft: 5 corp: 2/4b lim: 4 exec/s: 0 rss: 26Mb
| 输出 | 含义 |
|---|---|
INITED |
模糊测试初始化 |
NEW |
找到新覆盖率,添加到语料库 |
REDUCE |
输入最小化同时保持覆盖率 |
cov: N |
覆盖的边缘数 |
corp: X/Yb |
语料库大小: X条目,Y总字节 |
exec/s: N |
每秒执行数 |
rss: NMb |
常驻内存使用量 |
崩溃时:
==11672== ERROR: libFuzzer: deadly signal
artifact_prefix='./'; Test unit written to ./crash-a9993e364706816aba3e25717850c26c9cd0d89d
0x61,0x62,0x63,
abc
Base64: YWJj
崩溃保存到./crash<hash>,输入以十六进制、UTF-8和Base64显示。
可重现性: 使用-seed=<value>重现模糊测试活动(仅单核)。
模糊测试字典
字典通过提供关于输入格式的提示,帮助模糊测试器更快发现有趣的输入。
字典格式
创建文本文件,每行包含带引号的字符串:
# 以'#'开头的行是注释
# 魔数字节
magic="\x89PNG"
magic2="IEND"
# 关键字
"GET"
"POST"
"Content-Type"
# 十六进制序列
delimiter="\xFF\xD8\xFF"
使用字典
./fuzz -dict=./format.dict corpus/
生成字典
从头文件:
grep -o '".*"' header.h > header.dict
从man页面:
man curl | grep -oP '^\s*(--|-)\K\S+' | sed 's/[,.]$//' | sed 's/^/"&/; s/$/&"/' | sort -u > man.dict
从二进制字符串:
strings ./binary | sed 's/^/"&/; s/$/&"/' > strings.dict
使用LLM: 请求ChatGPT或类似工具为您的格式生成字典(例如,“为JSON解析器生成libFuzzer字典”)。
另见: 关于高级字典生成、格式特定字典和 字典优化策略,请参见fuzzing-dictionaries技术技能。
覆盖率分析
虽然libFuzzer显示基本覆盖率统计(cov: N),但详细覆盖率分析需要额外工具。
基于源文件的覆盖率
1. 使用覆盖率插桩重新编译:
clang++ -fsanitize=fuzzer -fprofile-instr-generate -fcoverage-mapping harness.cc target.cc -o fuzz
2. 运行模糊测试器收集覆盖率:
LLVM_PROFILE_FILE="coverage-%p.profraw" ./fuzz -runs=10000 corpus/
3. 合并覆盖率数据:
llvm-profdata merge -sparse coverage-*.profraw -o coverage.profdata
4. 生成覆盖率报告:
llvm-cov show ./fuzz -instr-profile=coverage.profdata
5. 生成HTML报告:
llvm-cov show ./fuzz -instr-profile=coverage.profdata -format=html > coverage.html
提高覆盖率
提示:
- 在语料库中提供更好的种子输入
- 使用字典进行格式感知模糊测试
- 检查harness是否正确测试目标
- 对于复杂格式,考虑结构感知模糊测试
- 运行更长的活动(天/周)
另见: 关于详细覆盖率分析技术、识别覆盖率差距、 系统覆盖率提高和跨模糊测试器比较覆盖率,请参见 coverage-analysis技术技能。
消毒剂集成
地址消毒剂(ASan)
ASan检测内存错误如缓冲区溢出和使用后释放错误。高度推荐用于模糊测试。
启用ASan:
clang++ -fsanitize=fuzzer,address -g -O2 -U_FORTIFY_SOURCE harness.cc target.cc -o fuzz
示例ASan输出:
==1276163==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6020000c4ab1
WRITE of size 1 at 0x6020000c4ab1 thread T0
#0 0x55555568631a in check_buf(char*, unsigned long) main.cc:13:25
#1 0x5555556860bf in LLVMFuzzerTestOneInput harness.cc:7:3
用环境变量配置ASan:
ASAN_OPTIONS=verbosity=1:abort_on_error=1 ./fuzz corpus/
重要标志:
verbosity=1: 显示ASan活动detect_leaks=0: 禁用泄漏检测(泄漏在结束时报告)abort_on_error=1: 在错误时调用abort()而不是_exit()
缺点:
- 2-4倍减速
- 需要约20TB虚拟内存(禁用内存限制:
-rss_limit_mb=0) - 在Linux上最佳支持
另见: 关于全面ASan配置、常见陷阱、符号化和 与其他消毒剂结合,请参见address-sanitizer技术技能。
未定义行为消毒剂(UBSan)
UBSan检测未定义行为如整数溢出、空指针解引用等。
启用UBSan:
clang++ -fsanitize=fuzzer,undefined -g -O2 harness.cc target.cc -o fuzz
与ASan结合:
clang++ -fsanitize=fuzzer,address,undefined -g -O2 harness.cc target.cc -o fuzz
内存消毒剂(MSan)
MSan检测未初始化内存读取。使用更复杂(需要重建所有依赖)。
clang++ -fsanitize=fuzzer,memory -g -O2 harness.cc target.cc -o fuzz
常见消毒剂问题
| 问题 | 解决方案 |
|---|---|
| ASan减慢模糊测试太多 | 使用-fsanitize-recover=address处理非致命错误 |
| 内存不足 | 设置ASAN_OPTIONS=rss_limit_mb=0或-rss_limit_mb=0 |
| 堆栈耗尽 | 增加堆栈大小: ASAN_OPTIONS=stack_size=8388608 |
_FORTIFY_SOURCE导致误报 |
使用-U_FORTIFY_SOURCE标志 |
| MSan在依赖中报告 | 使用-fsanitize=memory重建所有依赖 |
真实世界示例
示例1: 模糊测试libpng
libpng是广泛使用的读写PNG图像的库。错误可能导致安全问题。
1. 获取源代码:
curl -L -O https://downloads.sourceforge.net/project/libpng/libpng16/1.6.37/libpng-1.6.37.tar.xz
tar xf libpng-1.6.37.tar.xz
cd libpng-1.6.37/
2. 安装依赖:
apt install zlib1g-dev
3. 使用模糊测试插桩编译:
export CC=clang CFLAGS="-fsanitize=fuzzer-no-link -fsanitize=address"
export CXX=clang++ CXXFLAGS="$CFLAGS"
./configure --enable-shared=no
make
4. 获取harness(或自己编写):
curl -O https://raw.githubusercontent.com/glennrp/libpng/f8e5fa92b0e37ab597616f554bee254157998227/contrib/oss-fuzz/libpng_read_fuzzer.cc
5. 准备语料库和字典:
mkdir corpus/
curl -o corpus/input.png https://raw.githubusercontent.com/glennrp/libpng/acfd50ae0ba3198ad734e5d4dec2b05341e50924/contrib/pngsuite/iftp1n3p08.png
curl -O https://raw.githubusercontent.com/glennrp/libpng/2fff013a6935967960a5ae626fc21432807933dd/contrib/oss-fuzz/png.dict
6. 链接和编译模糊测试器:
clang++ -fsanitize=fuzzer -fsanitize=address libpng_read_fuzzer.cc .libs/libpng16.a -lz -o fuzz
7. 运行模糊测试活动:
./fuzz -close_fd_mask=3 -dict=./png.dict corpus/
示例2: 简单除零错误
查找除零错误的harness:
#include <stdint.h>
#include <stddef.h>
double divide(uint32_t numerator, uint32_t denominator) {
// 错误: 未检查分母是否为零
return numerator / denominator;
}
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if(size != 2 * sizeof(uint32_t)) {
return 0;
}
uint32_t numerator = *(uint32_t*)(data);
uint32_t denominator = *(uint32_t*)(data + sizeof(uint32_t));
divide(numerator, denominator);
return 0;
}
编译和模糊测试:
clang++ -fsanitize=fuzzer harness.cc -o fuzz
./fuzz
模糊测试器将快速找到导致崩溃的输入。
高级使用
提示和技巧
| 提示 | 为什么有帮助 |
|---|---|
| 从单核开始,切换到AFL++进行多核 | libFuzzer harness与AFL++兼容 |
| 为结构化格式使用字典 | 10-100倍更快发现错误 |
使用-close_fd_mask=3关闭文件描述符 |
如果SUT写入输出,提速 |
设置合理的-max_len |
防止在巨大输入上浪费时间 |
| 运行天/周,而不是分钟 | 覆盖率平台需要时间突破 |
| 使用测试套件中的种子语料库 | 从有效输入开始模糊测试 |
结构感知模糊测试
对于高度结构化的输入(例如,复杂协议、文件格式),使用libprotobuf-mutator:
- 使用Protocol Buffers定义输入结构
- libFuzzer突变protobuf消息(保留结构的突变)
- Harness将protobuf转换为原生格式
详情参见结构感知模糊测试文档。
自定义突变器
libFuzzer允许自定义突变器用于专门模糊测试:
extern "C" size_t LLVMFuzzerCustomMutator(uint8_t *Data, size_t Size,
size_t MaxSize, unsigned int Seed) {
// 自定义突变逻辑
return new_size;
}
extern "C" size_t LLVMFuzzerCustomCrossOver(const uint8_t *Data1, size_t Size1,
const uint8_t *Data2, size_t Size2,
uint8_t *Out, size_t MaxOutSize,
unsigned int Seed) {
// 自定义交叉逻辑
return new_size;
}
性能调优
| 设置 | 影响 |
|---|---|
-close_fd_mask=3 |
关闭stdout/stderr,加速模糊测试 |
-max_len=<合理大小> |
避免在巨大输入上浪费时间 |
-timeout=<秒数> |
检测挂起,防止卡住执行 |
| 禁用ASan进行基准测试 | 2-4倍速度提升(但错过内存错误) |
使用-jobs和-workers |
有限的多核支持 |
| 在Linux上运行 | 最佳平台支持和性能 |
故障排除
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 几小时后未找到崩溃 | 语料库差、覆盖率低 | 添加种子输入、使用字典、检查harness |
| 执行速度非常慢(<100次/秒) | 目标太复杂、过多日志 | 优化目标、使用-close_fd_mask=3、减少日志 |
| 内存不足 | ASan的20TB虚拟内存 | 设置-rss_limit_mb=0禁用RSS限制 |
| 模糊测试器在第一次崩溃后停止 | 默认行为 | 使用-fork=1 -ignore_crashes=1继续 |
| 无法重现崩溃 | harness/target中的非确定性 | 移除随机数生成、全局状态 |
与-fsanitize=fuzzer链接错误 |
缺少libFuzzer运行时 | 确保使用Clang,检查LLVM安装 |
| GCC项目无法用Clang编译 | GCC特定代码 | 改用gcc_plugin的AFL++ |
| 覆盖率未提高 | 语料库平台 | 运行更长时间、添加字典、改进种子、检查覆盖率报告 |
| 崩溃但ASan未触发 | 无ASan未检测到内存错误 | 用-fsanitize=address重新编译 |
相关技能
技术技能
| 技能 | 使用案例 |
|---|---|
| fuzz-harness-writing | 关于编写有效harness、结构感知模糊测试和FuzzedDataProvider使用的详细指导 |
| address-sanitizer | 内存错误检测配置、ASAN_OPTIONS和故障排除 |
| undefined-behavior-sanitizer | 在模糊测试期间检测未定义行为 |
| coverage-analysis | 测量模糊测试效果和识别未测试代码路径 |
| fuzzing-corpus | 构建和管理种子语料库、语料库最小化策略 |
| fuzzing-dictionaries | 创建格式特定字典以更快发现错误 |
相关模糊测试器
| 技能 | 何时考虑 |
|---|---|
| aflpp | 当您需要严肃的多核模糊测试,或libFuzzer覆盖率平台时 |
| honggfuzz | 当您想在Linux上使用基于硬件的覆盖率反馈时 |
| libafl | 当构建自定义模糊测试器或进行模糊测试研究时 |
资源
官方文档
- LLVM libFuzzer文档 - 官方参考
- Google的libFuzzer教程 - 逐步指南
- SanitizerCoverage - 覆盖率插桩详情