droidsaw/droidsaw-hermes

GitHub: droidsaw/droidsaw-hermes

droidsaw-hermes 是一款纯 Rust 编写的 Hermes 字节码解析器和反编译器,支持将 React Native 的 HBC 字节码(v40–v100)还原为 JavaScript 源代码。

Stars: 0 | Forks: 0

# droidsaw-hermes 用于 [droidsaw](https://github.com/droidsaw/droidsaw) 工作区的 Hermes bytecode 解析器和反编译器。解析 Meta 的 React Native JS 引擎输出的格式,并将其反编译回 JavaScript。在解析时接受 v40 到 v100 的版本;v98 有两种不兼容的头部布局,在加载时均会被检测到。每个反编译的函数都会通过 OXC 重新解析;OXC 拒绝的输出会被加上注释并返回,绝不会静默丢弃。纯 Rust 编写,BSD-3-Clause 许可证。 ## Pipeline ``` parse → decode (bytecode → typed instructions; per-version operand schemas in src/decompile/schemas.rs) → cfg (basic blocks; exception handlers as separate predecessor map) → ssa (Braun CC 2013, iterative — no stack overflow on long chains) → optimize (copy propagation, constant folding, DCE, variable naming from LoadParam / GetById / CreateClosure) → structure (region-based; post-dominators via droidsaw_common::post_dominators_with_virtual_exit) → sugar (flatten_early_returns, recover_switch, recover_for_in, recover_try_catch, recover_destructuring, recover_class, linearize_async, strip_tdz_traps) → emit (Region IR → JS via oxc_codegen) → verify (syntactic via OXC; semantic via verify_body) ``` 算法代码位于 `droidsaw-common` 中,并基于 `Instr` trait 进行泛型抽象(CFG、支配节点、SSA 脚手架)。Hermes 特定的 `Insn` 类型以及各版本独有的 opcode / schema 表保留在此 crate 中(`src/opcodes.rs`、`src/decompile/schemas.rs`)。Opcode 的知识不会跨越 trait 边界。 ## 版本覆盖 解析时接受 HBC 版本 v40 到 v100。头部是一个基于版本条件判断的状态机——存在哪些字段取决于解析到的版本。`src/header.rs` 会首先读取版本,并分发到五种布局等价的变体之一: | 变体 | 版本 | 判别条件 | |---|---|---| | `PreV84` | v40..=v83 | pre-bigint,pre-`function_source_count`,16字节 `SmallFuncHeader` | | `V84to86` | v84..=v86 | 添加 `function_source_count` | | `V87to96` | v87..=v96 | 添加 `big_int_count` + `big_int_storage_size` | | `V97toV98Early` | v97 + v98 早期形式 | 将 `obj_value_buffer_size` 替换为 `obj_shape_table_count`;12字节 `SmallFuncHeader` | | `V98LateToV99` | v98 晚期形式 + v99..= | 添加 `num_string_switch_imms`;扩展 `param_count` 字段 | v98 早期与 v98 晚期形式通过偏移量 108 / 112 处的 `BytecodeOptions` 字节结合 `debug_info_offset` 消歧来检测。超出范围的版本会在解析入口处直接失败并返回 `HermesError::UnsupportedVersion { observed, supported_min, supported_max }`,在任何依赖布局的读取操作运行之前。验收测试请参见 `tests/version_dispatch.rs`。 往返(round-trip)生成功能已在 V84 / V96 / V98 / V99 版本上实现——`tests/hbc_corpus_roundtrip.rs` 会遍历所有四个版本桶。 ## Scanner `scanner::scan_parsed` 使用基于版本的 opcode 查找表进行单次 bytecode 扫描: - `string_refs[str_id]` → 引用该字符串的函数 - `call_graph[func_id]` → 直接调用的函数 ID - `closure_refs[func_id]` → 由该函数创建的闭包 操作数大小(1 / 2 / 4 字节)从版本表中读取;无需进行指令解码。通过 `fuzz_scan` 进行模糊测试。针对截断的 debug-info 流进行了加固。 ## 库 API 稳定的接口层位于 `src/lib.rs`: ``` use droidsaw_hermes::{parser, decompile, scanner}; fn main() -> Result<(), Box> { let data = std::fs::read("app.hbc")?; let hbc = parser::HbcFile::parse(&data, None)?; // Decompile one function. With emit_js=true, output is OXC-validated. let js = decompile::decompile_function(&hbc, &data, 0, true)?; println!("{js}"); // Scan: string ↔ function and call-graph indices. let _scan = scanner::scan_parsed(&hbc, &data); Ok(()) } ``` `decompile_function` 不会运行过程间的参数名恢复——`ipa::collect_param_names` 会遍历每个函数,如果按单个函数调用,成本会乘以 `function_count`。如需带有 IPA 恢复参数名的全量反编译,请使用 `decompile_bundle`。 ## 正确性 ### 往返等价性 `HbcFileEquiv`(`src/parser/round_trip.rs`)是作为往返等价规范的 `PartialEq` 实例——包含四个带版本标签的变体 `V84` / `V96` / `V98` / `V99`。商定律(自反性、对称性、传递性)在 `tests/quotient_laws_proptest.rs` 中独立进行了属性测试,以确保该关系是真正的等价关系。 在此基础上的锁机制: - `tests/roundtrip_hbc_proptest.rs` —— 256 个(默认)proptest 测试用例,对一个 v96 固定样本进行种子变异,在每个解析器接受的变异体上断言 `HbcFileEquiv`。 - `tests/hbc_corpus_roundtrip.rs` —— 受环境变量控制(`DROIDSAW_HERMES_V96_CORPUS`)的扫描,覆盖 `.hbc` 文件目录。在每个样本上断言 `HbcFileEquiv` 以及字节级完全一致的往返结果。未设置环境变量时会干净地跳过。 在公开的 v96 语料库样本上验证无误:头部、全局字符串表和函数表逐字节匹配。 ### 固定样本棘轮测试 语言覆盖率固定样本位于 `tests/fixtures/language_surface/` —— 包含 36 个条目,涵盖 arrow / async / await / class (static fields) / closure / coalesce / computed-property / destructuring / for-loop / generators / if-else / labeled break-continue / object spread / optional chaining / promise chain / regex (named groups) / rest params / spread / Symbol (iterator) / tagged template (raw) / template / try-catch / while-loop。每个条目都包含 `src.js` + `expected.txt`;`manifest.toml` 带有 `status` 字段。 `tests/fixture_ratchet.rs` 对每个条目运行 `hermesc(src.js) → HbcFile::parse → decompile_bundle → hermesc`,并断言 `RatchetResult::is_clean`。`SEMANTIC_FAIL` 保持在 0;`COMPILE_FAIL` 单调递减。固定样本状态的翻转将阻止合并。 多版本冒烟测试固定样本位于 `tests/fixtures/multi_version/{v40,v76,v96}/`;由 `tests/fixture_matrix_multi_version.rs` 驱动。 ### 对抗性模糊测试 libFuzzer 目标 (`fuzz/fuzz_targets/`): | 目标 | 测试面 | |---|---| | `fuzz_parser` | 针对任意字节的 `HbcFile::parse` | | `fuzz_opcode_decode` | 基于版本的指令流解码器 | | `fuzz_decode_source_locations` | 截断后的 source-locations 重新同步 | | `fuzz_cfg` | basic-block + exception-handler 图构建 | | `fuzz_ssa` | Braun SSA 构造 | | `fuzz_emit_roundtrip_hbc` | 插桩环境下的 `parse(emit_hbc(parse(bytes))) ≡ 首次解析` | | `fuzz_scan` | 字符串 / 调用图 / 闭包扫描器 | | `parser_differential` | 解析端与 oracle 的差异化测试 | | `cfg_differential` | CFG 端与 oracle 的差异化测试 | 四个解析与解码目标进行了长期的测试活动,未发生任何 panic 且无任何异常 artifact。 位于 `tests/fixtures/adversarial/` 下的对抗性语料库涵盖 `version_dispatch`、`bound_count_amplification`、`bigint_decimal_quadratic_bomb`、`object_shape_num_props_bomb`、`overflow_string_oor`、`file_length_disagree`,以及精简后的模糊测试发现的 OOM 种子(位于 `oom/` 下)。 ### 跨工具差异化测试 对比 `hbcdump`(Meta 官方反汇编器),已在两个维度上对 v96 语料库进行了比较: - **结构层面**(头部 + 全局字符串表 + 函数表,逐字节比较):公开的 v96 bundle 在字节级别完全一致。 - **指令层面**(在函数 ID 交集上,针对每个函数的 `(opname, operand_count)` 元组):跨 v96 bundle 比较了 12,000 个采样元组,opcode 分歧为零。 测试套件位于 `droidsaw-bench`(同级 crate,非运行时依赖)。 ### OXC 往返测试 `src/decompile/emit.rs` 会通过 `oxc_parser` 解析每个反编译后的函数,并通过 `oxc_codegen` 重新格式化。如果 OXC 拒绝该输出,则会预置诊断注释并返回原始输出。无效的 JavaScript 绝不会被静默输出。 `src/decompile/verify.rs` 是语义层面的配套验证:OXC 验证语法,而 `verify_body` 验证结构化输出中使用的每个名称在函数体中都有定义。导致产生语法有效但语义上存在自由变量输出的流水线错误,将以警告的形式呈现。 ### Kani `proofs/` 下的 8 个文件中共包含 20 个验证套件(受 `cfg(kani)` 控制): | Harness | 属性 | |---|---| | `decode_function_truncation` | unknown-opcode + truncated-instruction-stream 均产生类型化的 `HermesError` 而非静默 `break`(2 个 harness) | | `disambiguate_both_options_valid` | 通过 `BytecodeOptions` + `debug_info_offset` 进行 v98 early/late 检测是无歧义的(1 个 harness) | | `exception_count_cap` | function-header 的 exception-handler 数量受限于 parser 验证的 sections(3 个 harness) | | `function_get_overflow_oob` | `function_table[idx]` 越界访问产生类型化错误(4 个 harness) | | `literal_buffer_truncation` | 越界读取 literal-buffer 会产生类型化 `Err`,而非部分解码(4 个 harness) | | `overflowed_and_large_off` | 偏移量计算期间的字段溢出检测(4 个 harness) | | `read_operand_size_dispatch` | 指令流上基于版本的操作数大小分发(1 个 harness) | | `source_locations_resync` | source-locations 流在截断时确定性地重新同步(1 个 harness) | 每个非测试模块的编译时底线: ``` #![forbid(unsafe_code)] #![deny( clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unreachable, clippy::todo, clippy::arithmetic_side_effects, clippy::indexing_slicing, clippy::string_slice, clippy::as_conversions, // … plus the cast-class group (cast_possible_truncation, cast_sign_loss, …) )] ``` 完整代码块请参见 `src/lib.rs`。抑制标记位置带有 `// WHY:` 注释,说明了具体的边界(HBC 头部边界、parser 验证的 section 大小、JS 规范的算术限制)。在对抗性输入下不会发生任何 panic。 ## 输入 `HbcFile::parse(bytes, opts)` 接受原始 HBC 字节。顶层二进制程序会从 APK / XAPK 容器中提取 `assets/index.android.bundle`(以及 `*.hbc` 变体);此 crate 负责处理提取后的字节。字节缓冲区由调用者所有——`HbcFile<'a>` 借用该数据;内部不进行任何拷贝。 ## 性能 - `parser/round_trip.rs::parse_literal_buffer` 使用 `Vec::with_capacity(num_items.min(buf.len()))`。上限设为 `buf.len()` 是因为每个 literal 至少需要一个字节标签,因此对抗性的 `num_items = u32::MAX` 也不会分配超过输入大小的内存。 - `decompile/sugar.rs` 在 sugar 处理阶段使用 `Vec::with_capacity(stmts.len())`,因为结果 vector 的大小受限于输入语句数量。 - `decompile/optimize.rs` 在顺序无关紧要的路径上,针对以 SSA-id 为键的 map 使用 `FxHashMap`。 ## 工作区 - `droidsaw-common` —— 此 crate 进行参数化的通用 CFG / 支配节点 / SSA / 区域算法。 - `droidsaw-dex` —— 同级反编译器;共享来自 `droidsaw-common` 的相同流水线中间阶段。 - `droidsaw-apk` —— APK 容器解析;生成此 crate 消费的 `.hbc` 字节。 - `droidsaw` —— 顶层二进制程序;暴露 `hbc info` / `hbc functions` / `hbc strings` / `hbc decompile` / `hbc disassemble` 子命令。 ## 许可证 BSD-3-Clause。
标签:Hermes引擎, React Native, Rust, 云安全监控, 云资产清单, 反编译器, 可视化界面, 编译器工具链, 网络流量审计, 逆向工程, 通知系统, 静态分析