rea-reverse

离线分析 EVM 字节码:用 REA 还原合约函数选择器和接口

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

结论先行: rea inspect-evm-interface <文件> hex --json(MCP 工具名 inspect_evm_interface)读取本地的一份 EVM 字节码文件,交给内置的 EVMole 0.9.3 分析。返回它找到的函数选择器,每条带字节码偏移、分发类型、推断出的参数字符串和推断出的状态可变性。整个过程完全离线,不连 RPC、区块浏览器或钱包,也不会给出函数名或 Solidity 源码,所有结果都标为 inferred。在 6.4.0 里,它只能在装有 util-linux prlimit 的 Linux x64 上运行。要读懂它的输出,最可靠的办法是先拿自己编译的合约跑一遍,因为这时 solc 能给你标准答案。

这篇写给手里有运行时字节码、想知道它分发了哪些外部函数的开发者、审计人员和 CTF 选手。比如:自己部署的合约和源码对不上了;集成某个依赖前想先核对;或者在做 Ethernaut 的关卡。它不是 Dedaub 或 heimdall 那种意义上的“EVM 反编译器”:它还原的是接口,不是逻辑。这条边界本文会讲清楚。

读完你会知道:

  • 怎么用 solc 把 OpenZeppelin 的 ERC-20 编译成运行时字节码
  • 怎么运行 inspect-evm-interface,以及返回的每个字段是什么意思
  • 怎么用 solc 的产物核对选择器、参数和可变性
  • 哪些信息始终是未知的,以及怎么防止 AI agent 夸大结论

前置条件

  1. REA 6.4.0(npm install --global rea-agents),见 安装 REA 并接入 Claude Code / Cursor / Codex。
  2. Linux x64 主机。 provider 会检查 process.platform === "linux" 和 process.arch === "x64",不满足就报错:“Initial real-verified offline EVM interface profile supports Linux x64 only.” 在 macOS 或 Windows 上,可以用 Linux x64 虚拟机、x64 机器上的 WSL2,或者 linux/amd64 容器。
  3. util-linux 的 prlimit,默认路径 /usr/bin/prlimit。REA 用它给 EVMole 工作进程加上 CPU、地址空间和文件大小限制。用 command -v prlimit 检查。如果它在别的位置,把 REA_EVM_PRLIMIT_COMMAND 设成它的绝对路径。REA 不会替你安装系统工具。
  4. solc(原生编译器,不是 solcjs,因为要用 --bin-runtime 和 --hashes),以及装 OpenZeppelin 包要用的 Node.js/npm,还有 jq。

本文全程不接触任何链。如果之后要从网络上取字节码,或者和已部署的合约交互,只针对你有权检查的合约和系统,并先读 DISCLAIMER.md。

第 1 步:编译一个已知合约,拿到标准答案

先从一个接口完全已知的合约开始,用它来校准你对推断结果的理解。

mkdir -p /work/evm && cd /work/evm
npm init -y >/dev/null
npm install @openzeppelin/contracts@5
mkdir -p contracts
cat > contracts/MyToken.sol <<'EOF'
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20 {
    constructor() ERC20("MyToken", "MTK") {
        _mint(msg.sender, 1_000_000e18);
    }
}
EOF

solc --bin-runtime --hashes --abi --optimize \
  --base-path . --include-path node_modules \
  -o out --overwrite contracts/MyToken.sol

out/ 里会有这些文件,各自的用处:

  • MyToken.bin-runtime:十六进制的运行时字节码,也就是部署后存放在合约地址上、包含分发逻辑的那段代码。这里不要用 --bin,它输出的是创建(init)代码,作用是部署运行时代码。REA 没法替你区分这两种(bytecode.kind 永远是 unknown)。
  • MyToken.signatures:solc 生成的方法标识符,在 Function signatures: 标题下每行一个 selector: signature。较新的 solc 还会在同一文件里加上 error 和 event 两段,这里只用 function 那段。这就是你的标准答案。
  • MyToken.abi:ABI,包含每个函数的 stateMutability。

solc 还会为被导入的合约(比如 ERC20、IERC20)生成文件,用 MyToken 开头的那几个就行。

作为参照,ERC-20 标准函数的选择器是公开的(签名做 Keccak-256 后取前 4 个字节),比如 transfer(address,uint256) = 0xa9059cbb、balanceOf(address) = 0x70a08231、approve(address,uint256) = 0x095ea7b3、totalSupply() = 0x18160ddd。你的 .signatures 文件里应该有这些,再加上 OpenZeppelin ERC-20 的其他公开函数。

第 2 步:运行 inspect-evm-interface

rea inspect-evm-interface /work/evm/out/MyToken.bin-runtime hex --json > /work/evm/mytoken.evm.json

两个参数都是位置参数,都必须给:

  • path:本地的“字节码载体”文件,CLI 会把它解析成绝对路径。
  • encoding:hex 或 raw。这里故意不做自动识别。hex 接受 UTF-8 十六进制文本,允许一个可选的 0x 前缀和首尾的 ASCII 空白(所以结尾有换行没关系),但会拒绝奇数长度、中间带分隔符、带 BOM 或不是合法 UTF-8 的内容。raw 表示文件里的字节本身就是字节码。

限制是固定的:输入最多 4 MiB,返回最多 16 MiB,工作进程的 JavaScript 堆 256 MiB,时限 30 秒。超出任何一项,整次运行直接失败,不会返回部分结果。

通过 MCP 调用也一样:

{ "name": "inspect_evm_interface", "arguments": { "path": "/work/evm/out/MyToken.bin-runtime", "encoding": "hex" } }

MCP 里的路径必须是绝对路径。仓库 main 分支的文档还提到一个 detail: "summary" 参数,但 6.4.0 的输入 schema 是严格模式,只接受 path 和 encoding,所以不要往 6.4.0 的服务器发 detail。

第 3 步:读懂结果

命令输出的是一条标准的 REA Evidence 记录,内容在 normalized_result 里:

jq '.normalized_result | {artifact, bytecode: (.bytecode | del(.hex)), evidence_kind, discovery_completeness, runtime_execution}' /work/evm/mytoken.evm.json
jq -r '.normalized_result.functions[] | [.selector, .dispatch, .inferred_arguments, .inferred_state_mutability, .bytecode_offset] | @tsv' /work/evm/mytoken.evm.json

逐个字段说明:

  • artifact:载体文件的路径、大小、编码和 SHA-256。这个哈希针对文件原样的内容,连 0x 和换行都算在内。
  • bytecode:解码后字节的 SHA-256 和长度,以及完整的十六进制。注意,这两个摘要都不是以太坊的 Keccak code hash,不要拿它们和浏览器上的 code hash 比。kind、hardfork 和 deployment_authenticity 永远是 unknown。
  • functions[]:每个候选函数一行。selector 以 0x 加 8 位十六进制显示。bytecode_offset 是解码后字节码里的字节偏移,不是十六进制文本里的位置。dispatch 为 abi 或 fallback。inferred_arguments 是逗号分隔的类型串,例如 address,uint256。inferred_state_mutability 是 EVMole 的推测。后两项都可能是 null。
  • evidence_kind: "inferred"、discovery_completeness: "unknown"、runtime_execution: "not-performed":固定值,字面意思就是它们的含义。
  • limitations:provider 自己写的注意事项,其中一条是:“Recovered selectors, argument strings and mutability are upstream inferences, not verified ABI signatures or names. Empty results do not prove the absence of callable dispatch.”
  • raw_result(Evidence 顶层):EVMole 的原始返回。请求选择器时,它还会带上从字节码末尾解析出的 CBOR 元数据(比如编译器的元数据哈希)。REA 保留这些值,但不对其精度做任何保证。

第 4 步:和 solc 的产物对照

先把推断出的选择器和标准答案对一遍:

cd /work/evm
jq -r '.normalized_result.functions[] | select(.dispatch == "abi") | .selector | ltrimstr("0x")' mytoken.evm.json | sort > inferred.txt
awk '/^Function signatures:/{f=1; next} /^$/{f=0} f' out/MyToken.signatures | cut -d: -f1 | sort > truth.txt
comm -3 truth.txt inferred.txt     # empty output = same selector set

再看可变性:

jq -r '.[] | select(.type == "function") | "\(.name)\t\(.stateMutability)"' out/MyToken.abi

通过 .signatures 里的选择器,把每个 ABI 条目和推断结果对上,重点看两者不一致的地方。REA 文档提醒过:经过优化的代码可能给出不同的可变性候选,solc 标成 view 的函数,在字节码里的样子取决于优化器保留了什么。去掉 --optimize 重新编译,再跑一次第 2 步,就能看出编译参数的影响有多大。这个练习比我们在这里贴任何表格都有用,因为答案取决于你的编译器版本和参数。

要记住的规律是:选择器通常最可靠,参数类型次之,可变性最不可靠。 你先在已知合约上做过验证,这个排序就有了依据,而不只是假设。

第 5 步:用到没有源码的字节码上

理解校准好之后,再去分析你真正关心的字节码:

  • CTF 关卡。 Ethernaut 的关卡合约在 OpenZeppelin/ethernaut 仓库里开源。按合约声明的 pragma 版本编译,然后重复第 2 到第 4 步。agent 解题的整体流程见 用 REA + AI agent 解 CTF 逆向题。
  • 你有权检查的已部署合约。 REA 不会替你去链上取代码。你需要自己拿到运行时代码(比如通过自己的节点,或者浏览器上已验证合约的页面),存成十六进制文件,并记下来源。REA 的结果依然会写 deployment_authenticity: "unknown",因为它无法知道你保存的字节是否就是链上的那份。尽量从已验证的合约入手,这样可以用公开的源码核对结果。

函数名要你自己补,并且要注明出处。 REA 不做签名查询。如果你用公开的签名库给某个选择器配上名字,请标成“签名库匹配”。4 字节选择器会碰撞,多个签名可能对应同一个选择器。出现在你自己 .signatures 文件里的才是标准答案,签名库里查到的只是假设。

配合 AI agent 使用

输出足够紧凑,agent 可以完整读完。提示词里要把边界写清楚:

Run inspect_evm_interface on /work/evm/target.hex with encoding hex.
For each function row, report selector, dispatch, inferred_arguments and
inferred_state_mutability exactly as returned, plus the Evidence ID.
Do not invent function names. If I provide a .signatures file, match against it
and mark matches "solc ground truth"; anything else stays "unnamed".
Quote the limitations array verbatim at the end. Do not describe the contract's
logic: this tool does not recover it.

要求每行都带上 Evidence ID。这样你就有了和原生分析流程一样的可追溯性,见 REA 的 Evidence、快照与版本对比。通过 MCP 调用时,这条记录也会留在会话的证据包里,可以用 export_evidence_bundle 导出。

常见问题和排错

“Initial real-verified offline EVM interface profile supports Linux x64 only.” 你在 macOS、Windows 或 ARM 版 Linux 上。改用 Linux x64 虚拟机、x64 上的 WSL2,或 linux/amd64 容器。

“Offline EVM interface inspection requires caller-supplied util-linux prlimit: …” 找不到 prlimit,或者 REA_EVM_PRLIMIT_COMMAND 不是指向普通可执行文件的绝对路径。安装 util-linux,或者修正这个变量。

unsupported_target,提到 EF00。 字节码以 0xEF00 开头,是 EOF 容器格式。6.4.0 只支持平铺的传统字节码。

十六进制格式错误。 去掉中间的空格和换行(分行的 hex dump 解析不了),确认长度是偶数,0x 最多只能有一个。

找到的函数很少或一个都没有。 多半是传了创建代码(--bin)而不是运行时代码(--bin-runtime),或者这是个最小代理合约,逻辑在别处。空列表不能证明没有可调用的接口。

同一份源码两次编译结果不同。 优化参数或编译器版本不同,字节码就不同。先比较两次的 bytecode.sha256。

下一步