混合应用逆向(Flutter / React Native)

SkillDev tools

Flutter/React Native 混合应用逆向。 触发词:Flutter、React Native、混合应用、dart、libapp.so、Hermes、Hermes 字节码、hbc、RN bundle、index.android.bundle、NativeModules、TurboModule、Metro

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the 混合应用逆向(Flutter / React Native) skill

What this skill tells your AI

The instructions your AI receives, as published by dslsdzc/rev-skills in .claude/skills/re-hybrid-app/SKILL.md and read by ahel’s review.

何时使用 / 何时不用

  • 用: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 <ip:port>

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. 引擎识别

    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/<abi>/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)

    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 结合 _kDartIsolateSnapshotInstructionsreadelf -Ws libapp.so)定位 hook 点
  3. RN:bundle.js 提取与还原

    # 明文 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 层面看函数调用关系(LoadConstStringCallN 配对);伪 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/<abi>/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)

Signals

GitHub stars
57
Forks
8
Last commit
Sep 2026

ahel review

  • K1binfo
    installs-packages

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Catalog kind
skill
Gateway key
re-hybrid-app
Source
github.com/dslsdzc/rev-skills