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)

六、局限性与未来方向

当前系统的主要局限

  1. LLM 对代码理解仍有盲区:极端的宏展开、复杂的编译时多态、inline assembly 等场景下 LLM harness 生成质量显著下降
  2. 状态空间爆炸:multipart 协议(如 HTTP + TLS + gzip compression)的多层解析链,LLM 很难一次性构建有效 harness
  3. 侧信道漏洞:时序侧信道、Spectre 类微架构漏洞不是内存安全类 sanitizer 的检测范围,LLM 目前对此类漏洞的理解有限
  4. 依赖复杂构建系统:automake/cmake 配置复杂的库,harness 编译环境构建本身可能就是难题

未来方向

  1. RL 强化的 fuzzing Agent:使用强化学习训练一个专门"理解 fuzzing 状态"的模型,替代通用 LLM 做变异策略决策
  2. 混合 sanitizer + LLM 分析:将 MSAN/CFI 等传统检测能力与 LLM 结合,检测逻辑漏洞(如权限绕过、业务逻辑缺陷)
  3. 跨语言 Fuzzing:使用 LLM 自动生成跨语言绑定 harness(如 Python C extension、Node.js native addon),扩展 fuzzing 覆盖面
  4. 自动化 Patch 推荐:在发现漏洞后,LLM 不仅生成 PoC,还生成 patch 建议——实现"发现-修复"全闭环

LLM 驱动的自动漏洞挖掘不是取代安全研究员,而是将人类从重复性极高的"调 harness 写 PoC"中解放出来,让他们专注于更高价值的漏洞利用链构建和防御体系建设。在这个领域,Agent 系统的工程实现质量直接决定了漏洞发现的 ROI。

------

*本文基于我们在 2025-2026 年期间构建的内部 fuzzing Agent 系统的工程实践,代码示例已做脱敏处理。该系统目前已开源核心组件(FuzzWeaver),用于 OSS-Fuzz 项目的自动化 harness 生成增强。*

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部