名称: 模糊测试障碍 类型: 技术 描述: > 通过修补代码来克服模糊测试中的障碍的技术。 当校验和、全局状态或其他障碍阻止模糊测试器进展时使用。
克服模糊测试障碍
代码库通常包含防模糊测试模式,这些模式阻止有效覆盖。校验和、全局状态(如时间种子的伪随机数生成器)和验证检查可能会阻止模糊测试器探索更深的代码路径。本技术展示了如何在模糊测试期间修补被测系统(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 |
分步指南
第一步:识别障碍
运行模糊测试器并分析覆盖率以找到不可达代码。常见模式:
- 查找在处理更深之前进行的校验和/哈希验证
- 检查调用
rand()、time()或使用系统种子的srand() - 找到拒绝大多数输入的验证函数
- 识别在不同运行中不同的全局状态初始化
辅助工具:
- 覆盖率报告(参见覆盖率分析技术)
- 使用
-fprofile-instr-generate进行分析 - 手动代码检查入口点
第二步:添加条件编译
修改障碍以在模糊测试构建期间绕过它。
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); // 仅在生产中强制执行
}
}
// 模糊测试器现在可以探索此检查之后的代码
第三步:验证覆盖率改进
修补后:
- 使用模糊测试仪器重新构建
- 运行模糊测试器短时间
- 比较与未修补版本的覆盖率
- 确认新代码路径正在被探索
第四步:评估误报风险
考虑跳过检查是否会引入不可能的程式状态:
- 检查之后的代码是否假设已验证的属性?
- 跳过验证是否会导致无法在生产中发生的崩溃?
- 是否有隐式状态依赖?
如果误报可能,考虑更针对性的补丁(参见常见模式)。
常见模式
模式:绕过校验和验证
用例: 哈希/校验和阻止所有模糊测试器进展
之前:
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或语法定义处理复杂格式
- 测试套件改进: 更好的测试套件有时可以完全避免障碍
反模式
| 反模式 | 问题 | 正确方法 |
|---|---|---|
| 跳过所有验证全面 | 创建误报和不稳定的模糊测试 | 仅跳过阻止覆盖的特定障碍 |
| 无风险评估 | 误报浪费时间和隐藏真实错误 | 分析下游代码是否有假设 |
| 忘记记录补丁 | 未来维护者不理解差异 | 添加注释解释为何补丁安全 |
| 无测量修补 | 不知道是否帮助 | 比较修补前后的覆盖率 |
| 过度修补 | 使模糊测试构建与生产差异太大 | 最小化构建之间的差异 |
工具特定指导
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检查宏 - 结合消毒剂检测新可达代码中的错误
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的自定义模糊测试器时有用
故障排除
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 修补后覆盖率未改进 | 障碍识别错误 | 配置文件执行以找到实际瓶颈 |
| 许多误报崩溃 | 下游代码有假设 | 添加防御性默认值或部分验证 |
| 代码编译不同 | 宏未在所有构建配置中定义 | 验证所有源文件和依赖中的宏 |
| 模糊测试器在修补代码中找到错误 | 补丁引入了无效状态 | 审查补丁的状态不变量;考虑更安全的方法 |
| 无法复制生产错误 | 构建差异太大 | 最小化补丁;对状态关键检查保持验证 |
相关技能
使用此技术的工具
| 技能 | 如何应用 |
|---|---|
| libfuzzer | 自动定义FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION |
| aflpp | 通过编译器包装器支持宏 |
| honggfuzz | 使用宏进行条件编译 |
| cargo-fuzz | 为Rust条件编译设置cfg!(fuzzing) |
相关技术
| 技能 | 关系 |
|---|---|
| 模糊测试套件编写 | 更好的测试套件可能避免障碍;修补允许更深探索 |
| 覆盖率分析 | 使用覆盖率识别障碍和测量补丁效果 |
| 语料库播种 | 种子语料库可以帮助克服障碍而无需修补 |
| 字典生成 | 字典帮助魔数字节,但非校验和或复杂验证 |
资源
关键外部资源
OpenSSL 模糊测试文档
OpenSSL的模糊测试基础设施展示了大规模使用FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION。该项目使用此宏来修改加密验证、证书解析和其他安全关键代码路径,以实现更深模糊测试,同时保持生产正确性。
LibFuzzer 文档标志 LLVM官方文档为libFuzzer,包括模糊测试器如何定义编译器宏以及如何有效使用它们。涵盖与消毒剂和覆盖率仪器的集成。
Rust cfg 属性参考
Rust条件编译的完整参考,包括cfg!(fuzzing)和cfg!(test)。解释编译时与运行时条件编译和最佳实践。