跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

c++ libfuzzer自定义mutator c++如何为fuzzing编写变异策略

LLVMFuzzerCustomMutator 是可选的语义感知变异接口,用于在默认变异无效时介入;需严格遵守无堆分配、无异常、仅用 async-signal-safe 函数、通过 Rand() 获取随机数、输出长度≤max_size 等约束。 libFuzzer 的
LLVMFuzzerCustomMutator
是什么角色 它不是必须实现的接口,而是当你发现默认变异(bit flips、byte swaps、dictionary insertion 等)对目标代码无效时,才需要介入的“最后手段”。libFuzzer 在每次调用
LLVMFuzzerCustomMutator
时,会传入原始输入缓冲区、长度、最大允许长度和一个随机数生成器句柄;你返回新长度,libFuzzer 负责后续执行与崩溃判定。 关键点:这个函数不能分配堆内存(libFuzzer 不管理你的 malloc/free)、不能抛异常、不能调用非 async-signal-safe 函数(如
printf
、
malloc
、
std::string::append
),否则会导致进程被 kill 或行为不可控。 必须用
uint8_t*
直接操作内存,避免
std::vector
或
std::string
所有随机数必须通过传入的
mutate_data->Rand()
获取,不能用
rand()
或
std::random_device
输出长度不能超过
max_size
,否则 libFuzzer 会截断并静默失败 如何编写一个安全可用的自定义 mutator 示例 假设你在 fuzz 一个解析 JSON 字符串的函数,发现默认变异很难生成合法但边缘的 JSON(比如嵌套过深、键名含控制字符、数值溢出)。你可以写一个轻量级 mutator,在已有输入上做语义感知调整:
extern "C" size_t LLVMFuzzerCustomMutator(uint8_t *data, size_t size, size_t max_size, unsigned int seed) { // 使用 seed 初始化本地 rng(仅用于确定性调试,实际应只用 mutate_data->Rand()) // 但注意:libFuzzer 不传 seed 给你 —— 这里只是示意;真实代码中删掉这行,直接用 Rand() auto &mutate_data = *reinterpret_cast(seed); // ❌ 错误!libFuzzer 不传 FuzzerMutateData*

// ✅ 正确方式:libFuzzer v14+ 传的是 FuzzerMutateData,但你不能依赖其内部布局 // 实际应这样(参考 llvm-project/llvm/lib/Fuzzer/FuzzerInterface.h): // struct FuzzerMutateData { uint32_t (Rand)(); }; // 所以你只能调用 ((FuzzerMutateData*)seed)->Rand()

if (size == 0 || max_size == 0) return size;

立即学习“C++免费学习笔记(深入)”;

C知道

CSDN推出的一款AI技术问答工具

下载

// 示例:50% 概率插入一个合法 JSON key("k":) if (((FuzzerMutateData*)seed)->Rand() % 2 == 0 && size + 4 <= max_size) { memmove(data + 4, data, size); data[0] = '"'; data[1] = 'k'; data[2] = '"'; data[3] = ':'; return size + 4; }

// 示例:20% 概率把某个数字改成 "9223372036854775807"(int64_max) if (((FuzzerMutateData)seed)->Rand() % 5 == 0) { for (size_t i = 0; i + 10 < size; ++i) { if (data[i] >= '0' && data[i] <= '9') { const char maxint = "9223372036854775807"; size_t len = strlen(maxint); if (i + len <= max_size) { memcpy(data + i, maxint, len); return std::min(size - (i+10) + len, max_size); } } } }

return size; // 默认不修改 }

注意:上面示例中的
strlen
和
memcpy
是允许的(async-signal-safe),但
std::min
必须替换成 C 风格三元表达式或手动比较,因为
可能触发 STL 内部锁或异常。 为什么
LLVMFuzzerCustomCrossOver
往往比自定义 mutator 更值得优先尝试 交叉变异(crossover)复用已有语料中的子结构,天然更贴近语法/协议边界。对 parser、decoder、config loader 类目标,它常比手工写 mutator 更快产出有效输入。
LLVMFuzzerCustomCrossOver
接收两个输入指针和长度,返回新长度;同样禁止堆分配和非 async-signal-safe 调用 简单策略:随机选一个分割点,拼接
data1[0..pos]
+
data2[pos..]
,再做一次小范围翻转 libFuzzer 默认已启用 crossover,但只有在 corpus 达到一定规模(默认 1000 个输入)后才高频触发 —— 如果你刚启动 fuzz,先等它跑一阵再评估是否需自定义 调试自定义 mutator 时最常踩的三个坑 libFuzzer 不会告诉你 mutator 崩溃的具体原因,只会打印
ERROR: libFuzzer: deadly signal
或静默跳过变异 —— 这让你很难定位问题。 误用
std::string
或
std::vector
:它们的构造/析构可能调用
malloc
或抛
std::bad_alloc
,触发信号终止 越界读写:比如用
data[size]
当作结束符判断,但
size
可能为 0;或 memcpy 长度算错导致覆盖
max_size
边界 忽略
max_size
限制:返回值 >
max_size
时 libFuzzer 会截断,但你的逻辑可能已破坏数据一致性(比如插入字符串后没更新后续偏移),导致后续解析崩溃而非你的 mutator 崩溃 建议在 mutator 开头加一句:
if (size > max_size) return size;
,并始终用
memmove
/
memcpy
替代手写循环——边界更可控。

相关文章