rea-reverse

用 REA 做固件分析:Binwalk、Unblob 解包再交给 Ghidra

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

先说结论: REA(morluto 维护的开源项目)本身不带固件解包器。它调用你已经装好的 Binwalk 和 Unblob,在固定的资源限制下运行它们,再把结果整理成 Evidence:区域偏移、提取出的文件路径、SHA-256、以及一份限制说明。之后你从里面挑一个 ELF,用 Ghidra provider 像分析普通原生程序一样打开它。固件这一步只在 Linux x64 上验证过,所以请直接准备一台 Linux x64 主机或虚拟机。

这篇适合想把一个开源固件镜像从“一整块二进制”一路走到“反编译出的函数”、又不想自己写胶水脚本的开发者和安全研究人员。读完你会知道:

  1. 怎么确认主机和工具满足 REA 的固件要求;
  2. 用 Binwalk 查看 OpenWrt 镜像的区域(inspect-firmware-regions);
  3. 用 Unblob 解包(extract-firmware),包括只解指定范围;
  4. 怎么正确理解 Evidence,尤其是 “complete” 到底意味着什么;
  5. 怎么通过 rea analyze 或 MCP 工具 open_binary 把提取出的 ELF 交给 Ghidra。

下文的命令和限制都来自 REA 6.4.0 的固件分析文档。凡是展示输出的地方,都是示意。

只分析你有权分析的固件:OpenWrt 这类开源构建、你自己编译的镜像,或者 CTF 题目。请先阅读 REA 的 DISCLAIMER.md。

先看平台限制

装任何东西之前先看这一节。REA 的固件支持范围比它的大多数功能都窄。

组件 支持范围(REA 6.4.0 文档)
固件区域查看与解包 仅在 Linux x64 上验证;README 里写的是 “Binwalk / Unblob on Linux”
Binwalk 接受 3.1.x,解析器用 3.1.0 验证
Unblob 接受 26.6.x,解析器用 26.6.4 验证
资源限制 依赖 util-linux 的 prlimit(通常是 /usr/bin/prlimit)
Ghidra provider(下一步) Linux x64/arm64、macOS x64/arm64;Windows x64 是实验性的 P0,只支持 PE
已验证格式 生成的 gzip、USTAR、ext4,内含 Linux x64 ELF
未验证 SquashFS、UBIFS、私有固件格式、其他主机和架构

实际影响有两点:

  • 别指望固件功能在 macOS 或 Windows 上跑。 prlimit 属于 util-linux,验证流程也只在 Linux x64 上跑。用 Mac 的话,整套流程放到 Linux x64 虚拟机或远程机器上做。
  • 大多数家用路由器固件是 MIPS 或 ARM。 REA 的 Ghidra MIPS 支持刻意做得很窄:只接受带显式 o32 和 MIPS32r2 标志的 ELF32 ET_EXEC/ET_REL,另外给 MIPS16 共享库单独留了一个有边界的 profile。很多真实的 MIPS 用户态程序不在这个范围里,REA 会明确拒绝并说明是哪条约束,而不是瞎猜一个处理器。详见 ghidra-mips.md。所以本文选 OpenWrt 的 x86/64 目标:它的用户态是 x86-64 ELF,在同一台 Linux x64 机器上 Ghidra provider 就能处理。

另外要清楚固件这一步不做什么:不连设备、不需要 root、不挂载、不模拟、不执行提取出的程序。它只是纯静态解包。

前置条件

  • REA 6.4.0 或更新版本。还没装的话先看REA 安装与接入。REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+。
  • 一台带 util-linux prlimit 的 Linux x64 主机。
  • 自己按上游说明装好的 Binwalk 3.1.x 和/或 Unblob 26.6.x(Binwalk、Unblob)。REA 不负责安装它们、它们的依赖或系统包。
  • PATH 上有 debugfs(来自 e2fsprogs),Unblob 处理 ext2/3/4 时要用它。
  • 最后一步需要 Ghidra 12.1.x 和 64 位 JDK(当前 12.1 版本要求 JDK 21 及以上)。参见REA + Ghidra 原生二进制分析。

区域查看(Binwalk)和解包(Unblob)的依赖互相独立,只需要其中一个功能就只装一个。

第 1 步:告诉 REA Binwalk 和 Unblob 在哪

REA 不会去 PATH 里找这两个工具,你要在运行 REA 的 shell(或 MCP 服务器)环境里给出绝对路径:

export REA_BINWALK_COMMAND=/absolute/path/to/binwalk
export REA_UNBLOB_COMMAND=/absolute/path/to/unblob
# 只有 prlimit 不在 /usr/bin/prlimit 时才需要:
export REA_FIRMWARE_PRLIMIT_COMMAND=/absolute/path/to/prlimit

为什么要绝对路径: REA 会把实际执行的程序路径和它的摘要写进 Evidence,这样每个结果都能追溯到具体是哪个工具产出的。每次请求时它还会检查 --version 的输出:Binwalk 必须输出 binwalk <version>,Unblob 必须只输出版本号。同一 3.1.x / 26.6.x 系列的其他构建也能用,但 Evidence 里会多一条限制,说明解析器没有针对这个构建验证过。

如果你是通过 agent 使用 REA,这些变量必须对 MCP 服务器进程可见,只在交互 shell 里 export 是不够的。重新跑一次 setup 或修改注册配置里的环境变量,然后重启 agent。

第 2 步:下载开源镜像并校验

OpenWrt 的 x86/64 镜像附带 sha256sums 文件。写这篇文章时 downloads.openwrt.org 上最新的稳定版是 25.12.5,如果有更新版本就替换掉。

mkdir -p ~/fw/openwrt && cd ~/fw/openwrt
curl -LO https://downloads.openwrt.org/releases/25.12.5/targets/x86/64/openwrt-25.12.5-x86-64-generic-ext4-rootfs.img.gz
curl -LO https://downloads.openwrt.org/releases/25.12.5/targets/x86/64/sha256sums
grep ext4-rootfs.img.gz sha256sums | sha256sum -c -

为什么选 ext4 rootfs 镜像: 它就是 gzip 包着一个 ext4 文件系统,正好对上 REA 验证过的格式(gzip 和 ext4)。SquashFS 版本也可以试,但文档把 SquashFS 列为未验证,那边遇到奇怪的结果属于预期之内,不一定是你环境的问题。

输入文件要小于 128 MiB。REA 会先把输入复制到私有工作区并计算哈希,从不修改源文件。

第 3 步:用 Binwalk 查看区域

rea inspect-firmware-regions ~/fw/openwrt/openwrt-25.12.5-x86-64-generic-ext4-rootfs.img.gz

对应的 MCP 调用(你让 agent 看这个镜像时它会这么调)需要绝对路径:

{
  "name": "inspect_firmware_regions",
  "arguments": { "path": "/home/you/fw/openwrt/openwrt-25.12.5-x86-64-generic-ext4-rootfs.img.gz" }
}

为什么先做这一步: 区域查看开销小、只读,能告诉你 Binwalk 识别出哪些签名、在什么偏移,方便你决定是整包解还是只解一段。

结果怎么看:

  • 偏移指的是原始文件里的字节位置。
  • reported_size 是 Binwalk 自己给的数。REA 会标上 size_basis=provider_reported_validation_unknown,因为 Binwalk 遇到未知长度时可能直接算到下一个签名或文件末尾。别把它当成验证过的分区边界。
  • 签名和置信度都不能证明分区有效、是否加密或运行时行为。
  • Binwalk 的签名 UUID 只在本次运行内有效。跨次比较请用内容哈希和源范围。

第 4 步:用 Unblob 解包

目标目录必须还不存在,必须是绝对路径,而且父目录要存在:

rea extract-firmware \
  ~/fw/openwrt/openwrt-25.12.5-x86-64-generic-ext4-rootfs.img.gz \
  ~/fw/openwrt/extracted

为什么这么严格: REA 从不覆盖已有目录。它用独占、抗符号链接的方式写文件,写完再回读校验摘要。提交前失败的话,半成品会被回滚。

需要知道的默认值:

限制 默认值
总时限 120 秒
单进程地址空间 1 GiB(通过 prlimit),120 CPU 秒,不允许 core dump
暂存预算 256 MiB、10,000 个条目
递归深度 3(允许 1–10)
JSON 报告大小 8 MiB,必须是合法 UTF-8
并发 Binwalk 一个线程、Unblob 一个 worker

OpenWrt 的 ext4 rootfs 会先解压成一个完整的文件系统镜像,再由 Unblob 遍历其中的文件,镜像较大时可能接近暂存预算。如果撞到暂存上限,就显式调大:

rea extract-firmware \
  ~/fw/openwrt/openwrt-25.12.5-x86-64-generic-ext4-rootfs.img.gz \
  ~/fw/openwrt/extracted-big \
  --max-output-bytes 536870912 --max-output-files 20000

上面的字节数和文件数只是示例,按你的磁盘情况来定。

只解指定范围

Binwalk 显示出你关心的区域后,可以只解那一段。文档里的例子:

rea extract-firmware /path/to/firmware.bin /path/to/selected-output \
  --offset 64 --length 4096 --max-depth 3

MCP 写法:

{
  "name": "extract_firmware",
  "arguments": {
    "path": "/path/to/firmware.bin",
    "output_directory": "/path/to/new-output",
    "range": { "offset": 64, "length": 4096 },
    "max_depth": 3
  }
}

这样做的好处: 暂存预算占用小,而且该范围内的 chunk 会有确定的 root_file_range(已按你给的偏移校正)。

第 5 步:读懂解包 Evidence

结果里有原始路径和 SHA-256、Unblob 原始报告、执行程序的路径/摘要/版本、独立校验过 SHA-256 的普通文件路径、chunk、诊断信息、任务派生关系和限制说明。

容易误解的几点:

  • complete 不等于“固件已被完整还原”。 它只表示 Unblob 按配置跑完了、没有报告问题。默认跳过规则和格式覆盖范围依然生效。
  • partial 表示存在未知或加密的 chunk、报告了解包问题、有未发布的链接或特殊文件,或者到达了深度上限。部分结果和缺失依赖的名字会保留在结果里,所以缺 debugfs 会直接显示出来,不会悄无声息地失败。
  • 符号链接只记录、不发布。 OpenWrt 的 /bin、/sbin 里很多命令都是指向 BusyBox 的符号链接。你能在 Evidence 里看到它们,但输出目录里只有真正的目标文件(比如 bin/busybox)。空目录、属主和可执行权限位也不会还原。
  • 解压出来的子文件,原始字节范围未知。 任务链能告诉你哪个文件从哪来,但不能穿过 gzip 给出逐字节映射。
  • 逻辑引用用 $input、$output 或 $output/<relative-path> 表示。Unblob 的 blob ID 只在本次运行内有效,比较时用哈希。

Unblob 默认的 Landlock 沙箱和跳过规则保持开启。沙箱起不来时 REA 会报错,不会改用 --no-sandbox 重试。

第 6 步:把提取出的 ELF 交给 Ghidra

任意固件字节都不会被当作原生可执行文件接收。你需要从解包结果里挑一个 ELF 单独打开。具体路径以 Evidence 为准;Unblob 会把输出嵌套在 _extract 目录下,下面的路径仅为示意。

export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.4_PUBLIC
rea doctor --provider ghidra --json

BIN=~/fw/openwrt/extracted/<path-from-evidence>/usr/sbin/uhttpd
rea analyze "$BIN" --provider ghidra --json
rea search "$BIN" "cgi" --provider ghidra --json
rea function "$BIN" main --provider ghidra --json

为什么先跑 rea doctor: 它不启动分析,只检查 Ghidra 版本、analyzeHeadless、JDK 版本范围和原生反编译器,比等第一次查询失败再排查快得多。

在 MCP 里,agent 会用提取出的路径加 provider_id: "ghidra" 调 open_binary,然后用 analyze_function、inspect_native_api 等原生工具:

{
  "name": "open_binary",
  "arguments": {
    "path": "/path/from/extraction/usr/sbin/uhttpd",
    "provider_id": "ghidra"
  }
}

第一次 Ghidra 查询会触发导入和自动分析,可能比较慢。大文件超时的话,在服务器环境里调大 REA_GHIDRA_STARTUP_TIMEOUT_MS(默认 330,000 毫秒)。

一个比较好用的 agent 提示词:

Extract /home/you/fw/openwrt/openwrt-25.12.5-x86-64-generic-ext4-rootfs.img.gz
into /home/you/fw/openwrt/extracted with REA. List the ELF files under usr/sbin,
then open uhttpd with Ghidra and explain how it dispatches CGI requests.
Cite function addresses and state any limitations from the Evidence.

用中文提问也可以,比如“用 REA 解包这个镜像,列出 usr/sbin 下的 ELF,再用 Ghidra 打开 uhttpd,解释它怎么分发 CGI 请求,并给出函数地址和 Evidence 里的限制”。

常见问题和排错

版本输出不被接受。 REA 要求 Binwalk 输出 binwalk <version>、Unblob 只输出版本号。包了一层会多打印内容的脚本会过不了检查,请直接指向真正的可执行文件。

找不到 prlimit。 安装 util-linux,或者把 REA_FIRMWARE_PRLIMIT_COMMAND 设成它的绝对路径。查看和解包都需要它。

结果是 partial,提示缺 extractor。 Unblob 需要每种格式对应的外部工具出现在它从 REA 继承来的 PATH 上,比如 ext4 要 debugfs。要加到 MCP 服务器的环境里,而不只是你的 shell。

输出被截断的报错。 每条命令 REA 最多保留 8 MiB 的 stdout/stderr,超出会明确报错停止。用 --offset/--length 缩小输入,或调小 --max-depth。

时限或内存限制。 文档里的策略写死了 120 秒时限和单进程 1 GiB 地址空间。镜像太大就先解一个小范围。

“目标目录已存在”。 这是设计如此,每次运行换一个新目录名。

Ghidra 拒绝 MIPS 程序。 拿报错里的具体约束对照 ghidra-mips.md。ELF64 MIPS、n32/n64、microMIPS 以及大多数共享库形态目前都不在支持范围内,REA 会直接说明而不是乱猜处理器。

更新后工具对不上。 固件工具已在 npm 6.4.0 里;如果你要用 main 上更新的功能,请查看版本可用性说明。

下一步