名称: harness-writing 类型: 技术 描述: > 跨语言编写有效模糊测试harness的技术。 当创建新的模糊目标或改进现有harness代码时使用。
编写模糊测试Harnesses
模糊测试harness是接收模糊器随机数据并路由到被测系统(SUT)的入口点函数。harness的质量直接影响哪些代码路径被执行以及是否发现关键漏洞。编写不良的harness可能错过整个子系统或导致不可复现的崩溃。
概述
harness是模糊器随机字节生成与应用程序API之间的桥梁。它必须将原始字节解析为有意义的输入,调用目标函数,并优雅地处理边缘情况。任何模糊测试设置中最重要的部分是harness——如果编写不良,应用程序的关键部分可能无法覆盖。
关键概念
| 概念 | 描述 |
|---|---|
| Harness | 接收模糊器输入并调用被测代码的函数 |
| SUT | 被测系统——正在模糊测试的代码 |
| 入口点 | 模糊器所需的函数签名(例如,LLVMFuzzerTestOneInput) |
| FuzzedDataProvider | 从原始字节中提取类型化数据的辅助类 |
| 确定性 | 确保相同输入始终产生相同行为的属性 |
| 交错模糊测试 | 基于输入执行多个操作的单个harness |
何时应用
应用此技术时:
- 首次为新目标创建模糊测试目标
- 模糊测试活动代码覆盖率低或未发现漏洞
- 模糊测试期间发现的崩溃不可复现
- 目标API需要复杂或结构化的输入
- 应一起测试多个相关函数
跳过此技术时:
- 使用项目中已有的良好测试的harnesses
- 工具提供满足需求的自动harness生成
- 目标已有全面的模糊测试基础设施
快速参考
| 任务 | 模式 |
|---|---|
| 最小C++ harness | extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) |
| 最小Rust harness | `fuzz_target!( |
| 大小验证 | if (size < MIN_SIZE) return 0; |
| 强制转换为整数 | uint32_t val = *(uint32_t*)(data); |
| 使用FuzzedDataProvider | FuzzedDataProvider fuzzed_data(data, size); |
| 提取类型化数据(C++) | auto val = fuzzed_data.ConsumeIntegral<uint32_t>(); |
| 提取字符串(C++) | auto str = fuzzed_data.ConsumeBytesWithTerminator<char>(32, 0xFF); |
分步指南
步骤1:识别入口点
查找代码库中的函数:
- 接收外部输入(解析器、验证器、协议处理器)
- 解析复杂数据格式(JSON、XML、二进制协议)
- 执行安全关键操作(身份验证、加密)
- 具有高圈复杂度或许多分支
良好目标通常包括:
- 协议解析器
- 文件格式解析器
- 序列化/反序列化函数
- 输入验证例程
步骤2:编写最小Harness
从最简单的可能harness开始,调用目标函数:
C/C++:
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
target_function(data, size);
return 0;
}
Rust:
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
target_function(data);
});
步骤3:添加输入验证
拒绝太小或太大无意义的输入:
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// 确保最小尺寸以获取有意义的输入
if (size < MIN_INPUT_SIZE || size > MAX_INPUT_SIZE) {
return 0;
}
target_function(data, size);
return 0;
}
原理: 模糊器生成各种尺寸的随机输入。harness必须处理空、极小、巨大或格式错误的输入,而不在harness本身引起意外问题(SUT中的崩溃是允许的——这正是我们要找的)。
步骤4:结构化输入
对于需要类型化数据(整数、字符串等)的API,使用强制转换或辅助工具如FuzzedDataProvider:
简单强制转换:
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;
}
使用FuzzedDataProvider:
#include "FuzzedDataProvider.h"
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);
concat(&str1[0], str1.size(), &str2[0], str2.size(), allocation_size);
return 0;
}
步骤5:测试和迭代
运行模糊器并监控:
- 代码覆盖率(是否达到所有有趣路径?)
- 每秒执行次数(是否足够快?)
- 崩溃复现性(能否用保存的输入复现崩溃?)
迭代harness以改进这些指标。
常见模式
模式:超越字节数组——强制转换为整数
使用场景: 当目标期望基本类型如整数或浮点数时
实现:
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// 确保恰好2个4字节数字
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;
}
Rust等效:
fuzz_target!(|data: &[u8]| {
if data.len() != 2 * std::mem::size_of::<i32>() {
return;
}
let numerator = i32::from_ne_bytes([data[0], data[1], data[2], data[3]]);
let denominator = i32::from_ne_bytes([data[4], data[5], data[6], data[7]]);
divide(numerator, denominator);
});
工作原理: 任何8字节输入都有效。模糊器学习到输入必须恰好为8字节,每个位翻转都产生新的潜在有趣输入。
模式:FuzzedDataProvider处理复杂输入
使用场景: 当目标需要多个字符串、整数或变长数据时
实现:
#include "FuzzedDataProvider.h"
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;
}
优势: FuzzedDataProvider处理从字节流中提取结构化数据的复杂性。对于需要多种不同类型参数的API特别有用。
模式:交错模糊测试
使用场景: 当应在一个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));
int32_t result = 0;
switch (mode % 4) {
case 0:
result = add(numbers[0], numbers[1]);
break;
case 1:
result = subtract(numbers[0], numbers[1]);
break;
case 2:
result = multiply(numbers[0], numbers[1]);
break;
case 3:
result = divide(numbers[0], numbers[1]);
break;
}
// 防止编译器优化掉调用
printf("%d", result);
return 0;
}
优势:
- 编写一个harness比多个单独harness更快
- 单一共享语料库意味着对一个操作有趣的输入可能对其他操作也有趣
- 可以发现操作间交互的漏洞
何时使用:
- 操作共享相似输入类型
- 操作逻辑相关(例如,算术操作、CRUD操作)
- 所有操作适用单一语料库
模式:使用Arbitrary的Rust结构感知模糊测试
使用场景: 当模糊测试使用自定义结构的Rust代码时
实现:
use arbitrary::Arbitrary;
#[derive(Debug, Arbitrary)]
pub struct Name {
data: String
}
impl Name {
pub fn check_buf(&self) {
let data = self.data.as_bytes();
if data.len() > 0 && data[0] == b'a' {
if data.len() > 1 && data[1] == b'b' {
if data.len() > 2 && data[2] == b'c' {
process::abort();
}
}
}
}
}
使用arbitrary的harness:
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: your_project::Name| {
data.check_buf();
});
添加到Cargo.toml:
[dependencies]
arbitrary = { version = "1", features = ["derive"] }
优势: arbitrary crate自动处理将原始字节反序列化为Rust结构,减少样板代码并确保有效结构构造。
限制: arbitrary crate不提供反向序列化,因此无法手动构造映射到特定结构的字节数组。这最适合从空语料库开始(libFuzzer适用,AFL++可能有问题)。
高级用法
提示与技巧
| 提示 | 为何有帮助 |
|---|---|
| 从解析器开始 | 漏洞密度高,入口点清晰,易于harness |
| 模拟I/O操作 | 防止阻塞I/O导致的挂起,支持确定性 |
| 使用FuzzedDataProvider | 简化从原始字节提取结构化数据 |
| 重置全局状态 | 确保每次迭代独立且可复现 |
| 在harness中释放资源 | 防止长时间活动中的内存耗尽 |
| 避免在harness中记录日志 | 日志缓慢——模糊测试需要每秒100-1000次执行 |
| 先手动测试harness | 启动活动前用已知输入运行harness |
| 尽早检查覆盖率 | 确保harness达到预期代码路径 |
使用Protocol Buffers的结构感知模糊测试
对于高度结构化的输入格式,考虑使用Protocol Buffers作为自定义突变器的中间格式:
// 在.proto文件中定义输入格式
// 使用libprotobuf-mutator生成有效突变
// 这确保模糊器突变消息内容,而不是protobuf编码本身
此方法设置更复杂,但防止模糊器浪费时间在无法解析的输入上。参见结构感知模糊测试文档获取详情。
处理非确定性
问题: 随机值或时间依赖导致不可复现的崩溃。
解决方案:
- 将
rand()替换为基于模糊器输入种子化的确定性PRNG:uint32_t seed = fuzzed_data.ConsumeIntegral<uint32_t>(); srand(seed); - 模拟返回时间、PID或随机数据的系统调用
- 避免从
/dev/random或/dev/urandom读取
重置全局状态
如果SUT使用全局状态(单例、静态变量),在每次迭代间重置它:
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// 每次迭代前重置全局状态
global_reset();
target_function(data, size);
// 清理资源
global_cleanup();
return 0;
}
原理: 全局状态可能导致在N次迭代后崩溃,而不是特定输入,使漏洞不可复现。
实用Harness规则
遵循这些规则确保有效的模糊测试harnesses:
| 规则 | 原理 |
|---|---|
| 处理所有输入尺寸 | 模糊器生成空、极小、巨大输入——harness必须优雅处理 |
永不调用exit() |
调用exit()会停止模糊器进程。如果需要,在SUT中使用abort() |
| 加入所有线程 | 每次迭代必须完成后再开始下一次迭代 |
| 保持快速 | 目标为每秒100-1000次执行。避免日志记录、高复杂性、过度内存使用 |
| 保持确定性 | 相同输入必须始终产生相同行为以支持复现性 |
| 避免全局状态 | 全局状态降低复现性——如果不可避免,在迭代间重置 |
| 使用窄目标 | 不要在同一harness中模糊测试PNG和TCP——不同格式需要单独目标 |
| 释放资源 | 防止长时间活动中内存泄漏导致资源耗尽 |
注意: 这些指南不仅适用于harness代码,也适用于整个SUT。如果SUT违反这些规则,考虑修补它(参见模糊测试障碍技术)。
反模式
| 反模式 | 问题 | 正确方法 |
|---|---|---|
| 全局状态未重置 | 不可确定的崩溃 | 在harness开始时重置所有全局状态 |
| 阻塞I/O或网络调用 | 挂起模糊器,浪费时间 | 模拟I/O,使用内存缓冲区 |
| harness内存泄漏 | 资源耗尽导致活动终止 | 返回前释放所有分配 |
在SUT中调用exit() |
停止整个模糊测试过程 | 使用abort()或返回错误代码 |
| 在harness中记录大量日志 | 每秒执行次数降低数量级 | 模糊测试期间禁用日志 |
| 每次迭代操作过多 | 减慢模糊器 | 保持迭代快速且专注 |
| 混合不相关输入格式 | 语料库条目在不同格式间无用 | 不同格式使用单独harnesses |
| 未验证输入大小 | harness在边缘情况下崩溃 | 访问data前检查size |
工具特定指南
libFuzzer
Harness签名:
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// 你的代码在此
return 0; // 非零返回值保留供将来使用
}
编译:
clang++ -fsanitize=fuzzer,address -g harness.cc -o fuzz_target
集成提示:
- 使用
FuzzedDataProvider.h进行结构化输入提取 - 编译时添加
-fsanitize=fuzzer以链接模糊测试运行时 - 添加消毒剂(
-fsanitize=address,undefined)以检测更多漏洞 - 使用
-g以便崩溃时获得更好的堆栈跟踪 - libFuzzer可以从空语料库开始——无需种子输入
运行:
./fuzz_target corpus_dir/
资源:
AFL++
AFL++支持多种harness风格。为获得最佳性能,使用持久模式:
持久模式harness:
#include <unistd.h>
int main(int argc, char **argv) {
#ifdef __AFL_HAVE_MANUAL_CONTROL
__AFL_INIT();
#endif
unsigned char buf[MAX_SIZE];
while (__AFL_LOOP(10000)) {
// 从标准输入读取输入
ssize_t len = read(0, buf, sizeof(buf));
if (len <= 0) break;
// 调用目标函数
target_function(buf, len);
}
return 0;
}
编译:
afl-clang-fast++ -g harness.cc -o fuzz_target
集成提示:
- 使用持久模式(
__AFL_LOOP)获得10-100倍速度提升 - 考虑延迟初始化(
__AFL_INIT())以跳过设置开销 - AFL++要求语料库目录中至少有一个种子输入
- 使用
AFL_USE_ASAN=1或AFL_USE_UBSAN=1进行消毒剂构建
运行:
afl-fuzz -i seeds/ -o findings/ -- ./fuzz_target
cargo-fuzz (Rust)
Harness签名:
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
// 你的代码在此
});
使用结构化输入(arbitrary crate):
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: YourStruct| {
data.check();
});
创建harness:
cargo fuzz init
cargo fuzz add my_target
集成提示:
- 使用
arbitrarycrate自动结构反序列化 - cargo-fuzz包装libFuzzer,因此所有libFuzzer功能都有效
- 通过cargo-fuzz自动使用消毒剂编译
- Harnesses放在
fuzz/fuzz_targets/目录中
运行:
cargo +nightly fuzz run my_target
资源:
go-fuzz
Harness签名:
// +build gofuzz
package mypackage
func Fuzz(data []byte) int {
// 调用目标函数
target(data)
// 返回代码:
// -1 如果输入无效
// 0 如果输入有效但无趣
// 1 如果输入有趣(例如,添加新覆盖率)
return 0
}
构建:
go-fuzz-build
集成提示:
- 为添加覆盖率的输入返回1(可选——模糊器可自动检测)
- 为无效输入返回-1以降低类似突变的优先级
- go-fuzz自动处理持久性
运行:
go-fuzz -bin=./mypackage-fuzz.zip -workdir=fuzz
故障排除
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 每秒执行次数低 | harness太慢(日志记录、I/O、复杂性) | 分析harness,移除瓶颈,模拟I/O |
| 未发现崩溃 | 覆盖率未达到有漏洞代码 | 检查覆盖率,改进harness以到达更多路径 |
| 不可复现的崩溃 | 非确定性或全局状态 | 移除随机性,迭代间重置全局状态 |
| 模糊器立即退出 | harness调用exit() |
用abort()替换exit()或返回错误 |
| 内存不足错误 | harness或SUT内存泄漏 | 释放分配,使用泄漏消毒剂查找泄漏 |
| 空输入时崩溃 | harness未验证大小 | 添加if (size < MIN_SIZE) return 0; |
| 语料库不增长 | 输入太受限或格式太严格 | 使用FuzzedDataProvider或结构感知模糊测试 |
相关技能
使用此技术的工具
| 技能 | 如何应用 |
|---|---|
| libfuzzer | 使用LLVMFuzzerTestOneInput harness签名和FuzzedDataProvider |
| aflpp | 支持持久模式harnesses,使用__AFL_LOOP提高性能 |
| cargo-fuzz | 使用Rust特定的fuzz_target!宏和arbitrary crate集成 |
| atheris | Python harness接收字节,调用Python函数 |
| ossfuzz | 要求云模糊测试的特定目录结构中的harnesses |
相关技术
| 技能 | 关系 |
|---|---|
| 覆盖率分析 | 测量harness有效性——是否达到目标代码? |
| 地址消毒剂 | 检测harness发现的漏洞(缓冲区溢出、释放后使用) |
| 模糊测试字典 | 提供令牌帮助模糊器通过harness中的格式检查 |
| 模糊测试障碍 | 当SUT违反harness规则(退出、非确定性)时修补SUT |
资源
关键外部资源
在libFuzzer中拆分输入 - Google模糊测试文档 解释在单个模糊测试harness中处理多个输入参数的技术,包括使用魔术分隔符和FuzzedDataProvider。
使用Protocol Buffers的结构感知模糊测试 使用protobuf作为自定义突变器中间格式的高级技术,确保模糊器突变消息内容而非格式编码。
libFuzzer文档 官方LLVM文档,涵盖harness要求、最佳实践和高级功能。
cargo-fuzz书 使用cargo-fuzz和arbitrary crate编写Rust模糊测试harness的全面指南。
视频资源
- 有效文件格式模糊测试 - 关于编写文件格式解析器harness的会议讲座
- C/C++项目的现代模糊测试 - 涵盖harness设计模式的教程