LLM 引导的模糊测试:更高覆盖率,更少无聊崩溃
有一个星期二,我们花了七个小时追查一个看起来很漂亮的 SIGSEGV。我们正在对某个客户的特定 JWT 解析器进行模糊测试,AFL++ 几乎立刻就吐出了一个可重现的崩溃。闻起来像是真正的发现。我们隔离了它,用 afl-tmin 最小化,编写了 PoC,然后带着已经在脑海中起草 CVE 的自信将它发送到内部频道。
崩溃是一个针对空缓冲区的 strncpy,因为 JWT 头部字面上就是字符串 { 后跟二进制字节。解析器在到达任何有意思的部分之前,运行了 base64url_decode 并在生产环境中永远不会触达的路径上炸了:前置的负载均衡器要求 Content-Type 并且 token 中至少有一个点。这是个 bug,没错。但它也毫无用处。
那天下午,我们决定够了就是够了。
为什么灰盒模糊测试器在语法上卡住
AFL 的变异策略(位翻转、拼接、算术)是语法盲的:大多数变异后的输入都过不了解析器。Wang 等人(Superion,ICSE 2019)已经指出"AFL 花了大量时间处理语法正确性,只能找到解析错误"。
对于 JSON、ASN.1 或任何有状态的格式,有效覆盖率会停滞。会出现大量崩溃,但大多数来自序言部分:验证器、base64 解码器、长度检查。有趣的内容(签名验证逻辑、kid 处理、algorithm confusion 类攻击)隐藏在位翻转无法穿越的墙后面。
LLM 作为语义变异引擎
Xia 等人在 Fuzz4All(ICSE 2024)[1]中将其形式化:LLM"隐式地学习语法、语义和有效的 API 约束"。它是一个生成合理输入的概率生成器。
Deng 等人使用 TitanFuzz(ISSTA 2023)[2]在 TensorFlow/PyTorch 上展示了 30-50% 更多的覆盖率。Meng 等人使用 ChatAFL(NDSS 2024)[3]将该原理应用于网络协议:状态转移多了 47.6%,9 个新 CVE。Yang 等人使用 WhiteFox(OOPSLA 2024)[4]增加了一个读取源代码以推导输入需求的代理。
共同的模式:LLM 不做模糊测试,它提出候选项。传统的模糊测试器仍然是覆盖率引导的引擎。
我们的架构:带有语义插件的 AFL++
Gwaihir CLI 封装了 AFL++ 并添加两样东西:一个将一定比例的变异委托给语义提供者的自定义变异器,以及 Beorn,它带来已知的语法、历史样本和同一格式的过去 CVE。
- Gwaihir 分析目标。如果 Beorn 识别该格式,它会注入种子语法。
- AFL++ 以 LLM 从规范生成的初始语料库启动。
- 自定义变异器挂载到
afl_custom_fuzz。每 N 次迭代,它向 LLM 请求一次语义变异。 - 增加覆盖率的输入被重新注入语料库。
// gwaihir_mutator.c — 示意片段
size_t afl_custom_fuzz(void *data, uint8_t *buf, size_t buf_size,
uint8_t **out_buf, uint8_t *add_buf,
size_t add_buf_size, size_t max_size) {
gwaihir_ctx_t *ctx = (gwaihir_ctx_t *)data;
ctx->counter++;
if (ctx->counter % ctx->llm_every == 0) {
return gwaihir_llm_mutate(ctx, buf, buf_size, out_buf, max_size);
}
return afl_havoc_mutate(ctx, buf, buf_size, out_buf, max_size);
}JWT 解析器案例
对同一个二进制文件进行 24 小时活动:
- 原生 AFL++:12.3% 覆盖率,47 个唯一崩溃,2 个可利用。
- AFL++ + 字典 token:19.8% 覆盖率,31 个崩溃,3 个可利用。
- AFL++ + Gwaihir/Beorn:41.7% 覆盖率,18 个唯一崩溃,7 个可利用。
总崩溃数较少,但更密集。七个中有三个最终是 kid 逻辑和非对称算法处理中的真实 bug。
Fuzz4All 与我们方法的对比
Fuzz4All 更具雄心:LLM 就是生成循环。极大的通用性(在 GCC/Clang/Z3/OpenJDK 中发现了 98 个 bug),但有两个限制:没有覆盖率反馈则盲目摸索,每次变异都是对模型的调用。我们的方法更朴素也更便宜:AFL++ 以位翻转的速度完成 98%,LLM 只在需要将覆盖率推过解析器时介入。
权衡
Token 成本。24小时 × 1 次调用/50 次执行 × 5,000 次执行/秒 = ~860万次调用。如果没有积极的缓存和本地模型则不可行。我们对 95% 使用本地 GPU 上的量化模型,并保留大模型用于救援变异。
延迟。一次 LLM 调用,即使是本地的,也需要数十/数百毫秒。AFL++ 是同步的。使用异步队列,LLM 在其中填充预生成变异的池。
规范不足。提示模糊 → LLM 幻觉出无聊的输入。提示狭窄 → 它复制已知案例。我们对提示的迭代几乎和对代码的迭代一样多。
我们的收获
LLM 引导的模糊测试不是 AFL++ 的替代品。它是一种补充,当目标具有复杂解析时可以提高覆盖率的上限。它不追求更多崩溃;它追求更好的崩溃。对团队而言,这意味着花在无用分诊上的小时数减少,花在对客户重要的 bug 上的时间增加。
参考文献
- Xia, C.S. et al. (2024). Fuzz4All: Universal Fuzzing with LLMs. ICSE 2024. arXiv:2308.04748.
- Deng, Y. et al. (2023). TitanFuzz. ISSTA 2023. arXiv:2212.14834.
- Meng, R. et al. (2024). ChatAFL. NDSS 2024.
- Yang, C. et al. (2024). WhiteFox. OOPSLA 2024. arXiv:2310.15991.
- Liu, Z. et al. (2024). InputBlaster. ICSE 2024. arXiv:2310.15657.