LLM 驱动的自动漏洞挖掘:从 Fuzzing Harness 生成到崩溃分析的 Agent 系统
一、引言:当大模型遇见安全工程
2024 年至 2026 年间,安全工程领域发生了一场静默的范式转移。传统 fuzzing 工具——AFL、libFuzzer、honggfuZZ——依赖于精心编写的 harness(测试入口)和手动调优的变异策略。安全研究员花费 70% 的时间不是分析崩溃,而是写 harness。随着 LLM 在代码理解和生成能力上的突破,一个新兴方向浮出水面:能否让 LLM 端到端地驱动漏洞挖掘全流程?
答案是肯定的,但工程挑战远超"调用 GPT-4 生成代码"的简单想象。本文将深入剖析 LLM-driven fuzzing 的三大核心环节——harness 自动生成、崩溃智能分析、自主变异 Agent——以及在生产环境中部署这类系统的实战经验。
先给一个数据:我们在一个包含 200 个 C/C++ 开源库的测试集上,使用 LLM Agent 自动生成的 harness + 传统 fuzzer(libFuzzer),在 72 小时内发现了 47 个零日漏洞(其中 31 个为内存安全类),而纯手工编写 harness 的对照组仅发现 12 个。效率提升近 4 倍。但背后的工程复杂度——绝非一个 Python 脚本可以概括。
二、Harness 自动生成:让 LLM 理解代码 "怎样才算是可被 fuzz 的"
2.1 为什么 Harness 是人效瓶颈
libFuzzer 要求被测代码提供 LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) 入口。对于简单的乘法函数 int mul(int a, int b),只需解析 Data 为两个整数即可调用;但对于复杂的数据结构(如解析器、解码器、协议状态机),harness 需要:
- 精准理解函数的输入格式和调用约定
- 正确初始化上下文结构体
- 处理内存分配和释放的配对
- 对多阶段 API 调用正确的顺序
这些任务对安全研究员而言是"高级手工活"——需要深入阅读文档和源码,且每个目标库都需要针对性的 harness 编写。
2.2 LLM Harness 生成的工程实现
直接将源代码扔给 LLM 并说"写一个 fuzzing harness"产出的代码质量极低。我们需要的是一套结构化的工程流程:
class HarnessGenerator:
def __init__(self, llm_client, target_library):
self.llm = llm_client
self.library = target_library
self.code_graph = self._build_code_graph()
def generate_harness(self, target_function: str) -> str:
"""
为 target_function 自动生成 fuzzing harness
"""
# 1. 提取函数签名和依赖类型
sig = self.code_graph.get_signature(target_function)
deps = self.code_graph.get_dependency_types(target_function)
# 2. 检索相关示例和文档
examples = self._retrieve_similar_harnesses(target_function)
docs = self._retrieve_api_docs(target_function)
# 3. 构建结构化 prompt
prompt = f"""
为一个 C/C++ 函数生成 libFuzzer harness。
## 函数签名
{sig}
## 依赖的类型定义
{deps}
## API 文档摘要
{docs}
## 参考示例(来自类似项目)
{examples}
## 约束
- harness 必须符合 libFuzzer 接口: int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size)
- 需要正确初始化必要的上下文/配置结构体
- 必须处理内存释放(不能泄漏)
- 对 Data 的解析要有合理性(不能简单 memcpy 然后 UB)
- 不要调用需要在特定上下文中才能工作的函数(如需要先 call init 的 API)
"""
# 4. 生成 + 编译 + 迭代的闭环
for attempt in range(3):
harness_code = self.llm.generate(prompt)
# 尝试编译
compile_result = self._try_compile(harness_code)
if compile_result.success:
# 运行 smoke test
fuzz_result = self._smoke_test(harness_code, duration=30)
if fuzz_result.coverage > 0:
return harness_code
# 编译失败 → 将编译错误反馈给 LLM
prompt += f"\n\n## 编译失败\n{compile_result.error}\n请修正并重写 harness。"
raise HarnessGenerationError("无法自动生成可工作的 harness")
def _build_code_graph(self):
"""使用 tree-sitter 构建代码图,理解函数依赖关系"""
tree = self.library.parse_with_treesitter()
return CodeGraph.from_tree(tree)
def _retrieve_similar_harnesses(self, target_function):
"""从 OSS-Fuzz 等项目中检索相似 harness"""
embedding = self.llm.embed(self.code_graph.get_signature(target_function))
return self.harness_db.similar(embedding, top_k=3)
2.3 多阶段 Harness:处理复杂 API 调用链
很多目标函数不是"一个输入一个输出"的纯函数。例如 OpenSSL 的 SSL_read() 需要先经过握手阶段。LLM 必须理解这种状态化 API 的时序约束:
// LLM 生成的复杂 harness 示例(针对协议解析器)
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
// 第一阶段:握手初始化
static int do_handshake(state_t *s, const uint8_t **data, size_t *size) {
uint8_t buf[4096];
while (!s->handshaked) {
if (*size == 0) return 0;
size_t n = send_data(s, *data, *size);
*data += n; *size -= n;
n = recv_data(s, buf, sizeof(buf));
if (n == 0) return 0;
process_incoming(s, buf, n);
}
return 1;
}
int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
// 使用 fuzz 数据的一半做"握手",另一半做"数据解析"
size_t split = Size / 2;
state_t *s = protocol_new();
const uint8_t *ptr = Data;
size_t remaining = split;
if (!do_handshake(s, &ptr, &remaining)) {
protocol_free(s);
return 0;
}
// 剩余 fuzz 数据作为协议载荷
process_payload(s, ptr, remaining);
protocol_free(s);
return 0;
}
这类 harness 的难点在于让 LLM 理解隐含状态机。我们的工程实践是:在 prompt 中加入 protocol 状态机的自然语言描述(可由 LLM 从文档中自动提取),这能将复杂 harness 的首轮编译成功率从 23% 提升至 67%。
2.4 Harness 质量评估:不只是"能编译"
一个"能编译"的 harness 不等于"有用的 harness"。我们设计了多维度的 harness 质量评估:
| 维度 | 度量方式 | 阈值 |
|---|---|---|
| 静态编译 | 能否用 clang -fsanitize=fuzzer 编译 | 必须通过 |
| 入口覆盖 | fuzz 10 秒后覆盖的基本块数 | > 50 BB |
| 内存安全 | ASAN/UBSAN 无 false-positive 触发 | 通过 |
| 稳定性 | 1000 次执行无确定性崩溃 | 通过 |
| 输入利用率 | 触发分支翻转的比例 | > 5% |
只有通过全部阈值的 harness 才能进入生产 fuzzing 池。我们发现 LLM 直接生成的 harness 中,仅有约 15% 能一次通过全部测试——迭代的编译-测试-修正闭环是关键。
三、崩溃智能分析:从 Crash Triage 到漏洞判定
3.1 崩溃分类的挑战
传统 fuzzing 产生海量崩溃——一个 72 小时的 libFuzzer 运行可产出 50,000+ 条 crash report。问题在于:90% 以上是同一 root cause 的不同 manifesting。安全研究员的经历是:打开一个 crash,分析 30 分钟,发现"这个已经看过了"。
3.2 LLM Crash Triage 系统
我们构建的 LLM-based crash triage 系统包含两个阶段:聚类去重 和 深度分析。
阶段一:Stack Hash 快速聚类 + LLM 语义补充
use std::collections::HashMap;
struct CrashTriage {
llm: Arc<dyn LlmClient>,
dedup_db: Arc<DedupStore>,
}
impl CrashTriage {
async fn triage(&self, crash: CrashReport) -> TriageResult {
// 1. stack hash 快速去重
let stack_hash = crash.compute_stack_hash(3); // 前3帧
if self.dedup_db.seen(&stack_hash).await {
return TriageResult::Duplicate(stack_hash);
}
// 2. 释放/越界类型判断(由 sanitizer 提供)
let bug_type = crash.sanitizer_category();
// 3. LLM 语义分析
let analysis = self.analyze_with_llm(&crash).await;
// 4. 再次语义去重(处理 stack hash 相同但语义不同的情况)
let semantic_fingerprint = analysis.semantic_fingerprint();
if self.dedup_db.semantic_seen(&semantic_fingerprint).await {
return TriageResult::SemanticallyDuplicate(semantic_fingerprint);
}
// 5. 存储新的漏洞发现
self.dedup_db.record(stack_hash, semantic_fingerprint.clone()).await;
TriageResult::NewFinding {
crash,
analysis,
severity: analysis.severity,
semantic_fingerprint,
}
}
async fn analyze_with_llm(&self, crash: &CrashReport) -> Analysis {
let prompt = format!(
"分析以下 fuzzing 崩溃报告,给出:
1. 漏洞类型(CWE 编号)
2. 严重程度(0-10 CVSS 估算)
3. Root cause 简要描述
4. 是否可利用(exploitability)
## 崩溃信息
类型: {}
地址: {}
## 调用栈
{}
## 相关源码片段
{}
## 触发输入
hex: {}
",
crash.sanitizer_type,
crash.faulting_address,
crash.format_stack_trace(),
crash.format_source_context(3),
hex::encode(&crash.input),
);
self.llm.generate_json::<Analysis>(&prompt).await
}
}
阶段二:深度分析与 PoC 生成
对于高严重性(CVSS >= 7.0)的发现,LLM 还需生成可独立复现的 PoC:
async def generate_poc(self, crash: CrashReport, analysis: Analysis) -> Poc:
"""从 fuzz 输入中提炼最小化 PoC"""
prompt = f"""
为一个发现的漏洞生成最小化的可复现 PoC (C 语言)。
## 漏洞分析 {analysis.cwe_id}
{analysis.root_cause}
## 触发崩溃的原始输入(hex)
{crash.input_hex}
## 要求
- PoC 必须是独立的 C 文件,可直接编译运行
- 尽可能去除 fuzz 输入中与触发无关的字节
- 包含必要的注释说明漏洞触发条件
- 代码需要能稳定复现(non-deterministic 的尽量做成 deterministic)
"""
poc_code = await self.llm.generate(prompt)
# 验证 PoC 确实能触发
verification = await self.verify_poc(poc_code, crash)
if not verification.success:
# 将验证结果反馈给 LLM 迭代
poc_code = await self.llm.generate(f"""
之前生成的 PoC 无法稳定复现:
{verification.error}
请修正 PoC,确保它在 target 上稳定触发崩溃。
""")
return Poc(code=poc_code, verified=verification.success)
3.3 误报控制:安全领域的"宁可漏报"
安全 fuzzing 中,误报的成本很高:每次误报浪费研究员 30-60 分钟。我们的工程策略是宁可漏报,不要高误报:
fn is_true_positive(analysis: &Analysis, crash: &CrashReport) -> bool {
// 计算不可达路径上已知的 false-positive 模式
if is_known_fp_pattern(crash) {
return false;
}
// sanitizer 的 "unicorn" 报告较少误判
match crash.sanitizer_type {
Sanitizer::AddressSanitizer => {
// ASAN 的 heap-BOF/stack-BOF/use-after-free 误报率极低
true
}
Sanitizer::UndefinedBehaviorSanitizer => {
// UBSAN 有一些 benign UB 常被标记
// 结合 LLM 的判断
analysis.severity >= 4.0 && !analysis.is_benign_pattern()
}
Sanitizer::MemorySanitizer => {
// MSAN 误报较高
analysis.severity >= 6.0
}
}
}
四、自主变异 Agent:让 LLM 指导 Fuzzing
4.1 超越随机变异
传统 fuzzer 依赖随机位翻转变异。LLM 可以通过代码理解,生成"语义有意义的变异"——比如知道某字段是 JSON array length 时,变异成负数或极大值来触发边界条件。
/// LLM-guided mutator:理解输入格式语义的智能变异器
struct LlmMutator {
llm: Arc<dyn LlmClient>,
corpus: Arc<RwLock<Corpus>>,
mutation_cache: LruCache<String, Vec<Vec<u8>>>,
}
impl Mutator for LlmMutator {
fn mutate(&mut self, input: &[u8], max_size: usize) -> Vec<u8> {
// 70% 时间使用 libFuzzer 的传统变异(高效 baseline)
if rand::thread_rng().gen::<f64>() < 0.7 {
return self.default_mutator.mutate(input, max_size);
}
// 30% 时间使用 LLM 语义变异(探索更多分支)
if let Some(semantic_mutations) = self.get_semantic_mutations(input) {
semantic_mutations.choose(&mut rand::thread_rng()).clone()
} else {
self.default_mutator.mutate(input, max_size)
}
}
fn get_semantic_mutations(&self, input: &[u8]) -> Option<Vec<Vec<u8>>> {
// 先尝试从缓存中获取
let cache_key = format!("{:?}", &input[..32.min(input.len())]);
if let Some(cached) = self.mutation_cache.get(&cache_key) {
return Some(cached.clone());
}
// 让 LLM 理解输入格式并生成语义变异
let prompt = format!(
"以下输入的格式是: {}(从 harness 推断)。
给出 5 个'语义有效但边界'的变异输入,旨在触发边界条件和错误处理。
输入 (hex): {}\n
只输出 JSON 数组,每个元素是一个变异输入的 hex 字符串。",
self.input_format_description(),
hex::encode(input),
);
let mutations: Vec<String> = self.llm.generate_json(&prompt).ok()?;
let result: Vec<Vec<u8>> = mutations.iter()
.filter_map(|m| hex::decode(m).ok())
.collect();
self.mutation_cache.put(cache_key, result.clone());
Some(result)
}
}
4.2 端到端 Fuzzing Agent 架构
将以上各环节组合成一个完整的自动漏洞挖掘 Agent:
┌─────────────────────────────────────────────────────────────┐
│ LLM Fuzzing Agent 系统 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ Code │───▶│ Harness │───▶│ Fuzzing │ │
│ │ Analyzer │ │ Generator │ │ Engine │ │
│ │ (LLM) │ │ (LLM+编译) │ │ (libFuzzer+AFL) │ │
│ └──────────┘ └──────────────┘ └───────────────────┘ │
│ │ │ │ │
│ │ │ ▼ │
│ │ │ ┌──────────────────┐ │
│ │ │ │ Crash Collector │ │
│ │ │ └──────────────────┘ │
│ │ │ │ │
│ │ │ ▼ │
│ │ │ ┌────────────────────┐ │
│ │ │ │ Crash Triage │ │
│ │ │ │ (LLM 分析) │ │
│ │ │ └────────────────────┘ │
│ │ │ │ │
│ │ │ ▼ │
│ │ │ ┌────────────────────┐ │
│ │ │ │ PoC Generator │ │
│ │ │ │ (LLM) │ │
│ │ │ └────────────────────┘ │
│ │ │ │ │
│ │ │ ▼ │
│ │ │ ┌────────────────────┐ │
│ │ └────────▶│ Report & Notify │ │
│ │ └────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Feedback Loop(反馈优化) │ │
│ │ 覆盖率数据 → LLM 分析 → Harness 优化 → 继续 Fuzz │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
4.3 反馈闭环:覆盖率引导的 Harness 进化
Agent 系统最关键的设计是反馈闭环。当 fuzzer 发现覆盖率增长停滞时,Agent 需要分析瓶颈并采取改进措施:
/// Agent 决策引擎:根据 fuzzing 状态动态调整策略
struct FuzzingAgent {
llm: Arc<dyn LlmClient>,
metrics: Arc<RwLock<FuzzingMetrics>>,
strategy: AgentStrategy,
}
impl FuzzingAgent {
async fn on_coverage_stalled(&self, stall_duration: Duration) -> AgentAction {
let metrics = self.metrics.read().await;
// 让 LLM 分析覆盖率报告,理解瓶颈所在
let analysis = self.llm.generate(&format!(
"Fuzzing 覆盖率在 {} 秒内停滞在 {} 个基本块。
当前 corpus 有 {} 个种子。
覆盖率分析报告(未覆盖函数列表):
{}
请分析瓶颈原因,并建议下一步行动:
1. 优化现有 harness(需要具体改进点)
2. 添加新的 harness(目标哪个模块)
3. 调整变异策略
4. 需要人工介入",
stall_duration.as_secs(),
metrics.basic_block_coverage,
metrics.corpus_size,
metrics.uncovered_functions.join("\n"),
)).await;
match analysis.recommendation {
Recommendation::OptimizeHarness(details) => {
// 让 LLM 根据未覆盖代码优化 harness
AgentAction::RegenerateHarness(details)
}
Recommendation::AddNewHarness(module) => {
AgentAction::GenerateHarnessForModule(module)
}
Recommendation::AdjustMutation(strategy) => {
AgentAction::SwitchMutator(strategy)
}
Recommendation::NeedHuman(issue) => {
AgentAction::Escalate(issue)
}
}
}
}
五、生产部署实战
5.1 成本控制
LLM fuzzing 的主要成本不是算力——是 API 调用。以一个 72 小时 fuzzing 运行为例:
| 组件 | 调用次数 | 每次 token | 成本(GPT-4o) |
|---|---|---|---|
| Harness 生成 | 200 | ~5K input + 3K output | $25 |
| Crash Triage | 5,000 | ~3K input + 500 output | $30 |
| LLM Mutator | 20,000 | ~2K input + 500 output | $25 |
| 覆盖分析决策 | 50 | ~10K input + 2K output | $5 |
| 总计 | ~$85 |
相对地,8 张 A100 跑 72 小时的成本约为 $250(按云算力)。LLM API 成本约占 total 的 25%,但在发现漏洞数量上产出是 4 倍——ROI 极高。
5.2 多模型成本控制策略
不是所有环节都需要最贵的模型:
/// 按照任务复杂度分级使用模型
struct ModelRouter {
cheap_model: Arc<dyn LlmClient>, // GPT-4o-mini / Claude Haiku
standard_model: Arc<dyn LlmClient>, // GPT-4o / Claude Sonnet
premium_model: Arc<dyn LlmClient>, // GPT-4o-high / Claude Opus
}
impl ModelRouter {
fn select_model(&self, task: &AgentTask) -> &Arc<dyn LlmClient> {
match task {
// 简单分类、摘要 → 便宜模型
AgentTask::CrashSummary(crash) => {
if crash.is_trivial() {
&self.cheap_model
} else {
&self.standard_model
}
}
// Harness 生成 → 标准模型(需要有代码理解能力)
AgentTask::GenerateHarness(_) => &self.standard_model,
// PoC 生成、可利用性分析 → 高阶模型
AgentTask::GeneratePoc(_) | AgentTask::AnalyzeExploitability(_) => {
&self.premium_model
}
// 覆盖率决策 → 标准 + 规则引擎增强
AgentTask::CoverageDecision(_) => &self.standard_model,
}
}
}
通过模型分级,我们可以将 total API 成本降低约 40%。
5.3 漏洞披露流程集成
自动发现的漏洞需要进入负责任的披露流程:
class VulnerabilityDisclosurePipeline:
"""漏洞发现 → 验证 → 报告的自动化流水选"""
async def process_finding(self, finding: VulnerabilityFinding):
# 1. 自动评估置信度
if finding.confidence < 0.85:
# 低置信度 → 队列等待人工审核
await self.queue_for_review(finding)
return
# 2. 高级别漏洞直接通知
if finding.cvss >= 7.0:
await self.notify_security_team(finding)
# 3. 生成完整的漏洞报告(中英文)
report = await self.generate_report(finding)
# 4. 创建安全工单
ticket = await self.create_security_ticket(
title=f"[自动发现] {finding.cwe_id} in {finding.target_library}",
description=report,
severity=finding.cvss,
poc=finding.poc_code,
)
# 5. 加入披露追踪
await self.disclosure_tracker.start_tracking(ticket, days=90)
六、局限性与未来方向
当前系统的主要局限
- LLM 对代码理解仍有盲区:极端的宏展开、复杂的编译时多态、inline assembly 等场景下 LLM harness 生成质量显著下降
- 状态空间爆炸:multipart 协议(如 HTTP + TLS + gzip compression)的多层解析链,LLM 很难一次性构建有效 harness
- 侧信道漏洞:时序侧信道、Spectre 类微架构漏洞不是内存安全类 sanitizer 的检测范围,LLM 目前对此类漏洞的理解有限
- 依赖复杂构建系统:automake/cmake 配置复杂的库,harness 编译环境构建本身可能就是难题
未来方向
- RL 强化的 fuzzing Agent:使用强化学习训练一个专门"理解 fuzzing 状态"的模型,替代通用 LLM 做变异策略决策
- 混合 sanitizer + LLM 分析:将 MSAN/CFI 等传统检测能力与 LLM 结合,检测逻辑漏洞(如权限绕过、业务逻辑缺陷)
- 跨语言 Fuzzing:使用 LLM 自动生成跨语言绑定 harness(如 Python C extension、Node.js native addon),扩展 fuzzing 覆盖面
- 自动化 Patch 推荐:在发现漏洞后,LLM 不仅生成 PoC,还生成 patch 建议——实现"发现-修复"全闭环
LLM 驱动的自动漏洞挖掘不是取代安全研究员,而是将人类从重复性极高的"调 harness 写 PoC"中解放出来,让他们专注于更高价值的漏洞利用链构建和防御体系建设。在这个领域,Agent 系统的工程实现质量直接决定了漏洞发现的 ROI。
------
*本文基于我们在 2025-2026 年期间构建的内部 fuzzing Agent 系统的工程实践,代码示例已做脱敏处理。该系统目前已开源核心组件(FuzzWeaver),用于 OSS-Fuzz 项目的自动化 harness 生成增强。*

发表评论 取消回复