关于 fuzzing-obstacles
这项技术指南介绍如何通过条件编译调整被测代码,绕过阻碍模糊测试深入探索的校验或非确定性状态,同时保留生产构建行为。内容强调逐项评估覆盖率变化和误报风险。
主要功能
- 01识别校验与全局状态障碍
- 02用条件编译调整模糊测试构建
- 03固定随机种子以保持确定性
- 04逐步测量覆盖率变化
- 05评估绕过检查引入的误报风险
适用场景
- 01校验和检查阻止测试深入代码路径
- 02时间种子导致运行结果不稳定
- 03复杂格式校验难以生成有效输入
- 04覆盖率显示大量代码不可达
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ trailofbits/ skills --skill fuzzing-obstacles安装后在 Claude Code 中输入/fuzzing-obstacles确认已加载。
附带文件agents/openai.yamlassets/trail-of-bits-mark.svg
详细介绍
克服模糊测试障碍
代码库中常常包含阻碍有效覆盖率的反模糊测试模式。校验和、全局状态(如使用时间作为种子的伪随机数生成器)以及验证检查,都可能阻止模糊测试器探索更深层的代码路径。本技术介绍如何在模糊测试期间修补被测系统(SUT)以绕过这些障碍,同时保留生产环境中的行为。
概述
许多真实世界的程序并非为模糊测试而设计。它们可能会:
- 在处理输入前验证校验和或加密哈希
- 依赖全局状态(例如系统时间、环境变量)
- 使用非确定性随机数生成器
- 执行复杂的验证,导致模糊测试器难以生成有效输入
这些模式会让模糊测试变得困难,因为:
- 校验和: 模糊测试器必须猜中正确的哈希值(成功概率低到几乎不可能)
- 全局状态: 相同输入在不同运行中会产生不同的行为(破坏确定性)
- 复杂验证: 模糊测试器耗费精力处理验证失败,而非探索更深层的代码
解决方案是条件编译:在模糊测试构建中修改代码行为,同时保持生产代码不变。
关键概念
| 概念 | 说明 |
|---|---|
| SUT 修补 | 修改被测系统,使其适用于模糊测试 |
| 条件编译 | 根据编译时标志而表现不同的代码 |
| 模糊测试构建模式 | 启用模糊测试专用补丁的特殊构建配置 |
| 误报 | 在模糊测试期间发现、但在生产环境中不会发生的崩溃 |
| 确定性 | 相同输入始终产生相同的行为(对模糊测试至关重要) |
何时应用
在以下情况应用本技术:
- 模糊测试器卡在校验和或哈希验证处
- 覆盖率报告显示,大块代码因验证而无法触达
- 代码使用基于时间的种子或其他非确定性全局状态
- 复杂的验证让生成有效输入几乎不可能
- 你看到模糊测试器反复遇到相同的验证失败
在以下情况跳过本技术:
- 使用良好的种子语料库或字典即可克服障碍
- 验证足够简单,模糊测试器可以学会(例如魔数)
- 你正在进行可处理验证的基于语法或了解结构的模糊测试
- 跳过检查会引入过多误报
- 代码已经适用于模糊测试
快速参考
| 任务 | C/C++ | Rust |
|---|---|---|
| 检查是否为模糊测试构建 | #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION | cfg!(fuzzing) |
| 在模糊测试期间跳过检查 | #ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION return -1; #endif | if !cfg!(fuzzing) { return Err(...) } |
| 常见障碍 | 校验和、伪随机数生成器、基于时间的逻辑 | 校验和、伪随机数生成器、基于时间的逻辑 |
| 支持的模糊测试器 | libFuzzer、AFL++、LibAFL、honggfuzz | cargo-fuzz、libFuzzer |
分步指南
第 1 步:识别障碍
运行模糊测试器并分析覆盖率,找出无法触达的代码。常见模式:
- 查找深层处理前的校验和/哈希验证
- 检查是否调用了带有系统种子的
rand()、time()或srand() - 找出会拒绝大多数输入的验证函数
- 找出在不同运行中初始化结果不同的全局状态
可用工具:
- 覆盖率报告(参见覆盖率分析技术)
- 使用
-fprofile-instr-generate进行性能分析 - 手动检查入口点代码
第 2 步:添加条件编译
修改障碍处的代码,使其在模糊测试构建中绕过该障碍。
C/C++ 示例:
// Before: Hard obstacle
if (checksum != expected_hash) {
return -1; // Fuzzer never gets past here
}
// After: Conditional bypass
if (checksum != expected_hash) {
#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
return -1; // Only enforced in production
#endif
}
// Fuzzer can now explore code beyond this check
Rust 示例:
// Before: Hard obstacle
if checksum != expected_hash {
return Err(MyError::Hash); // Fuzzer never gets past here
}
// After: Conditional bypass
if checksum != expected_hash {
if !cfg!(fuzzing) {
return Err(MyError::Hash); // Only enforced in production
}
}
// Fuzzer can now explore code beyond this check
第 3 步:验证覆盖率提升
修补后:
- 使用模糊测试插桩重新构建
- 短时间运行模糊测试器
- 将覆盖率与未修补版本进行比较
- 确认正在探索新的代码路径
第 4 步:评估误报风险
考虑跳过检查是否会导致不可能出现的程序状态:
- 检查之后的代码是否会假设某些属性已通过验证?
- 跳过验证是否会导致生产环境中不会发生的崩溃?
- 是否存在隐式状态依赖?
如果误报风险较高,可以考虑采用更有针对性的补丁(参见下文“常见模式”)。
常见模式
模式:绕过校验和验证
用例: 哈希/校验和阻碍了模糊测试器的进展
之前:
uint32_t computed = hash_function(data, size);
if (computed != expected_checksum) {
return ERROR_INVALID_HASH;
}
process_data(data, size);
之后:
uint32_t computed = hash_function(data, size);
if (computed != expected_checksum) {
#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
return ERROR_INVALID_HASH;
#endif
}
process_data(data, size);
误报风险: 低 — 如果数据处理不依赖校验和的正确性
模式:使用确定性的 PRNG 种子
用例: 非确定性的随机状态会妨碍复现
之前:
void initialize() {
srand(time(NULL)); // Different seed each run
}
之后:
void initialize() {
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
srand(12345); // Fixed seed for fuzzing
#else
srand(time(NULL));
#endif
}
误报风险: 低 — 使用固定种子时,模糊测试器可以探索所有代码路径
模式:谨慎跳过验证
用例: 必须跳过验证,但下游代码存在假设
之前(危险):
#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
if (!validate_config(&config)) {
return -1; // Ensures config.x != 0
}
#endif
int32_t result = 100 / config.x; // CRASH: Division by zero in fuzzing!
之后(安全):
#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
if (!validate_config(&config)) {
return -1;
}
#else
// During fuzzing, use safe defaults for failed validation
if (!validate_config(&config)) {
config.x = 1; // Prevent division by zero
config.y = 1;
}
#endif
int32_t result = 100 / config.x; // Safe in both builds
误报风险: 已缓解 — 提供安全默认值,而不是直接跳过
模式:绕过复杂的格式验证
用例: 多步骤验证让生成有效输入变得几乎不可能
Rust 示例:
// Before: Multiple validation stages
pub fn parse_message(data: &[u8]) -> Result<Message, Error> {
validate_magic_bytes(data)?;
validate_structure(data)?;
validate_checksums(data)?;
validate_crypto_signature(data)?;
deserialize_message(data)
}
// After: Skip expensive validation during fuzzing
pub fn parse_message(data: &[u8]) -> Result<Message, Error> {
validate_magic_bytes(data)?; // Keep cheap checks
if !cfg!(fuzzing) {
validate_structure(data)?;
validate_checksums(data)?;
validate_crypto_signature(data)?;
}
deserialize_message(data)
}
误报风险: 中 — 反序列化必须能够妥善处理格式错误的数据
高级用法
提示与技巧
| 提示 | 帮助原因 |
|---|---|
| 保留开销低的验证 | 魔数和大小检查能以较低成本引导模糊测试器 |
| 为 PRNG 使用固定种子 | 探索所有代码路径的同时,让行为保持确定性 |
| 逐步打补丁 | 每次跳过一个障碍,并衡量对覆盖率的影响 |
| 添加防御性默认值 | 跳过验证时,提供安全的回退值 |
| 记录所有补丁 | 方便未来的维护者了解模糊测试与生产环境之间的差异 |
真实案例
OpenSSL: 使用 FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION 来修改密码算法的行为。例如,在 crypto/cmp/cmp_vfy.c 中,模糊测试期间会放宽某些签名检查,以便更深入地探索证书验证逻辑。
ogg crate(Rust): 使用 cfg!(fuzzing) 在模糊测试期间跳过校验和验证。这样,模糊测试器无需费力猜测正确的校验和,就能探索音频处理代码。
衡量补丁效果
应用补丁后,量化改进情况:
- 行覆盖率: 使用
llvm-cov或cargo-cov查看新增的可达代码行 - 基本块覆盖率: 比行覆盖率更精细
- 函数覆盖率: 现在还能触达多少个函数?
- 语料库大小: 模糊测试器是否生成了更多样的输入?
有效的补丁通常能将覆盖率提高 10-50% 或更多。
与其他技术结合使用
障碍补丁可以与以下方法很好地结合:
- 语料库种子: 提供能够通过初始解析的有效输入
- 字典: 帮助模糊测试器学习魔数和常见值
- 结构感知模糊测试: 对于复杂格式,使用 protobuf 或语法定义
- 测试框架改进: 更好的测试框架有时可以彻底避开障碍
反模式
| 反模式 | 问题 | 正确做法 |
|---|---|---|
| 跳过所有验证 | 会产生误报,并导致模糊测试不稳定 | 仅跳过阻碍覆盖率提升的特定障碍 |
| 未进行风险评估 | 误报会浪费时间,并掩盖真正的漏洞 | 分析下游代码中的假设 |
| 忘记记录补丁 | 后续维护者无法理解差异 | 添加注释,说明补丁为何安全 |
| 未衡量效果就打补丁 | 不知道补丁是否有效 | 比较打补丁前后的覆盖率 |
| 打补丁过多 | 模糊测试构建与生产构建的差异过大 | 尽量减小两种构建之间的差异 |
工具专项指导
libFuzzer
libFuzzer 会在编译时自动定义 FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION。
# C++ compilation
clang++ -g -fsanitize=fuzzer,address -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION \
harness.cc target.cc -o fuzzer
# The macro is usually defined automatically by -fsanitize=fuzzer
clang++ -g -fsanitize=fuzzer,address harness.cc target.cc -o fuzzer
集成提示:
- 该宏会自动定义;通常无需手动定义
- 使用
#ifdef检查该宏 - 配合 Sanitizer 使用,以检测新近可触达代码中的漏洞
AFL++
使用其编译器包装器时,AFL++ 也会定义 FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION。
# Compilation with AFL++ wrappers
afl-clang-fast++ -g -fsanitize=address target.cc harness.cc -o fuzzer
# The macro is defined automatically by afl-clang-fast
集成提示:
- 使用
afl-clang-fast或afl-clang-lto自动定义该宏 - 持久模式的测试桩最能受益于障碍修补
- 可考虑使用
AFL_LLVM_LAF_ALL,执行额外的输入到状态转换
honggfuzz
构建目标时,honggfuzz 也支持该宏。
# Compilation
hfuzz-clang++ -g -fsanitize=address target.cc harness.cc -o fuzzer
集成提示:
- 使用
hfuzz-clang或hfuzz-clang++包装器 - 可通过该宏进行条件编译
- 与 honggfuzz 的反馈驱动模糊测试结合使用
cargo-fuzz(Rust)
cargo-fuzz 会在构建期间自动设置 fuzzing cfg 选项。
# Build fuzz target (cfg!(fuzzing) is automatically set)
cargo fuzz build fuzz_target_name
# Run fuzz target
cargo fuzz run fuzz_target_name
集成提示:
- 在生产构建中使用
cfg!(fuzzing)进行运行时检查 - 使用
#[cfg(fuzzing)]进行编译时条件编译 fuzzingcfg 仅在cargo fuzz构建期间设置,常规cargo build不会设置- 可通过
RUSTFLAGS="--cfg fuzzing"手动启用以进行测试
LibAFL
对于以 C/C++ 编写的目标,LibAFL 支持该 C/C++ 宏。
# Compilation
clang++ -g -fsanitize=address -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION \
target.cc -c -o target.o
集成提示:
- 手动定义该宏或使用编译器标志
- 用法与 libFuzzer 相同
- 在构建基于 LibAFL 的自定义模糊测试工具时很有用
故障排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 打补丁后覆盖率没有提升 | 识别错了障碍 | 对执行过程进行性能分析,找出真正的瓶颈 |
| 出现大量误报崩溃 | 下游代码中存在假设 | 添加防御性默认值或部分验证 |
| 代码编译结果不同 | 并非所有构建配置都定义了宏 | 检查所有源文件和依赖项中的宏 |
| 模糊测试工具在打过补丁的代码中发现漏洞 | 补丁引入了无效状态 | 检查补丁是否遵守状态不变量;考虑采用更安全的方法 |
| 无法重现生产环境中的漏洞 | 构建差异过大 | 尽量减少补丁;保留对状态关键检查的验证 |
相关技能
使用此技术的工具
| 技能 | 应用方式 |
|---|---|
| libfuzzer | 自动定义 FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION |
| aflpp | 通过编译器包装器支持该宏 |
| honggfuzz | 使用该宏进行条件编译 |
| cargo-fuzz | 为 Rust 条件编译设置 cfg!(fuzzing) |
相关技术
| 技能 | 关联 |
|---|---|
| fuzz-harness-writing | 更好的测试桩可能无需绕过障碍;打补丁则有助于进行更深入的探索 |
| coverage-analysis | 使用覆盖率识别障碍并衡量补丁效果 |
| corpus-seeding | 种子语料库可能无需打补丁就能克服障碍 |
| dictionary-generation | 字典有助于处理魔术字节,但无法应对校验和或复杂验证 |
资源
关键外部资源
OpenSSL 模糊测试文档
OpenSSL 的模糊测试基础设施展示了 FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION 的大规模应用。该项目使用此宏修改加密验证、证书解析和其他安全关键代码路径,以便进行更深入的模糊测试,同时确保生产环境中的正确性。
LibFuzzer 标志文档 LLVM 官方文档,介绍 libFuzzer 如何定义编译器宏,以及如何有效使用这些宏。涵盖与消毒器和覆盖率插桩的集成。
Rust cfg 属性参考
Rust 条件编译的完整参考,涵盖 cfg!(fuzzing) 和 cfg!(test)。说明编译时与运行时条件编译的区别及最佳实践。