name: libfuzzer type: fuzzer description: > 内置LLVM的C/C++项目的覆盖率引导模糊测试器。用于模糊测试可以用Clang编译的C/C++代码。
libFuzzer
libFuzzer是LLVM项目的一部分,是一个进程内、覆盖率引导的模糊测试器。由于简单性和与LLVM工具链的集成,它是模糊测试C/C++项目的推荐起点。尽管libFuzzer自2022年底以来处于仅维护模式,但比其替代品更容易安装和使用,具有广泛支持,并在可预见的未来将继续维护。
何时使用
| 模糊测试器 | 最适合 | 复杂度 |
|---|---|---|
| libFuzzer | 快速设置,单一项目模糊测试 | 低 |
| AFL++ | 多核模糊测试,多样化变异 | 中 |
| LibAFL | 自定义模糊测试器,研究项目 | 高 |
| Honggfuzz | 基于硬件的覆盖率 | 中 |
选择libFuzzer当:
- 您需要一个简单、快速的C/C++代码设置
- 项目使用Clang进行编译
- 单核模糊测试最初足够
- 稍后过渡到AFL++是一个选项(测试线束兼容)
注意: 为libFuzzer编写的测试线束与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。参考微软文档获取设置说明。
推荐: 如果可能,在本地x86_64虚拟机或租用DigitalOcean、AWS或Hetzner的虚拟机上进行模糊测试。Linux对libFuzzer提供最佳支持。
验证
clang++ --version
# 应显示LLVM版本信息
编写测试线束
测试线束结构
测试线束是模糊测试器的入口点。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;
}
测试线束规则
| 做 | 不做 |
|---|---|
| 处理所有输入类型(空、巨大、畸形) | 调用exit() - 停止模糊测试进程 |
| 返回前加入所有线程 | 让线程运行 |
| 保持测试线束快速简单 | 添加过多日志记录或复杂性 |
| 保持确定性 | 使用随机数生成器或读取/dev/random |
| 在运行之间重置全局状态 | 依赖之前执行的状态 |
| 使用狭窄、聚焦的目标 | 在单个测试线束中混合不相关的数据格式(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。
交错模糊测试
使用单个测试线束测试多个相关函数:
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;
}
另见: 有关详细测试线束编写技术、处理复杂输入的模式、结构感知模糊测试和基于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函数,定义此宏
使用Sanitizers
AddressSanitizer(推荐):
clang++ -fsanitize=fuzzer,address -g -O2 -U_FORTIFY_SOURCE harness.cc target.cc -o fuzz
多Sanitizers:
clang++ -fsanitize=fuzzer,address,undefined -g -O2 harness.cc target.cc -o fuzz
另见: 有关详细Sanitizer配置、常见问题、ASAN_OPTIONS标志和高级Sanitizer用法,请参阅address-sanitizer和undefined-behavior-sanitizer技能。
构建标志
| 标志 | 用途 |
|---|---|
-fsanitize=fuzzer |
启用libFuzzer运行时和仪器 |
-fsanitize=address |
启用AddressSanitizer(内存错误检测) |
-fsanitize=undefined |
启用UndefinedBehaviorSanitizer |
-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
- 将静态库与测试线束链接:
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-<哈希>,输入以十六进制、UTF-8和Base64显示。
可重现性: 使用-seed=<值>重现模糊测试活动(仅限单核)。
模糊测试字典
字典通过提供输入格式提示帮助模糊测试器更快发现有趣输入。
字典格式
创建文本文件,每行带引号字符串:
# 以'#'开头的行是注释
# 魔术字节
magic="\x89PNG"
magic2="IEND"
# 关键词
"GET"
"POST"
"Content-Type"
# 十六进制序列
delimiter="\xFF\xD8\xFF"
使用字典
./fuzz -dict=./format.dict corpus/
生成字典
从头文件:
grep -o '".*"' header.h > header.dict
从手册页:
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
改进覆盖率
提示:
- 提供更好的种子输入到语料库
- 使用字典进行格式感知模糊测试
- 检查测试线束是否适当执行目标
- 考虑复杂格式的结构感知模糊测试
- 运行更长时间活动(天/周)
另见: 有关详细覆盖率分析技术、识别覆盖率差距、系统性覆盖率改进和比较不同模糊测试器覆盖率,请参阅coverage-analysis技能。
Sanitizer集成
AddressSanitizer (ASan)
ASan检测内存错误,如缓冲区溢出和use-after-free漏洞。强烈推荐用于模糊测试。
启用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配置、常见陷阱、符号化和与其他Sanitizers结合,请参阅address-sanitizer技能。
UndefinedBehaviorSanitizer (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
MemorySanitizer (MSan)
MSan检测未初始化内存读取。使用更复杂(需要重建所有依赖)。
clang++ -fsanitize=fuzzer,memory -g -O2 harness.cc target.cc -o fuzz
常见Sanitizer问题
| 问题 | 解决方案 |
|---|---|
| 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. 获取测试线束(或自行编写):
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:简单除法漏洞
发现除零漏洞的测试线束:
#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测试线束与AFL++兼容 |
| 为结构化格式使用字典 | 10-100倍更快漏洞发现 |
用-close_fd_mask=3关闭文件描述符 |
如果SUT写入输出,加速 |
设置合理-max_len |
防止在巨大输入上浪费时间 |
| 运行天/周,而非分钟 | 覆盖率平台期需要时间突破 |
| 从测试套件使用种子语料库 | 从有效输入开始模糊测试 |
结构感知模糊测试
对于高度结构化输入(例如,复杂协议、文件格式),使用libprotobuf-mutator:
- 使用Protocol Buffers定义输入结构
- libFuzzer变异protobuf消息(保留结构的变异)
- 测试线束将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上运行 | 最佳平台支持和性能 |
故障排除
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 运行数小时后无崩溃 | 差语料库,低覆盖率 | 添加种子输入,使用字典,检查测试线束 |
| 非常慢的执行/秒(<100) | 目标太复杂,过多日志记录 | 优化目标,使用-close_fd_mask=3,减少日志记录 |
| 内存不足 | ASan的20TB虚拟内存 | 设置-rss_limit_mb=0禁用RSS限制 |
| 第一次崩溃后模糊测试器停止 | 默认行为 | 使用-fork=1 -ignore_crashes=1继续 |
| 无法重现崩溃 | 测试线束/目标中的非确定性 | 移除随机数生成,全局状态 |
使用-fsanitize=fuzzer链接错误 |
缺少libFuzzer运行时 | 确保使用Clang,检查LLVM安装 |
| GCC项目无法用Clang编译 | GCC特定代码 | 改用AFL++与gcc_plugin |
| 覆盖率无改进 | 语料库平台期 | 运行更长时间,添加字典,改进种子,检查覆盖率报告 |
| 崩溃但ASan未触发 | 未用ASan检测到的内存错误 | 用-fsanitize=address重新编译 |
相关技能
技术技能
| 技能 | 用例 |
|---|---|
| fuzz-harness-writing | 编写有效测试线束、结构感知模糊测试和FuzzedDataProvider使用的详细指导 |
| address-sanitizer | 内存错误检测配置、ASAN_OPTIONS和故障排除 |
| undefined-behavior-sanitizer | 模糊测试期间检测未定义行为 |
| coverage-analysis | 测量模糊测试效果和识别未测试代码路径 |
| fuzzing-corpus | 构建和管理种子语料库、语料库最小化策略 |
| fuzzing-dictionaries | 为更快漏洞发现创建特定格式字典 |
相关模糊测试器
| 技能 | 何时考虑 |
|---|---|
| aflpp | 当您需要严肃多核模糊测试,或libFuzzer覆盖率平台期时 |
| honggfuzz | 当您想在Linux上获得基于硬件的覆盖率反馈时 |
| libafl | 当构建自定义模糊测试器或进行模糊测试研究时 |
资源
官方文档
- LLVM libFuzzer文档 - 官方参考
- Google的libFuzzer教程 - 逐步指南
- SanitizerCoverage - 覆盖率仪器详情