Ghidra 无头 MCP 进阶:用 REA 分析剥离符号和非常见架构的二进制
本页目录
REA + Ghidra 入门篇用的是一个带符号的小程序,Ghidra 默认设置就能处理好。真实目标往往没这么配合:函数名被 strip 掉了;文件头给出的语言不对;有些代码只能通过指针表到达;agent 的第一个问题就超时;JVM 堆内存不够用。REA(morluto 维护的开源项目)为这些情况提供了一组设置,本文讲清楚每个设置管什么、什么时候该用。
本文假设 rea doctor --provider ghidra --json 已经通过,你也跑过基本流程。读完你会掌握:
- REA 的 Ghidra 会话是怎么启动的,每个设置作用在哪个阶段;
- 剥离符号二进制的分析方法,包括让命名跨会话保留;
- 语言 / 编译器规格覆盖和分析种子(seed);
- 6.4.0 provider 接受哪些架构,包括有边界的 MIPS 通道;
- 启动超时、堆内存设置和快照复用。
本文所有内容都对照 npm 6.4.0 发布包(rea-agents)核实过。REA 仓库 main 分支的文档里已经有一些 6.4.0 不支持的设置,文中会单独标出,免得你设了一个装好的版本根本不读的变量。
前置条件
- REA 6.4.0,配好 Ghidra 12.1.x 和 64 位完整 JDK 21+,配置方法见入门篇。
- Linux 或 macOS。Windows x64 只有实验性的只读 P0 支持(见 Windows EXE/DLL 篇),下文的重命名和 no-return 修复在 Windows 上都不可用。
- C 编译器。MIPS 一节还需要 MIPS 交叉工具链,例如
mips-linux-gnu-gcc。 - 你有权分析的目标。本文全部用自己编译的程序。
Ghidra 会话的启动过程
下面每个设置都作用在会话生命周期的某个阶段:
- 解析分析 profile。 启动任何东西之前,REA 先为目标确定一个分析 profile:格式、架构、语言选择、种子摘要等。每条 Evidence 都会记录这个 profile,每个快照也以它为键。
- 导入。 REA 用
-readOnly和-deleteProject启动AnalyzeHeadless,导入到一个私有的临时项目。在 Linux 和 macOS 上,REA 按 Ghidra 自带的 LaunchSupport 配置直接启动 JVM。 - 分析前脚本。 应用种子等准备工作。
- 默认自动分析。 自动分析完成之前,REA 不返回任何结果,也不暴露部分结果。
- 桥接就绪。 REA 打包的 Java bridge 连上之后,查询才开始执行。
在 agent 里,open_binary 只是选定目标。第一个走 Ghidra 的查询才会触发第 2 到第 5 阶段,所以第一个问题慢,后面的快。
6.4.0 实际读取的设置如下,最后一列是它在发布包 dist/ 里的定义位置:
| 设置 | 阶段 | 6.4.0 dist/ 中的出处 |
|---|---|---|
GHIDRA_INSTALL_DIR、JAVA_HOME |
全部 | config/environment.js |
REA_ANALYSIS_PROVIDER |
选择 provider | config/environment.js |
REA_GHIDRA_LANGUAGE_ID、REA_GHIDRA_COMPILER_SPEC_ID |
1–2 | config/environment.js、config/ghidraLanguageOverride.js |
REA_GHIDRA_SEED_FILE |
1、3 | config/environment.js、ghidra/GhidraAnalysisSeeds.js |
REA_GHIDRA_STARTUP_TIMEOUT_MS(默认 330000) |
2–5 | config/ghidraStartupTimeout.js |
GHIDRA_HEADLESS_MAXMEM,其次 GHIDRA_MAXMEM(默认 2G) |
2 | ghidra/GhidraJavaLaunch.js |
这些变量都必须出现在运行 REA 的那个进程的环境里。对 agent 来说,就是 REA MCP server 条目里的 env,而不是你的交互式 shell。改了任何一项,都要重新跑 rea setup 或手动改条目,然后重启客户端。
第 1 步:编译一个剥离符号的目标
沿用入门篇的 vault.c,编译两个版本:
cc -O0 -g -o vault-debug vault.c
cc -O2 -s -o vault-stripped vault.c
BIN="$PWD/vault-stripped"
debug 版是你的参考答案。加了 -O2 -s 之后,mix_byte 通常会被内联,符号表也没了,Ghidra 会按地址给函数命名(例如 FUN_00101189)。
第 2 步:没有函数名时怎么找路
分析剥离符号的二进制,要从数据入手,而不是从名字入手。字符串不会被 strip 掉,顺着引用就能回到代码:
rea search "$BIN" "Wrong passphrase" --provider ghidra --json
rea xrefs "$BIN" 0xSTRING_ADDRESS --provider ghidra --json
rea function "$BIN" 0xFUNCTION_ADDRESS --provider ghidra --json
把占位符换成上一步返回的地址。地址一律带 0x 前缀:在 Ghidra 上,有这个前缀才会按地址解析,不带前缀的字符串会先被当成符号名。地址要原样使用 REA 返回的值。对于位置无关(PIE)的 ELF,Ghidra 的映像基址可能和 objdump 显示的偏移不一样。
找到目标函数之后,6.4.0 的几个精细命令可以回答更具体的问题,不用每次都整体反编译:
# One instruction with decoded operand facts
rea inspect-native-instruction "$BIN" 0xINSN_ADDRESS --provider ghidra --json
# What can this call site actually reach?
rea resolve-native-call-targets "$BIN" 0xCALL_ADDRESS --provider ghidra --json
# A recovered type, or the typed data at an address
rea inspect-native-data-type "$BIN" --address 0xDATA_ADDRESS --provider ghidra --json
# Bounded static def-use across resolved calls
rea trace-native-values "$BIN" 0xFUNCTION_ADDRESS --provider ghidra \
--max-depth 2 --max-functions 8 --json
最值得调参的是 trace-native-values。默认值是:深度 3、最多反编译 16 个函数、256 个调用点、5,000 个节点、10,000 条边,节点列表可以用 --offset / --limit 分页。分析大型剥离二进制时,先用小深度,只有结果里出现你关心的未解析依赖时再调大。
函数被截断。 如果函数档案(dossier)里有 “Ghidra Listing reports a terminal call at …” 这类限制说明,意思是 Ghidra 认定某个被调用函数不会返回,于是把这次调用之后的代码排除在函数体之外。在 Linux 和 macOS 上,REA 会在自动分析后运行一个修复脚本,把内部确实有返回指令的函数的 no-return 标记清掉。限制说明会告诉你 Ghidra 的控制流模型在哪里仍然结束了这个函数,以及依据是 Ghidra 的标记还是未知的被调用方。这之后的代码应视为“未分析”,而不是“不存在”。
档案太大。 在 agent 里,先取一次函数档案,然后用 inspect_analysis_view 分面、分页查看,不要重复分析。原生分面包括 pseudocode、assembly、basic_blocks、callers、callees、unresolved_calls、referenced_strings,以及一组 value_flow_* 视图,数据源是同一会话里保留的 evidence_id。
第 3 步:重命名,并让名字保留下来
在 Linux 或 macOS 上,agent 可以在一个 MCP 会话里给分析出的结论命名:
You decompiled 0xFUNCTION_ADDRESS and concluded it validates the passphrase.
Use set_addresses_names to name it check_passphrase, then report the
per-address success values. Tell me which names you were unsure about.
set_addresses_names 接受 {"0x101189": "check_passphrase"} 这样的映射,每个地址在独立事务里处理,逐个报告成功与否;set_address_name 处理单个地址。6.4.0 的工具契约里有两点要注意:
- 改动只存在于临时的 Ghidra 数据库里,不会碰你的文件,会话关闭就没了。
- 元数据改动会让不可变的分析快照失效,所以你设的名字进不了快照回放。
想让名字跨会话、跨 CLI 调用保留,就写进种子文件(下一步)。function 和 label 种子都可以带名字,REA 会在每次重新导入、自动分析之前应用它们。这样,每个构建对应一份经过复核的 names.tsv,就成了你分析结论的持久记录。
第 4 步:在自动分析之前注入代码和名字
REA_GHIDRA_SEED_FILE 指向一个绝对路径的 UTF-8、制表符分隔文件,格式由 dist/ghidra/GhidraAnalysisSeeds.js 定义(rea-ghidra-seeds-v1):
# address<TAB>kind[<TAB>name] (illustrative addresses)
0x101189 function check_passphrase
0x101260 function dispatch_handler_3
0x1012a0 code
0x104010 label expected_key
6.4.0 解析器强制执行的规则:
kind只能是function、code或label。label必须带名字,code不能带名字。- 地址是 Ghidra 默认地址空间里的
0x十六进制值。名字不能含空白字符。同一地址、同一 kind 重复出现会被拒绝。 - 文件最大 32 MiB、1,000,000 条。空行和
#开头的注释行会被忽略。 - 分析前脚本先处理 function(从高地址到低地址),再处理 code,最后处理 label。function 种子落在已有函数上时,效果是给它改名。
- function 和 code 种子要求地址上有已初始化的字节。label 种子可以给
.bss这类未初始化存储命名。
function 和 code 种子用来补上自动分析到不了的代码,比如经由计算跳转调用的处理函数,或者只以指针形式存在数据里的入口。种子落在 Ghidra 已经解码的某条指令内部,或者落在已定义的数据上,都会失败。
种子是调用方的断言,不是分析发现。REA 会把种子文件的 SHA-256 和条目数写进分析 profile。如果会话实际应用的种子和提交的不一致,会话直接失败,不会返回结果。每个结果都带有限制说明,注明哪些函数和名字来自你的种子,并给出结果计数(新建、已存在、失败、未映射)。第一个查询完成后要看这些计数,function_failed 不为 0 就说明有些种子地址写错了。
第 5 步:覆盖语言和编译器规格
Ghidra 根据可执行文件头选择语言。如果文件头有误导性,或者你需要非默认的编译器约定,可以设置:
export REA_GHIDRA_LANGUAGE_ID="x86:LE:32:default"
export REA_GHIDRA_COMPILER_SPEC_ID="borlandcpp" # optional
6.4.0 的强制规则:
- 语言 ID 必须是 Ghidra 的四段式
processor:endian:size:variant;编译器规格必须是default、borlandcpp这样的简单 ID。 - 只设
REA_GHIDRA_COMPILER_SPEC_ID而不设REA_GHIDRA_LANGUAGE_ID,属于配置错误。 - 导入前,REA 会对照你的 Ghidra 安装里的语言定义检查这两个 ID,并作为“已配置的选择”记入 profile。
- DOS MZ/COM 导入本来就固定使用实模式语言,会拒绝覆盖。
覆盖设置对 server 打开的每个目标都生效,所以应该放在专用的 shell 或单独的 MCP server 条目里,不要全局设置。
第 6 步:弄清架构边界
在 Linux 和 macOS 上,6.4.0 的 Ghidra provider 接受架构被 REA 识别为 x86、x86-64、ARM、ARM64 或 MIPS 的可执行文件。有两个常见的坑:
- 通用(fat)Mach-O 会被拒绝。 Ghidra 没法保证导入的是你想要的那个切片。先瘦身,例如
lipo -thin arm64 -output app-arm64 App,再分析这个切片。 - MIPS 是有边界的通道,不是“所有 MIPS”。 在 6.4.0 里(
dist/ghidra/GhidraMipsProfile.js),provider 只接受声明 o32 和 MIPS32r2 的 ELF32ET_EXEC映像。它们必须有与文件头一致的 ABI flags 记录,浮点 ABI 必须是双精度或 FPXX。MIPS64/n64、n32、MIPS16、microMIPS、ASE 扩展、厂商特定变体、EF_MIPS_FP64、EF_MIPS_NAN2008、soft-float 以及缺少 ABI 记录的文件都会被拒绝,每种情况都有明确的原因说明。设置语言覆盖也绕不过这项检查。
交给 agent 之前,先自己检查一下候选文件:
readelf -h target | grep -E 'Class|Type|Machine|Flags'
readelf -A target # shows the MIPS ABI flags record if present
想要一个确定能用的 MIPS 目标,就自己编译:mips-linux-gnu-gcc -mips32r2 -mabi=32 -no-pie -O1 -o hello-mips hello.c。从固件里解出来的二进制(见 Binwalk 与 Unblob 固件分析)经常是 PIE(DYN)或共享库,6.4.0 会拒绝这类文件。不要猜,直接跑 REA,看它给出的拒绝原因。6.4.0 有一点处理得不错:对于节头表超出已截取文件末尾的 carved ELF,REA 会检查手头所有完整的节头,把缺失部分记为限制说明,而不是直接失败。这只针对节头表:程序头表和其中的 MIPS ABI flags 记录仍必须完整,否则文件会被拒绝。
只在 main 上,6.4.0 没有。 仓库 main 的文档里提到了 MIPS ET_REL 目标文件、MIPS16 共享映像、soft-float 原型、raw-binary 导入、RISC-V、PSP,以及 REA_GHIDRA_DISABLED_ANALYZERS 变量。这些在 6.4.0 发布包里都不存在,设置那个变量不会有任何效果。
第 7 步:解决第一个查询超时
这里有两个超时,分别归不同的组件管:
- REA 的 Ghidra 启动超时,覆盖导入、自动分析、桥接连接和就绪检查,默认 330,000 毫秒。
REA_GHIDRA_STARTUP_TIMEOUT_MS接受最大 2,147,483,647 的正十进制整数。值为空或不合法会报配置错误,而不是悄悄回退到默认值。 - 客户端的单次工具调用超时,由你的 MCP 客户端控制,可能更短。调大 REA 的超时并不会延长客户端的超时。
分析大型二进制时,两边都要调大:
export REA_GHIDRA_STARTUP_TIMEOUT_MS=900000
然后在客户端自己的设置里调大单次工具调用的超时(如果客户端支持)。CLI 没有客户端超时,所以用 CLI 冷启动跑一次,就能测出自动分析在你机器上实际要多久:
time rea analyze "$BIN" --provider ghidra --json > /dev/null
如果客户端在启动过程中放弃了,不要在同一个会话里直接重问。按 REA MCP 契约文档里的恢复流程操作:先调用 close_binary 并检查结果,再用 provider_id: "ghidra" 重新 open_binary,然后用更长的客户端超时重试。
第 8 步:设置 JVM 内存
在 Linux 和 macOS 上,REA 按顺序取第一个非空值作为 JVM 的 -Xmx:GHIDRA_HEADLESS_MAXMEM,其次 GHIDRA_MAXMEM,都没有就用 2G:
export GHIDRA_HEADLESS_MAXMEM=8G # large target on a big host
export GHIDRA_HEADLESS_MAXMEM=512M # small target on a constrained host
6.4.0 启动器里有几点值得知道:
- REA 会清空
_JAVA_OPTIONS、JAVA_TOOL_OPTIONS、JDK_JAVA_OPTIONS和GHIDRA_JAVA_OPTIONS,并接管GHIDRA_HEADLESS_JAVA_OPTIONS。通过这些变量注入的参数都会被丢掉,堆大小是唯一受支持的内存设置。 - JVM 还会带上
-XX:ParallelGCThreads=2和-XX:CICompilerCount=2。这只限制这两个线程池,不限制总 CPU。REA 不会传 Ghidra 的-max-cpu。要限制 CPU,就对 REA 进程使用主机工具(taskset、cgroups)。 -Xmx只限制 Java 堆,不限制整个进程家族的 RSS。
要在真正的 JVM 上验证,而不是看你的 shell:在 Linux 上,等第一个查询启动后,找到 ghidra.Ghidra 那个 Java 进程,从 /proc/<pid>/cmdline 读 -Xmx,从 /proc/<pid>/status 读 VmRSS。
第 9 步:有意识地复用快照
快照缓存的是精确的查询结果,不是保存下来的 Ghidra 项目。最常见的错误是改了 profile,却还指望旧快照继续命中。
- CLI:
analyze、inspect、decompile、function、instructions、search、xrefs、trace和inspect-native-api支持--snapshot /absolute/path.json。第 2 步里那几个精细命令、read-bytes和annotate-native-function不支持。 - MCP:
open_binary可以用snapshot_path加载快照,close_binary可以用snapshot_path在会话结束前原子地写出快照。 - 回放绑定 profile。 只有目标摘要、操作、参数、provider 版本和分析 profile 全部一致,REA 才会回放缓存结果。语言覆盖、编译器规格、种子摘要和 MIPS ABI 信息都属于 profile;堆大小和超时不属于。
实用规则:一个 profile 对应一个快照文件。 文件名里写上设置,例如 vault-stripped.seeds-v3.json。改了 names.tsv,种子摘要就变了,要换一个新文件。旧结果是在不同断言下算出来的,REA 不会混用。
export REA_GHIDRA_SEED_FILE="$PWD/names.tsv"
SNAP="$PWD/analysis/vault-stripped.seeds-v1.json"
rea analyze "$BIN" --provider ghidra --snapshot "$SNAP" --json
rea decompile "$BIN" --procedure check_passphrase --provider ghidra --snapshot "$SNAP" --json
在 profile 不变的情况下重复同一条命令,REA 会直接返回缓存结果,不启动 Ghidra。对比不同构建见 Evidence、快照与版本对比。
常见问题和排错
配置错误里提到 REA_GHIDRA_COMPILER_SPEC_ID。 你设了编译器规格却没设语言 ID。要么两个都设,要么都不设。
种子结果里有 unmapped。 这些地址在 Ghidra 地址空间里不属于已初始化内存。地址要从 REA 的输出里取,不要用别的工具给出的文件偏移。
MIPS 文件报 architecture_unsupported。 看原因字符串,它会指出是哪个标志位或 ABI 记录超出了 6.4.0 的支持范围。
设置堆大小后 Java 启动失败。 值必须是 JVM 认可的大小写法,例如 512M 或 4G,其他写法会让 Java 启动直接失败。
合规使用
这些方法请用在自己编译的程序、开源软件、CTF 题目或 REA 官方 showcase 的目标上。分析其他目标之前,先确认你已获得授权,并阅读 REA 的 DISCLAIMER.md。
下一步
- 本文的基础:REA + Ghidra 免费分析原生二进制
- 从固件镜像里取出 MIPS 候选文件:Binwalk 与 Unblob 固件分析
- 把同一个 provider 用在 PE 文件上:用 AI 逆向 Windows EXE/DLL
- 对比剥离版和 debug 版:Evidence、快照与版本对比
- 看 REA 的 Ghidra 通道和其他 server 有什么区别:REA 与 GhidraMCP、IDA Pro MCP 对比