nutfuzz 是一个完全用 Rust 从零编写、零依赖的覆盖率引导 fuzzer,通过变异引擎与 edge coverage 反馈自动发现目标程序中的 bug 并最小化崩溃输入。
nutfuzz
一个完全使用 Rust 从零编写的覆盖率引导 fuzzer —— 包含变异引擎、
edge-coverage 反馈、语料库调度、dictionaries 和崩溃最小化器。无依赖。
## 这是什么
Fuzzer 通过向目标程序输入畸形数据直到出现问题来发现 bug。
有趣的地方在于*它如何选择输入的内容*:变异策略、
能够注意到输入触达了新位置的覆盖率反馈,以及
决定下一个变异哪个输入的调度机制。
nutfuzz 完全从零实现了所有这些 —— 不依赖 `libfuzzer`,不依赖 `afl`,也不依赖
`rand` crate。
## 真正重要的结果
一个隐藏在**七道门**(四个 magic byte、一个版本、一个模式和一个
大小检查)之后的 bug。相同的变异引擎、相同的预算、相同的种子 —— 唯一的
区别在于是否开启了覆盖率反馈:
```
$ nutfuzz --bench
budget 300000 executions, 10 seeds
fuzzer found median execs
blind mutation 0/10 not found
coverage-guided 8/10 126059
```
**盲目变异始终无法突破** —— 在十个种子上进行了 300 万次执行,
零发现。覆盖率引导逐个攻克了这些门,因为
通过每一道门都会触达新的代码,而这个输入就会被保留下来:
```
$ nutfuzz --target parse_nested
235783 executions in 4.21s (55988 exec/s)
edges 17 corpus 9 (9 favoured) unique bugs 1
1 unique bug(s):
reason integer overflow: size 255 exceeds the 200-byte buffer
input 85 bytes "FUZZ\x02B\xff\xff\xff\x7fBB\xb8 \xb8A\x92..."
minimised 7 bytes "FUZZ\x02B\xff"
replay nutfuzz --target parse_nested --seed 24301 --iterations 235783
```
一个 85 字节的崩溃被缩小到了**真正起作用的 7 个字节**,
并附带了一条能够精确复现它的命令。
而让这一切具有意义的检查是 —— 同一个 fuzzer 针对修正后的目标运行:
```
$ nutfuzz --target parse_record_fixed --iterations 200000
200000 executions ... no crashes found
```
**零误报。** 一个总是报告崩溃的 fuzzer 毫无价值。
## 覆盖率反馈做不到的事
值得坦率说明,因为这是此处最需要理解的一点。
覆盖率引导的作用机制是提供**部分功劳**:正确通过一道门
就会触达一个新的 edge,因此该输入会被保留并进一步细化。但是一个*单一的*四字节
比较 ——
```
if &data[0..4] != b"FUZZ" { return; } // all four bytes, one operation
```
—— 不会泄漏任何进度。四个字节中猜对了三个与全错
所触达的 edge 完全相同,因此没有任何可以引导的依据,受引导的 fuzzer 会
像盲目的 fuzzer 一样恰好停滞不前。nutfuzz 保留了一个 `parse_magic` 目标来
演示这一点,并提供了一个测试,证明 **dictionary** 能够打开单靠
覆盖率无法打开的门。这就是为什么真实的 fuzzer 都会配备 dictionaries 和
比较追踪(AFL 的 cmplog,libFuzzer 的 value profile)。
## 为什么它很有趣(展示的深度)
- **AFL 风格的 Edge coverage** —— `index = hash(block) ^ prev`,命中次数
被分桶,这样循环迭代的次数就不会淹没语料库。Block id 之所以被
*哈希*,是因为连续的 id 在 XOR 运算下会发生灾难性的冲突 —— 当
id 处于 20–30 之间时,两个不同的 edge 会落在同一个索引上,导致信号
消失。([docs/06-coverage.md](docs/06-coverage.md))
- **语料库调度** —— 能量根据大小和新颖度进行加权,加上一个贪婪的
集合覆盖 "favoured" 子集,该子集依然能触达每一个已知的 edge。
- **按根本原因进行崩溃去重** —— 根据 coverage signature 分组,而不是
输入字节。通过十种方式触达的同一个 bug 就是同一个 bug。
- **真正的变异引擎** —— bit/byte 翻转,两种字节序下 8/16/32 位的
interesting integers,算术增量,块复制,splicing,
堆叠深度为 1–8。([docs/05-mutation.md](docs/05-mutation.md))
- **外部目标** —— 运行真实的二进制文件,并区分出 **崩溃**(SIGSEGV/
SIGABRT/…)、**挂起**(超时),以及程序以非零退出码
正确拒绝错误输入的情况。将这些混淆是
一个经典的错误。
- **确定性作为一种契约** —— 每一个决策都来自一个带种子的 PRNG,因此
`--seed N` 可以逐字节复现一次测试活动,包括
崩溃出现的迭代次数。
## 快速开始
```
cargo run --release # fuzz the record parser
cargo run --release -- --target parse_nested # the seven-gate target
cargo run --release -- --target parse_nested --mode blind # the baseline
cargo run --release -- --bench # blind vs guided, 10 seeds
cargo run --release -- --dict FUZZ # with a dictionary
cargo run --release -- --seed 42 --iterations 500000
cargo test # 56 tests
```
## 状态 —— 已完成
| # | 里程碑 | 状态 |
|---|-----------|-------|
| 0 | 变异引擎,fuzzing 循环,最小化器 | ✅ done |
| 1 | 覆盖率插桩 + 反馈 | ✅ done |
| 2 | 语料库调度,能量,崩溃去重 | ✅ done |
| 3 | 外部二进制文件:信号,挂起,超时 | ✅ done |
| 4 | dictionaries + token 变异 | ✅ done |
| 5 | 真实目标的测试工具 *(见下文)* | ◐ 能力已构建 |
| 6 | 基准测试,CI,`v1.0.0` | ✅ done |
**关于里程碑 5,老实说:** 用于 fuzz 真实外部软件的底层机制已经
构建并测试完毕(进程执行,信号/挂起分类,dictionaries,
最小化,去重)。**目前尚未通过它发现或披露任何真正的第三方 bug**
—— 这是一项需要运行的测试活动,而不是一行等待编写的代码,如果
声称不然,那正好就是本项目极力避免的那种
夸大其词。请参阅 [里程碑 5](docs/milestones/milestone-5-real-target.md) 了解如何
正确运行一次测试。
## 范围与道德
这是一个为你拥有或被授权测试的代码寻找 bug(*bug-finding*)的工具。真实的发现
需要通过**协调披露**流程:私下向维护者报告,
商定时间表,在修复后发布。本仓库永远不会携带
武器化的 exploit 或针对未修复第三方 bug 的复现代码 —— 这里的价值
在于搜索技术。
## 仓库结构
```
nutfuzz/
├── src/ # rng, mutations, coverage, corpus, dictionary, exec, loop
├── tests/ # bug-finding, false positives, and coverage-beats-blind
├── docs/ # mutation, coverage, roadmap, milestones, ADRs
└── Cargo.toml # zero dependencies
```
## 许可证
MIT —— 见 [LICENSE](LICENSE)。