--- name: re-hybrid-app description: > Flutter/React Native 混合应用逆向。 触发词:Flutter、React Native、混合应用、dart、libapp.so、Hermes、Hermes 字节码、hbc、RN bundle、index.android.bundle、NativeModules、TurboModule、Metro capabilities: [hybrid-app-analysis] --- # 混合应用逆向(Flutter / React Native) ## 何时使用 / 何时不用 - 用:App 内发现 libflutter.so / libapp.so(Flutter)或 assets/index.android.bundle(React Native) - 用:需要还原混合框架的业务逻辑(dart 快照 / JS bundle) - 用:需要定位 JS↔原生桥接(MethodChannel / NativeModules)交换的敏感数据 - 不用:纯 Java/Kotlin 原生 App(走 [[re-apk]]) - 不用:纯原生 so 逻辑(走 [[re-binary-core]]) - 不用:需要运行时 hook——先静态定位再 [[re-frida]] ## 工具准备 静态分析可免沙箱([[re-analyze/platform-tips]] 最高原则);动态(reFlutter 重打包 / 运行)在受控设备 / 模拟器快照内。所有工具先验证再使用。 ### 引擎识别 —— jadx / unzip(复用 [[re-apk]]) - `unzip -l app.apk` 看 assets/ 与 lib/ 结构即可判引擎,跨 OS 安装见 [[re-apk]] 工具准备 ### blutter —— Flutter AOT 快照静态还原(libapp.so) - 安装: `git clone https://github.com/worawit/blutter && cd blutter`——C++20 项目,需较新编译器(g++>=13 / Clang>=16)+ cmake/ninja/pkg-config/libicu/libcapstone + pyelftools/requests - Debian/Ubuntu: `apt install python3-pyelftools python3-requests git cmake ninja-build build-essential pkg-config libicu-dev libcapstone-dev` - Fedora: `dnf install gcc-c++ clang cmake ninja-build pkgconf-pkg-config libicu-devel capstone-devel python3-pyelftools python3-requests git` - Arch: `pacman -S gcc cmake ninja pkgconf icu capstone python-pyelftools python-requests git` - macOS: `brew install cmake ninja pkg-config icu4c capstone` + `pip3 install pyelftools requests` - 用法: `python3 blutter.py libapp.so <输出目录>`(自动检测 Dart 版本,必要时自动下载 Dart 源码编译引擎)——目前支持 Android arm64 libapp.so 与较新 Dart 版本 - 产物: `asm/`(带符号反汇编)、`objs.txt`(对象池对象完整 dump)、`pp.txt`(对象池中所有 Dart 对象)、`blutter_frida.js`(Frida 脚本模板) - 验证: 输出目录出现 `pp.txt` 且包含目标 App 类名 ### reFlutter —— Flutter 动态(重打包 + 流量截获) - 安装: `pip3 install reflutter`(Python 3,依赖 frida-tools) - 用法: `reflutter main.apk` → 输入 Burp 代理 IP → 产出 `release.RE.apk` → uber-apk-signer 重签(`java -jar uber-apk-signer.jar --allowResign -a release.RE.apk`)→ 安装运行 - 产物: 运行后 `adb -d shell "cat /data/data/<包名>/dump.dart" > dump.dart`(类/函数清单 + 代码偏移) - 验证: `reflutter -h` 输出版本与用法;重打包后能安装运行并产出 dump.dart - 注意: Flutter >= 3.24 需手动在设备上设置代理(`adb shell settings put global http_proxy `) ### dart 工具链(理解快照结构,可选) - Arch: `pacman -S dart`(extra 官方仓库) - Debian/Ubuntu / Fedora: 官方仓库无 dart 包 → 用 Flutter SDK 自带(`snap install flutter` 或官网 SDK) - macOS: `brew install dart`(或 `brew install --cask flutter`);Windows: Flutter SDK zip - 验证: `dart --version` ### RN bundle 还原 —— hermes-dec(Hermes 字节码) - 安装: `pip3 install hermes-dec`(Python 3,纯标准库);Ubuntu 另有 `snap install hermes-dec`(snap 版命令带 `hermes-dec.` 前缀,如 `hermes-dec.hbc-disassembler`,pip 版无前缀);Arch AUR `yay -S hermes-dec` - 命令: `hbc-file-parser`(解析 HBC 头 / 版本 / 函数表)、`hbc-disassembler`(字节码 → .hasm 汇编)、`hbc-decompiler`(→ 伪 JS) - 验证: `hbc-decompiler --help` 可用;对样本文件能跑通三步 - 明文 JS bundle 美化: `pip install jsbeautifier`(Python 3,`js-beautify bundle.js`) ## 操作步骤 按顺序执行,每步产物(so / bundle / hasm / 反编译输出 + sha256)存档。 1. **引擎识别**: ```sh unzip -l app.apk | grep -E 'libapp|libflutter|assets/|bundle' file lib/arm64-v8a/libapp.so file assets/index.android.bundle # Hermes: "Hermes JavaScript bytecode, version XX" ``` - Flutter: `lib//libapp.so`(AOT 快照)+ `libflutter.so`(引擎)+ `assets/flutter_assets/` - RN: `assets/index.android.bundle`(明文 JS 或 Hermes HBC);RN 0.70+ 默认 Hermes - 其他: WebView 壳(assets/*.html)/ Cordova / uni-app——按 webview 路径处理 - 识别结论决定分支: Flutter → 步骤 2;RN → 步骤 3 2. **Flutter:libapp.so 快照分析(blutter)**: ```sh python3 blutter.py lib/arm64-v8a/libapp.so ./flutter_out grep -r "目标类名" ./flutter_out/pp.txt # 类/函数名在快照字符串表里 grep -rn "https://\|api\|token\|secret" ./flutter_out/asm/ | head ``` - blutter 产物给出 Dart 类/函数与对象池结构——业务逻辑按类名追踪(release AOT 保留 dart 层名称,见坑 1) - 动态补充: reFlutter 重打包 → dump.dart 拿运行期类/函数与偏移 → `frida -U -f <包名> -l frida.js` 结合 `_kDartIsolateSnapshotInstructions`(`readelf -Ws libapp.so`)定位 hook 点 3. **RN:bundle.js 提取与还原**: ```sh # 明文 JS unzip -p app.apk assets/index.android.bundle > bundle.js js-beautify bundle.js > bundle.pretty.js # Hermes 字节码 hbc-file-parser index.android.bundle > meta.txt hbc-disassembler index.android.bundle out.hasm hbc-decompiler index.android.bundle out.js # 线索提取 grep -aoE 'https?://[^"'"'"' ]+' out.js | sort -u # 端点半程 grep -aoE '"[A-Za-z0-9_/+=]{16,}"' out.js | sort -u # 疑似密钥 / base64 ``` - hasm 层面看函数调用关系(`LoadConstString` 与 `CallN` 配对);伪 JS 循环/条件不全时以 hasm + 字符串表为准(见坑 2) 4. **原生部分走 [[re-binary-core]]**: - `libflutter.so` / `libapp.so`(ELF)→ [[re-format-elf]] 解析 → [[re-ghidra]] 反编译引擎层 C++(dart:: VM、通道注册、Skia)与 JNI 桥接(FlutterEngine / PlatformChannel) - RN: `libhermes.so`(引擎,可选)、JNI 桥(`com.facebook.react` 包)→ 原生模块逻辑 - 反调试 / 加密 / 证书校验常在此层(如 BoringSSL verify 函数在 libflutter.so)——hook 点见步骤 2 5. **逻辑定位(桥接边界)**: - Flutter: 全局 grep MethodChannel 通道名(`flutter_` 前缀常见);PlatformChannel 收发处即敏感数据(token / 配置)交换点 - RN: NativeModules 注册表与 `requireNativeComponent` / `NativeModules.xxx`——JS 侧找不到的加解密在原生模块里(转步骤 4) - 从 UI 文案 / 网络字符串反查: 字符串表(Flutter 快照 / JS bundle)→ 对应函数 → 逻辑 ## Flutter MethodChannel 动态拦截 Flutter 平台通道(PlatformChannel)是 Dart ↔ native 通信主干,拦截可观察全部原生能力调用: - **Java/Kotlin 侧**:hook `io.flutter.plugin.common.MethodChannel` 的 MethodCallHandler——记录 channel 名 / 方法名 / 参数 JSON - **channel 名获取**:hook `MethodChannel.setMethodCallHandler`(onEnter 捕获 channel 实例,含名字)或 `MethodChannel` 构造器直接取 channel 名;更底层可 hook `BinaryMessenger.send`(首参即 channel 名) - **engine messenger 层**:`io.flutter.embedding.engine.FlutterEngine` 的 messenger 消息(低层兜底) - **与 Dart 侧静态观察互补**:静态找 channel 名与调用点(字符串字面量),动态确认实际流量 - **输出**:结构化 JSON(channel / method / args),供 [[re-analyze/analysis-contract]] 数据契约消费(证据存档) Flutter 专项详见 [[re-flutter]] ## React Native / Hermes(RN 字节码与桥接) ### 识别:HBC 与 RN 产物 - **`.hbc` / Hermes 字节码**:文件头 4 字节魔数 `c61fbc03` 类(小端),`file` 输出形如 "Hermes JavaScript bytecode, version XX";无魔数、以 `var __BUNDLE_START_TIME__` 等 JS 源码开头的是明文 bundle - **RN 应用结构**:`assets/index.android.bundle`(Metro 打包的业务 JS / HBC,RN 0.70+ 默认 Hermes 编译)、`lib//libhermes.so`(Hermes 引擎)、`libreactnativejni.so`(JNI 桥) - **Hermes 与标准 JS 引擎区别**:Hermes 是 AOT 预编译字节码(VM 指令、运行期不做 JIT 编译),标准引擎(JSC / V8 类)产物是明文 JS 源码包;二者决定后续走字节码反汇编还是纯 JS 恢复 ### Hermes 字节码:反汇编思路 - **结构**:文件头(魔数 + 版本号)→ 字节码段 → 函数表 / 字符串表;版本号在文件头,直接决定工具兼容性 - **反汇编**:hermesc 类工具(`-dump-bytecode` 系)或 hbc 反汇编类 Python 实现(见「工具准备」)→ 产出 hasm 汇编;字符串表可整体提取(跨段抓可打印串)先拿线索 - **函数表特征**:每条函数含 id / 偏移 / 指令数 / 参数个数——按调用关系(LoadConstString 与 CallN 配对)追踪业务入口 ### JS bundle 提取:Metro 结构与恢复 - **Metro 打包结构**:模块工厂经 `__d(factory, moduleId, deps)` 注册进全局注册表,`__r(moduleId)` 运行时 require;bundle 顶部有 `__BUNDLE_START_TIME__` 与 polyfill 前缀 - **恢复业务 JS**:美化(jsbeautify 类工具)→ 按 `__d(` 切分模块边界 → 每模块独立还原(工厂体 + 依赖数组)→ 混淆 / 字符串加密衔接 [[re-script-deob]] - **模块对应**:大块模块(字符串长 / 依赖多)常是业务主包,可对照页面功能缩小范围 ### 原生桥接:NativeModules / TurboModule - **JS 侧定位**:`NativeModules.xxx` 访问点 / `TurboModuleRegistry.get('Xxx')`——桥接名即模块名 - **原生侧映射**:JNI 桥 `com.facebook.react` 包下的模块注册表(`@ReactMethod` 注解类方法 / `getNativeModules()` 列表);TurboModule 接口方法签名与 JS 调用名一一对应 - **桥接边界即敏感数据交换点**:JS 侧找不到的加解密在原生模块(转 [[re-binary-core]]),与 Flutter MethodChannel 同理 ### 动态:hook JS 运行时([[re-frida]]) - **模块加载点**:hook `__d` / `__r` 可枚举全部已加载模块与 id、dump 模块工厂源码;运行期再注册的模块即远程下发(热更新)特征 - **console 输出**:hook console.log / warn 实现,拿业务日志与错误栈 - **桥接层兜底**:hook NativeModules 方法调用(入参 / 返回值),观察 JS↔原生交换数据 ### 坑与陷阱 - **Hermes 字节码不跨版本**:现象——同一样本在一种环境下反汇编正常,换版本或换工具输出乱码、直接报错;原因——HBC 格式随引擎版本演进、不保证向后兼容,反汇编类工具只覆盖特定版本区间;对策——先取版本(`file` 输出 / libhermes.so 内版本字符串 / 构建时间),选对应版本的 hermesc 类工具或更新反汇编工具,版本不匹配时优先换工具而非手动修字节 - **bundle 加密 / 混淆(Metro 加密层)**:现象——bundle 解出后是密文 / 乱码,或模块函数名不可读;原因——构建管线在 Metro 输出层加密(bundle 内嵌解密器,运行期解密)或启用混淆插件;对策——在 bundle 内找解密器特征(XOR 循环 / 自解密函数 / 硬编码密钥),静态还原解密器整体解密,或 [[re-frida]] 在解密函数返回处 dump 明文,混淆部分衔接 [[re-script-deob]] - **字节码与明文 JS 混合(部分页面走远程 bundle)**:现象——本地 bundle 是明文 JS 且内容正常,但关键页面逻辑缺失,或运行期出现本地没有的模块;原因——混合交付:部分页面打包进 assets,敏感 / 更新页走远程下发 bundle(热更新、灰度),本地包只含壳与加载逻辑;对策——先核对本地 bundle 完整性(`__d` 模块数与页面功能对比),抓包 / 文件系统快照归档远程 bundle,再按明文或 HBC 路径处理 ## 跨域联合 - [[re-mobile]]: 混合应用分支固定调用本技能(引擎识别后按框架分流) - [[re-apk]] / [[re-ios]]: 容器侧静态(manifest、签名、iOS 等价物) - [[re-binary-core]]: 原生部分——[[re-format-elf]] + [[re-ghidra]] 反编译 libapp.so / libflutter.so 与 RN JNI 桥 - [[re-frida]]: 运行时 hook(桥接层、证书校验、反检测) - [[re-analyze/platform-tips]]: 默认沙箱、工具解析 ≠ 加载器视图(快照偏移 / 工具版本差异) - 本技能被 [[re-analyze]] 的 triage「移动 App 分析」路径引用(re-mobile → re-hybrid-app) ## 常见坑与陷阱 - **Flutter AOT 快照无源码级符号**:现象——IDA/Ghidra 直接看 libapp.so 只见 dart:: 内部函数与裸偏移,业务函数名不可见;原因——AOT 快照没有 DWARF 符号,但 Dart 类/函数名实际保留在快照字符串表;对策——blutter 还原(pp.txt / asm/),字符串表 grep 定位类名再对照 asm 分析;不要在 ELF 符号表里找业务符号 - **RN Hermes 字节码难还原**:现象——hbc-decompiler 出的伪 JS 循环/条件全缺失、变量名成寄存器号,逻辑读不通;原因——HBC 是 VM 指令,反编译到源码级不完整;对策——hbc-disassembler 的 hasm 看调用关系(LoadConstString + CallN 定位字符串使用点),字符串表 / bundle 内明文(URL、错误提示)反查语义,必要时动态抓明文流量对照 - **引擎版本差异**:现象——blutter 报不支持该 Dart 版本 / hermes-dec 解析失败或错位;原因——工具只支持特定版本区间,App 用新引擎(Dart 3.x 新快照格式、Hermes 新 bytecode 版本);对策——先取版本(libflutter.so 内版本字符串 / `file` 输出 HBC version / 构建时间),更新工具(blutter 会按需下载对应 Dart 源码重编)或换支持该版本的专用工具 - **JS 与原生桥接边界**:现象——JS/bundle 里找不到加解密与密钥逻辑;原因——敏感逻辑在原生模块(RN NativeModules / Flutter MethodChannel 原生端);对策——按步骤 5 找桥接注册(通道名 / 原生模块表)→ 转 [[re-binary-core]] 分析对应 so;别在 JS 层死磕 - **混淆构建**:现象——函数名全是 a/b/c 或 hash;原因——Flutter `--obfuscate` / RN Hermes + 混淆选项构建;对策——仍可用的字符串(报错文案 / API 路径)与行为观察驱动,配合动态 [[re-frida]] hook 桥接层定位 - **Flutter SSL 校验抓不到包**:现象——抓包工具看不到 Flutter 应用流量或报证书错误;原因——Flutter 自带 SSL 校验(不走系统代理/证书信任链),通杀方案在 github 有开源代码;对策——先用通杀 Flutter SSL 校验方案或 frida 绕过抓包;blutter 还原后 libapp.so 符号可见,可直接基于地址 hook 目标函数(如签名函数 generateMD5 的入参/返回值拿盐值与明文)——不需要完整还原算法 - **Dart 字符串内存布局(frida 读串关键)**:现象——hook libapp.so 函数拿到指针却读不出字符串;原因——Dart 字符串不是 C 字符串:**指针 +7 偏移处 4 字节 Smi 编码长度(右移 1 位为真实长度),+15 偏移处为 UTF-8 数据**;对策——按此布局写 readDartStringExact 工具函数(加长度上限防误读),hook 入参/返回值都能还原明文 - **Hermes 函数表头是位域压缩而非结构体**:现象——按普通结构体读函数表,函数边界/名字/参数个数错乱,或把溢出函数的大头部偏移当普通字段读;原因——HBC 函数头把 10 个字段压进单个 128 位字(legacy 16 字节,版本 <97)或 96 位字(modern 12 字节,版本 >=97,RN 0.75+ 默认形态),位宽分配按版本不同;对策——先取版本号再选位域图;flags 位可辅助语义推断(bit0 strict、bit3 hasExceptionHandler、bit5 overflowed),overflowed 置位时内联头字段不可信,改按大头部拼装(legacy 为 (info_offset<<16)|offset、modern 为 (function_name<<24)|(offset&0xffffff));函数边界 = 头内偏移 + bytecode_size,全局代码即函数 0(来源:hermes-decomp(SymbioticSec),MIT) - **Hermes 小字符串表:u32 位域 + 哨兵溢出表 + run 编码 + packed 共享存储**:现象——字符串长度/内容错位、xref 反查到错函数;原因——每项 1 个 u32:bit0 为 UTF-16 标记、bits1-23 存存储偏移、bits24-31 存长度(最长 255),长度或偏移遇哨兵值(0xff / 0x800000)时真实值在溢出表中按 (offset,length) 对补充;字符串种类表是 run-length 编码(最高位 0=普通 String、1=Identifier,低 31 位为连续同类个数);对策——按位域解析并走溢出表,条目一律按表项偏移定位、绝不子串搜索(多个条目可能共享同一段存储 packed),写回路径同理;按 run 展开可区分代码字符串与标识符(标识符用于属性访问解析与命名提示)(来源:hermes-decomp(SymbioticSec),MIT) - **opcode 定义按版本漂移,builtin 索引只在发布边界变动**:现象——同一字节码换反汇编/反编译工具或版本后输出错位、遇未知 opcode 崩溃;原因——每个 HBC 版本一份指令表(40-99):opcode 编号、操作数类型序列(Reg8/Reg32/UInt8/Addr8 等)逐版本不同;对策——per-version 取表(缺失版本用「最近 <= 目标版本」的表降级并报告实际版本),未知 opcode 兜底为 而非越界;反汇编层先把语义变体归一化到抽象定义(Mov 两种编码、GetById 三种变体归并同一抽象操作),IR 层只处理抽象语义;跳转目标按 Addr8/Addr32 相对自身偏移计算(来源:hermes-decomp(SymbioticSec),MIT) - **变量还原不要上完整 SSA+Phi**:现象——反编译输出大量临时变量与边拷贝,可读性差;原因——目标语言 JS 没有 Phi,完整 SSA 的 De-SSA 阶段反而制造噪音;对策——用活区间分裂式还原:到达定义 + 并查集,两定义在同一使用处汇聚(if/else 合并点、循环回边)时并为一类,否则独立成变量,无 Phi;命名先做寄存器角色推断(用属性/方法词汇表反推类型:length/push/map/filter→Array、then/catch/finally→Promise、next/done/value→Iterator、toString/split/trim→String),名称优先级:调试段作用域名 > 解构键名 > 具名函数名 > 参数 > 类型兜底 > 编号,函数名过「有意义性」过滤(拒绝纯数字/生成名/通用核心名)(来源:hermes-decomp(SymbioticSec),MIT) - **Metro 模块工厂参数个数是版本信号**:现象——扫描 __d 注册识别不到工厂、工厂形参角色搞错,模块骨架还原失败;原因——classic bundle 工厂是 4 参(require/module/exports/deps),现代 bundle 是 7 参(含 importDefault/importAll);对策——param_count-1(去 this)即角色判定依据;匹配 __d(undefined, factory, moduleId, deps) 模式,参数经寄存器转发时维护「寄存器→函数/整数/数组」流内 map 做值追踪;导出分析分直接赋值/批量赋值/Object.defineProperty 三模式;模块命名走传播链(逆向 require 命名→再导出薄包装继承→单依赖链)比单点推断可靠(来源:hermes-decomp(SymbioticSec),MIT) - **字节码写回补丁:同长原位、变长搬迁、重叠拒绝**:现象——直接改 .hbc 字节后 bundle 运行崩溃、结构表错位;原因——字符串存储允许 packed(多条目共享同一段),偏移被多处引用,变长修改必须联动修正所有后续段;对策——只改字节码不重编 JS(HASM 往返:emit-hasm 出、改、asm 回);字符串补丁按长度分派:同长→原位覆写不动表,变长→重建字符串表+存储并搬迁尾部所有段(偏移联动修正);UTF-16 串、packed 重叠的串一律拒绝补丁;modern(>=97)只支持同长串补丁,变长串/函数 resize/注入 stub 扩容/从零 create 全部显式报错不写坏;补丁后重解析验证结构一致,不一致即拒绝输出(来源:hermes-decomp(SymbioticSec),MIT) - **Hermes 反编译的能力边界**:现象——反编译结果缺变量名、控制流失真,或期望直接拿回原源码;原因——反编译器无反混淆能力(混淆后只能得到混淆后的语义);发布包常裁剪调试段(offset=0 或 u32::MAX),变量名/行号全无;反汇编≠反编译——指令流只能给精确控制流与操作数,变量/结构/模块语义依赖 IR + 全程序分析;对策——「无调试信息」本身是分析结论,命名只能靠启发式;优先在字符串表里找编译期嵌入的源码/JSON/映射(如 RN Reanimated 的 Babel 插件把 worklet 原始源码作为普通字符串编进 .hbc 字符串表,按函数名 join 即得真源码),比反编译省力且保真(来源:hermes-decomp(SymbioticSec),MIT)