2026/10/14 21:54:35

大模型驱动的逆向工程Agent:原理、架构与实战

大模型驱动的逆向工程Agent:原理、架构与实战 要说这两年哪个方向在一浪接一浪的技术热点里还能保持上升曲线“逆向工程 Agent”绝对算一个。我身边不少做业务开发的同行都开始回头翻二进制分析的老资料原因很直接大模型把传统逆向里最贵的人力成本——读汇编、猜意图、找调用关系——砍掉了一大截。这里的 Agent 不是指去逆向某个 AI 机器人而是指以大语言模型为大脑、能够自主执行分析动作的“逆向分析智能体”。它的适用面很广恶意样本分析、漏洞挖掘、应急响应、CTF 解题、闭源库协议梳理……凡是过去需要人坐在反编译工具前反复看的场景都有它的位置。1. 为什么逆向工程偏要“Agent化”先泼一盆冷水逆向工程里的“Agent化”不是一夜之间从零蹦出来的。过去十几年这个方向一直被一个问题卡着那就是自动化工具能帮你找到“这里有什么”却很难告诉你“这里在干什么”。1.1 从“人工找函数”到“让模型自己找”传统的逆向工作流熟悉的人都知道长什么样拿到一个未知二进制先查壳、看入口、跑一下字符串然后点开函数列表顺着交叉引用一点点还原逻辑。这个过程不是不能自动化传统脚本也能批量提取字符串、标记库函数但本质上是预先写死规则的机械操作。遇到没见过的混淆模式、被有意抹掉的符号信息、自定义的加密状态机规则一断分析就得回到人工。Agent 把分析变成了一场有目标的对话模型先提出一个假设下一步不是人去看而是直接调用工具验证比如“查一下 0x401000 这个函数被谁调用”“在可疑分支上打断点看内存变化”。工具返回结果后模型再修正假设直到结论收敛。在实操里最直观的差别是以前我在函数列表里一个个点开看现在只需要给 Agent 一个入口地址它会自己顺着调用关系把相关函数串起来。传统脚本回答的是“这里有什么”Agent 回答的是“这里在干什么、接下来该看哪里”。1.2 与传统脚本和插件方案的本质区别有人可能会说这不就是写个自动化脚本吗还真不一样。我把两者的差别整理成一张表看完会更清楚。维度传统脚本 / 插件逆向工程 Agent决策者人预先定好规则模型根据当前上下文动态决策分支处理遇到未知模式直接中断可以提出新假设并尝试验证工具调用固定流程串行执行按需选择工具、循环查询可审计性输出固定格式结果每一步动作都能留证据链维护成本换个样本可能就要改脚本换样本通常不用重写逻辑传统脚本的最大问题是“分析逻辑”和“样本特征”耦合得太紧。你写了一个针对某种加壳特征的提取脚本换个样本可能就失效了。Agent 不依赖特定特征生存它依赖的是“观察—假设—验证—修正”这个循环所以面对未知模式时有更强的泛化能力。1.3 真正的引爆点大模型补上了“语义理解”之前也不是没有自动化逆向的研究符号执行、污点分析都很强但它们的强项是逻辑精确性缺的是语言层面的归纳能力。比如一段反复异或 0x7F 的循环传统工具能告诉你“这里有一个循环、对缓冲区逐字节做异或”但要你说出“这像是在做某种简单的字符变换”仍然需要人脑参与。大模型在大量真实代码上训练过对这类模式的语义联想能力刚好补上这一环。代码生成模型还熟悉常见的编译产物特征比如虚表、引用计数、标准库调用样式能直接把汇编“翻译”成接近源码的表达。更重要的是大模型能理解“目的”它能根据函数命名风格、调用对象、常量使用等方式猜测这段代码在一个完整系统里扮演什么角色。不过语义联想是把双刃剑联想过头就是幻觉这一点后面会专门展开讲。2. 逆向工程 Agent 的内部长什么样如果你只是用提示词套一个聊天窗口让它分析二进制那不叫 Agent那叫“高级问问”。真正能稳定干活儿的 Agent 有一层清晰的内部结构下面这四层是我反复调出来的框架。2.1 一套典型流水线的分层设计层次职责典型组成感知层从二进制中解析出可分析的结构文件格式解析、反汇编、字符串提取、符号恢复认知层根据当前观察决定下一步大语言模型、任务规划器、假设维护工具层执行具体查询与验证反编译框架、调试器、模拟执行、搜索工具记忆层保存与检索中间结论摘要缓存、结构化“案件笔记”、向量检索很多人以为 Agent 的核心是模型本身真正跑起来才发现记忆层才是区分“短期玩具”和“长期助手”的分水岭。感知层决定你喂给模型的信息质量认知层决定推理方向工具层决定它能执行什么动作而记忆层决定它能不能在分析几个小时之后还不忘最初的结论。举个例子分析一个大型闭源库时可能涉及上千个函数、几十个调用链。如果模型每轮都要从头理解全貌要么上下文爆掉要么逻辑前后矛盾。正确的做法是让记忆层把已经确认的部分固化成摘要后续只加载与当前任务相关的局部信息。2.2 工具调用不是替代原有生态而是编排它们一个常见的误区是“Agent 要替代现有逆向工具”。实际上Agent 自己写不出一个稳定可靠的反编译器它要做的是把现有工具编排起来。这样做的收益有两条一是十多年沉淀下来的工具识别准确率远超临时脚本二是保留人在环里的可审计性每一步动作都能落到具体的工具输出上。工具调用也不是给模型越多的工具越好。我实测过动作范围控制在五类以内最稳定反汇编、交叉引用、字符串定位、模拟执行、内存查看。这五类能覆盖大多数分析场景而且模型不容易混淆。给几十个工具让它自由选择它反而会在选工具这件事上犯低级错误比如用内存查看工具去查字符串表。下面是一段工具接口示意重点在于工具返回结果要结构化方便模型直接读取# 工具接口示意查询某个地址的交叉引用 def cross_references(address: int) - list[dict]: 返回引用该地址的位置列表。 每一项包含来源地址、指令类型、是否通过跳转到达。 这个结构能让模型直接判断“谁在调用我”。 return disasm_framework.refs_to(address)2.3 上下文记忆让 Agent 不“失忆”大模型上下文窗口再长也装不下一个完整的二进制项目而且越长越容易混淆。更稳的方式是分层记忆全局只保留函数级摘要具体到当前任务才加载对应反汇编块。比如分析一个解密函数先把函数列表和调用关系存进“案件笔记”模型需要时才读取某个目标地址的指令。已经确认的结论用固定格式写回笔记后续不再重复读原始数据。这样有两个好处一是节省 token二是减少模型把不同上下文搅在一起的概率。我通常把“案件笔记”设计成三列地址、结论、验证状态。模型每确认一条就写一行。人介入复核时先看笔记里的验证状态就知道哪些结论可信、哪些还需要跟进。2.4 校验与求证Agent 也有“证据链”逆向工程最怕的不是“不知道”而是“知道得特别自信但方向错了”。所以我的 Agent 里有一条硬规则所有结论必须挂证据链。表述形式类似下面这样结论: 函数 0x401260 是一个 16 位查表型校验函数 证据: 表基址 0x402300 共 256 项循环内有查表 异或运算 验证动作: 模拟执行 100 轮输入输出与预期算法一致不给证据的结论直接判为“未验证”。一开始这么做会觉得啰嗦但遇到复杂样本时这条规则能帮人大幅节省复核时间。你不需要重新推理一遍只需要顺着证据链确认几个关键点可信度就清楚了。3. 实操从零搭一个能跑起来的逆向 Agent理论说再多不如动手搭一个。下面我按自己实际搭建的过程把每一步要做什么、要注意什么讲清楚。3.1 环境准备与基础工具链搭建前先确认你有这四样东西一是能脚本化的反编译框架二是能动态执行的调试或模拟环境三是大语言模型接口可以是云 API也可以是本地部署的开源模型四是 Python 胶水层用来串起前面三样。有一点特别提醒不要只装命令行版本的工具必须选有库接口、能批量调用的版本。Agent 每一步都在动态调工具如果每一步都要打开图形界面操作效率会低到没法用。环境示意# 建议直接用 Python 3.10确保以下能力可用 # - 二进制格式解析与反汇编 # - 函数边界识别 / 伪代码生成 # - 交叉引用与字符串定位 # - 轻量级模拟执行或调试器协议 # 初始化流程伪代码 binary load_binary(target.bin) module parse_symbols(binary) # 建立符号白名单 disasm init_disassembler(binary) dbg init_debugger(binary) # 用于动态验证3.2 指令分解与任务拆解示例提示词Agent 的质量很大程度取决于你给它的任务约束。下面这份提示词模板是我常用的基线你是一个二进制逆向分析助手。你的任务是分析 {函数地址} 的行为。 约束 - 只能引用工具返回中的真实地址和符号禁止自行编造函数名。 - 每个结论必须包含地址、证据、置信度高/中/低。 - 如果证据不足以得出结论请列出下一步要查的地址或操作。 - 不要一次性输出长篇报告先完成小任务再汇总。 目标输出格式 结论 - 功能... - 推荐函数名... 证据 - 地址 0x... 处存在 ... 下一步 - 检查 0x... 的调用者这份提示词有三个关键点限定信息来源、强制结构化输出、允许说“不知道”。后面两个比前面一个还重要。逆向场景里“猜错方向”比“不知道”更浪费时间所以一定要给模型安全退出当前任务的出口而不是逼它硬答。3.3 关键一步把反汇编结果“投喂”给模型同样一段代码喂原始字节、喂汇编、喂伪代码效果差别巨大。我的原则是能上伪代码就上伪代码同时保留关键地址和分支结构如果样本有反调试、自修改代码伪代码可能失真这时就必须回到汇编层确认。比如模型直接看伪代码能快速识别出这是一个解码器但如果只喂十六进制它可能连循环边界都要猜半天。反过来如果遇到花指令伪代码把异常控制流优化掉了模型反而看不到真实结构。所以这里要推荐给模型一个信息选择策略先看伪代码建立整体印象遇到可疑之处再回到底层细节而不是一股脑全塞给它。还有一个容易被忽略的输入字符串表和交叉引用。它们往往是分析入口比代码本身更能说明问题。模型如果先看到“Usage:”这类字符串就很容易判断出相邻函数是参数解析逻辑。3.4 带工具循环的完整工作流演示伪代码核心循环可以写成下面这样def run_agent(initial_task: str): pending [initial_task] visited set() notes [] while pending: task pending.pop() if task.key in visited: continue visited.add(task.key) prompt build_prompt(task, notes_summary()) response llm_call(prompt) if response.tool_calls: for call in response.tool_calls: result invoke_tool(call) append_evidence(call, result) pending.extend(result.new_tasks) else: append_conclusion(response.conclusion) return consolidate(notes, visited)这个循环里的三个设计都对应真实踩过的坑visited用来防止模型在两个函数之间反复查询交叉引用notes_summary()用来控制每次调用模型时的上下文长度append_evidence负责把每一步工具返回记录下来作为审计依据。在实际运行里我会给整个循环加一个最大步数限制比如 200 步就强制停止并输出中间报告。不要让它无限跑下去逆向分析没有绝对的“完成”只有“足够好”。3.5 效果不理想时先自查这五处如果你搭完发现效果一般先按下面顺序排查大概率能解决八成的低级问题架构和字节序是否配错。ARM/Thumb 混用、大小端识别错误后面的分析全都会歪。函数边界是否切对。反编译框架把多个函数并成一个时模型判断必然出问题。上下文裁剪策略是否太粗暴。随机截断函数体关键跳转丢失模型就看不到完整逻辑。工具调用是否真实执行。模型要求查内存结果给的是静态缓存反馈自然失真。是否给了“不知道”出口。没有出口时模型被逼着给结论幻觉率立刻上升。4. 运行中的坑与排查实录真正用起来之后你会发现问题往往不在模型智商而在工程细节。下面几个坑我基本都踩过一个个说破。4.1 幻觉补充“从不存在的函数”有一次让 Agent 梳理一个闭源库的初始化流程它在笔记里自信地写下“调用 base64 解码函数完成配置解码”。可我在符号表里根本没有找到对应的函数名。回头看模型把训练数据里见过的函数名套到了当前样本上。排查方式很笨但有效把所有符号名从反编译框架导出建一个白名单提示里明确要求只能从白名单引用符号。模型声称的任何一个函数名如果不在白名单里就把那条结论标记为“待确认”。自从加上这道工序这类幻觉基本绝迹。4.2 上下文塞爆与无脑裁剪一开始我图省事把某个大函数的伪代码全部塞进去。上下文窗口倒是足够但模型开始混淆局部变量和全局变量把偏移量张冠李戴。后来改成按基本块组织模型每轮只看与当前假设相关的部分并把每块结论写回笔记准确率明显回升。这里的关键认知是上下文长度不是越长越好信息密度才是。你给一堆无关代码模型反而找不到重点。不如只喂它当前分支相关的代码块让它每一步都专注解决一个小问题。4.3 工具循环死循环Agent 会在两个函数之间反复查询交叉引用A 要看 BB 又引回 A如果没有访问记录它理论上可以一直转下去。加上visited集合之后问题消失。还有一次更隐蔽因为工具返回结果解析出错函数每次生成不一样的 task key导致visited失效。后来统一改用地址作为唯一标识才算彻底解决。所以在设计任务队列时标识稳定性比名称可读性更重要。4.4 “看起来对但密钥全错”的典型翻车某次分析一个带内存加密的样本模型判断“这里从某个地址起读取 16 字节作为密钥”。听起来毫无破绽但实际密钥在另一个分支里模型指向的地址只是明文缓存。最终靠模拟执行把关键分支走一遍对比解密输出的文件头才确认是错的。这个案例说明一个道理关键结论不能只看代码形态必须有动态执行佐证。静态角度上两个分支长得几乎一样只有实际跑一遍才能看到真正被使用的数据。所以我现在对涉及密钥、偏移量、加解密结果的结论一律强制加上“动态验证”环节。下面是一张常见问题速查表可以贴在手边现象根因处理方式编造符号模型依赖训练先验建立符号白名单混淆局部变量与全局变量上下文过载分块加载 摘要回写反复查询同一地址无访问记录已访问集合 最大步数结论自洽但实际错误缺少动态校验模拟执行或调试器验证工具调用返回乱格式解析层不健壮统一 JSON 返回结构5. 评估与优化怎么才算“变强了”如果你准备长期做这个方向不能只看一两个样本上“好像挺聪明”的感觉。要建立一套自己的评估方式否则没法判断改动到底是改善还是退化。5.1 任务难度分级与基准集我习惯把逆向任务分成五级每一级用不同的核心指标评估难度典型目标核心指标L1说清某函数作用功能描述与人工标注一致率L2指出它用了哪种编码/校验算法识别准确率L3还原一条数据从输入到输出的链路关键源点/汇点命中率L4推断协议字段与状态迁移字段类型/偏移正确率L5输出可复核的行为报告动态验证通过率我自己会先拿公开教学样本做回归确认低级别任务没有因为改动而退步再用新样本测高级别任务。不要只测高级别那样不好定位问题。5.2 关键优化路线检索增强、微调、工具约束优化方向有三个按性价比排序分别是工具约束、检索增强、微调。工具约束投入最小回报最快。把模型能调用的动作限制到几个固定 JSON 格式的接口比如read_memory、query_xref、disassemble_at模型行为立刻变得更可预测。检索增强适合积累到一定量历史分析结果之后再做。把确认过的模式入库比如“某地址存在 256 项表 查表操作 → 可能是校验或加密”遇到相似代码时先检索相似片段比让模型硬猜稳得多。微调是成本最高的路线。它适合用来固定输出格式和术语风格比如让模型始终用“地址: 作用”的格式写笔记。但微调需要收集大量高质量标注数据项目初期没必要上。5.3 人与 Agent 的协作边界目前没有任何 Agent 能在完全无人监督的情况下可靠完成高难度样本的分析尤其在对抗样本面前。比较合适的定位是“可信实习生”它负责枯燥的搜索、批量函数粗筛、假设生成人负责设方向、审证据链、对关键结论做动态验证。如果你发现 Agent 在某个类别的任务上连续犯错先别急着换模型先检查是不是没给它正确的工具反馈。很多时候不是模型不行而是工具层返回的信息形式和模型预期不一致。我自己这段时间用得最多的场景其实没那么“高大上”让 Agent 去把那些我平时懒得仔细看的交叉引用一个个点开把可疑调用归类再把值得人工深挖的代码标出来。它真正提升的不是“自动找到答案”而是把分析广度从“人能看十几个函数”扩到“人配合 Agent 能看几百个函数”。热度不降反升的原因也就在这——它没有让逆向工程师失业而是让逆向工程师的时间花在更值得花的地方。最后再补一句不管 Agent 输出多像真的关键结论一定要有地址、有证据、能复现。守住这一条你才是在用工具而不是被工具带到沟里。