最近我在给自己的 AI 编程助手配一个逆向分析模块,内部代号叫 REA(Reverse Engineering Agent)。起因是手头有个老项目的二进制产物,源码散落在历史分支里,线上跑的关键模块却只有编译好的可执行文件,我需要把几个核心函数的真实行为搞清楚。手动开反汇编工具一行行读汇编,眼睛是真的受不了;直接把二进制丢给 AI 助手,它却只会一本正经地编,因为模型根本读不到文件内容,只是在根据文件名和提问在猜。REA 要解决的就是这个问题:给 AI 助手装上"眼睛"和"手",眼睛负责读字节流并解析成结构化信息,手负责调用二进制分析工具并把结果喂回上下文。这篇文章会把我在设计、编码、调试中踩过的路完整写出来,从整体架构到核心代码,再到排错经验,尽量让有 Python 基础、接触过一点汇编的读者也能照着落地。
1. 项目概述:让 AI 助手真正"看见"二进制
1.1 AI 编程助手在逆向场景上的天然短板
先聊一个很多人忽视的问题:AI 编程助手擅长处理的是文本符号世界。它能读懂.py、.c、.md、.json,因为模型在训练阶段见过海量这种文本,推理时也能直接消费这些内容。但换成.elf、.exe、.so、.bin这类二进制文件,情况就完全不同了。模型根本没有办法直接"看"到文件里每个字节的含义,它的常见反应是:根据文件名猜测格式,根据你的描述脑补代码,然后在你不知情的情况下编出一段"看起来特别合理"的分析结论。
我先试过直接把文件路径丢给助手,结果它给出了一段非常自信的"加密压缩包"鉴定。我当时还挺奇怪,后来复盘才发现,它压根没读取文件,只是在用文件名后缀做"即兴分析"。这个问题的根源不在于模型笨,而在于工具链没有打通:模型缺少感知二进制内容的入口,也缺少调用专业分析工具的路径。
所以,给 AI 助手配逆向能力,本质上不是"换一个更聪明的模型",而是补一条完整的能力链路:文件格式识别、指令级反汇编、字符串与符号提取、函数边界划分、交叉引用关系,以及最重要的——把上述结果安全地组装成模型可以消费的结构化上下文。
1.2 REA 是什么,以及它适合谁用
REA 的定位是一层非常薄的"中间件",也叫智能体工具层。它介于大语言模型和操作系统之间,负责把模型的语言意图翻译成真实的系统操作,再把系统操作的输出翻译成模型能看懂的文本摘要。说白了,它让 AI 编程助手从"一个只会聊天的顾问"变成"一个能动手的工程师"。
这个方案适合谁用?我自己的体会是三类人收益最大。第一类是像我这样维护历史项目的开发者,手里有二进制、缺源码,需要快速搞清模块行为。第二类是打 CTF 逆向题的选手,题目给的是一个编译产物,你需要还原关键算法,REA 能帮你省掉大量人工翻汇编的时间。第三类是正在做软件兼容性核对的技术人员,需要交叉验证不同版本构建之间的行为差异。这些场景都有一个共同点:目标不是"从零写一个反编译器",而是"让 AI 作为副驾驶、带工具链、人做复核"。
需要提醒的是,REA 不是反编译器。它不会自动把汇编变成 100% 正确的 C 代码,它做的是"缩小范围 + 提供证据 + 生成假设"。最终判断仍然由人类完成,模型的作用是加速阅读过程。明白这一点,你就不会对它的输出抱有不切实际的期待,也不会因为某个函数还原不全就否掉整套方案。
2. 总体架构:工具链选型与消息循环设计
2.1 三条设计原则,避开"直接灌入"的陷阱
我在动手写第一版代码之前,犯过一个典型错误:想一口气把整个二进制文件"喂"给模型。结果非常糟糕,一个 1MB 的 .text 段反汇编出来有几万条指令,模型上下文窗口根本装不下,而且即使装下了,大量无关指令也会把注意力彻底带偏。所以这个项目的第一条设计原则就是:原始字节不出边界。给模型的永远只能是经过提炼的摘要,而不是未经处理的原始数据。
第二条原则是工具小而专。每个工具只做一件事,比如"列出段信息"、"反汇编某个地址范围"、"提取字符串"、"找某个字符串被谁引用"。这样做有两个好处:工具的输出格式稳定,模型容易学会在什么时候调用哪一个;工具箱出问题时,人也容易定位是哪一个环节坏了。
第三条原则是结果可复核。每一次工具调用,都要保留输入的参数、输出的摘要和耗时。这个设计在后面的调试阶段帮了大忙。模型是不是在一个并不存在的地址上编故事,是不是反反复复调用同一个工具,是不是答非所问,都可以从调用日志里查得一清二楚。我把这个日志叫"审计轨迹",它同时也是后续提示词工程的重要依据——哪些信息模型用得上,哪些它视而不见,看日志比猜要准得多。
2.2 工具链选型:为什么选这几个库
依赖选择上,我优先考虑纯 Python、可嵌入、输出结构化这三个条件。最终锁定四个库:pyelftools、capstone、re(标准库,不用额外装)、unicorn。对应功能如下表:
| 功能需求 | 选用的方案 | 选型理由 |
|---|---|---|
| ELF 文件格式解析 | pyelftools | 纯 Python,能拿节区表、符号表、动态链接信息,API 稳定 |
| 多架构反汇编 | capstone | 覆盖 x86/ARM/RISC-V 等多种指令集,输出带地址、字节、助记符,非常适合结构化喂给模型 |
| 字符串与模式提取 | 标准库 re + 自研打分 | 零依赖,速度快,适合第一版快速迭代 |
| 动态执行与模拟 | unicorn | 可选模块,处理需要动态执行的场景再引入,避免第一版复杂度失控 |
| 伪代码/反编译 | 自研简化还原 + 模型推理 | 依赖反汇编结果,让模型基于真实指令序列生成伪代码,而不是凭空生成 |
这里专门解释一下为什么没有直接用现成的重量级逆向套件。不是它不好,而是对我们这个场景太重。首先,重量级套件默认是给人类分析师用的图形界面,输出内容非常多,落盘格式不友好;其次,它的全量分析往往让结果信息密度过低,模型消化不了。我需要的不是"更全的信息",而是"更懂模型的信息"。REA 的核心价值,是把二进制分析变成一段段短小、清晰、可追问的结构化文本,这才是大模型真正擅长消费的形态。
2.3 数据流与消息循环:从字节流到模型结论
整个 REA 的数据流可以分成六步。第一步,用户给一个文件路径和一句自然语言问题。第二步,预处理模块读取文件头,完成格式识别、哈希记录、段信息汇总,这一步永远先跑,等于给后续所有操作定了坐标系。第三步,根据问题的类型,调度器决定调用反汇编、字符串提取、符号提取还是交叉引用查询。第四步,工具层的执行结果按照统一模板压缩成摘要,连同元信息(地址、大小、耗时)一起追加进对话上下文。第五步,模型基于上下文生成回复,如果它认为还需要更多信息,可以再次发起工具调用,REA 就重复第三、四步。第六步,模型判断任务完成,输出结论,REA 把完整调用链日志打印出来,供人复核。
这个流程本质上就是我们常说的 agent 循环,只不过在 REA 里,循环的每一步都被压缩得很紧。我建议把max_rounds控制在 10 次以内。理由很简单:超过 10 轮,模型很可能已经被上下文里的冗余工具结果带偏,或者陷入无意义的重复调用。宁可让它先给一个不完整的答案,人再来追问,也不要让它无限循环刷日志。这个"有限轮次 + 人做复核"的交互模型,是整个 REA 最重要的工作方式。
3. 核心模块分步实现
3.1 格式解析模块:先搞清楚文件"姓什么"
REA 的第一步永远是解析文件格式。我以 ELF 为例,因为 Linux 下的可执行文件、共享库基本上都是它。用pyelftools读取核心信息,代码量很少,但信息密度极高:
from elftools.elf.elffile import ELFFile def parse_elf(path): with open(path, 'rb') as fp: elf = ELFFile(fp) info = { 'class': elf.elfclass, # 32 还是 64 'little_endian': elf.little_endian, 'type': elf['e_type'], # EXEC / DYN / REL 'machine': elf['e_machine'], # x86_64 / ARM 等 'entry': hex(elf['e_entry']), # 入口点 'sections': [], } for sec in elf.iter_sections(): info['sections'].append({ 'name': sec.name, 'addr': hex(sec['sh_addr']), 'size': sec['sh_size'], 'type': sec['sh_type'], }) return info这段代码返回的内容对后续判断非常重要。elfclass决定反汇编器用 32 位还是 64 位模式;machine决定用哪个架构的指令集解析;entry给出程序入口地址;sections则相当于整个文件的"地图"。我还喜欢把每个 section 的地址、大小单独打印出来,因为模型后续回答"这个函数在 .text 的哪个位置"这类问题时,必须依赖这个坐标系。
这里要提一个我踩过的坑:一定要在读取段数据之前获取段对象,然后在需要时再调用sec.data()。千万不要在同一个循环里反复调用data()方法去倒腾大段数据,尤其是.rodata或.text很大的文件,性能会肉眼可见地下降。
3.2 反汇编引擎接入与函数边界识别
格式解析完成后,进入重头戏:反汇编。我使用capstone,它可以把一段字节流翻译成带地址、助记符和操作数的指令列表。这里给一个最精简的接入方式:
from capstone import Cs, CS_ARCH_X86, CS_MODE_64 md = Cs(CS_ARCH_X86, CS_MODE_64) md.detail = True def disasm_range(code, start_addr, count=200): return [ { 'addr': hex(i.address), 'mnemonic': i.mnemonic, 'op_str': i.op_str, 'bytes': i.bytes.hex(), 'size': i.size, } for i in md.disasm(code, start_addr, count) ]习惯上我会把bytes也放进去。虽然模型不需要每次都看字节,但在追问某个指令"为什么编码成这样"时,字节信息非常重要。人脑解析汇编偶尔会怀疑助记符来源,可以对照字节确认。小事,但真用得上。
函数边界识别,是初版 REA 里最实用的一个模块。大部分未混淆的编译产物,函数开头会有一段非常明显的 "prologue" 模式,x86_64 下最常见的是:
PROLOGUE_PATTERNS = [ (b'\x55\x48\x89\xe5', 'push rbp; mov rbp, rsp'), (b'\x48\x83\xec', 'sub rsp, imm'), (b'\x53\x48\x83\xec', 'push rbx; sub rsp, imm'), ] def detect_prologue(raw): for pat, desc in PROLOGUE_PATTERNS: if raw.startswith(pat): return desc return None我采用"线性扫描 + 边界标记":从头开始扫,命中 prologue 就标记一个函数起点,往后找到第一个ret指令(字节0xc3)标记为函数终点。这个方法对-O2 -fno-omit-frame-pointer编译出来的代码准确率还不错。当然遇到尾调用(函数末尾直接用jmp跳到别处)会漏判,但那是后续优化的问题,初版先把主干跑通最重要。
3.3 字符串与符号的语义提取
字符串提取是整个 REA 里成本最低、收益最高的模块。很多时候,一个函数在干什么,从它引用的字符串就能猜出七八分。基础实现甚至不用装库,直接用正则匹配可打印字节:
import re def extract_strings(raw, min_len=5): return re.findall(rb'[\x20-\x7e]{%d,}' % min_len, raw)但只提取不够,还要打分排序,否则模型会被一大堆库函数字符串带偏。我第一次就是没做过滤,把整个.rodata段倒给模型,结果它看到一个库前缀就断定项目在连数据库,实际上那只是静态链接进去的公共库代码。后来我加了权重打分:
def score_string(s): score = 0 if re.fullmatch(r'[A-Za-z_][A-Za-z0-9_ ]{3,}', s): score += 2 if re.search(r'(error|fail|success|config|path|host|key|res)', s, re.I): score += 3 if len(s) < 4: score -= 2 if s.startswith('.') or '/' in s: score += 1 return score规则很简单:长度太短的要降权,看起来像变量名或提示词的升权,包含路径、库名前缀的给一点权重但不让它压过业务字符串。排序以后取前 80 条给模型,效果立刻不一样。模型看到的是业务字符串,而不是几百条从公共库带进来的配置路径。这一步的经验是:在数据链路里加"信息过滤器",它的价值不亚于换更强的模型。
3.4 工具暴露给 AI 助手的正确姿势
工具模块怎么设计,决定了模型用得顺手不顺手。我给 REA 设计了一套轻量工具注册器,每个工具都包含三层元信息:名字、描述、参数模式。描述尤其重要,模型是通过描述来决定何时调用工具的,描述写得太模糊,它就会乱来。
TOOLS = {} def register(name, description, parameters, fn): TOOLS[name] = {'description': description, 'parameters': parameters, 'fn': fn} def call_tool(name, kwargs): if name not in TOOLS: return {'ok': False, 'error': f'unknown tool: {name}'} try: result = TOOLS[name]['fn'](**kwargs) return {'ok': True, 'result': result} except Exception as exc: return {'ok': False, 'error': str(exc)} register( name='disasm_function', description='反汇编指定地址开始的指令序列,用于查看函数内部逻辑。', parameters={'address': {'type': 'string', 'description': '十六进制起始地址,如 0x401130'}}, fn=disasm_function, )实际使用中,我发现参数描述比函数描述还重要。模型经常拿着"十进制字符串"来填地址,或者把一个很大的count值传进来,导致输出超长。应对方法有两个:一是在参数模式里明确写 "address 必须是十六进制字符串";二是在函数内部做参数校验,超出范围就裁剪而不是报错。工具函数要足够"宽容",模型才能少出错。
Agent 的循环逻辑本身并不复杂,核心就三件事:把用户目标转成可拆分的小动作,执行动作,把结果续进对话。伪代码可以理解为:
def run_agent(messages, max_rounds=10): for _ in range(max_rounds): resp = llm.chat(messages, tools=tools_schema()) if not resp.tool_calls: return resp.content messages.append(resp.message) for call in resp.tool_calls: output = call_tool(call.name, call.arguments) messages.append({'role': 'tool', 'tool_call_id': call.id, 'content': compact(output)}) return 'max rounds exceeded, 请人工介入'这个循环看起来很普通,但有两个细节我花了不少时间调试。第一,compact函数必不可少,工具输出必须压缩成摘要,否则反复调用几个工具后上下文就爆了。第二,max_rounds必须设上限,我见过模型在没有上限时反复查询同一个函数、来回试同一个错误地址,空转三十轮。设了上限以后,它反而会更谨慎地只在确定需要时才调用工具。
4. 完整实操:从环境搭建到第一次逆向对话
4.1 环境准备与测试样本
REA 的运行环境其实很轻,只要有 Python 3.9 以上、pip 能联网就行。我习惯先建一个独立虚拟环境,避免污染系统 Python:
python -m venv rea_env source rea_env/bin/activate # Windows 使用 rea_env\Scripts\activate pip install capstone pyelftoolsunicorn先不装,等需要动态执行时再加。接下来准备测试样本。我用 gcc 编译一个小工具,既能验证 REA 的格式解析,也能验证函数边界识别:
// sample.c #include <stdio.h> #include <stdlib.h> #include <string.h> static int process_secret(const char *key, int value) { if (strlen(key) < 3) { return -1; } return value * 7 + key[1] * 3; } int main(int argc, char **argv) { int v = (argc > 1) ? atoi(argv[1]) : 10; printf("res=%d\n", process_secret("abc", v)); return 0; }编译命令:
gcc -O2 -fno-omit-frame-pointer -o sample sample.c为什么要加-fno-omit-frame-pointer?因为在 O2 优化下,编译器通常会省略栈帧指针,函数边界不那么规整,我用 prologue 模式的准确率会下降。第一版教程先不挑战自己,用保留栈帧指针的版本,把主流程跑通,后面再升级边界识别算法。
4.2 组装 REA 主流程
我建议把 REA 做成一个命令行工具,入口函数接收文件路径和用户问题,输出一段分析摘要。下面是一个可运行的精简版主流程:
# rea_core.py import re from elftools.elf.elffile import ELFFile from capstone import Cs, CS_ARCH_X86, CS_MODE_64 PROLOGUE_PATTERNS = [ (b'\x55\x48\x89\xe5', 'push rbp; mov rbp, rsp'), (b'\x48\x83\xec', 'sub rsp, imm'), ] class ReaAnalyzer: def __init__(self, path): self.path = path self._fp = open(path, 'rb') self.elf = ELFFile(self._fp) def summary(self): return { 'class': self.elf.elfclass, 'machine': self.elf['e_machine'], 'entry': hex(self.elf['e_entry']), 'sections': [{'name': s.name, 'addr': hex(s['sh_addr']), 'size': s['sh_size']} for s in self.elf.iter_sections()], } def get_section(self, name): for s in self.elf.iter_sections(): if s.name == name: return s return None def disasm_section(self, name='.text', max_insns=300): sec = self.get_section(name) if not sec or sec['sh_type'] == 'SHT_NOBITS': return [] md = Cs(CS_ARCH_X86, CS_MODE_64) md.detail = True code = sec.data() start = sec['sh_addr'] return [ {'addr': hex(i.address), 'mnemonic': i.mnemonic, 'op_str': i.op_str} for i in md.disasm(code, start) ][:max_insns] def strings(self, min_len=5, top=80): raw = b'' for s in self.elf.iter_sections(): if s['sh_type'] != 'SHT_NOBITS': raw += s.data() found = re.findall(rb'[\x20-\x7e]{%d,}' % min_len, raw) scored = [(score_string(x.decode(errors='ignore')), x.decode(errors='ignore')) for x in found] scored.sort(key=lambda t: t[0], reverse=True) return [s for _, s in scored[:top]] def functions(self): sec = self.get_section('.text') if not sec: return [] code = sec.data() funcs, i = [], 0 while i < len(code): desc = detect_prologue(code[i:i+8]) if not desc: i += 1 continue j = i while j < len(code): if code[j] == 0xc3: funcs.append({ 'start': hex(sec['sh_addr'] + i), 'end': hex(sec['sh_addr'] + j + 1), 'prologue': desc, }) i = j + 1 break j += 1 return funcs这个类基本涵盖了前三个模块:格式解析、反汇编、函数边界识别。拿到对象后,我可以在一段交互脚本里组合使用:
a = ReaAnalyzer('sample') print(a.summary()) for fn in a.functions(): print(fn)执行后你会看到类似下面的输出:
{'class': 64, 'machine': 'EM_X86_64', 'entry': '0x401050', ...} {'start': '0x401110', 'end': '0x401129', 'prologue': 'push rbp; mov rbp, rsp'} {'start': '0x401130', 'end': '0x40114e', 'prologue': 'sub rsp, imm'}这时候 REA 的工具层已经齐了,剩下就是把它们和 AI 助手的对话拼接起来。
4.3 与 AI 助手的"对话实录"
工具接好之后,真正让我兴奋的是第一次"人机协作式逆向"成功时的体验。我把问题写得很直接:"请分析 sample 里 process_secret 函数的大致行为,并给出你的判断依据。"REA 是这样运作的:先给出文件信息摘要,然后它建议调用函数列表拿到候选函数,发现 0x401130 处的函数 prologue 是sub rsp, imm,于是调用反汇编工具查看指令序列:
0x401130 movzx eax, byte ptr [rdi+1] 0x401134 imul eax, eax, 3 0x401137 cmp rdi, 2 0x40113b jbe 0x401148 ... 0x401143 lea eax, [rdi+rcx*8] 0x401147 add eax, ecx 0x401149 ret模型结合字符串列表里的res=%d和入口附近调用点,给出了合理推断:这个函数先检查字符串长度,如果太短返回 -1;否则用密钥第二个字符乘 3,再加上入参乘 7,得到最终返回值。我用实际执行验证了一下,结论是对的。这个过程中最有价值的部分不是最终答案,而是模型能够引用真实地址和真实指令——它在每一个判断点都有据可查,而不是像之前那样凭空猜。
我还建议给模型立一条规矩:每次给出结论后,必须附一行"依据",注明自己看了哪些地址的指令。这条规矩让输出质量提升明显。以前它会说"该函数可能在进行某种变换",现在它会说"根据 0x401134 处的 imul 指令和 0x401137 处的长度校验,可以确认..."。有依据的回复,才是可复核、可信任的回复。
4.4 进阶玩法:自动生成函数调用关系
跑通基础流程以后,你可以再进一步,让 REA 帮你画一张轻量的函数调用图。原理很简单:在反汇编指令流里找call指令,取出目标地址,记录"当前函数 → 目标函数"的边。Capstone 的op_str在 x86_64 下通常直接就是目标十六进制地址,例如call 0x401130。
我用一个约 20 行的函数就能完成这项工作:
def call_edges(code, start_addr): md = Cs(CS_ARCH_X86, CS_MODE_64) edges = [] for insn in md.disasm(code, start_addr): if insn.mnemonic == 'call': try: target = int(insn.op_str, 16) except ValueError: continue edges.append((hex(insn.address), hex(target))) return edges注意处理间接调用,比如call rax、call [rip+xxx],这类目标在静态阶段算不出来,我直接跳过,不产生边。拿到边集合后,把它们整理成邻接表放进上下文中,模型就能回答"哪个函数调用了 process_secret"这类问题。从我的实践来看,调用图对理解大工程的整体结构,帮助比逐个函数分析大得多。
5. 常见问题与避坑记录
5.1 五大典型问题速查表
把这段时间我和使用者共同踩过的坑整理成一张表,方便你对照排查:
| 现象 | 根本原因 | 解决方案 |
|---|---|---|
| 模型给出不存在的地址 | 上下文没有地址坐标系,模型靠猜 | 每条指令都带 addr 字段,工具描述里强制十六进制格式 |
| 结论被公共库字符串带偏 | 字符串提取没有打分过滤 | 用 score_string 打分,只保留 topN |
| 反汇编出一堆乱码数据 | 线性扫描把数据段当代码段 | 先按 prologue+ret 切函数,再按函数逐段反汇编 |
| 模型反复调用同一工具 | 缺乏轮次上限,上下文太长导致"迷失" | 设置 max_rounds=10,工具输出要 compact 摘要 |
| 工具输出太长刷爆上下文 | 没有限制单次返回条数 | 反汇编默认 max_insns=200,字符串默认 top=80 |
这五条几乎覆盖了我初版 REA 的大部分调试工作。每一条背后都是真实翻车记录,尤其是第一条,我在分析一个较大的.text段时,模型连续三次引用了一个并不存在的地址,编译出来的分析报告错误百出。加了地址坐标系之后,这种情况基本消失。
5.2 排查思路与调试技巧
如果 REA 输出异常,我的排查顺序是固定的。先看格式解析结果是不是对的,ELF 头都读不对,后面全是空中楼阁。再看工具层能不能单独跑通,直接在 Python 命令行调用disasm_section、strings,确认它们返回的内容完整。然后看模型拿到的上下文摘要里有没有被截断或者被过滤掉关键信息,这一步我通常打印最终提示词,一眼就能看出问题。最后才考虑模型本身的问题,比如是不是指令模式描述不清楚。
我强烈建议你实现一个"打印最终上下文"的开关,这在调试 agent 类项目时几乎是必备功能。你只需要在调用大模型 API 之前加一行日志,把拼接好的消息列表打印出来。许多"模型乱答"的问题,根源都在上下文里的信息是错误的或者缺失的,看到最终上下文,你往往不需要再做任何额外推断,直接就能定位问题。
5.3 上下文与性能优化
二进制分析最容易踩的性能坑,就是试图把整个.text段一次性反汇编。几十万条指令,无论是时间还是上下文都是灾难。我的优化策略是增量式分析。先给模型一个函数列表和每个函数的指令数概览,让它选择最值得关注的几个函数,REA 再针对这些函数单独反汇编。这样单次工具调用的数据量始终控制在 200 条指令以内,耗时和上下文占用都稳定。
另一个实用做法是结果缓存。同一个二进制文件我可能反复对话很多次,每次重新反汇编会浪费时间。我一般以文件哈希加参数作为 key,把工具结果落盘缓存;如果文件本身没变,第二次直接读缓存,速度提升非常明显。
还有一个小技巧:指令统计法。在真正进入细节之前,先把函数内指令按类别做汇总,比如mov出现了多少次、call多少次、xor多少次。模型看这个统计其实比看原始指令更快建立印象。判断一个函数是不是跟加密相关,往往不是看某条指令,而是看xor和移位类指令的占比有没有异常高。
6. 我在实际落地中的体会
最后聊点没写进代码、但对使用效果影响最大的体会。REA 这个项目从想法到跑通,最关键的转折点不是某个算法想明白了,而是我接受了一个事实:它不会替我做逆向分析,它只负责帮我快速读完不懂的东西。以前的流程是我自己一行行翻汇编,现在的流程是 REA 把汇编整理好,我只需要对模型给出的假设做验证。单位时间内能读的函数数量至少翻了五倍,这个效率提升才是它真正的价值。
另外,我还打算把 REA 继续扩展下去。下一步计划加入基于指令概率的启发式,自动标注像 memcpy 模式、strlen 模式这类常见函数模板;再往后想把它接进 CI,每次发版自动生成"本次二进制行为变更说明"。到那时候,REA 就不只是辅助我逆向的老工具,而是一个带记忆的长期协作伙伴了。如果你也在维护没有源码的老系统,或者经常对着陌生二进制发愁,建议可以先把这篇文章里的精简版跑起来,等你跑通一次完整对话,就会知道这套方案的威力在哪里。