前阵子朋友丢给我一个只有编译产物的内部小工具,问里面某个函数的逻辑到底是干什么的。我顺手把导出结果扔给 AI 编程助手,结果它先是甩出一段像模像样的 C 代码,再仔细一看,寄存器名是编的,函数地址对不上,甚至还在我很可能需要它定位危险操作的时候,给出了一个完全相反的结论。问题不在模型,而在我没有给 AI 一套用于逆向分析的输入格式。
后来我把常用逆向工具的输出整理成标准上下文,设计了一套叫 REA(Reverse Engineering Agent)的交互流程:先由工具把二进制转成结构化文本,再由固定模板把文本转成可推理的信息,最后让 AI 编程助手输出带地址依据的结论。这套流程补上了 AI 编程助手“看不懂二进制”的短板,让它真正具备逆向分析能力。
这篇文章把这套流程完整拆开讲。适合手里有闭源 SDK、历史遗留程序、竞赛题目,或者单纯想把反汇编结果讲清楚的人。不需要你有很深的逆向功底,会用命令行就能跟着走。我尽量把每一步为什么这么做也写明白,少让读者走我踩过的弯路。
1. 为什么需要 REA:AI 编程助手在逆向场景的真正短板
先说一个容易产生误解的地方:AI 编程助手不是看不懂汇编,而是它缺少一套“把现场信息组织起来”的方法。直接丢一段objdump原始输出给模型,它也能逐行解释指令,但一旦涉及函数边界、调用约定、加载基址、字符串引用关系,它就容易失控。这不是模型能力不够,是输入材料本身不够结构化。
1.1 普通 AI 编程助手的三个盲区
第一个盲区是“看不见二进制上下文”。对于源码项目,AI 编程助手能看到清晰的函数、变量、模块依赖;对于编译产物,这些结构化信息需要从可执行文件里重新挖出来。符号可能被剥离,函数名变成sub_401000,字符串散落在只读数据段,跳转关系要靠交叉引用才能还原。没有这些上下文,AI 只能对着指令流猜。
第二个盲区是上下文窗口的浪费。一个几 MB 的二进制,反汇编文本可能几百 MB,AI 编程助手不可能全吃下去。如果只截取其中一段,又往往缺乏“全貌”,模型就会开始编造不存在的函数和行为。REA 的思路不是让 AI 代替逆向工具,而是让逆向工具先把信息压缩成模型可以消费的高密度摘要。
第三个盲区是缺少“验证回路”。只读反汇编,AI 的判断没有反馈。它说某个函数是memcpy的封装,你没法在对话里确认。REA 把“推测”和“验证手段”一起写进输出格式,让每个结论都带地址、指令或字符串依据,你在调试器里点一下就能确认。这个回路很关键,它把 AI 从“话痨专家”角色拉回到“分析辅助工具”的位置。
1.2 REA 到底做了什么:从二进制到可推理上下文
REA 的核心是把流程切成三层:
- 采集层:用反汇编、反编译、字符串提取、导入函数分析等工具,把二进制转成文本。
- 提炼层:从原始输出里挑出模型真正需要的片段,比如函数列表、反编译伪代码、交叉引用、可疑字符串。
- 推理层:用固定提示词模板组织这些片段,交给 AI 编程助手,要求它给出按证据分级的结论。
这个设计借鉴了“外挂工具 + 提示词工程”的做法。逆向不是靠模型硬扛,而是靠工具先完成“读”的动作,AI 再完成“想”的动作。很多模型在通用代码理解上已经很强,缺的就是喂给它的材料像样。
举一个对比就能看出差别。传统手动逆向,你要打开反汇编窗口、找到函数、逐条看指令、再对照导入表确认调用关系,遇到混淆代码还得慢慢理控制流。手动过程加上 AI 辅助,只是把反汇编文本粘进对话,让 AI 逐行解释,结论依然需要你自己汇总。REA 则把整个流程固定成一条命令,从二进制到结构化报告一次打通。
| 方式 | 优点 | 典型痛点 |
|---|---|---|
| 纯手动逆向 | 可控性强,结论可靠 | 效率低,函数一多容易漏 |
| 手动 + AI 解释片段 | 上手快 | 上下文不完整,模型容易编造 |
| REA 流程 | 自动提炼、按证据输出、可复查 | 需要一点脚本和模板设计成本 |
1.3 适合谁用、不适合谁用
REA 最适合三类人:一是做产品兼容性排查,手里只有一个第三方闭源库,想知道某个导出函数的行为;二是打竞赛题经常需要快速还原算法逻辑的选手,REA 可以把重复劳动压缩到一条命令;三是做安全研究但主要精力在业务侧的人,想用编程助手加速定位危险函数。
不太适合的场景,是遇到高强度混淆、反调试保护、虚拟化壳这类对抗性很强的样本。工具阶段就先卡住了,REA 帮不了多少。这时候需要花时间处理反混淆和动态脱壳,已经是另一个范畴的问题。
接下来从工具链开始,讲怎么把这个流程真正跑起来。
2. 环境准备与工具选型解析
REA 对工具没有硬性要求,核心思路是“能导出结构化文本就行”。我自己最常用的组合是:radare2 做命令行快速分析,Ghidra 做无头模式的批量反编译,GDB/LLDB 做动态验证。
2.1 核心工具链怎么选
如果只能装一个,我建议先装 radare2。它是命令行工具,一条命令就能完成自动分析、函数列表、反汇编、字符串提取。配合 r2pipe,可以写脚本把这些输出拼装成 JSON 或 Markdown。对 REA 来说,它的优势是把产出结构化数据的成本压到最低。
Ghidra 适合需要反编译伪代码的场景。它支持无头模式运行,可以脱离图形界面批量导入二进制并导出函数、反编译结果和交叉引用。虽然安装时需要 Java 运行环境,但对分析复杂程序出伪代码,它的效果在开源方案里很突出。
GDB 和 LLDB 是动态调试工具。反汇编毕竟是静态推断,很多结论需要真跑一遍,在断点处看寄存器、内存、调用栈,才能确认。比如一个函数参数到底是不是某块堆内存,静态文本里经常有歧义,调试器里一查就清楚。
| 工具 | 主要用途 | 在 REA 中的角色 |
|---|---|---|
| radare2 / r2pipe | 自动分析、反汇编、字符串、交叉引用 | 采集层主力 |
| Ghidra 无头模式 | 反编译伪代码、批量导出 | 补全高层语义 |
| GDB / LLDB | 动态运行、断点、寄存器 | 结论验证回路 |
| file / readelf / objdump | 文件头、区段、导入导出表 | 信息预处理 |
商业反编译工具也很成熟,如果你已经有授权许可,完全可以替代 Ghidra 做无头导出。REA 关注的是输出格式,不是特定牌子。
2.2 最小可用环境的安装与验证
以 radare2 为例,安装方式取决于你的操作系统。最简单的办法是找官方仓库的预编译包,或者用系统包管理器直接装。装完先验证一下能不能进入命令行:
r2 -v能正常输出版本信息就说明环境没问题。再找一个小的可执行文件,比如你自己写的 C 程序编译产物,跑一下:
r2 -A demo_bin-A表示自动分析。进入交互模式后,输入afl查看函数列表。有输出就说明最基本的流程已经通了。
Ghidra 的无头模式一般叫analyzeHeadless。它需要 Java 环境,不同版本对 JDK 版本有要求,安装时留意说明就好。第一次使用可以建一个空项目,把二进制导入,然后跑一个最简单的导出脚本,确认能拿到反编译结果。这个阶段不需要追求最优配置,先把链路跑通。
2.3 三种接入 AI 编程助手的方式
REA 的输出最终要交给 AI 编程助手,接入方式有三种,按复杂度递增:
第一,手动粘贴。把导出报告复制到对话窗口,这是最快的验证方式。适合先确认整个流程是否合理。
第二,通过工具调用协议。现在很多编程助手支持自定义工具接口,你可以在交互界面里配置一个rea_export命令,让编程助手分析时自己调用脚本取结果。好处是一次配置、反复使用。
第三,封装成独立命令行工具。写一个rea脚本,输入是二进制路径,输出是整理好的分析报告,再在报告里追加提示词模板,生成一段完整提问文本。这个方案最可控,也是后面重点讲的。
不管哪种方式,核心不变:AI 拿到的不是原始反汇编,而是经过筛选和结构化的分析材料。
3. 核心实操:让 AI 还原一个函数的行为
下面用“自写 Demo + radare2 + 提示词模板”走一遍完整流程。这个 Demo 故意做成剥离符号后的形态,模拟最麻烦的现场。
3.1 先梳理二进制“身份信息”
不要一上来就反汇编。先花十几秒确认二进制的架构、位数、字节序、入口点和导入函数。信息缺了,AI 后面很容易把 ARM 指令当 x86 解释。
file demo_bin readelf -h demo_bin readelf -s demo_bin通常file会直接告诉你这是什么格式、32 位还是 64 位。readelf能看到入口点、字节序、程序头等信息。这些字段值得单独记进 REA 报告里:
| 信息字段 | 作用 |
|---|---|
| 架构与位数 | 决定指令集解释方式 |
| 字节序 | 影响常量读取 |
| 入口点 | 程序启动位置 |
| 导入函数 | 指出可能的外部行为 |
| 区段信息 | 定位只读数据、可写数据 |
把这些信息整理成 5 行左右的文本,作为报告头部。AI 编程助手在解读后续指令时,是以这套坐标系为基准的。
3.2 用无头模式导出反编译结果
在交互模式里逐条执行命令很方便,但 REA 要可重复,所以用-q -c走批处理。以下命令会把自动分析、函数列表、主函数反汇编、字符串表一次导出:
r2 -q -A -e scr.color=0 -c "afl; echo ===STRINGS===; iz; echo ===MAIN===; s sym.main; pdf" demo_bin-e scr.color=0是为了去掉 ANSI 颜色代码,避免污染交给模型的内容。afl输出函数列表,iz提取只读字符串,s sym.main跳到主函数,pdf打印函数反汇编。如果程序没有符号,入口不叫main,可以根据入口点地址调整,比如s 0x401000。
如果需要反编译伪代码,就换到 Ghidra 无头模式。核心思路是一样的:导入二进制,定位目标函数,导出反编译文本。伪代码的好处是 AI 更容易理解逻辑,坏处是它可能掩盖底层细节。REA 建议把反汇编和伪代码都保留,互相印证。
实际操作时,我建议把导出内容按区块拆开,而不是一股脑堆在一起。函数列表单独一块,每个函数按地址、大小、可疑调用单独列;反汇编按函数为单位切;字符串单独放。模型对“有边界的输入”更擅长,也更容易给出精确结论。
3.3 构造 REA 分析提示词
这是整个教程里最关键的部分。直接粘贴反汇编文本,AI 容易泛泛而谈;换成约束明确的提示词模板,结论质量会明显提升。我的模板大概长这样:
你是一名逆向分析助手。下面是对某个二进制片段的自动导出结果。 请依次完成: 1. 身份确认:根据文件头、架构、导入函数和区段信息判断程序类型。 2. 目标定位:从函数列表和字符串中,找出与指定行为相关的函数。 3. 行为还原:结合反汇编和伪代码,说明函数参数、返回值、关键调用关系。 4. 判定分级:每个结论必须标注【确认】【推测】【待验证】之一。 约束条件: - 所有结论必须指出地址、指令或字符串作为依据。 - 不确定时直接说“不确定”,不要为了让回答完整而编造。 - 不要输出与给定内容无关的通用解释。 - 如果存在多种可能,列出优先级最高的两种并说明理由。把第 3.1 节的身份信息、第 3.2 节的函数列表和字符串,按顺序接在模板后面,就能得到第一轮分析结果。
我实际跑过一次之后,AI 的输出大概长这样:
【身份确认】64 位 x86 可执行文件,导入了 printf、malloc、free,推测是命令行分析工具。 【目标定位】函数 sub_401260 调用了 malloc 和 free,且引用字符串区段中的 “error: invalid input”,疑似参数校验与错误处理。 【确认】sub_401260 在 0x4012a6 处调用 malloc(0x20),返回值被写入 [rbp-0x18]。 【推测】该函数可能先校验输入长度,超过阈值才分配缓冲区。 【待验证】是否在 free 前存在对缓冲区边界的二次写入,需要动态调试确认。注意,模型没有直接说“这就是漏洞”,而是给了明确的证据链和验证方向。这就是 REA 比其他方法实用的地方。它不会替你下结论,但能把下结论所需的所有材料摆到台面上。
3.4 动态调试信息补齐与交叉验证
静态分析得到的是可能性,动态调试才是确认依据。针对上一步输出中的【待验证】项,我用 GDB 做一次最小验证:
gdb demo_bin (gdb) break *0x4012a6 (gdb) run (gdb) info registers (gdb) x/8wx 0x... # 查看某地址处的内存在断点处观察参数、返回地址、栈布局,把关键值补进报告,再让 AI 重新评估原先的推测。这一步的重要性在于:AI 编程助手对“反编译伪代码和实际行为不一致”这件事几乎没有判断能力,只有人在调试器里跑一遍,才能把伪代码里的“真话”挑出来。
动态信息不是每轮都要补。通常一次分析会先做静态三件套(身份信息、函数列表、字符串),再挑一个关键函数做动态确认。动态结果作为追加证据,让 AI 更新结论。
4. 把 REA 封装成可重复使用的分析命令
每次手动跑命令、复制输出、粘贴模板,虽然能用,但不够高效。我后来把它封装成一条命令,输入二进制路径,输出一份完整的分析提问文本。
4.1 极简 rea 脚本:一条命令产出结构化报告
思路是用 Python 调子进程,执行 radare2 的批处理命令,再把输出拼成固定格式。一个能跑的骨架大概长这样:
import subprocess import sys import json def run_r2_export(binary: str) -> dict: cmds = ( "afl; " "echo ===STRINGS===; iz; " "echo ===MAIN===; s sym.main; pdf" ) result = subprocess.run( ["r2", "-q", "-A", "-e", "scr.color=0", "-c", cmds, binary], capture_output=True, text=True, timeout=120, ) return { "binary": binary, "export": result.stdout, } def build_prompt(export: dict) -> str: template = open("rea_template.txt", encoding="utf-8").read() return template + "\n" + export["export"] if __name__ == "__main__": path = sys.argv[1] data = run_r2_export(path) with open("rea_report.md", "w", encoding="utf-8") as fp: fp.write(build_prompt(data)) print("REA report written to rea_report.md")脚本里rea_template.txt放的就是上一节那段提示词。实际使用时不一定要用文件读模板,但拆开写会更好维护。脚本产出的rea_report.md可以直接粘进对话,也可以配合工具接口让编程助手自己读。
4.2 报告模板与置信度管理
REA 报告里我会固定几种区块:头部信息、函数清单、字符串、目标函数反汇编、动态调试补充。每类区块标注来源,让 AI 知道哪些是工具的输出、哪些是人工验证的结果。
置信度管理是另一个容易被忽略的点。让 AI 只输出“确认”级别的内容,会过度保守;让它全用“推测”,又会失去参考价值。我的做法是要求它在【推测】【待验证】后面补一句“用什么方法能确认”。比如怀疑某个函数是字符串拼接,就要求它说明“在调试器中下断点、观察目标缓冲区内容”这类验证路径。这样输出质量会高很多。
4.3 上下文溢出怎么办:函数级切片策略
REA 最常见的失败情况,是模型上下文装不下导出报告。函数列表几百行、每个函数反汇编几百行,加在一起超出限制后,AI 会开始忽略后半段内容,甚至胡编。
解决办法是函数级切片:第一轮只让 AI 分析函数列表和字符串,让它挑选和问题最相关的 1 到 3 个候选函数;第二轮把选中函数的完整反汇编单独喂进去;第三轮再补动态调试证据。整个过程像漏斗一样,层层收窄。
为了让每一轮都能衔接,第一轮要求 AI 输出的函数清单带编号和地址,第二轮提问时直接引用编号。这样即使中间人工介入,也不会丢失上下文。
5. 实战问题与排查技巧实录
这套流程我用过很多次,踩过的坑大多集中在这几个方向。单独列一节,希望能帮读者省掉重复排错的时间。
5.1 典型问题速查表
| 现象 | 可能原因 | 排查方法 | 建议 |
|---|---|---|---|
| AI 把 lea 指令解释成字符串读取 | 忽略了 x86 寻址语义 | 单步执行并观察寄存器 | 提示词里加一条“先区分 lea 与 mov” |
| 函数名全是 sub_xxx,AI 无法定位 | 符号被剥离 | 结合导入表与字符串引用 | 让 AI 按地址与调用关系分析,不要猜名字 |
| 字符串偏移与区段对不上 | 未考虑虚拟地址与文件偏移 | 用 readelf 对照区段表 | 报告头部写入加载基址 |
| 上下文过长,后半段被忽略 | 导出内容未切片 | 按函数分批投喂 | 先列清单,再选目标函数 |
| 反编译伪代码与实际运行不一致 | 编译器优化或反编译误判 | GDB 下断点对比 | 以动态调试结果为准 |
这张表不是全量,但覆盖了绝大多数第一次使用 REA 会遇到的问题。
5.2 反编译输出失真时的判定原则
反编译伪代码有一个天然问题:它把汇编重构成高级语言,自然会丢掉一些底层信息。比如编译器生成的算术表达式会被简化,某些位运算会被合并成看似复杂的赋值语句。AI 直接基于伪代码做判断,可能把逻辑“翻译”得过度有条理。
我现在的原则是“伪代码看思路,汇编看细节,调试看真相”。做整体理解时优先用伪代码,做边界条件和内存操作判断时回到汇编,做最终确认时上调试器。三层依据如果冲突,以调试器为准。这个原则写进提示词里,能少出现很多误判。
另一个经验是注意函数调用约定。同一套代码在不同架构、不同编译选项下,参数传递规则不一样。AI 编程助手有时会默认按 SysV x64 约定解释所有平台,导致参数个数和类型判断出错。在报告头部写明架构和平台,能显著降低这类错误。
5.3 扩展 REA 到固件、App 与 CTF 练习
REA 并不只适用于 PC 可执行文件。分析固件时,可以把 radare2 换成支持嵌入式架构的配置,再用 QEMU 配合 GDB 做动态仿真。分析移动端 App 时,先反编译出中间语言,再套同样的结构化提取流程。
练手资源方面,我建议从自己编译的小 Demo 开始,接下来再做竞赛题里去掉符号的题目。效果最好的练习方式是:先让 REA 输出完整报告,然后自己用调试器逐条验证 AI 的【确认】结论和【推测】结论。这个过程既能练逆向能力,也能顺便校准提示词模板。
最后再分享一个小经验:别让 AI 从头到尾一口气分析整个二进制,按函数切片提问。每轮只给一个函数,让它给出结论和验证路径,再拿到调试器里点一遍。REA 的价值不在于让 AI 替你做逆向,而在于把工具输出整理成模型能真正消化的结构。这个边界想清楚,用法就活络了。