最近两年我跟不少尝试把“逆向分析”塞进 AI 工作流的开发者聊过,大家的体感出奇一致:AI 编程助手改代码、补单元测试、解释开源项目的时候很强,可一旦丢给它一个没有源码、没有符号表、只剩机器码的二进制文件,它就很容易开始“编”。你问它某个函数做了什么,它能面不改色地给你画一份漂亮的伪代码;等你拿真实反汇编去核对,发现从偏移量到调用关系全是错的。这不是模型不聪明,而是我们只给模型喂了“字节”,却没给它分析二进制的“手和眼”。我这个 REA 上手教程要解决的就是这个问题:把可自动化执行的逆向工具串成一套结构化上下文,交给 AI 编程助手消费,让它从“猜”变成“查”,从“编”变成“标证据”。
这套方法不是要重建一个图形化反编译软件,而是补上模型和二进制之间的那层黏合层。下面这六千多字,我会从 REA 的定位、环境搭建、第一个无符号表分析任务、交叉引用用法,到我自己踩过的坑依次展开。适合三类人看:被老闭源程序兼容性折磨的维护者、准备入门 CTF 逆向题但不知道从哪下手的同学,以及想给本地 AI 编程助手扩展能力的工具党。
1. 让 AI 直接看二进制,等于让它去“盲人摸象”
先讲清楚 REA 要解决的问题。很多朋友第一次把二进制文件拖给 AI 编程助手,Agent 会表现得非常自信,但它的自信来源于“语言模型的世界知识”——它见过大量“类似看起来”的代码片段,于是把见过的模式套上去。这在开源代码上很有效,因为源码本身就是语言模型的训练语料;但对于一个被剥离了符号表的 ELF、固件包或老式.so文件,模型根本没有稳定的事实锚点。
1.1 通用编程 Agent 的三个软肋
我总结下来,问题主要集中在三处。
第一,模型把二进制当普通文本看。你给它\x7fELF...的原始字节流,它既不能执行,也不能反汇编,只能根据文件头猜架构、猜入口点,再往下就全是臆测。给它一段十六进制它还能“读”出一些字符串,可用混凝土无法真正观察到有效指令。
第二,代码与数据的界限完全丢失。源码世界里,变量和函数分得清清楚楚;但在二进制里,同一个偏移处的字节既可能是指令,也可能是常量表,还可能是对齐填充。没有工具辅助,模型根本分不清,于是它把数据段里的一长串数值当反汇编,很快就会输出一堆看起来像指令、实际上谁也不会去执行的“死代码”。你可以把这个问题想成:一个没去过厨房的人,看到案板上的东西,只顾照着菜谱瞎念,却不知道哪些能下锅。
第三,缺少交叉验证手段。人做逆向时,最常用的一个动作就是“看到某个地址,跳过去看谁引用了它”,也就是 XREF(交叉引用)。模型没有 Agent 工具时,它只能靠记忆去“猜”引用关系;一旦猜错,后面的结论全崩。
1.2 REA 的设计思路:补一块“分析编排层”
我这套 REA 工作流的思路其实非常简单,一句话就能概括:把二进制分析工具封装成 AI 编程助手能主动调用的“技能层”。REA 全称 REverse-engineering Assistant,它本身不负责“思考”,只负责把底层能力整理成干净的结构化输出。
具体来说,REA 处理一个二进制文件时,会先完成四件事:
- 解析文件头,识别架构、位数、入口点、各段区段。
- 提取字符串和常量,并标记它们在文件中的绝对地址。
- 对可执行段做反汇编,初步划分函数边界。
- 收集基本的交叉引用关系,比如哪个地址被 call,哪个地址被跳转。
关键点在于:REA 不会把几十 MB 的反汇编全部丢给模型,而是按需裁剪。Agent 问“0x4010a0 这个函数做了什么”,REA 就给这个小函数的指令、它引用的字符串、它的 caller 列表。这就好比给模型配了一个随叫随到的“二进制助手”,模型需要什么线索,就自己去取。
1.3 一次调用背后发生的三件事
为了让这套流程可复用,我把 REA 封装成一个命令行工具,任何支持工具调用(function calling)的编程 Agent 都能把它接进来。一次典型调用,服务端会经历三个内部步骤。
第一步是探测。REA 对输入文件做快速判断:是 PE、ELF,还是裸二进制?架构是 x86、ARM 还是 RISC-V?有没有明显的加壳特征?这些信息会在一个非常轻量的 JSON 里返回。
第二步是提取。根据 Agent 的查询参数,REA 决定做字符串提取、反汇编、引用查询还是熵分析。每个结果都要带着地址信息,这是后来防止模型“地址幻觉”的关键。
第三步是裁剪组装。REA 把结果压缩成模型友好的上下文块,例如只保留当前函数附近 200 行指令、把无关字符串过滤掉、给每个调用点加上目标地址。这样模型拿到的不是大海捞针,而是围绕问题的一小簇证据。
我实际用下来的体验是:给 AI 编程助手配上 REA 后,它面对二进制文件时的表现,从“读天书”变成了“有根据地说”,虽然它仍然做不到像资深逆向工程师那样灵活,但至少每一步推论都能追溯到具体地址了。
2. 搭建 REA:让编程助手长出手和眼
说清楚原理,现在进入实际搭建。我的配置环境不挑具体产品,所有代理、编程助手、本地大模型都适用。毕竟 REA 只是一层工具,前提是你用的编程助手支持自定义工具调用。
2.1 环境准备:核心依赖最小清单
我在某台 Linux 机器上搭 REA 时,依赖列表非常简单:
- Python 3.10+,用于跑 REA 主脚本。
- 一个支持多架构的反汇编库,REA 用它的 API 做指令解码。
- 一个能解析 ELF/PE 文件头的读取库,或者直接调用系统自带的二进制查看工具。
- 可选的符号执行引擎,用于后面做更复杂的路径分析(新手可以先不装)。
这套依赖并不是缺一不可。如果只分析 x86 架构下的 ELF,连反汇编库都可以省,直接用系统自带的工具导出文本再清洗。但为了后面接 ARM 固件和裸二进制,我还是建议把反汇编库装上,REA 的auto_arch配置会帮你自动选择。
2.2 配置文件:第一次配置重点放在边界控制
REA 的配置本质是控制“给模型多少信息量”。我踩过的第一个坑就是信息给太多,模型反被垃圾指令淹没。因此配置里最关键的字段都是边界类参数:
rea: auto_arch: true max_insn_per_query: 400 max_func_bytes: 4096 xref_depth: 2 string_min_len: 5 include_raw_bytes: false allow_partial_analysis: true我把每个字段的意图说清楚。max_insn_per_query是单次反汇编返回的最大指令数;max_func_bytes限制一个函数最多解析多大尺寸,避免把整个大函数一次塞爆上下文;xref_depth控制交叉引用追踪几层,默认 2 层就足够大多数分析;include_raw_bytes建议先设为 false,因为原始字节对模型判断功能用处不大,反而占 token;allow_partial_analysis允许超长函数只做部分分析,不会因为一个超大函数就让整个查询失败。
2.3 准备一个最小样例:一个被 -s 剥离的 ELF
接着我用一个极小的 C 程序测试流程。先用编译器生成一个没有符号表的可执行文件:
cat > demo.c <<'EOF' #include <stdio.h> int helper(int x) { return x * 2 + 1; } int main() { printf("demo rea: %d\n", helper(42)); return 0; } EOF gcc -O2 -s -o demo demo.c注意-s参数会干掉所有符号表,这是逆向分析最常遇到的情况。文件只有一二十 KB,但里头再找不到main、helper这样的名字。你问 AI 助手这个函数在哪、做什么,它没有 REA 之前只能瞎猜,而 REA 可以先给出一个文件整体画像:
python3 rea_cli.py inspect --input ./demo --format json输出会是类似这样的结构(我做了简化排版):
{ "file": "./demo", "format": "elf64-x86-64", "entry": "0x401020", "sections": [ {"name": ".text", "addr": "0x401000", "size": "0x180", "executable": true}, {"name": ".rodata", "addr": "0x402000", "size": "0x30", "executable": false} ], "strings": [ {"addr": "0x402004", "value": "demo rea: %d\n", "length": 15} ], "functions": [ {"start": "0x401040", "size": "0x30", "insn_count": 12}, {"start": "0x401080", "size": "0x60", "insn_count": 24} ] }这一刻,AI 编程助手终于拿到了一份“有地址标识的地图”。它不是看到一个黑盒文件,而是知道入口在哪、代码段在哪、字符串在哪、哪些位置可能是函数。这听起来简单,但实际帮模型省下了大量无效猜测。
3. 第一次完整分析:从字符串猜测到伪代码生成
配置好 REA 之后,我强烈建议你不要一上来就让 AI 编程助手直接读反汇编,而是按一套固定顺序走。这套顺序是我在实际使用中摸索出来的,能有效减少幻觉。
3.1 第一步:先看字符串,给模型建立“功能预期”
面对一个没有符号表的目标,我通常先下rea strings查询。为什么要先看字符串?因为字符串是二进制固件里的“人类注释”。比如上面那个 demo 文件,模型看到字符串demo rea: %d\n,就会合理猜测这个程序会打印一条带整数的消息。
但在更复杂的二进制里,字符串还能告诉你更多隐藏信息。我在某个闭源演示程序里分析时,仅靠字符串就发现它有usage: <input file> <output file>,立刻锁定这是一个命令行转换工具;又看到一个config key not found,进而判断它有配置读取流程。“字符串先行”的本质,是先用人类可读信息给模型划定功能蓝图,再让模型带着预期去找实现,比直接从指令猜逻辑准确得多。
3.2 第二步:让 REA 自动划分函数边界
函数边界是后面所有分析的基础。REA 在划分函数时用了两种策略的混合:线性扫描和递归下降。
线性扫描很好理解,就是从段首开始一条条解码指令,遇到结束指令就切一刀。缺点很明显:数据夹杂在代码里时容易把常量当成指令,造成函数边界偏移。递归下降则会提前把分支指令的目标地址加入待分析队列,优先解码目标地址附近区域,更像人做逆向时的思路,但遇到没有显式 call 的间接跳转时会漏掉路径。
REA 默认两种模式都跑,再做交叉对齐,最后给每个候选函数一个置信度分数。我在实战中会重点关注置信度 0.8 以上的函数,置信度低的先丢给模型做辅助推测,但会明确标注“该函数边界未经 XREF 验证”。
3.3 第三步:把“分析素材表”喂给 AI 编程助手
这一步是 REA 的灵魂。它不会一次性丢 10000 条指令给你,而是把每个函数浓缩成一个素材块。比如我对 demo 文件查询rea query --func 0x401040 --with-calls,REA 给出:
{ "function": { "start": "0x401040", "size": "36, "callers": ["0x4010a0"], "callees": [] }, "disasm": [ {"addr": "0x401040", "mnemonic": "mov", "operands": "edi, edi"}, {"addr": "0x401042", "mnemonic": "imul", "operands": "edi, edi, 2"}, {"addr": "0x401045", "mnemonic": "add", "operands": "eax, 1"}, {"addr": "0x401048", "mnemonic": "ret"} ] }当模型拿到这份素材时,它的推理就不是从零开始了。它看到imul edi, edi, 2和add eax, 1,能非常有把握地说“这个函数把入参乘以 2 再加 1”,然后结合caller=main 所在地址,顺理成章地把它命名为helper。我后来读了模型生成的伪代码:
int helper(int x) { return x * 2 + 1; }这一次不是幻觉,因为每一行都对应了真实地址上的真实机器指令。给模型合适素材后,它做的是“编译翻译”,而不是“凭空编造”,这也正是 REA 最核心的价值。
4. 交叉引用与调用图:让模型学会“按图索骥”
初学者最容易问一个问题:让 AI 编程助手看反汇编不就行了?反汇编确实能告诉它“指令长什么样”,但真正的难点是“指令之间有什么关系”。
4.1 没有源码时,引用关系就是骨架
在源码世界里,代码的骨架是函数定义、函数调用、变量引用;在二进制世界里,对应物就是交叉引用(XREF)。REA 把 XREF 分成两类:代码引用和数据引用。代码引用包括call、jmp、条件跳转;数据引用则是一个指令地址直接引用了某个数据段地址,比如加载了一个字符串地址。
我一般在分析中让 Agent 优先追踪三类引用:入口点能到达哪些函数、某个函数调用或跳转去哪些目标、某个字符串地址被哪些函数引用。这套逻辑很像“谁在什么地方用了这个变量”,可以快速拼出程序的数据流。
4.2 一个地址引用链的实际解读过程
用前面 demo 程序来说,如果我忽略字符串信息,只把 main 函数附近的反汇编交给模型,它会看到一堆指令但不知道在干嘛。而 REA 提供rea xref --string 0x402004后,返回:
{ "string_addr": "0x402004", "refs": [ {"from": "0x401090", "type": "lea", "comment": "load address of string"} ] }模型立刻明白:0x402004 这个demo rea: %d\n被 0x401090 处的指令引用了,所以 0x401090 所在函数就是负责打印的地方。再结合 main 地址附近对 helper 的调用,整个程序结构在模型眼中就变成了清晰的调用图:入口点 → main → helper,以及 main → printf。
这个过程和人做逆向时的思路完全一致:先找字符串,再看谁引用字符串,再看引用者又被谁调用。没有 REA 之前,让模型自己设计一套 XREF 查询逻辑并不现实;有 REA 之后,它只要按格式查询即可。
4.3 让 Agent 用“可证伪”的方式报告结论
我在与 AI 编程助手配合时,收获最大的一条经验是:要求模型给每一个结论都挂上证据地址,否则它的结论就可能滑向幻觉。因此我在 REA 后面加了一句很简单的系统指令:
每个结论必须引用至少一个 REA 查询得到的地址,格式为
[addr:0x401045];拿不出地址的结论一律标记为“推测”。
这个策略很笨但极好用。它逼着模型在做伪代码还原时,把每一条 C 语句对应到真实机器指令地址。比如模型写:
// 0x401040 - 0x401048 int helper(int x) { // 0x401042 imul: x = x * 2 // 0x401045 add: ret = x + 1 return x * 2 + 1; }读者也好、我也好,都能直接拿去反汇编器里核对。出了偏差,能立刻定位是模型翻译错,还是 REA 函数边界划分错。这样的过程才真正让 AI 从“帮你猜”变成了“帮你查”。
4.4 静态结论不够用时,加一个动态确认环节
有些函数逻辑只看静态反汇编会非常绕:编译器优化会让寄存器反复横跳,或者函数内部又有内联展开。这时候我建议 REA 接一个调试后端,让 Agent 可以设置临时断点、读取执行时的寄存器值和内存值。
不过这个环节对新手建议晚一点再碰。先练熟静态 XREF 分析,再上动态确认,否则错误安装调试会话会搞得很狼狈。我的习惯是:静态分析已经能把流程猜出七成左右时,才用调试器验证剩下的三成。
5. 我踩过的几个坑:指望 REA 输出“绝对正确”是行不通的
任何工具都不是银弹。REA 用久了之后,我发现有几类典型“生物性”的坑,就算 REA 在场也依然会出现。这里一条条摆出来,给后来者省点时间。
5.1 数据段被误当成指令段:最经典的边界幻觉
第一次用 REA 跑一个稍大的二进制时,我在函数列表里看到一个非常规整的“函数”,起始地址在.rodata段内。REA 的递归下降分析把字符串区域里的某些字节组合解码成了看起来合理的指令,比如0x402014: add byte ptr [rax], al这种垃圾代码。如果你不仔细核对段属性,模型拿到这函数规划理由就废了。
后来我专门在 REA 的输出模块里加了一个字段section: "rodata",凡是落在非执行段的“函数”,置信度直接降权。这也提醒所有使用者:函数列表的每个候选,都要看它在哪个段、有没有实际 call 或 jmp 指向它。
5.2 地址幻觉:模型把自己编的地址当事实
在没有 REA 强制地址引用之前,我遇到过模型写出一份完整的分析报告,里头充满了0x7f00...这种明显不是目标文件里的高地址。它从记忆里抓取了一个“常见堆地址”塞进了报告,用来证明自己的结论。这就是典型的地址幻觉。
解决方式就是我前面说的那套“必须挂证据地址”的指令格式,并且在 REA 查询里加了verify_addr接口。模型给出可疑地址后,REA 会去查这个地址是否真在可执行段内、是否真的对应一条指令。两步之后,这类地址幻觉基本绝迹。
5.3 编译优化后寄存器搬运太多,模型容易“绕晕”
编译器在-O2级别下会产生大量寄存器之间的mov和不必要的栈操作。模型看多了这种指令,经常误以为某个变量被移动了三次,实际上编译器只是做了寄存器重命名。我总结的方法是:让 REA 过滤掉纯mov、lea等搬运类指令,在输出里增加一层“简化指令流”,只保留影响内存访问或明显运算语义的指令。
当然,这层简化也有风险,比如丢失边界条件。所以 REA 的完整反汇编结果会保留,简化流只是给模型的“第一眼”,模型需要深入细节时再点开完整指令。
5.4 大而复杂的函数:一次返回 400 条指令反而伤输出质量
REA 的max_insn_per_query: 400是一个经验值。如果一个函数大小超过这个限制,我宁可让它分两段查询,也不一次性全部塞给模型。实测下来,模型在上下文尾部的注意力会减弱,指令长度一长,它对前 100 条的细节记忆明显下降。
更好的做法是让它先看函数头几条、尾部几条,再看关键跳转点;需要中间细节时,REA 支持按地址区间range查询。这样既节省 token,也不容易漏掉关键分支。
5.5 提示词写得太“鼓励创作”会得到一堆散文
我还试过一种“充分鼓励”的提示词风格:让模型写出详细逆向思路、开放假设、梳理多种可能性。结果它把劣势的空白全填上了,一整页充满流畅但无法核对的伪代码。专业一点的做法是明确设置格式:先给结论,再列表格展示每条指令对应的伪码,最后只留一行“不确定项”。REA 的硬地址引用当然重要,但你不要求,它就不会主动做。
6. 这套工作流还能往哪发力
把 REA 接到 AI 编程助手上之后,我的使用范围已经远远超出最初的“给 Agent 看懂 ELF”这个小目标。实际上,只要底层二进制读取能力合格,它的分析思路可以套到多个场景里。
6.1 嵌入式固件和旧程序兼容性分析
分析老式软件、路由器备份固件、或者厂家停止维护的驱动文件时,经常只有固件没有源码,还得兼容它。REA 可以先把固件按架构识别、字符串提取、函数划分三件套跑一遍,再让 AI 编程助手针对某个中断向量查引用。如果底层反汇编库支持 ARM 和 Thumb 混合指令,整套流程同样能跑通,关键是把架构识别配置成auto_arch: true,免得手动切模式切出病。
6.2 CTF 逆向题里的“刷题笔记”
玩 CTF 逆向题的话,REA 还有一个很舒服的用法:把每道 bin 题的字符串、重点函数、反编译结果自动整理成一份结构化笔记。以前每道题我还要打开图形界面反复浏览,现在直接让 Agent 拉取材料生成 map,然后按步骤解答。它不能替代你自己做“弯弯绕绕的趣味逻辑”,但能帮你省下大量“这函数是做什么的”的基础排查时间。
6.3 和调试后端结合,形成“静态定范围、动态验结果”的闭环
我在后面的实际工作中已经给 REA 加了动态确认能力:REA 先返回候选函数和关键地址,调试后端在候选地址上下断点,Agent 读一次寄存器和内存,再与静态分析结果做对比。这套流程非常适合定位一些只在运行时才暴露的逻辑问题。不过我依然建议先把静态流程走熟,再上动态,因为一次错误断点往往比十次错误反汇编更让新人抓狂。
6.4 我这段时间的真实体会
用 REA 这几个月,我最明显的感觉是:AI 编程助手在二进制面前的“智商门槛”消失了,但“基本盘能力”并没有凭空变强。它该读不懂的复杂混淆还是读不懂,该被编译器优化绕晕时还会绕晕。REA 做的是让 AI 每一步都有据可查,把它的幻觉率压到可以接受的范围。对于一个需要处理没有源码的程序的人来说,这已经是质的提升。
如果你正准备入坑,我的建议很简单:先拿一个你自己编译、自己剥离符号表的小程序练手,一步步看 REA 输出的字符串表、函数列表、引用关系,再对照系统自带的二进制查看工具去验证。跑成这个闭环,你再把真实工作里的老闭源程序拿给它,心里就有底了。不要一上来就分析最复杂的包,也别让模型替你下结论。二进制分析这件事,真正值钱的能力永远是“判断证据靠不靠谱”的能力,AI 只是把你从大量重复检索里解放出来的人手。希望这套 REA 工作流能给你同样的感觉。