rea-reverse

Ghidra 无头 MCP 进阶:用 REA 分析剥离符号和非常见架构的二进制

基于 REA 6.4.0更新于 约 11 分钟读完
本页目录

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 会话的启动过程

下面每个设置都作用在会话生命周期的某个阶段:

  1. 解析分析 profile。 启动任何东西之前,REA 先为目标确定一个分析 profile:格式、架构、语言选择、种子摘要等。每条 Evidence 都会记录这个 profile,每个快照也以它为键。
  2. 导入。 REA 用 -readOnly 和 -deleteProject 启动 AnalyzeHeadless,导入到一个私有的临时项目。在 Linux 和 macOS 上,REA 按 Ghidra 自带的 LaunchSupport 配置直接启动 JVM。
  3. 分析前脚本。 应用种子等准备工作。
  4. 默认自动分析。 自动分析完成之前,REA 不返回任何结果,也不暴露部分结果。
  5. 桥接就绪。 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 的 ELF32 ET_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。

下一步