mytechnotalent/G-Pulley
GitHub: mytechnotalent/G-Pulley
为 Ghidra 提供 Wasmtime Pulley 字节码的反汇编与反编译能力,使逆向工程师能够分析嵌入式固件中编译后的 WebAssembly 模块。
Stars: 0 | Forks: 0
# G-Pulley
用于 **Pulley** 的 Ghidra 处理器模块 — Wasmtime 的可移植字节码解释器 ISA (v43.0.0)。反汇编和反编译已编译 WebAssembly 模块 中的 Pulley 字节码。同时支持 **stripped** 和 **non-stripped** 二进制文件。支持 **pulley32** (32位地址空间) 和 **pulley64** (64位地址空间) 目标。
## 作者
**Kevin Thomas** — kevin@mytechnotalent.com
## 什么是 Pulley?
Pulley 是 Wasmtime 内置的可移植解释器。当平台没有 Cranelift 原生代码后端(例如 Cortex-M33 / RP2350)时,Wasmtime 会将 WebAssembly 编译为 Pulley 字节码而不是机器码。结果以 **cwasm** (compiled WebAssembly) 格式存储 — 一个 ELF64 二进制文件,其 `.text` 节中包含 Pulley 字节码。
```
wasmtime compile
.wasm component ─────────────────────> cwasm ELF64
(WebAssembly --target pulley32 .text = Pulley bytecode
Component Model) .symtab = function symbols
```
在嵌入式固件(RP2350 等)中,cwasm 作为 `static` 字节数组嵌入在 ARM 二进制文件的 `.rodata` 节中。在运行时,Pulley 解释器逐条分派字节码指令。
## cwasm 二进制格式
cwasm 文件是一个 **ELF64 little-endian** 二进制文件 (`e_machine = EM_NONE`),包含:
| 节 | 内容 |
| ------------- | ------------------------------------------------- |
| `.text` | Pulley 字节码 (可执行代码) |
| `.rodata` | 只读数据 (字符串字面量, 跳转表) |
| `.data` | 已初始化的可变数据 |
| `.custom_...` | Wasm 自定义节 (名称, 组件元数据) |
| `.symtab` | 符号表 — 函数名和字节边界 |
| `.strtab` | 符号名称的字符串表 |
| `.shstrtab` | 节头字符串表 |
`.symtab` 将 Wasm 函数索引映射到 `.text` 内的字节偏移和大小。条目看起来像 `function[15]` (Wasm 函数索引)。当符号存在时 (non-stripped),G-Pulley 会应用精确的函数边界和名称。当符号缺失时 (stripped),Java 分析器会通过跟踪 `call` 目标和 `push_frame_save` 序言来发现函数。
### cwasm 如何嵌入到固件中
```
wasmtime compile
app.wasm ──────────────────────────> app.cwasm (ELF64, Pulley bytecode)
--target pulley32
Rust build.rs
app.cwasm ──────────────────────────> static CWASM: &[u8] = include_bytes!(...)
cargo build
firmware.rs ────────────────────────> firmware.elf (ARM ELF32)
+ CWASM blob .rodata contains the entire cwasm ELF bytes
```
在运行时,固件将 `&CWASM` 传递给 `wasmtime::Module::deserialize()`,该函数对 ELF 节进行内存映射并创建一个可运行的模块。Pulley 解释器 (`pulley_interpreter::interp`) 随后通过分派循环执行 `.text` 字节码。
## Pulley ISA 概述
Pulley 是一种**基于寄存器**的、**可变长度**的、**little-endian** 字节码 ISA。
### 寄存器
| 寄存器 | 编码 | 用途 |
| ----------------- | ------ | ---------------------------- |
| `x0` - `x15` | 0 - 15 | Caller-saved (参数, 临时变量) |
| `x16` - `x29` | 16 - 29| Callee-saved |
| `xsp` (x30) | 30 | 栈指针 |
| `spilltmp0` (x31) | 31 | 内部溢出临时变量 |
| `lr` | — | 链接寄存器 (返回地址) |
| `fp` | — | 帧指针 |
| `f0` - `f31` | 0 - 31 | 浮点/向量寄存器 (128b) |
寄存器内部为 64 位。SLEIGH 规范定义了 8 字节 (`x0`) 和 4 字节子寄存器 (`x0l`) 别名。在 pulley32 模式下,指针使用低 32 位 (32位地址空间)。在 pulley64 模式下,使用完整的 64 位寄存器进行寻址。
### 操作码编码
- **主操作码**: 1 字节 (0x00 - 0xDB), 220 个操作码
- **扩展操作码**: 3+ 字节 — 0xDC 前缀 + u16 LE 扩展操作码
- **所有多字节字段**: little-endian
### 操作数类型
| 类型 | 大小 | 描述 |
| ------------------- | ---- | -------------------------------------------------------------------- |
| XReg | 1 B | 寄存器索引 0-31 (完整字节,使用 4:0 位) |
| BinaryOperands | 2 B | 打包的 u16 LE: `dst[4:0]`, `src1[9:5]`, `src2[14:10]` |
| BinaryOperands/U6 | 2 B | 打包的 u16 LE: `dst[4:0]`, `src[9:5]`, `imm6[15:10]` |
| PcRelOffset | 4 B | 有符号 i32 LE,相对于指令**起始位置** |
| AddrO32 | 5 B | XReg (1B 基址) + i32 (4B 有符号偏移) |
| AddrG32 | 4 B | 打包的 u32 LE: `off[15:0]`, `wasm[20:16]`, `bound[25:21]`, `base[30:26]` |
| UpperRegSet | 2 B | 位掩码 u16 — 位 N 保存寄存器 x(N+16) |
#### BinaryOperands 打包 (u16 LE)
```
15 10 9 5 4 0
+──────────────+─────────+───────+
| src2 (5b) | src1(5b)| dst(5b)|
+──────────────+─────────+───────+
```
示例: 字节 `20 42` -> u16 = 0x4220 -> dst = x0, src1 = x17, src2 = x16 -> `xadd32 x0, x17, x16`
#### AddrG32 打包 (u32 LE)
```
31 30 26 25 21 20 16 15 0
+──+─────────+──────────+─────────+────────────────+
| | base(5b)| bound(5b)| wasm(5b)| offset (16b) |
+──+─────────+──────────+─────────+────────────────+
```
受保护的内存访问: `address = heap_base + wasm_addr + offset`,针对 `heap_bound` 进行边界检查。
## 项目结构
```
G-Pulley/
├── README.md # This file
├── extension.properties # Ghidra extension metadata
├── Module.manifest # Module class: PROCESSOR
├── build.gradle # Gradle build (compile + zip)
├── data/
│ └── languages/
│ ├── pulley.slaspec # SLEIGH processor specification — pulley32
│ ├── pulley64.slaspec # SLEIGH processor specification — pulley64
│ ├── pulley.ldefs # Language definitions (32-bit and 64-bit)
│ ├── pulley.pspec # Processor spec (PC, register groups)
│ ├── pulley.cspec # Compiler spec — pulley32 (pointer_size=4)
│ ├── pulley64.cspec # Compiler spec — pulley64 (pointer_size=8)
│ └── pulley.opinion # Format opinion
├── src/
│ └── main/java/gpulley/
│ ├── PulleyCwasmLoader.java # ELF loader — imports cwasm into Ghidra
│ └── PulleyCwasmAnalyzer.java # Post-load analyzer — discovers functions
├── ghidra_scripts/
│ └── ExtractCwasmBlob.java # Script: extract cwasm from ARM ELF
└── docs/ # (reserved for additional documentation)
```
### 组件描述
**`pulley.slaspec`** — 用于 pulley32 (32位地址空间) 的 SLEIGH 指令规范。定义了所有 token、寄存器附件以及约 65 个指令构造器,涵盖:控制流 (call, ret, jump)、分支 (基于寄存器测试、寄存器-寄存器比较、寄存器-立即数比较的条件分支)、寄存器移动和常量、32/64位算术、移位 (寄存器和 U6 立即数)、按位操作 (带寄存器和立即数变体的 AND/OR/XOR/NOT)、比较、计数/扩展操作 (clz, ctz, zext, sext)、内存加载/存储 (偏移 O32, 零检查 Z, 受保护 G32)、帧操作 (push/pop_frame, push/pop_frame_save) 以及扩展操作码 (trap, call_indirect_host, xpcadd, xmov_fp/lr)。包含一个针对未识别操作码的 `undecoded_op` 兜底处理。
**`pulley64.slaspec`** — 用于 pulley64 (64位地址空间) 的 SLEIGH 指令规范。操作码和编码与 pulley32 相同。区别在于:`ram size=8`,分支目标导出 8 字节地址,加载/存储地址计算使用完整的 64 位寄存器并对 32 位偏移使用 `sext()`,`call_indirect` 使用 8 字节函数指针。所有 32 位 ALU 操作 (xadd32, xsub32 等) 保持不变 — 它们仍然对低 32 位进行操作并进行零扩展。
**`PulleyCwasmLoader.java`** — Ghidra `AbstractLibrarySupportLoader` 子类。处理两种情况:
1. **独立 cwasm**:检测 ELF64 + `EM_NONE` (Pulley)。解析节头,将 `.text` 作为可执行内存块加载,解析 `.symtab`/`.strtab` 以创建函数标签。
2. **嵌入在 ARM ELF 中**:检测 ELF32 + `EM_ARM`,扫描所有字节以查找嵌套的 ELF64 + `EM_NONE` 头 (`.rodata` 中嵌入的 cwasm blob),并将其提取加载。
**`PulleyCwasmAnalyzer.java`** — 加载后的字节分析器。扫描已加载的 `.text` 字节码以:
- 在 `push_frame_save` / `push_frame` 序言处创建函数
- 解析 `call` / `call2` / `call3` / `call4` PC 相对目标并在目标地址处创建函数
- 使用显示宿主函数 ID 的标牌注释来标注 `call_indirect_host` 调用点
- 为 `trap` 指令标记行尾注释
**`ExtractCwasmBlob.java`** — 用于从已加载的 ARM ELF 中提取 cwasm 的 Ghidra 脚本。搜索内存块以查找嵌入的 ELF64 魔数,根据节头计算大小范围,并将其保存到文件中。
## 安装说明
### 前置条件
- Ghidra 11.x 或更高版本
- Java 17+
### 选项 A:使用 Gradle 构建
```
cd G-Pulley
# 设置你的 Ghidra 安装路径
export GHIDRA_INSTALL_DIR=/path/to/ghidra_11.x
# 构建扩展 zip
gradle build
# -> build/dist/G-Pulley-1.0.zip
```
在 Ghidra 中:**File -> Install Extensions -> Add (+)** -> 选择 `G-Pulley-1.0.zip`。重启 Ghidra。
### 选项 B:手动复制
```
GHIDRA_DIR=/path/to/ghidra_11.x
# 复制语言文件
mkdir -p "$GHIDRA_DIR/Ghidra/Processors/Pulley/data/languages"
cp G-Pulley/data/languages/* "$GHIDRA_DIR/Ghidra/Processors/Pulley/data/languages/"
# 复制已编译的 Java 类(如果已构建)
mkdir -p "$GHIDRA_DIR/Ghidra/Processors/Pulley/lib"
cp G-Pulley/bin/*.class "$GHIDRA_DIR/Ghidra/Processors/Pulley/lib/"
# 复制 Ghidra 脚本
cp G-Pulley/ghidra_scripts/*.java ~/ghidra_scripts/
```
重启 Ghidra。**Pulley:LE:32:default** 和 **Pulley:LE:64:default** 语言将出现在处理器选择器中。
## 使用方法
### 选择 32 位还是 64 位
| 目标 | 语言 | 使用场景 |
| --------------------------- | -------------------- | ----------------------------------------- |
| `--target pulley32-...` | Pulley:LE:32:default | 嵌入式 (RP2350), 32位 Wasm 内存 |
| `--target pulley64-...` | Pulley:LE:64:default | 桌面/服务器,64位宿主地址空间 |
| 包含嵌入式 cwasm 的 ARM ELF | Pulley:LE:32:default | 始终为 32 位 (固件使用 pulley32) |
对于独立的 cwasm 文件,加载器同时提供 32 位 (首选) 和 64 位选项。请选择与您的 `wasmtime compile --target` 设置相匹配的选项。
### 工作流 1:导入独立 cwasm 文件
1. **File -> Import File** -> 选择 `.cwasm` 文件
2. **Pulley Cwasm Loader** 会自动检测 ELF64/EM_NONE 格式
3. Ghidra 选择 **Pulley:LE:32:default** 作为语言
4. 点击 **OK** — 加载器导入 `.text`,应用符号,开始反汇编
5. **Pulley Cwasm Analyzer** 自动运行以发现额外的函数
### 工作流 2:导入包含嵌入式 cwasm 的 ARM 固件
1. **File -> Import File** -> 选择 ARM `.elf` 固件
2. 加载器检测到嵌入的 cwasm blob 并提供 Pulley 语言选项
3. 选择 **Pulley:LE:32:default** -> **OK**
4. 加载器提取内部的 cwasm ELF 并加载 Pulley 字节码
### 工作流 3:从已打开的 ARM ELF 中提取 cwasm
如果 ARM 固件已在 Ghidra 中打开 (使用 ARM 处理器):
1. **Script Manager** -> 运行 **ExtractCwasmBlob.java**
2. 将提取的 cwasm 保存到文件
3. 将保存的文件作为新程序导入 (自动检测为 Pulley)
## Stripped 与 Non-Stripped 二进制文件
| 特性 | Non-Stripped cwasm | Stripped cwasm |
| -------------------- | ---------------------------- | --------------------------------- |
| 包含 `.symtab` | 是 | 否 |
| 函数边界 | 精确 (来自符号表) | 由分析器发现 |
| 函数名 | `function[N]` 或已 demangle | 自动生成 (`FUN_xxxx`) |
| 发现方法 | 符号表解析 | 调用目标 + 序言扫描 |
| 反编译器质量 | 完整 (带有命名函数) | 完整 (带有自动命名函数) |
对于 stripped 二进制文件,`PulleyCwasmAnalyzer` 通过以下方式查找函数入口点:
1. **调用目标解析**:每条 `call`/`call2`/`call3`/`call4` 指令都有一个 PC 相对偏移量,该偏移量会解析为一个绝对地址 — 分析会在每个目标处创建一个函数。
2. **序言检测**:Pulley 函数以 `push_frame_save` (操作码 0xAA) 或 `push_frame` (操作码 0xA8) 开始 — 分析器会在这些模式处创建函数。
这两种方法都能产生完整的函数覆盖。反编译器在两种情况下的工作方式完全相同;非 stripped 版本只是提供了更好的命名。
## 指令类别
| 类别 | 操作码 | 数量 |
| ------------ | -------------------------------------------------- | ----- |
| 控制流 | ret, call, call1-4, call_indirect, jump | ~8 |
| 分支 | br_if32, br_if_xeq32, br_if_xult32_u8, ... | ~18 |
| 寄存器操作 | xmov, xzero, xone, xconst8/16/32, xselect32 | ~7 |
| 算术 | xadd32, xsub32, xneg32, xmin32_u, xmax32_u | ~8 |
| 移位 | xshl32, xshr32_u, xshl32_u6, xshr32_u_u6, xrotl32 | ~6 |
| 按位操作 | xband32, xbor32, xbxor32, xbnot32 (+imm 变体) | ~8 |
| 比较 | xeq32, xult32 | ~2 |
| 计数/扩展 | xclz32, xctz32, zext8, zext32 | ~4 |
| 内存 (O32) | xload32le_o32, xstore32le_o32, xload64le_o32, ... | ~6 |
| 内存 (Z) | xload8_u32_z, xload32le_z, xstore8_z, xstore32le_z | ~6 |
| 内存 (G32) | xload32le_g32, xstore32le_g32, xstore64le_g32, ... | ~4 |
| 帧操作 | push_frame, pop_frame, push/pop_frame_save | ~4 |
| 扩展操作 | trap, call_indirect_host, xpcadd, xmov_fp, xmov_lr | ~6 |
总计:约 65 个指令定义 + `undecoded_op` 兜底。
## 反编译器输出
使用 `pulley.cspec` 中定义的调用约定:
- 参数通过 `x0` - `x7` 传递,返回值在 `x0` 中
- Callee-saved 寄存器:`x16` - `x29`,`xsp`,`fp`
- 栈向下增长
- 返回地址存储在 `lr` 中
Ghidra 的反编译器生成可读的类 C 输出,显示:
- 具有已解析 PC 相对目标的函数调用
- 通过 `heap_base + wasm_addr + offset` 模式进行的内存访问 (受保护的加载/存储)
- 来自条件分支指令的分支条件和循环结构
- 带有 `HOST IMPORT #N` 标牌注释的宿主函数调用
## 理解反汇编:函数映射
从 WebAssembly 组件(通过 `wit-bindgen` + `dlmalloc`)编译而来的 Pulley 字节码遵循可预测的结构。函数索引空间从导入的函数(没有字节码体)之后开始,因此定义的函数根据 WIT 导入的数量从索引 2 或更高处开始。
### 典型函数布局
| 函数索引 | 源码级含义 | 签名 |
| -------------------------- | ------------------------------------ | ----------------------------------------- |
| function[0], function[1] | 导入的宿主函数 (无字节码) | 由 WIT 导入定义 |
| function[2] | `cabi_realloc` 空操作桩 | 空:push_frame -> pop_frame -> ret |
| function[3] | `dlmalloc` (malloc 核心) | 庞大,复杂的位操作 |
| function[4] | `dlfree` (free 核心) | 调用多个内部辅助函数 |
| function[5] - function[13] | dlmalloc 内部辅助函数 | 仅被 malloc/free 调用 |
| function[14] | `cabi_realloc` 导出桩 | 小型跳板 |
| **function[15]** | **Guest 应用逻辑 (`run()`)** | 包含 `call_indirect_host` 调用 |
| function[16] | `panic()` 处理程序 | 极小:`loop { spin_loop() }` |
| function[17] | `dlrealloc` (realloc) | 由 `cabi_realloc` 调用 |
| function[18] | `cabi_realloc` 包装器 | 轻量级跳板 -> function[17] |
### 跳板与内置函数
| 符号 | 用途 |
| ----------------------------------------- | ------------------------------------------------------------------ |
| `signatures[N]::wasm_to_array_trampoline` | Guest 到宿主的调用适配器 (guest 如何调用 WIT 导入) |
| `array_to_wasm_trampoline[N]` | 宿主到 guest 的入口点 (宿主如何调用导出的 Wasm 函数) |
| `component-lower-import[N]` | WIT 导入 lowering (每个导入的接口函数对应一个) |
| `component-trampolines[N]` | 组件模型调用适配器 |
| `wasmtime_builtin_memory_grow` | Wasm 线性内存扩展 |
| `wasmtime_builtin_memory_copy` | Wasm 线性内存拷贝 (memcpy) |
### 识别宿主调用
在 guest 应用函数 (例如 function[15]) 内部,`call_indirect_host` 指令代表对 WIT 导入的宿主函数的调用。操作数字节是宿主函数 ID:
```
dc 01 00 03 call_indirect_host #3 ; HOST IMPORT #3
```
这些 ID 映射到 `component-lower-import[N]` 条目,它们按声明顺序对应于 WIT 导入。例如,对于导入 `gpio.set-high`、`gpio.set-low`、`timing.delay-ms`:
| 宿主 ID | component-lower-import | WIT 函数 |
| ------- | -------------------------- | ------------------ |
| 0 | component-lower-import[0] | `gpio::set_high` |
| 1 | component-lower-import[1] | `gpio::set_low` |
| 2 | component-lower-import[2] | `timing::delay_ms` |
### 定位您的应用逻辑
1. 导航到 **function[15]** (或具有 `call_indirect_host` 指令的编号最大的非跳板函数)
2. 查找 `call_indirect_host` — 每一个都是一个 WIT 导入调用 (GPIO、timing 等)
3. 周围的寄存器设置 (`xconst8`、`xmov`) 用于加载参数 (引脚编号、延迟值)
4. 分支/循环结构 (`br_if32`、`jump`) 对应 Rust 的 `loop {}` 和 `if` 块
## 局限性
- **浮点/向量寄存器**:`f0`-`f31` 已在 `pspec` 中声明,但 SLEIGH 规范中尚未实现任何浮点指令。如果分析重度使用浮点的 Wasm 模块,请添加它们。
- **扩展操作码**:在约 310 个扩展操作码中,目前仅实现了 6 个 (trap, call_indirect_host, xpcadd, xmov_fp, xmov_lr, profile)。其他将命中 `undecoded_op`。
- **Pulley 版本**:目标为 `pulley-interpreter` v43.0.0。操作码编号可能会在未来的 Wasmtime 版本中发生偏移。
## 文档
`docs/` 目录包含补充文档:
- **[pulley-isa-reference.md](docs/pulley-isa-reference.md)** — 完整的操作码表,包含编码详细信息、操作数格式和 p-code 语义
- **[cwasm-internals.md](docs/cwasm-internals.md)** — ELF 节布局、符号表格式和嵌入式 cwasm 检测
- **[adding-new-opcodes.md](docs/adding-new-opcodes.md)** — 在 Wasmtime 添加指令时扩展 SLEIGH 规范的分步指南
- **[reverse-engineering-workflow.md](docs/reverse-engineering-workflow.md)** — 在 stripped 固件二进制文件中分析 Pulley 字节码的技术
## 参考文献
- [Wasmtime Pulley RFC](https://github.com/bytecodealliance/rfcs/pull/35)
- [pulley-interpreter crate (v43.0.0)](https://crates.io/crates/pulley-interpreter)
- [Ghidra SLEIGH 文档](https://ghidra.re/courses/languages/html/sleigh.html)
- [Wasmtime cwasm 代码内存](https://github.com/bytecodealliance/wasmtime/tree/main/crates/wasmtime/src/runtime/code_memory.rs)
## 许可证
MIT 许可证 — 版权所有 (c) 2026 Kevin Thomas (kevin@mytechnotalent.com)
标签:AI工具, cwasm, ELF64, Ghidra, ISA, JS文件枚举, Portable Bytecode Interpreter, Pulley, URL提取, VPS部署, Wasmtime, Wayback Machine, WebAssembly, 二进制分析, 云安全监控, 云安全运维, 云资产清单, 代码分析, 凭证管理, 反汇编, 反编译, 后台面板检测, 固件逆向, 域名枚举, 处理器模块, 字节码, 嵌入式安全, 软件分析, 逆向工程, 静态分析