检查、反编译并结构化修补来自 React Native 应用的 Hermes 字节码(HBC),具备 GDB 风格的 REPL、CFG、反编译器和可重新序列化的修补器。
Hermes 逆向工程与操作交互式工具包。
hermit 可以检查、反编译并以结构化方式修补 Hermes 字节码(HBC)——这是 React Native 应用打包其 JavaScript 所使用的格式。打开一个 .hbc 文件,浏览其中的函数,阅读反编译后的 JavaScript,在交互式编辑器中编辑指令、字符串和字面量,然后写回一个 Hermes VM 仍能加载并运行的文件。
与那些只分析字节码、或在不改变大小的情况下就地修补字节码的工具不同,hermit 会重新序列化整个文件。它可以插入和删除指令、重定位跳转和异常处理器、重建字符串表和字面量表、添加新函数并重新计算 SHA-1 尾部校验。未经修改的文件可以逐字节往返一致。
| 版本 | 角色 |
|---|---|
| 96 | 主要目标。正在发布的 React Native 应用所使用的版本:RN 0.73 到 0.83,以及禁用 Hermes V1 的 RN 0.84+。 |
| 94、95 | 已验证。格式与 96 相同,仅操作码表不同。 |
使用 Hermes V1(RN 0.84 起的默认版本)构建的应用会生成 HBC 98,目前尚不支持。格式说明见 doc/hbc-format/。
每个 release 都包含适用于 macOS 和 Linux 的归档文件,支持 arm64 和 x86_64。无需 Python 或编译器:
| 归档文件 | 内容 |
|---|---|
hermit-<version>-<platform>.tar.gz | 交互式 REPL:./hermit file.hbc。 |
hermit-tools-<version>-<platform>.tar.gz | 独立的 hbc_* 命令行工具。 |
SHA256SUMS | 校验和:shasum -a 256 -c SHA256SUMS。 |
tar -xzf hermit-0.1.0-macos-arm64.tar.gz
./hermit-0.1.0-macos-arm64/hermit --version
macOS 二进制文件未签名。下载后,清除一次隔离标志:xattr -dr com.apple.quarantine hermit-0.1.0-macos-arm64。Linux 构建需要 glibc 2.35 或更新版本。cfg 命令仍需要安装 graph-easy。
发布版本在推送 v* 标签时由 .github/workflows/release.yml 使用 packaging/build-release.sh 生成,你也可以在本地运行该脚本。
FetchContent 获取。graph-easy 用于 cfg 命令(cpanm Graph::Easy),less 作为其分页器。从本目录(包含 CMakeLists.txt 的目录)运行所有命令:
cmake -B build
cmake --build build -j
这会构建 libhbc 静态库、Python 模块 _libhbc(位于 build/bindings/)、命令行工具(位于 build/tools/)以及单元测试。REPL 只需要 Python 模块:
cmake --build build --target _libhbc
工具和测试可以从构建中排除,例如用于发布:
cmake -B build -DCMAKE_BUILD_TYPE=Release -DHERMIT_BUILD_TOOLS=OFF -DHERMIT_BUILD_TESTS=OFF
每次修改 core/ 或 bindings/ 后,用相同命令重新构建。
macOS: 如果链接失败并报错提示 SDK 格式错误,请将构建指向一个完整的 SDK,例如
SDKROOT=/Library/Developer/CommandLineTools/SDKs/MacOSX26.5.sdk cmake --build build。
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
模块 _libhbc 不会安装到虚拟环境中。每次运行 hermit 时,将其构建目录添加到 PYTHONPATH:
export PYTHONPATH=$PWD/build/bindings:$PYTHONPATH
.hbc 文件React Native 应用将其 bundle 作为普通文件打包:
| 平台 | 包内位置 |
|---|---|
Android(.apk,zip 文件) | assets/index.android.bundle |
iOS(.ipa,zip 文件) | Payload/<App>.app/main.jsbundle |
如果文件以字节 C6 1F BC 03 C1 03 19 1F 开头,则它是 Hermes 字节码。REPL 只打开扩展名为 .hbc 的文件,因此请重命名:
unzip -p app.apk assets/index.android.bundle > app.hbc
xxd app.hbc | head -1 # c61f bc03 c103 191f ...
只分析你被允许研究的应用,例如你自己的应用,或用于研究的开源应用。
要获得小型、可控的输入,请使用能生成 HBC 96 的 Hermes 编译器编译 JavaScript 或 TypeScript(例如 react-native 0.73 到 0.83 中的 hermesc,位于 node_modules/react-native/sdks/hermesc/):
hermes -emit-binary -g0 -out hello.hbc hello.js # -g0: no debug info
seeder/ 包为 seeder/samples/ 中的示例程序自动化此过程,并将结果写入 seeder/corpus/:
.venv/bin/python -m seeder.compile # all samples
.venv/bin/python -m seeder.compile --g0 seeder/samples/loop_sum.js
.venv/bin/python -m seeder.compile --debug seeder/samples/greeting.js
--g0 写入不带调试信息的 <name>_g0.hbc,--debug 写入带完整调试信息的 <name>_g.hbc。.ts 示例需要 tsc 在 PATH 中;编译器由 --hermes 指定(默认为 hermes)。seeder/build-real-bundle.sh 会将一个真实的开源 React Native 应用构建为 .hbc。
export PYTHONPATH=$PWD/build/bindings:$PYTHONPATH
.venv/bin/python -m cli.main app.hbc
不带文件参数时,REPL 以空状态启动;使用 open <file.hbc>。提示符显示 HBC 版本和文件名,有未保存更改时带 +:[hbc v96 | app+] >。历史记录保存在 ~/.hermit_history 中,命令支持 Tab 补全。
> info
> strings --identifiers filter:fetch
> functions
> disasm func:12
> decompile func:isPremium
> xrefs string:"api.example.com"
> edit func:isPremium
> patch patched.hbc
函数通过索引或名称寻址(func:12、func:isPremium),字符串通过索引或值寻址(string:42、string:"hello"),字面量位置通过索引寻址(array:7)。
| 命令 | 作用 |
|---|---|
open <file.hbc> | 打开文件。 |
info | 头部摘要:版本、大小、计数、全局函数、源哈希。 |
strings [--identifiers|--strings] [filter:<text>] | 带类型的字符串表。 |
arrays [--arrays|--objects] [filter:<text>] [func:<N|name>] | 解码后的数组和对象字面量位置。 |
functions | 所有函数及其参数、寄存器和标志;Metro 模块 id。 |
disasm func:<N|name> | 带源码行的反汇编。s 切换调试源表。 |
src [func:<N|name>] | 调试源表,针对所有函数或单个函数。 |
cfg func:<N|name> [--compact=true|false] | 使用 graph-easy 渲染的控制流图。 |
xrefs | 字符串交叉引用摘要。 |
xrefs string:<value|N> | 使用某字符串的每条指令。 |
xrefs func:<N|name> [--callers] [--callees] | 某函数周围的调用图。 |
decompile func:<N|name> | 反编译后的 JavaScript。 |
decompile all [--output=<dir>] | 将每个函数反编译为 .js 文件。 |
edit func:<N|name|new> | 字节码编辑器;func:new 构建新函数。 |
edit string:<idx|value|new> | 字符串编辑器;string:new 添加字符串。 |
edit array:<N> | 数组和对象字面量编辑器。 |
patch [file.hbc] | 将编辑后的映像写入新文件。 |
help、exit | 帮助和退出。 |
长列表会在可滚动查看器中打开:方向键、PgUp/PgDn、g/G 跳到顶部和底部、/ 边输入边搜索、Esc 清除搜索、q 退出。
编辑在内存中进行。edit ... 打开全屏编辑器;用 Ctrl+X 然后 Y 确认后,会在完整性检查(重新解析文件并与预期内容比较)通过后将更改应用到内存映像。在 patch 之前不会写入磁盘,patch 也绝不会覆盖原始文件。
指令编辑器(edit func:):
| 按键 | 操作 |
|---|---|
| Left / Right | 在助记符和操作数之间移动。 |
| Up / Down、PgUp / PgDn | 在指令之间移动。 |
Enter | 编辑选中的标记。助记符可以变为任何具有相同操作数签名的操作码;操作数会根据其类型进行验证。 |
o / O | 在下方 / 上方插入指令(操作码选择器)。对于跳转,在其地址操作数上按 Enter 可选择目标。 |
Ctrl+K | 删除当前指令。 |
R | 恢复当前行。 |
Ctrl+Z / Ctrl+R | 撤销 / 重做。 |
Ctrl+X | 保存并退出(Y 应用,N 丢弃,Esc 取消)。 |
相同大小的编辑会就地拼接。插入或删除指令会经过结构化序列化器,它会在需要时将短跳转松弛为长跳转、重映射异常处理器范围并重写所有移动过的偏移量。
字符串编辑器(edit string:):单行编辑器。相同长度的值会就地写入;其他长度会将新字节追加到字符串存储并重新指向该条目,这也会分离共享存储的字符串。Ctrl+N 切换到添加新字符串。标识符是只读的,因为它们与哈希表绑定。
字面量编辑器(edit array:):用 Up 和 Down 选择元素,按 Enter 重新输入。Ctrl+T 更改其类型(包括 null、true 和 false),a 追加元素或键值对,d 删除最后一个。
共享字节码。 Metro 和 Hermes 会将相同的函数体只存储一次,因此多个函数可能指向相同的字节。编辑器会对此发出警告,并询问是否将编辑应用到所有函数;不支持只编辑其中一个。
一个典型的会话会修补应用内检查,并用相同版本的 Hermes VM 运行结果:
> edit func:isPremium # change a LoadConstFalse into LoadConstTrue
> patch isPremium_patched.hbc
hermes isPremium_patched.hbc
小型独立工具构建在 build/tools/ 中:
| 工具 | 用途 |
|---|---|
hbc_dump <file> | 每个头部字段。 |
hbc_section_map <file> | 每个节的偏移、大小和对齐,并带有覆盖率检查。 |
hbc_functions <file> | 函数表。 |
hbc_strings <file> | 带类型的字符串。 |
hbc_disasm <file> [func:N] [-o out.txt] | 所有函数或单个函数的反汇编。 |
hbc_roundtrip <in> <out> | 解析并重新序列化;用 cmp 比较。 |
hbc_patch <in> --offset <hex> --value <hex> -o <out> | 覆盖一个字节(不修复 SHA-1 尾部校验)。 |
tools/ 中的辅助脚本:inspect_real_bundle.py <bundle.hbc> 汇总 bundle 的结构特征(共享函数体、溢出函数、异常处理器、UTF-16 字符串),gen_opcode_table_typ.py 生成论文的操作码表。
REPL 是 _libhbc 模块之上的薄层,你可以直接使用它:
import _libhbc
f = _libhbc.HBCFile.open("app.hbc")
print(f.version(), len(f.functions()), "functions")
for fn in f.functions()[:5]:
print(fn.index, fn.name, fn.bytecodeSizeInBytes)
print(f.decompile(3)) # JavaScript for function 3
for ins in f.disasm(3):
print(f"{ins.offset:#06x} {ins.mnemonic}")
cfg = f.cfg(3) # basic blocks and edges
结构化修补通过 Module 进行:排队一种编辑,序列化,写入。