rea-reverse

REA + Ghidra 免费分析原生二进制:从反编译到交叉引用

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

Ghidra 是一套免费、开源的逆向工程工具。REA(morluto 维护的开源项目)可以把它当作 provider,以无界面(headless)方式驱动:REA 把你的二进制导入一个用完即删的临时 Ghidra 项目,等自动分析完成后,再回答搜索、反编译、引用、追踪之类的具体问题,每个结果都附带 Evidence。你不用打开 Ghidra 图形界面,就能在终端或 AI 编程 agent 里用上 Ghidra 的反编译器。

本文面向已经装好 REA 的开发者(还没装的话先看 REA MCP 安装指南),目标是搭一套零授权成本、可重复的原生分析流程。我们会:

  1. 把 Ghidra 配置成 REA 的 provider;
  2. 写一个很小的 C 程序并自己编译,作为分析目标;
  3. 按 analyze → search → decompile → xrefs → trace 走一遍,每一步都同时给出 CLI 命令和在 agent 里对应的提问方式。

目标是你自己写、自己编译的程序,所以不存在“有没有权限分析”的问题。

前置条件

REA 的 Ghidra provider 要求比较具体,下面这些都来自安装文档,REA 会逐项检查:

  • 主机: Linux x64/arm64 或 macOS x64/arm64。Windows x64 是实验性的只读 P0 支持,只接受本地 NTFS 卷上的原生 x86/x86-64 PE 文件(见 Windows Ghidra 指南)。
  • Ghidra 12.1.x,需要事先解压好。REA 不会下载或安装 Ghidra。桥接组件在 Ghidra 12.1.4 上验证过。
  • 64 位完整 JDK 21 或更高(要带 javac),并且落在该 Ghidra 安装声明的版本范围内。
  • 与主机架构匹配的 Ghidra 原生反编译器,位于 Ghidra/Features/Decompiler/os/<platform>/。部分平台(尤其是 macOS 和 Linux arm64)的官方压缩包里可能没有,见后文排错。
  • REA 6.4.0 或更高,以及 Node.js 22.19+/24.11+/26+。
  • C 编译器(cc、clang 或 gcc),用来编译示例程序。

第 1 步:让 REA 找到 Ghidra

导出已有安装的绝对路径,然后让 REA 检查:

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 和引擎,可能因为一个无关项(比如没装 Hopper)而报告“不健康”。限定后,结果只反映 Ghidra 本身:Ghidra 版本、support/analyzeHeadless、Java 版本与位数、javac 以及原生反编译器。按报告把问题修好再继续。

然后运行 setup,让 agent 启动 REA 时也带上这些设置:

rea setup

setup 会在计划里列出要写入各 agent 的 REA 注册中的 GHIDRA_INSTALL_DIR/JAVA_HOME 条目,批准后才写入。这一步很关键:Cursor 这类 GUI agent 不会继承你刚在终端里 export 的环境变量。完成后重启 agent。

最后确认 REA 已把 Ghidra 识别为 provider:

rea providers --json

如果同时配置了 Hopper 或 IDA,可能有多个 provider 都支持同一个文件。这时像下文那样给每条命令加 --provider ghidra,或者一次性设置 REA_ANALYSIS_PROVIDER=ghidra。

第 2 步:编译一个小目标

把下面的代码保存为 vault.c。这是一个故意写得很小的“口令保险箱”:一个字节混淆函数、一个比较循环、两条输出字符串和一条用法提示。结构刚好够让每个分析步骤都有东西可看。

/* 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;
}

用不优化、带调试符号的方式编译,这样 Ghidra 里的函数边界和源码一一对应:

cc -O0 -g -o vault vault.c
BIN="$PWD/vault"

关于编译的两点说明:

  • 用 -O2 编译时,编译器通常会把 mix_byte 内联进 check_passphrase。这正好可以当第二个练习:用 -O2 -s(去符号)重新编译,再走一遍流程,体会没有函数名时难度上升了多少。
  • macOS 上生成的是 Mach-O,C 符号带前导下划线(_check_passphrase)。REA 文档建议完全按 inventory 返回的名称使用,包括平台前缀。下文命令用的是 Linux/ELF 的写法,如果你的搜索结果带下划线,相应调整即可。

CLI 会话与 agent 会话的区别

每条 rea CLI 命令都是一个独立进程:导入二进制到 Ghidra、等默认自动分析完成、回答一个问题、关闭会话。对这么小的文件来说很快,但大型二进制光导入就可能要几分钟。而 agent 通过 MCP 使用 REA 时,只打开一次二进制(open_binary),后续所有追问都复用已分析好的程序。所以 CLI 适合写脚本和快速核对,真正的调查交给 agent。

第 3 步:analyze(总览)

CLI:

rea analyze "$BIN" --provider ghidra --json > vault-overview.json

analyze 和 inspect 走的是同一个原生总览工作流(MCP 中对应 binary_overview)。结果会描述程序的格式与架构、函数,以及背后的 Evidence 记录。终端默认输出格式是 TOON;要保存或者交给 jq 处理时,--json 更合适。

Agent:

Use REA with the Ghidra provider to open /absolute/path/to/vault and give me an
overview: format, architecture, the functions it defines, and anything that looks
like input validation.

中文提问同样可行,例如:“用 REA 的 Ghidra provider 打开 /absolute/path/to/vault,概述它的格式、架构、定义了哪些函数,以及看起来像输入校验的部分。“如果配置了多个引擎,点名”Ghidra provider“很有用,agent 会在 open_binary 里传 provider_id: "ghidra"。一个会话里的第一次 Ghidra 查询最慢,因为导入和自动分析其实是在这时才发生。

第 4 步:search(搜索字符串和函数)

进入一个陌生二进制,最快的入口通常是用户能看到的字符串。我们的程序会打印 Wrong passphrase。

CLI:

rea search "$BIN" "passphrase" --provider ghidra --json
rea search "$BIN" check --kind procedures --provider ghidra --json

search 默认是 --kind strings,按字面量、不区分大小写匹配;加 --case-sensitive 则区分大小写。--kind procedures 改为搜索函数名;--mode regex 在 Ghidra 上切换为 Java 正则。结果是完整的,没有分页,也没有数量上限。

从字符串搜索结果里记下 Wrong passphrase 的地址,后面查 xrefs 要用;从函数搜索结果里记下 Ghidra 报告的 check_passphrase 的准确名称。

Ghidra provider 文档里有一条 Ghidra 特有的注意事项:字符串搜索只覆盖 Ghidra 定义为字符串数据的内容。被打包或未定义的字面量(Rust 字符串就是已知的例子)可能搜不到,所以搜索结果为空并不能证明文本不存在。

Agent:

Search the vault binary for strings containing "passphrase" and for procedures
whose names contain "check". Give me the exact addresses and names.

第 5 步:decompile(反编译)

现在把校验函数读成类 C 伪代码。

CLI:

rea decompile "$BIN" check_passphrase --provider ghidra --json
rea function "$BIN" check_passphrase --provider ghidra --json
rea instructions "$BIN" check_passphrase --provider ghidra --json

三条命令回答的问题不同:

  • decompile 返回单个函数的 Ghidra 伪代码。
  • function 返回函数档案(function dossier):伪代码加汇编、调用者与被调用者、引用、控制流基本块以及有上限的 p-code 数据流,全部附带 Evidence。
  • instructions 返回全部原始指令,不调用反编译器;当伪代码遗漏了某些细节时很有用。

参数既可以是名称也可以是地址。在 Ghidra 上,显式的 0x 前缀表示地址,否则优先按精确符号名匹配。以短横线开头的名称要用等号形式,比如 --procedure=-name。

在 -O0 下,伪代码的结构应该和源码一致:一个与 8 比较的 strlen 检查、一个循环、一次 mix_byte 调用,以及与一个全局字节数组的比较。Ghidra 会用自己的变量名和类型。要把它当作分析结果,而不是原始源码——REA 文档明确说明 provider 的伪代码永远不会被当作原始源码。

Agent:

Decompile check_passphrase and mix_byte. Explain what input passes the check,
and show the instructions or data bytes that support each step of your reasoning.

最后一句很重要。kExpected 的字节位于数据段而不是代码里。好的 agent 会去读这些字节(REA 提供 read_bytes 操作),而不是靠猜,然后反推 mix_byte 得出 rea-demo。因为源码在你手里,你可以直接核对它的答案。

第 6 步:xrefs(谁在用它?)

交叉引用回答的是“这个东西在哪里被用到”。用第 4 步得到的字符串地址:

CLI:

rea xrefs "$BIN" 0xSTRING_ADDRESS --provider ghidra --json
rea xrefs "$BIN" check_passphrase --provider ghidra --json

把 0xSTRING_ADDRESS 换成搜索返回的地址。xrefs 接受地址或符号名(值以短横线开头时用 --address=<value>),返回引用它的源地址。第一条命令应该指回 main;第二条应该显示 main 对 check_passphrase 的调用。

在 Ghidra 上,引用边会保留精确类型:调用、跳转、数据读、写、间接、计算等。调用者和被调用者只包含已解析的函数;没有已知目标的计算型调用会标记为未知,而不是被猜出来——以后分析带函数指针的代码时,这正是你想要的行为。

Agent:

Which functions reference the "Wrong passphrase" string, and who calls
check_passphrase? Resolve each reference to its containing function.

第 7 步:trace(追踪一个功能)

trace 把前面几步串起来。给它一个字面量查询,它会找出所有匹配的字符串和函数,沿着它们的 xrefs 走,并把每个引用解析到所在函数,最后返回观察结果和剩余的未知项。

CLI:

rea trace "$BIN" "Wrong passphrase" --provider ghidra --json

查询按字面量匹配,包括空格(对应 MCP 工具 trace_feature)。文本以短横线开头时用 --query=<text>。

如果关心的是数值层面的问题,Ghidra 还支持一种有上限的静态数据流追踪,它基于反编译器的 p-code,并能跨越已解析的调用:

rea trace-native-values "$BIN" check_passphrase --provider ghidra --json

它适合回答“哪个常量最终进入了这次比较”这类问题。但要看清它的限制:别名效应、持久状态和缺失的参数绑定仍然是未知的,它也不能证明运行时行为。

Agent:

Trace the "Wrong passphrase" feature: from the string to the code that prints it,
back to the decision that chooses between "Vault opened" and "Wrong passphrase".
Show the call path from main and list anything you could not resolve.

在一个会话里,agent 可以组合使用 trace_feature、xrefs、analyze_function 和 trace_call_path。在 Linux 和 macOS 上,它还可以通过 annotate_native_function 重命名函数或添加注释。这些修改只存在于临时 Ghidra 数据库中:可执行文件的字节不会改变,会话关闭后修改也随之丢弃。

保存成果:快照

既然每次 CLI 调用都要重新导入,脚本里反复执行的查询就适合配合快照:

rea analyze "$BIN" --provider ghidra --snapshot /absolute/path/to/analysis/vault.json

用同一个快照重复执行完全相同的查询,REA 会直接返回保存的结果,不启动 Ghidra。快照缓存的是精确结果(字节、操作、参数、provider 和设置都相同),并不是保存下来的 Ghidra 数据库,所以新的问题仍然会启动 provider。完整流程见 Evidence、快照与版本对比。

排错

macOS 上报告缺少 native_decompiler。 压缩包里可能只有反编译器源码而没有 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"
rea doctor --json

Intel Mac 上直接用 make ghidra_opt,目录换成 mac_x86_64。REA 不会替你编译这个组件。

agent 第一次提问就超时。 REA 默认给 Ghidra 330,000 ms 完成导入、自动分析和桥接就绪,但 MCP 客户端可能更早取消单次工具调用。分析大型二进制时,在 REA server 的环境中设置 REA_GHIDRA_STARTUP_TIMEOUT_MS,如果客户端有工具超时设置也一并调大。参见首次查询超时与恢复。

内存不足或机器性能弱。 REA 根据 GHIDRA_HEADLESS_MAXMEM 设置 Ghidra 的 JVM 堆大小(默认 2G)。export GHIDRA_HEADLESS_MAXMEM=512M 在小目标上验证过。对 agent 来说,这个变量必须写在 REA server 条目的环境里,只在自己的 shell 里设置没用。

provider 报 ambiguous。 有多个引擎支持该文件。从 details.candidate_ids 中选一个,用 --provider ghidra 或 MCP 中的 provider_id 指定。

架构或语言识别错误。 Ghidra 根据文件头识别语言。遇到特殊镜像时,设置 REA_GHIDRA_LANGUAGE_ID(例如 x86:LE:32:default)。安装文档还介绍了分析种子以及禁用卡住的分析器。

artifact_changed。 会话打开期间你重新编译了 vault。重新打开目标,让 REA 读取新的字节和摘要。

合规使用

请用自己编译的程序、开源程序、CTF 题或 REA 官方 showcase 里的目标来练习。分析其他软件之前,先确认已经获得授权,并阅读项目的 DISCLAIMER.md。

下一步