rea-reverse

EVM 合约函数选择器提取:从字节码读出选择器、参数和存储布局

粘贴已部署合约的 runtime 字节码,列出全部 4 字节函数选择器,以及推断出的参数类型、状态可变性、存储布局和编译器元数据。不需要 ABI、验证过的源码,也不需要 RPC。

字节码不会离开你的浏览器. EVMole 以 WebAssembly 形式在你设备上的 Web Worker 中运行,不上传任何内容。 如何自己验证

在单独页面打开工具(无广告、无统计)

部署到链上的智能合约并不携带 ABI,它携带的只有字节码。但几乎所有 Solidity 或 Vyper 合约的字节码开头都有一段分发器(dispatcher):它把调用数据的前 4 个字节和合约支持的各个外部函数的选择器逐一比较,然后跳转到对应的函数体。所以,即使没有源码,也能从字节码里把这些选择器,以及每个函数的不少信息读出来。

这个工具就在你的浏览器里做这件事。粘贴 runtime 字节码或者选择一个文件,页面会列出它找到的选择器,以及能推断出的参数类型、状态可变性和存储布局。字节码自始至终不会离开你的电脑。

使用步骤

  1. 拿到 runtime(已部署)字节码:用你使用的节点对地址调用 eth_getCode,或者取 Hardhat、Foundry 产物里的 deployedBytecode,或者 solc --bin-runtime 的输出。不要用 creation 代码。
  2. 把十六进制粘贴到上方输入框(带 0x 前缀、有换行都没关系),点“提取选择器”;也可以载入一个装着十六进制文本或原始字节的文件。
  3. 先看“函数”表:每个选择器及其推断参数和可变性。代码访问了存储时再看存储布局,最后从“编译器元数据”读出 solc 版本。

真实输出示例

我们用 eth_getCode 取回以太坊主网上 Multicall3 的 runtime 代码(命令见下文),粘贴到本页,再把页面显示的结果复制下来。排版做了压缩(标签和值放在同一行),所有值都原样保留。

样本: 以太坊主网上 Multicall3(0xcA11bde05977b3631167028862bE2a173976CA11)的 runtime 字节码(源码在 GitHub 上,MIT 许可,Copyright (c) 2023 Matt Solomon),2026-11-02 从公共 RPC 节点获取 (github.com/mds1/multicall3)

概要
输入          粘贴的十六进制
字节码大小    3808 B
函数数量      16
离线库匹配数  0
分析引擎      EVMole 0.9.3

函数 (16)
选择器      函数签名  推断参数                           可变性
0x0f28c97d  未知      (无)                             view
0x174dea71  未知      (address,uint256,uint256,bytes)[]  payable
0x252dba42  未知      (address,bytes)[]                  payable
0x27e86d6e  未知      (无)                             view
0x3408e470  未知      (无)                             view
0x399542e9  未知      bool,(address,bytes)[]             payable
0x3e64a696  未知      (无)                             view
0x42cbb15c  未知      (无)                             view
0x4d2301cc  未知      address                            view
0x72425d9d  未知      (无)                             view
0x82ad56cb  未知      (address,uint256,bytes)[]          payable
0x86d516e8  未知      (无)                             view
0xa8b0574e  未知      (无)                             view
0xbce38bd7  未知      bool,(address,bytes)[]             payable
0xc3077fa9  未知      (address,bytes)[]                  payable
0xee82ac5e  未知      uint256                            view

编译器元数据
ipfs  1220bb2b5c71a328032f97c676ae39a1ec2148d3e5d6f73d95e9b17910152d61f162
solc  0.8.12 (00080c)

能做什么

输入。 在文本框里粘贴十六进制的 runtime 字节码。0x 前缀可带可不带,空白字符(空格、换行)会被忽略,所以复制来的整块内容直接粘贴就行。也可以选择一个文件,文件内容是十六进制文本或者原始字节都可以,页面会自动处理。上限:字节码最多 1 MiB,输入最多 4 MiB,分析超时时间 30 秒。

输出。 对分析器找到的每个函数,你会看到:

  • 4 字节选择器,例如 0xa9059cbb
  • 函数体在字节码中的字节偏移
  • dispatch 类型,说明这个函数是怎样从分发器到达的
  • 推断的参数类型,例如 address,uint256
  • 推断的状态可变性:pure、view、payable 或 nonpayable

函数列表之外,报告还包括:

  • 存储布局:代码访问的存储槽(slot),每个槽的偏移(offset)和推断类型,以及哪些选择器读、哪些选择器写这个槽。transient storage(TLOAD/TSTORE)也包含在内。
  • 编译器 metadata:solc 等编译器附加在 runtime 代码末尾的 CBOR 数据块,能解码的会解码出来,常见内容是 solc 版本号和 metadata 文件的 IPFS 哈希。

整个结果可以导出为 JSON,方便留档、和以后的版本做对比,或者交给其他工具继续处理。

函数名从哪里来

选择器是函数签名的 keccak256 哈希的前 4 个字节,例如 keccak256("transfer(address,uint256)")。哈希不可逆,所以函数名不可能从字节码里“解码”出来,唯一的办法是拿选择器去已知签名表里查。

页面内置了一份离线签名表,收录两百多个常见签名:ERC-20、ERC-721、ERC-1155、Ownable、AccessControl、Uniswap 等常用接口。工具不会查询 4byte.directory 或任何在线数据库。在表里查到的选择器标为离线库匹配,查不到的标为未知。如果签名表里的参数和从字节码推断出的参数对不上,页面会提示这可能是选择器碰撞或者推断偏差。4 字节选择器确实会碰撞,所以名字匹配只是一个假设,不是证明。

原理

分析引擎是 EVMole 0.9.3,编译成 WebAssembly,在 Web Worker 中运行,因此页面不会卡住。EVMole 不需要源码,也不需要 ABI。它对分发器做符号执行,找出选择器比较和对应的跳转目标;然后沿着每个函数体继续分析:函数怎样读取调用数据(据此推断参数类型)、是否检查 CALLVALUE(据此判断是否 payable)、会执行到哪些读写存储和改变状态的操作码(据此判断 view、pure 还是 nonpayable)。对 SLOAD、SSTORE、TLOAD、TSTORE 的同一套追踪,就得到了存储布局。

metadata 解码则是读取代码最后两个字节表示的长度,再解析它前面的 CBOR 映射,solc 就是在这里写入 solc、ipfs 等字段的。

REA 命令行的 inspect-evm-interface 用的也是同一个 EVMole 版本,所以你可以离线用 REA 复核浏览器里的结果,见文末的相关教程。

隐私是怎么保证的

字节码不会被上传,这是页面的实现方式决定的:

  • 提取器运行在一个单独的框架(iframe)里,来源是 tools.rea-reverse.com;分析在框架里的 Web Worker 中、在你的设备上进行。页面一加载,Worker 就从 tools.rea-reverse.com 的 /_astro/ 路径下载好 EVMole 的 WebAssembly 文件(约 470 KB),所以之后即使断网,分析也照样能做。
  • Worker 运行在一条内容安全策略(CSP)之下,只允许向它自己的源 tools.rea-reverse.com 发请求,这是为了加载那个 .wasm 文件。Worker 发往其他任何地址的网络请求都会被浏览器拦截。
  • 这个页面在正文下方展示一个广告。广告脚本运行在 rea-reverse.com 上,而框架属于另一个源(origin),浏览器不允许广告脚本或本页的任何其他脚本读取你粘贴的字节码或分析结果。
  • 框架的内容安全策略拦截它的脚本能发起的所有请求(connect-src 'none',也不加载任何第三方资源)。框架不能自行打开窗口或让本页跳转;只有你点击报告里的链接时才会打开页面。它传给本页的只有自身高度和下面这些统计事件。
  • 唯一的统计事件是 evm_analyze,只包含成功或错误码、找到的函数数量区间、耗时区间,绝不包含字节码、选择器、函数名或任何其他内容。框架把事件交给本页,由本页发送。完整的事件清单见隐私政策。
  • 不想要任何广告和统计?在单独页面打开提取器。

如何自己验证

  1. 打开浏览器开发者工具的 Network(网络) 面板,加载本页。发往 tools.rea-reverse.com 的请求就是框架:它的 HTML、样式、字体和脚本,以及 Worker 脚本 /_astro/evm-worker-*.js。发往 Google 域名的请求是本页的广告。
  2. 清空日志,然后粘贴字节码并做一次分析。在过滤框里输入 domain:tools.rea-reverse.com:新出现的请求只有为下一次分析预先启动的新 Worker 和它自己的 evmole_bg-*.wasm,都来自 tools.rea-reverse.com,常常直接从缓存读取。它们都不带你的字节码。广告请求可能还会继续,但它们来自本页,不带框架里的任何内容。
  3. 点开来自 tools.rea-reverse.com 的 evm-selectors/ 文档,查看 Headers:它的 Content-Security-Policy 里有 connect-src 'none' 和 frame-ancestors https://rea-reverse.com。在 Chrome 里,Application(应用)> Frames 也能看到框架的源和策略。Worker 运行在 default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self' 这条策略下:connect-src 'self' 只让它加载那个 .wasm 文件。
  4. 检查隔离:在 Console(控制台) 里,上下文选 top,运行 document.getElementById('tool-frame').contentDocument,结果是 null;frames[0].document 会抛出 SecurityError。广告脚本拿到的也是这个结果。
  5. 或者先打开页面,等它加载完成再断网,然后做分析,照样能用。

局限

请把输出当作“从字节码推断出的结果”来读,而不是经过验证的 ABI。

  • 状态可变性是从字节码推断的,可能和源码里的声明不同。 例如,一个声明为 view 但实际上没有读取任何状态的函数,可能被推断成 pure。编译器和优化器也会改变字节码里能看到的东西。
  • 暂不支持 EOF 合约。 以 0xEF00 开头的代码(EVM Object Format)会被拒绝。
  • 需要 runtime(已部署)字节码,而不是 creation/init 代码。creation 代码里有构造函数,它负责把 runtime 代码复制到内存再返回,它的开头并不是你要找的分发器。用 solc 时请用 --bin-runtime,不要用 --bin。
  • 代理合约只能看到代理自身的选择器。 代理会用 DELEGATECALL 把它不认识的调用转发给实现合约。要看真正的接口,请分析实现合约的字节码。
  • 参数类型是推断的。 简单类型通常是对的;嵌套结构体、数组等复杂或动态类型可能不准确。
  • 不同编译器效果不同。 Vyper 的产物或手写字节码,结果可能与常见的 solc 产物不同,有时不完整。
  • 不产出源码,也不反编译。 工具还原的是接口和存储布局,不是业务逻辑,不会生成 Solidity 或其他任何源码。
  • 列表为空或很短,不能证明没有别的可调用入口。 不常见的分发器写法可能让部分函数逃过分析。

合规说明

请只用本工具分析你自己编写的合约、你获得授权审查的合约,或者字节码已经公开在链上的公开合约。请遵守所分析代码的许可协议以及适用于你的法律法规。还原出接口并不意味着你对这些代码享有任何权利,本工具也不提供任何攻击合约的建议。

对应的命令行

用一条普通的 JSON-RPC 调用取回 runtime 代码(任何你有权使用的节点都行;输出只保留开头和结尾):

$ curl -s -X POST -H 'content-type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":["0xcA11bde05977b3631167028862bE2a173976CA11","latest"]}' https://ethereum-rpc.publicnode.com
{"jsonrpc":"2.0","id":1,"result":"0x6080604052600436106100f35760003560e01c80634d2301cc1161…64736f6c634300080c0033"}

把这个响应存成 multicall3.json。Foundry 的 cast 也能离线从字节码列出选择器。这里只给出命令、没有输出:抓取本页时用的机器上没有安装 Foundry,所以我们没有运行它。

$ cast selectors "$(jq -r .result multicall3.json)"

常见问题

怎么从字节码里拿到函数选择器?

把 runtime 字节码粘贴到这里,点“提取选择器”。编译成 WebAssembly 的 EVMole 会沿着分发器(把调用数据前 4 个字节和各个选择器比较的那段代码)走一遍,列出它能到达的每个选择器,上面的 Multicall3 一共 16 个。

能从字节码还原 ABI 吗?

能还原一部分。你会得到每个选择器及其推断的参数类型和状态可变性,也就是 ABI 的骨架,但没有函数名和参数名:选择器是函数签名哈希的前 4 个字节,无法反推。只有选择器在页面内置的离线表里时才会显示名字。

未验证的合约,怎么看它的 ABI?

用 eth_getCode 取回它的 runtime 代码,粘贴到这里。结果是推断,不是经过验证的 ABI。如果只得到很少甚至 0 个选择器,它可能是代理合约:请改为分析它委托调用的实现合约。

函数名会去网上查吗?

不会。函数名只来自页面内置的离线签名表,所以上面 Multicall3 的每个选择器都显示为未知。如果你自己去公开数据库查,请把查到的结果当作假设,因为选择器会碰撞。

源码里写的是 view,为什么显示 pure?

可变性是根据字节码实际做了什么推断出来的。如果一个 view 函数从不读取存储或环境数据,代码里就没有任何东西能把它和 pure 区分开。反过来的情况也可能出现。手上有 ABI 时,请以 ABI 为准对照。

这里的存储布局和 solc 输出的 storage layout 一样吗?

不一样。这里是根据代码实际访问的槽位重建出来的:能看到槽位、偏移、推断类型和哪些选择器访问了它,但看不到变量名;运行时才算出来的槽位(例如 mapping 的元素)不一定能完整呈现。

更新于

相关教程