rea-reverse

Ghidra MCP:用 REA 让 AI agent 无界面调用 Ghidra 反编译

基于 REA 6.7.0发布者 rea-reverse更新于 约 7 分钟读完
本页目录

想让 AI agent 不开图形界面就用上 Ghidra,做法是让一个 MCP 服务器指向已解压的 Ghidra,由它替你跑无头分析器。第三方开源工具 REA 把这件事做成了 provider:设置 GHIDRA_INSTALL_DIR,用 rea doctor --provider ghidra 检查,然后在 CLI 或 MCP 里请求 decompile、xrefs、trace。REA 会把二进制导入一个临时 Ghidra 项目,等自动分析完成,再连同 Evidence 记录一起返回结果。它不会下载或安装 Ghidra。

本页实际跑了什么。 下面的内容都在 2026-11-02 实际跑过:REA 6.7.0 搭配 Ghidra 12.1.4,macOS 27.0.1(Apple silicon),OpenJDK 25.0.2,目标是用 Apple clang 21.0.0 编译的 -O0 样本 vault(SHA-256 5c3149…4ba4)。所有命令退出码都是 0。输出复制自这次运行,删减处标 …;本地路径缩写为 ./vault 和 ./ghidra_12.1.4_PUBLIC。nm/otool 给出的地址是标准答案,可以拿来核对 Ghidra。rea instructions 和 rea trace-native-values 这次没有跑。rea 命令取自 6.7.0 的 CLI 和 REA 的 Ghidra provider 文档。

环境要求

以下来自 REA 的安装文档,rea doctor 会逐项检查:

  • Linux 或 macOS(x64 或 arm64)。Windows x64 是实验性的只读支持,只接受本地 NTFS 卷上的原生 x86/x86-64 PE 文件(Windows 说明)。
  • 已解压的 Ghidra 12.1.x(桥接在 12.1.4 上验证过)。
  • 64 位完整 JDK 21 或更新版本,要带 javac。
  • 本机架构对应的 Ghidra 原生反编译器,位于 Ghidra/Features/Decompiler/os/<platform>/。12.1.4 的压缩包只带了 linux_x86_64 和 win_x86_64 两个,所以在 Mac(包括 Apple silicon)上要自己编译一次,做法见文末。
  • Node.js 22.19+、24.11+ 或 26+。

配置并检查

export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.4_PUBLIC
export JAVA_HOME=/absolute/path/to/jdk-21   # optional if java/javac are on PATH
rea doctor --provider ghidra --json

一定要加 --provider ghidra:不带它的 rea doctor 会检查所有引擎和 agent 注册,会因为无关的项目报不健康。在我们的机器上编好 arm64 反编译器之后,限定范围的检查用时 5 秒,返回 "healthy": true,九条 scope_checks 全部通过(节选):

{
  "healthy": true,
  "environment_healthy": false,
  "scope": {
    "mode": "explicit",
    "providers": [
      "ghidra"
    ]
  },
  "scope_checks": [
    …
    {
      "name": "ghidra-version",
      "ok": true,
      "classification": "healthy",
      "detail": "12.1.4"
    },
    …
    {
      "name": "ghidra-native_decompiler",
      "ok": true,
      "classification": "healthy",
      "detail": "./ghidra_12.1.4_PUBLIC/Ghidra/Features/Decompiler/os/mac_arm_64/decompile reports mach-o arm64, which matches darwin/arm64"
    },
    {
      "name": "ghidra-java",
      "ok": true,
      "classification": "healthy",
      "detail": "25.0.2; 64-bit; JDK; /opt/homebrew/Cellar/openjdk/25.0.2/libexec/openjdk.jdk/Contents/Home; accepted a 64-bit full JDK 21 or newer"
    }
  ]
}

九条检查是 node、ghidra、ghidra-platform、ghidra-architecture、ghidra-installation、ghidra-version、ghidra-headless、ghidra-native_decompiler 和 ghidra-java。environment_healthy: false 来自另一个 informational_checks 列表:25 项里有 24 项失败,都是本文用不到的东西(Hopper、IDA、JADX、Kuna、REA skill 和 19 个 agent 注册)。注意原生反编译器检查会比对可执行文件架构和本机架构,所以拷一个 x86-64 的 decompile 过来,在 Apple silicon 上是过不了的。

Cursor 这类图形界面 agent 不会继承你在终端里 export 的变量。rea setup 会在你确认后把 GHIDRA_INSTALL_DIR/JAVA_HOME 写进各个 agent 的 REA 注册里,写完重启 agent。如果同时配置了 Hopper 或 IDA,每条命令加 --provider ghidra,或设置 REA_ANALYSIS_PROVIDER=ghidra,否则可能收到列出 details.candidate_ids 的 ambiguous 错误。

一个有标准答案的样本

vault.c 把 8 字节口令藏在一个字节混淆函数后面:

/* vault.c - a tiny practice target for REA + Ghidra */
#include <stdio.h>
#include <string.h>

#define KEY_LEN 8

/* "rea-demo" after mix_byte() */
static const unsigned char kExpected[KEY_LEN] = {
    0x28, 0x42, 0x41, 0x80, 0x4A, 0x4E, 0x49, 0x4A
};

static unsigned char mix_byte(unsigned char c, int i) {
    return (unsigned char)((c ^ 0x5A) + i * 3);
}

int check_passphrase(const char *input) {
    if (strlen(input) != KEY_LEN)
        return 0;
    for (int i = 0; i < KEY_LEN; i++) {
        if (mix_byte((unsigned char)input[i], i) != kExpected[i])
            return 0;
    }
    return 1;
}

int main(int argc, char **argv) {
    if (argc != 2) {
        fprintf(stderr, "usage: %s <passphrase>\n", argv[0]);
        return 2;
    }
    if (check_passphrase(argv[1])) {
        puts("Vault opened");
        return 0;
    }
    puts("Wrong passphrase");
    return 1;
}
$ cc -O0 -g -o vault vault.c
$ ./vault rea-demo; echo $?
Vault opened
0
$ ./vault wrong; echo $?
Wrong passphrase
1

这个 -O0 Mach-O 构建的标准答案,来自 nm 和 otool:

$ nm vault
00000001000004b0 T _check_passphrase
0000000100000660 s _kExpected
0000000100000590 T _main
000000010000055c t _mix_byte
                 U _puts
                 U _strlen
(4 lines trimmed)

$ otool -v -s __TEXT __cstring vault
0000000100000668  usage: %s <passphrase>\n
0000000100000680  Vault opened
000000010000068d  Wrong passphrase

$ otool -tV vault | grep -B1 'Wrong passphrase'
0000000100000614	adrp	x0, 0 ; 0x100000000
0000000100000618	add	x0, x0, #0x68d ; literal pool for: "Wrong passphrase"

也就是说,正确的分析应该找到位于 0x10000068d 的 Wrong passphrase,它在 main 的 0x100000614/0x100000618 处被引用,紧跟在 0x1000005f4 的 bl _check_passphrase 之后;8 个期望字节在 __TEXT,__const 的 0x100000660。Mach-O 上 C 符号带前导下划线,名字照工具返回的原样使用。

命令一览

每次 rea CLI 调用都是独立进程:导入、自动分析、回答一个问题、关闭。走 MCP 时 agent 只打开一次(open_binary),后续问题复用已分析的程序,所以只有第一个问题慢。

BIN="$PWD/vault"
rea analyze "$BIN" --provider ghidra --json            # overview (MCP: binary_overview)
rea search "$BIN" "passphrase" --provider ghidra --json
rea search "$BIN" check --kind procedures --provider ghidra --json
rea decompile "$BIN" _check_passphrase --provider ghidra --json
rea function "$BIN" _check_passphrase --provider ghidra --json
rea instructions "$BIN" _check_passphrase --provider ghidra --json
rea xrefs "$BIN" 0x10000068d --provider ghidra --json
rea trace "$BIN" "Wrong passphrase" --provider ghidra --json
rea trace-native-values "$BIN" _check_passphrase --provider ghidra --json

下一节给出除 instructions 和 trace-native-values 之外每条命令的实际结果。

按 provider 文档,各自的用途:

  • search 默认按字面、不区分大小写搜字符串;--kind procedures 搜函数名,--mode regex 接受 Java 正则。只搜 Ghidra 定义为字符串数据的内容,打包或未定义的字面量(Rust 字符串是已知情况)可能搜不到,所以搜不到不代表不存在。
  • decompile 返回单个函数的伪代码;function 额外给出汇编、调用方与被调用方、引用、基本块和有界的 p-code 值流;instructions 不调用反编译器,直接列原始指令。0x 前缀表示地址,否则精确的符号名优先。
  • xrefs 只返回来源地址列表。它自己的 limitations 写明“does not expose their kinds; procedure_references and analyze_function preserve available kind metadata”,即引用类型(call、jump、数据读、写、computed 等)要用 procedure_references 或 analyze_function 才能看到。合成的入口点引用会被省略,没有已知目标的计算调用保持 unknown,不会乱猜。
  • trace(MCP 里叫 trace_feature)按字面匹配查询,沿 xrefs 找到所在函数,并列出解析不了的部分。trace-native-values 是有界的静态数据流追踪,别名效应和持久状态仍是未知。
  • 以短横线开头的参数要用 = 形式,比如 --procedure=-name、--query=-text。

Ghidra 对 vault 的实际输出

每次 CLI 调用都是冷启动(导入、自动分析、启动桥接),耗时主要花在启动上,而不是问题本身。这台机器上:analyze 18 秒,两次 search 各 18 秒,decompile 21 秒,function 49 秒,xrefs 0x10000068d 76 秒,xrefs _check_passphrase 68 秒,trace 69 秒。即使是很小的二进制,每次 xrefs 或 trace 也要预留一分钟左右。

analyze(binary_overview,只保留计数):

{
  "document": "target-5c3149797769.bin",
  "image_base": "0x100000000",
  …
  "segment_count": 11,
  "procedure_count": 9,
  "string_count": 30
}

search "passphrase" 找到了 otool 地址上的两个程序字符串,另外还有两个位于 __LINKEDIT(同一份概览里它从 0x100008000 开始)的符号名字符串:

{
  "operation": "search_strings",
  "parameters": {
    "pattern": "passphrase",
    "mode": "literal",
    "case_sensitive": false
  },
  "normalized_result": [
    {
      "address": "0x100000668",
      "value": "usage: %s <passphrase>\n"
    },
    {
      "address": "0x10000068d",
      "value": "Wrong passphrase"
    },
    {
      "address": "0x1000080b5",
      "value": "check_passphrase"
    },
    {
      "address": "0x1000082be",
      "value": "_check_passphrase"
    }
  ]
}

search check --kind procedures 只有一个结果 {"address": "0x1000004b0", "value": "_check_passphrase"},和 nm 一致。

decompile _check_passphrase(normalized_result,confidence: "observed"):

undefined4 _check_passphrase(char *param_1)

{
  uint uVar1;
  size_t sVar2;
  int local_24;
  undefined4 local_14;
  
  sVar2 = _strlen(param_1);
  if (sVar2 == 8) {
    for (local_24 = 0; local_24 < 8; local_24 = local_24 + 1) {
      uVar1 = _mix_byte(param_1[local_24],local_24);
      if (uVar1 != (byte)(&_kExpected)[local_24]) {
        return 0;
      }
    }
    local_14 = 1;
  }
  else {
    local_14 = 0;
  }
  return local_14;
}

结构和源码一致:长度和 8 比较、循环调用 _mix_byte、逐字节和 _kExpected 比较。但它不会给出那 8 个字节本身,所以下面给 agent 的提示要求用 read_bytes。

function _check_passphrase 补上调用关系:调用方只有一个,0x100000590 的 entry;被调用方两个,0x10000055c 的 _mix_byte 和 0x10000063c 的 _strlen thunk。entry 是 Ghidra 给同一地址起的名字,nm 里那里是 _main。

xrefs(两次调用各自的 normalized_result):

$ rea xrefs ./vault 0x10000068d --provider ghidra --json
["0x100000618"]
$ rea xrefs ./vault _check_passphrase --provider ghidra --json
["0x1000005f4","0x1000080d0"]

0x100000618 就是 otool 里的 add x0, x0, #0x68d,0x1000005f4 是 bl _check_passphrase。0x1000080d0 是来自 __LINKEDIT 的数据引用,不是第二个调用方。

trace "Wrong passphrase"(trace_feature,函数细节已删减)一次调用就把字符串和所在函数对上了:

{
  "query": "Wrong passphrase",
  "search_mode": "literal",
  "matches": [
    {
      "type": "string",
      "address": "0x10000068d",
      "value": "Wrong passphrase"
    }
  ],
  "references": [
    {
      "target_address": "0x10000068d",
      "source_address": "0x100000618",
      "containing_procedure": {
        "query_address": "0x100000618",
        "found": true,
        "procedure": {
          "address": "0x100000590",
          "name": "entry",
          …
          "body": {
            "ranges": [
              {
                "start": "0x100000590",
                "end": "0x10000063b"
              }
            ],
            "total_bytes": 172
            …
          }
        }
      }
    }
  ],
  "truncated": false,
  "residual_unknowns": []
}

所有地址都和上面 nm/otool 的标准答案一致,residual_unknowns 为空。

同一个二进制直接跑 analyzeHeadless。 作为交叉核对,我们直接运行 Ghidra 无头分析器,配一个我们自己写的小后置脚本(VaultEvidence.java,不属于 REA 也不属于 Ghidra)。导入、分析加脚本共 27 秒。应用 DWARF 后,Ghidra 用调试信息给函数和参数命名,而上面 REA 的结果用的是 Mach-O 符号名和 param_1:

=== DECOMPILE check_passphrase @ 1000004b0 ===

int check_passphrase(char *input)
…
                    /* Unresolved local var: int i@[DW_OP_breg31(sp): +12] */
    for (local_24 = 0; local_24 < 8; local_24 = local_24 + 1) {
      uVar1 = mix_byte(input[local_24],local_24);
      if (uVar1 != kExpected[local_24]) {
…
=== XREFS to "Wrong passphrase" @ 10000068d ===
100000618  PARAM  in main
=== XREFS to check_passphrase @ 1000004b0 ===
Entry Point  EXTERNAL  in <no function>
1000080d0  DATA  in <no function>
1000005f4  UNCONDITIONAL_CALL  in main

(已删去 INFO VaultEvidence.java> 前缀。)引用和前面一样,这次带上了类型;Entry Point 那条就是 REA 省略的合成引用。无头日志在导入 DWARF 时还会打出这一段:

ERROR DWARF un-recoverable expressions: (DWARFImportSummary)  
ERROR   DW_OP_fbreg -> 3 (DWARFImportSummary)  
ERROR   DW_OP_breg31 -> 3 (DWARFImportSummary)  
ERROR   DW_OP_reg31 -> 1 (DWARFImportSummary)  
ERROR   DW_OP_reg29 -> 2 (DWARFImportSummary)  

这是无害的噪音:Ghidra 没能映射部分局部变量的位置(所以才有 Unresolved local var 注释),同一份日志里仍然报告 REPORT: Analysis succeeded。

给 agent 的话,一条提示就够了。最后一句最重要,因为 kExpected 在数据里,不在代码里:

Use REA with the Ghidra provider on /absolute/path/to/vault. Find what prints
"Wrong passphrase", decompile the check that decides it, and work out which input
passes. Read the data bytes it compares against (read_bytes) instead of guessing,
and cite the addresses for each step.

对这些字节反推 mix_byte 得到 rea-demo,可以用 ./vault rea-demo 验证。agent 做的重命名和注释(annotate_native_function)只存在于临时 Ghidra 数据库里,可执行文件本身不会被改。

-O2 反而让这个样本更简单

我们原以为优化后的构建会藏得更深,结果正相反。用 cc -O2 编译后,nm 里已经没有 _mix_byte 和 _kExpected:编译器在编译期就把整个校验反算出来了,直接逐字节和明文比较:

$ otool -tV vault-o2 | sed -n '/_check_passphrase:/,/ret/p'
0000000100000470	bl	0x1000005f0 ; symbol stub for: _strlen
0000000100000474	cmp	x0, #0x8
000000010000047c	ldrb	w8, [x19]
0000000100000480	cmp	w8, #0x72
0000000100000494	ldrb	w8, [x19, #0x2]
0000000100000498	cmp	w8, #0x61
(b.ne branches and the other six byte compares trimmed: 0x65 0x2d 0x64 0x65 0x6d 0x6f)

0x72 0x65 0x61 0x2d 0x64 0x65 0x6d 0x6f 就是 ASCII 的 rea-demo。任何反汇编器都能看到,反编译器在这里帮不上什么忙。这种期望值是常量的校验不适合用来测反编译器,换一个期望值在运行时才算出来的目标。

脚本化运行用快照

每次 CLI 调用都会重新导入,所以可以加 --snapshot /absolute/path/vault.json 缓存精确结果:字节、参数和 provider 设置都相同的同一个查询,REA 直接返回保存的答案,不再启动 Ghidra。它不是保存下来的 Ghidra 数据库,新问题仍会启动 provider。二进制补丁比对用它来比较两个构建。

排错(来自 REA 文档)

macOS 上报 native_decompiler 缺失。 Ghidra 12.1.4 的发行包没有预编译的 Apple silicon 反编译器(Decompiler/os/ 下只有 linux_x86_64 和 win_x86_64),所以在 arm64 macOS 上要自己编译一次,需要 Xcode Command Line Tools、make 和 bison。Apple silicon 上:

cd "$GHIDRA_INSTALL_DIR/Ghidra/Features/Decompiler/src/decompile/cpp"
make ghidra_opt ARCH_TYPE="-arch arm64" OSDIR=mac_arm_64
decompiler_dir="$GHIDRA_INSTALL_DIR/Ghidra/Features/Decompiler/os/mac_arm_64"
mkdir -p "$decompiler_dir"
test ! -e "$decompiler_dir/decompile" && test ! -L "$decompiler_dir/decompile" &&
  install -m 755 ghidra_opt "$decompiler_dir/decompile"

本文用的就是这次编译;装好后 file 显示 decompile 为 Mach-O 64-bit executable arm64,rea doctor 的 ghidra-native_decompiler 检查通过。Intel Mac 用普通的 make ghidra_opt 和 mac_x86_64 目录。

agent 第一个问题就超时。 REA 给 Ghidra 的导入、自动分析和桥接启动留了 330,000 毫秒,但 MCP 客户端可能更早取消这次工具调用。要同时调大 REA 服务器条目里的 REA_GHIDRA_STARTUP_TIMEOUT_MS 和客户端自己的工具超时,两者互不影响。time rea analyze "$BIN" --provider ghidra --json > /dev/null 能在没有客户端限时的情况下测出真实冷启动时间(本机 analyze 18 秒,xrefs 和 trace 约 70 秒)。客户端中途放弃后,先 close_binary,再用 provider_id: "ghidra" 重新 open_binary,然后重试(详情)。

内存不够。 JVM 堆大小取自 GHIDRA_HEADLESS_MAXMEM(默认 2G,文档称小目标上 512M 可用)。它必须设在 REA 服务器的环境里,而且是唯一的内存开关:REA 会清掉 _JAVA_OPTIONS、JAVA_TOOL_OPTIONS、JDK_JAVA_OPTIONS 和 GHIDRA_JAVA_OPTIONS。

语言识别错误,或代码只能通过计算跳转到达。 设置 REA_GHIDRA_LANGUAGE_ID(例如 x86:LE:32:default)。漏掉的代码可以用 REA_GHIDRA_SEED_FILE 指向一个绝对路径的制表符分隔文件,每行 address<TAB>kind[<TAB>name](kind 为 function、code 或 label);REA 会记录它的 SHA-256,种子地址写错时会给出 function_failed 计数。

artifact_changed。 会话打开期间重新编译了二进制,重新打开即可。

想比较 REA 和其他 Ghidra、IDA 桥接,参见 REA 与 GhidraMCP、IDA Pro MCP 对比。