name: 模糊测试障碍克服 type: 技术 description: > 用于补丁代码以克服模糊测试障碍的技术。 当校验和、全局状态或其他障碍阻止模糊器进展时使用。
克服模糊测试障碍
代码库通常包含反模糊测试模式,阻止有效覆盖。校验和、全局状态(如时间种子伪随机数生成器)和验证检查可以阻止模糊器探索更深代码路径。该技术展示了如何在模糊测试期间补丁您正在测试的系统(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++ 示例:
// 之前:硬障碍
if (checksum != expected_hash) {
return -1; // 模糊器无法通过这里
}
// 之后:条件绕过
if (checksum != expected_hash) {
#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
return -1; // 仅在生产中强制执行
#endif
}
// 模糊器现在可以探索此检查后的代码
Rust 示例:
// 之前:硬障碍
if checksum != expected_hash {
return Err(MyError::Hash); // 模糊器无法通过这里
}
// 之后:条件绕过
if checksum != expected_hash {
if !cfg!(fuzzing) {
return Err(MyError::Hash); // 仅在生产中强制执行
}
}
// 模糊器现在可以探索此检查后的代码
步骤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);
误报风险: 低 - 如果数据处理不依赖于校验和正确性
模式:确定性伪随机数生成器种子
使用案例: 非确定性随机状态阻止可重现性
之前:
void initialize() {
srand(time(NULL)); // 每次运行不同种子
}
之后:
void initialize() {
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
srand(12345); // 固定种子用于模糊测试
#else
srand(time(NULL));
#endif
}
误报风险: 低 - 模糊器可以探索所有具有固定种子的代码路径
模式:谨慎验证跳过
使用案例: 验证必须跳过但下游代码有假设
之前(危险):
#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
if (!validate_config(&config)) {
return -1; // 确保 config.x != 0
}
#endif
int32_t result = 100 / config.x; // 崩溃:在模糊测试中除零!
之后(安全):
#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
if (!validate_config(&config)) {
return -1;
}
#else
// 在模糊测试期间,为失败验证使用安全默认值
if (!validate_config(&config)) {
config.x = 1; // 防止除零
config.y = 1;
}
#endif
int32_t result = 100 / config.x; // 在两种构建中都安全
误报风险: 缓解 - 提供安全默认值而不是跳过
模式:绕过复杂格式验证
使用案例: 多步骤验证使有效输入生成几乎不可能
Rust 示例:
// 之前:多验证阶段
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)
}
// 之后:在模糊测试期间跳过昂贵验证
pub fn parse_message(data: &[u8]) -> Result<Message, Error> {
validate_magic_bytes(data)?; // 保留廉价检查
if !cfg!(fuzzing) {
validate_structure(data)?;
validate_checksums(data)?;
validate_crypto_signature(data)?;
}
deserialize_message(data)
}
误报风险: 中 - 反序列化必须优雅处理格式错误数据
高级用法
提示和技巧
| 提示 | 为什么有帮助 |
|---|---|
| 保留廉价验证 | 魔术字节和大小检查引导模糊器,成本低 |
| 使用固定种子伪随机数生成器 | 使行为确定性,同时探索所有代码路径 |
| 增量补丁 | 一次跳过一个障碍并测量覆盖影响 |
| 添加防御性默认值 | 当跳过验证时,提供安全后备值 |
| 记录所有补丁 | 未来维护者需要理解模糊测试与生产差异 |
现实世界示例
OpenSSL: 使用 FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION 修改加密算法行为。例如,在 crypto/cmp/cmp_vfy.c,某些签名检查在模糊测试期间放宽,以允许深入探索证书验证逻辑。
ogg crate (Rust): 使用 cfg!(fuzzing) 在模糊测试期间跳过校验和验证。这允许模糊器探索音频处理代码,无需花费精力猜测正确校验和。
测量补丁效果
应用补丁后,量化改进:
- 行覆盖: 使用
llvm-cov或cargo-cov查看新可到达行 - 基本块覆盖: 比行覆盖更精细
- 函数覆盖: 现在可到达多少更多函数?
- 语料库大小: 模糊器是否生成更多样化输入?
有效补丁通常增加覆盖10-50%或更多。
与其他技术结合
障碍补丁与以下技术配合良好:
- 语料库种子: 提供有效输入以通过初始解析
- 字典: 帮助模糊器学习魔术字节和常见值
- 结构感知模糊测试: 使用 protobuf 或语法定义处理复杂格式
- 工具改进: 更好的工具有时可以完全避免障碍
反模式
| 反模式 | 问题 | 正确方法 |
|---|---|---|
| 全部跳过验证 | 创建误报和不稳定模糊测试 | 仅跳过阻止覆盖的特定障碍 |
| 无风险评估 | 误报浪费时间并隐藏真实 bug | 分析下游代码假设 |
| 忘记记录补丁 | 未来维护者不理解差异 | 添加注释解释为什么补丁安全 |
| 补丁不测量 | 不知道是否有帮助 | 比较补丁前后覆盖 |
| 过度补丁 | 使模糊测试构建与生产差异太大 | 最小化构建间差异 |
工具特定指南
libFuzzer
libFuzzer 在编译期间自动定义 FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION。
# C++ 编译
clang++ -g -fsanitize=fuzzer,address -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION \
harness.cc target.cc -o fuzzer
# 宏通常由 -fsanitize=fuzzer 自动定义
clang++ -g -fsanitize=fuzzer,address harness.cc target.cc -o fuzzer
集成提示:
- 宏自动定义;手动定义通常不必要
- 使用
#ifdef检查宏 - 与消毒器结合以检测新可到达代码中的 bug
AFL++
AFL++ 在使用其编译器包装器时也定义 FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION。
# 使用 AFL++ 包装器编译
afl-clang-fast++ -g -fsanitize=address target.cc harness.cc -o fuzzer
# 宏由 afl-clang-fast 自动定义
集成提示:
- 使用
afl-clang-fast或afl-clang-lto进行自动宏定义 - 持久模式工具最受益于障碍补丁
- 考虑使用
AFL_LLVM_LAF_ALL进行额外输入到状态转换
honggfuzz
honggfuzz 在构建目标时也支持宏。
# 编译
hfuzz-clang++ -g -fsanitize=address target.cc harness.cc -o fuzzer
集成提示:
- 使用
hfuzz-clang或hfuzz-clang++包装器 - 宏可用于条件编译
- 结合 honggfuzz 的反馈驱动模糊测试
cargo-fuzz (Rust)
cargo-fuzz 在构建期间自动设置 fuzzing cfg 选项。
# 构建模糊目标(cfg!(fuzzing) 自动设置)
cargo fuzz build fuzz_target_name
# 运行模糊目标
cargo fuzz run fuzz_target_name
集成提示:
- 使用
cfg!(fuzzing)在生产构建中进行运行时检查 - 使用
#[cfg(fuzzing)]进行编译时条件编译 - fuzzing cfg 仅在
cargo fuzz构建期间设置,不在常规cargo build中 - 可使用
RUSTFLAGS="--cfg fuzzing"手动启用进行测试
LibAFL
LibAFL 支持用于用 C/C++ 编写的目标的 C/C++ 宏。
# 编译
clang++ -g -fsanitize=address -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION \
target.cc -c -o target.o
集成提示:
- 手动定义宏或使用编译器标志
- 与 libFuzzer 相同方式工作
- 在构建自定义 LibAFL 基础模糊器时有用
故障排除
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 补丁后覆盖不改善 | 错误识别障碍 | 性能分析执行以找到实际瓶颈 |
| 许多误报崩溃 | 下游代码有假设 | 添加防御性默认值或部分验证 |
| 代码编译不同 | 宏未在所有构建配置中定义 | 在所有源文件和依赖项中验证宏 |
| 模糊器在补丁代码中发现 bug | 补丁引入无效状态 | 审查补丁状态不变性;考虑更安全方法 |
| 无法重现生产 bug | 构建差异太大 | 最小化补丁;保留状态关键检查的验证 |
相关技能
使用此技术的工具
| 技能 | 如何应用 |
|---|---|
| libfuzzer | 自动定义 FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION |
| aflpp | 通过编译器包装器支持宏 |
| honggfuzz | 使用宏进行条件编译 |
| cargo-fuzz | 为 Rust 条件编译设置 cfg!(fuzzing) |
相关技术
| 技能 | 关系 |
|---|---|
| fuzz-harness-writing | 更好的工具可能避免障碍;补丁允许更深探索 |
| coverage-analysis | 使用覆盖识别障碍并测量补丁效果 |
| corpus-seeding | 种子语料库可以帮助克服障碍而无需补丁 |
| dictionary-generation | 字典帮助魔术字节,但不帮助校验和或复杂验证 |
资源
关键外部资源
OpenSSL Fuzzing Documentation
OpenSSL 的模糊测试基础设施展示了大规模使用 FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION。该项目使用此宏修改加密验证、证书解析和其他安全关键代码路径,以启用更深模糊测试,同时保持生产正确性。
LibFuzzer Documentation on Flags libFuzzer 的官方 LLVM 文档,包括模糊器如何定义编译器宏以及如何有效使用它们。涵盖与消毒器和覆盖工具集成。
Rust cfg Attribute Reference
Rust 条件编译完整参考,包括 cfg!(fuzzing) 和 cfg!(test)。解释编译时与运行时条件编译以及最佳实践。