mcpField
H
harness-writing

harness-writing

github.com/trailofbits测试与质量7.9k5.5k 次安装今天更新
代码仓库

关于 harness-writing

harness-writing 面向编写模糊测试 harness 的开发者,说明如何将模糊测试器输入传递给应用代码。内容涵盖输入处理、结构化数据、确定性,以及 C/C++ 和 Rust 的 harness 模式,帮助可重复地测试目标 API。

主要功能

  1. 01选择合适的模糊测试入口
  2. 02处理输入长度并进行校验
  3. 03从字节中提取结构化数据
  4. 04在单个 harness 中测试相关操作
  5. 05重置状态并保持运行确定性
  6. 06管理模糊测试期间的资源

适用场景

  1. 01为解析器或校验器创建 harness
  2. 02测试需要结构化输入的 API
  3. 03在测试停滞时提升代码覆盖率
  4. 04确保崩溃能在不同运行中复现

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/trailofbits/skills --skill harness-writing

安装后在 Claude Code 中输入/harness-writing确认已加载。

附带文件agents/openai.yamlassets/trail-of-bits-mark.svg

详细介绍

编写模糊测试 Harness

模糊测试 Harness 是入口函数,用于接收模糊测试器生成的随机数据,并将其传递给被测系统(SUT)。Harness 的质量直接决定了哪些代码路径会被执行,以及能否发现关键漏洞。编写不当的 Harness 可能会遗漏整个子系统,或导致崩溃无法复现。

概述

Harness 是模糊测试器生成随机字节与应用程序 API 之间的桥梁。它必须将原始字节解析为有意义的输入、调用目标函数,并妥善处理边缘情况。任何模糊测试配置中最重要的部分都是 Harness——如果编写不当,应用程序的关键部分可能无法得到覆盖。

关键概念
概念描述
Harness接收模糊测试器输入并调用待测目标代码的函数
SUT被测系统(System Under Test)——正在进行模糊测试的代码
入口点模糊测试器要求的函数签名(例如 LLVMFuzzerTestOneInput)
FuzzedDataProvider用于从原始字节中按结构提取类型化数据的辅助类
确定性确保相同输入始终产生相同行为的特性
交错式模糊测试根据输入执行多个操作的单个 Harness

适用时机

在以下情况下使用此技术:

  • 首次创建新的模糊测试目标
  • 模糊测试活动的代码覆盖率较低,或未能发现漏洞
  • 模糊测试期间发现的崩溃无法复现
  • 目标 API 需要复杂或结构化的输入
  • 需要同时测试多个相关函数

在以下情况下跳过此技术:

  • 项目中已有经过充分测试的 Harness
  • 工具提供的自动 Harness 生成满足需求
  • 目标已有完善的模糊测试基础设施

快速参考

任务模式
最简 C++ Harnessextern "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);
使用 FuzzedDataProviderFuzzedDataProvider 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) {
    // Ensure minimum size for meaningful input
    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) {
    // Ensure exactly 2 4-byte numbers
    if (size != 2 * sizeof(uint32_t)) {
        return 0;
    }

    // Split input into two integers
    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);

    // Extract different types of data
    size_t allocation_size = fuzzed_data.ConsumeIntegral<size_t>();

    // Consume variable-length strings with terminator
    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,它尤其有用。

模式:交错式模糊测试

用例: 需要在单个测试框架中测试多个相关操作时

实现:

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    if (size < 1 + 2 * sizeof(int32_t)) {
        return 0;
    }

    // First byte selects operation
    uint8_t mode = data[0];

    // Next bytes are operands
    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;
    }

    // Prevent compiler from optimizing away the calls
    printf("%d", result);
    return 0;
}

优点:

  • 编写一个测试框架比编写多个独立的测试框架更快
  • 单一共享语料库意味着对某个操作有意义的输入,对其他操作也可能有意义
  • 能够发现操作之间交互产生的错误

适用情况:

  • 操作使用相似的输入类型
  • 操作在逻辑上相关(例如,算术操作、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 的测试框架:

#![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++ 来说有问题)。

高级用法

提示与技巧
提示优点
从解析器开始错误密度高,入口点明确,易于构建测试框架
模拟 I/O 操作防止阻塞 I/O 导致挂起,并确保确定性
使用 FuzzedDataProvider简化从原始字节中提取结构化数据的过程
重置全局状态确保每次迭代彼此独立且可复现
在测试框架中释放资源防止长时间运行的测试活动耗尽内存
避免在测试框架中记录日志日志速度慢——模糊测试需要达到每秒数百至数千次执行
先手动测试测试框架启动测试活动前,先用已知输入运行测试框架
尽早检查覆盖率确保测试框架能够到达预期的代码路径
使用 Protocol Buffers 进行结构感知模糊测试

对于高度结构化的输入格式,可以考虑使用 Protocol Buffers 作为中间格式,并配合自定义变异器:

// Define your input format in .proto file
// Use libprotobuf-mutator to generate valid mutations
// This ensures fuzzer mutates message contents, not the protobuf encoding itself

这种方法需要更多设置,但能防止模糊测试器在无法解析的输入上浪费时间。详情请参阅结构感知模糊测试文档。

处理非确定性

问题: 随机值或时间依赖会导致崩溃无法复现。

解决方案:

  • 将 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) {
    // Reset global state before each iteration
    global_reset();

    target_function(data, size);

    // Clean up resources
    global_cleanup();
    return 0;
}

理由: 全局状态可能导致程序在迭代 N 次后崩溃,而不是由特定输入触发,从而使错误无法复现。

实用 Harness 规则

遵循以下规则,确保模糊测试 Harness 有效:

规则原因
处理所有输入大小Fuzzer 会生成空、小型和超大输入,Harness 必须能够妥善处理
绝不调用 exit()调用 exit() 会停止 Fuzzer 进程。如有需要,可在 SUT 中使用 abort()
等待所有线程结束每次迭代都必须在下一次迭代开始前运行完毕
保持快速目标是每秒执行 100 到 1000 次。避免日志记录、高复杂度和过度占用内存
保持确定性相同输入必须始终产生相同行为,以确保结果可复现
避免使用全局状态全局状态会降低可复现性;如无法避免,请在迭代之间重置
使用范围明确的目标不要在同一个 Harness 中同时对 PNG 和 TCP 进行模糊测试;不同格式需要单独的目标
释放资源防止内存泄漏在长时间测试活动中导致资源耗尽

**注意:**这些准则不仅适用于 Harness 代码,也适用于整个 SUT。如果 SUT 违反这些规则,请考虑对其进行修补(参见模糊测试障碍技巧)。

反模式

反模式问题正确做法
未重置全局状态导致崩溃具有非确定性在 Harness 开始时重置所有全局状态
阻塞式 I/O 或网络调用导致 Fuzzer 挂起、浪费时间模拟 I/O,使用内存缓冲区
Harness 中存在内存泄漏资源耗尽会终止测试活动返回前释放所有分配的内存
在 SUT 中调用 exit()停止整个模糊测试进程使用 abort() 或返回错误代码
Harness 中有大量日志记录每秒执行次数会大幅下降在模糊测试期间禁用日志记录
每次迭代执行过多操作降低 Fuzzer 速度保持迭代快速且专注
混合无关的输入格式Corpus 条目无法跨格式复用为不同格式创建单独的 Harness
未验证输入大小Harness 会在边界情况下崩溃访问 data 前检查 size

特定工具指南

libFuzzer

Harness 签名:

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    // Your code here
    return 0;  // Non-zero return is reserved for future use
}

编译:

clang++ -fsanitize=fuzzer,address -g harness.cc -o fuzz_target

集成提示:

  • 使用 FuzzedDataProvider.h 提取结构化输入
  • 使用 -fsanitize=fuzzer 编译,以链接模糊测试运行时
  • 添加 Sanitizer(-fsanitize=address,undefined)以检测更多错误
  • 使用 -g,以便在发生崩溃时获得更好的堆栈跟踪
  • libFuzzer 可以从空的 Corpus 开始,无需种子输入

运行:

./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)) {
        // Read input from stdin
        ssize_t len = read(0, buf, sizeof(buf));
        if (len <= 0) break;

        // Call target function
        target_function(buf, len);
    }

    return 0;
}

编译:

afl-clang-fast++ -g harness.cc -o fuzz_target

集成提示:

  • 使用持久模式(__AFL_LOOP),可提升 10 到 100 倍速度
  • 可考虑延迟初始化(__AFL_INIT()),以减少初始化开销
  • AFL++ 要求 Corpus 目录中至少有一个种子输入
  • 使用 AFL_USE_ASAN=1 或 AFL_USE_UBSAN=1 构建 Sanitizer 版本

运行:

afl-fuzz -i seeds/ -o findings/ -- ./fuzz_target
cargo-fuzz(Rust)

Harness 签名:

#![no_main]
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
    // Your code here
});

使用结构化输入(arbitrary crate):

#![no_main]
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: YourStruct| {
    data.check();
});

创建 Harness:

cargo fuzz init
cargo fuzz add my_target

集成提示:

  • 使用 arbitrary crate 自动反序列化结构体
  • cargo-fuzz 封装了 libFuzzer,因此 libFuzzer 的所有功能都可用
  • cargo-fuzz 会自动使用 Sanitizer 编译
  • Harness 放在 fuzz/fuzz_targets/ 目录中

运行:

cargo +nightly fuzz run my_target

资源:

go-fuzz

Harness 签名:

// +build gofuzz

package mypackage

func Fuzz(data []byte) int {
    // Call target function
    target(data)

    // Return codes:
    // -1 if input is invalid
    //  0 if input is valid but not interesting
    //  1 if input is interesting (e.g., added new coverage)
    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()将 exit() 替换为 abort() 或返回错误
内存不足错误Harness 或 SUT 存在内存泄漏释放分配的内存,使用泄漏检测器查找泄漏
空输入时崩溃Harness 未验证大小添加 if (size < MIN_SIZE) return 0;
语料库未增长输入限制过多或格式过于严格使用 FuzzedDataProvider 或结构感知模糊测试

相关技能

使用此技术的工具
技能应用方式
libfuzzer使用带有 FuzzedDataProvider 的 LLVMFuzzerTestOneInput Harness 签名
aflpp支持使用 __AFL_LOOP 的持久模式 Harness,以提升性能
cargo-fuzz使用 Rust 专用的 fuzz_target! 宏,并集成 arbitrary crate
atherisPython Harness 接收字节并调用 Python 函数
ossfuzz要求 Harness 遵循特定目录结构,以支持云端模糊测试
相关技术
技能关联
coverage-analysis衡量 Harness 的有效性——是否触及目标代码?
address-sanitizer检测 Harness 发现的错误(缓冲区溢出、释放后使用)
fuzzing-dictionary提供令牌,帮助模糊测试器通过 Harness 中的格式检查
fuzzing-obstacles当 SUT 违反 Harness 规则时(退出、非确定性),对其进行修补

资源

重要外部资源

libFuzzer 中的输入拆分 - Google 模糊测试文档 介绍如何在单个模糊测试 Harness 中处理多个输入参数,包括使用魔数分隔符和 FuzzedDataProvider。

使用 Protocol Buffers 进行结构感知模糊测试 一种高级技术,将 protobuf 用作中间格式,并通过自定义变异器确保模糊测试器变异的是消息内容,而不是格式编码。

libFuzzer 文档 LLVM 官方文档,涵盖 Harness 要求、最佳实践和高级功能。

cargo-fuzz 手册 使用 cargo-fuzz 和 arbitrary crate 编写 Rust 模糊测试 Harness 的综合指南。

视频资源

评论

支持 Markdown