MokiMeow/nutfuzz

GitHub: MokiMeow/nutfuzz

nutfuzz 是一个完全用 Rust 从零编写、零依赖的覆盖率引导 fuzzer,通过变异引擎与 edge coverage 反馈自动发现目标程序中的 bug 并最小化崩溃输入。

Stars: 0 | Forks: 0

nutfuzz

一个完全使用 Rust 从零编写的覆盖率引导 fuzzer —— 包含变异引擎、 edge-coverage 反馈、语料库调度、dictionaries 和崩溃最小化器。无依赖。

Rust no dependencies deterministic v1.0.0 MIT

## 这是什么 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)。
标签:Rust, 可视化界面, 安全测试, 攻击性安全, 网络流量审计, 通知系统