Back to skills
SKILL.md
Re Flutter
ASecurityFlutter/Dart AOT 逆向:libapp.so 快照分区解析、符号还原、Dart VM 动态分析。触发词:Flutter、Dart、libapp.so、AOT snapshot、kernel_blob、flutter_assets、Dart VM
- 112 stars
- 0 votes
- 0 copies
- 3 views
- Added September 19, 2026
Works with
Security analysis
96/100- Uses curl or wget to download content
npx -y skills add dslsdzc/rev-skills --skill re-flutter --agent claude-codeAre you the author of Re Flutter?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/dslsdzc-re-flutter)---
name: re-flutter
description: Flutter/Dart AOT 逆向:libapp.so 快照分区解析、符号还原、Dart VM 动态分析。触发词:Flutter、Dart、libapp.so、AOT snapshot、kernel_blob、flutter_assets、Dart VM
capabilities: [hybrid-app-analysis]
---
# Flutter / Dart AOT 逆向(快照解析 / 符号还原 / VM 动态分析)
## 何时使用 / 何时不用
- 用:Android 包内发现 `lib/<abi>/libapp.so`(Dart AOT 快照)与 `libflutter.so`(引擎)——业务逻辑在 libapp.so
- 用:iOS 的 `App.framework/App`(AOT 快照嵌入 Mach-O,`Flutter.framework` 为引擎)
- 用:debug 构建的 `assets/flutter_assets/kernel_blob.bin`(Dart kernel 二进制,含类/函数名与 AST)
- 用:需要还原 Flutter App 的 Dart 业务逻辑(网络接口、签名/加密算法、协议、MethodChannel 通道)
- 不用:纯原生 Android App(无 libapp.so / kernel_blob,走 [[re-apk]])
- 不用:纯原生 iOS App(无 App.framework,走 [[re-ios]])
- 不用:Flutter Web(产物是 JS/WASM 而非快照,走 [[re-wasm]])
- 不用:引擎层本身(libflutter.so / Flutter.framework 内部逻辑,见步骤 4)
- 注意:动态步骤在受控设备 / 模拟器快照内执行([[re-analyze/platform-tips]] 最高原则);静态优先(大型样本原则)
## 工具准备
参考 [[re-analyze/platform-tips]]——Flutter 产物大(libapp.so 数十 MB 起),遵循「静态优先(大型样本)」:先静态定位、动态按需补充;动态(运行 / 重打包 App)默认在受控设备 / 模拟器快照内。
### python3(快照头部 / 分区解析主力)
- Linux: `apt install python3` / `dnf install python3` / `pacman -S python`
- macOS: 自带;Windows: 官方安装器或 `choco install python`
- 验证: `python3 --version`
### binutils(readelf / strings / objcopy,快照定位与段提取)
- Debian/Ubuntu: `apt install binutils`;Fedora: `dnf install binutils`;Arch: `pacman -S binutils`
- macOS: Xcode 命令行工具自带(`xcode-select --install`)
- Windows: 无自带 binutils——用 WSL,或 Ghidra 内置解析替代
- 验证: `readelf --version && strings --version`
### Ghidra(指令段反汇编 / 反编译与交叉引用)
- 装法与验证见 [[re-ghidra]];无 Ghidra 时 IDA / radare2 等价([[re-ida]] / [[re-radare2]])
- 验证: 导入 libapp.so 后能定位 `_kDart*` 符号
### frida(动态侧,Dart VM 入口 hook)
- 安装、frida-server 部署与版本匹配见 [[re-frida]] 工具准备(主机与设备版本必须一致)
- 验证: `frida --version`;`frida-ps -U` 列出设备进程
### blutter(AOT 快照自动还原,可选)
- release libapp.so 的类/函数/对象池自动还原,安装与用法见 [[re-hybrid-app]] 工具准备;本技能步骤 2 以手动解析为核心方法,blutter 作自动化替代与核对
- 验证: 输出目录出现 `pp.txt` 且含目标 App 类名
## 操作步骤
按顺序执行;每步产物(段提取 / 解析脚本 / hook 脚本 + sha256)存档。
1. **识别构建模式(debug / release)**:
```sh
unzip -l app.apk | grep -E 'kernel_blob|libapp|libflutter' # Android 容器内定位
file lib/arm64-v8a/libapp.so
readelf -sW lib/arm64-v8a/libapp.so | grep _kDart # release: AOT 快照符号
xxd -l 32 assets/flutter_assets/kernel_blob.bin # debug: 前 4 字节应为 ef cd ab 90(kernel magic 0x90abcdef 的小端)
readelf -sW libapp.so | grep -c _kDartIsolateSnapshot # AOT 段符号(data/instructions),比按 ASCII 串搜可靠
```
- debug: `assets/flutter_assets/kernel_blob.bin` 存在(**header 共 8 字节 = magic `0x90abcdef` + formatVersion**,Dart kernel 二进制,类/函数名与 AST 保留,还原成本低)→ 直接步骤 3
- release: 无 kernel_blob,业务代码在 libapp.so 的 `_kDartIsolateSnapshotData`(数据)/ `_kDartIsolateSnapshotInstructions`(指令)→ 步骤 2
- iOS: `Payload/<App>.app/Frameworks/App.framework/App`(业务)与 `Flutter.framework/Flutter`(引擎),Mach-O 内同段名,`otool -l` / strings 定位
2. **Dart AOT 快照分区解析(release)**:
```sh
objcopy --dump-section .rodata=snap.rodata libapp.so # 数据段(含 VM + isolate 两份快照)
objcopy --dump-section .text=snap.text libapp.so # 指令段
python3 - <<'PY'
import struct
data = open('snap.rodata', 'rb').read()
MAGIC = 0xDCDCF5F5 # Dart VM snapshot magic(int32;文件里是小端 f5 f5 dc dc)
KINDS = {0: 'full', 1: 'full-jit', 2: 'full-aot'}
off = 0
while True:
off = data.find(struct.pack('<I', MAGIC), off)
if off < 0: break
length, kind = struct.unpack('<qq', data[off+4:off+20]) # 基础头共 20 字节
print('snapshot @', hex(off), 'kind:', KINDS.get(kind, kind), 'length:', length)
off += 4
PY
```
- **头部格式**:`magic(4, 0xdcdcf5f5)` + `length(8)` + `kind(8)` = **20 字节基础头**(其后是哈希等字段)。**没有 ASCII `"SNAPSHOT"`,也没有固定 12 字节版本串**——按字符串搜 `SNAPSHOT` 在真实 AOT 样本上找不到任何东西
- **头之后是版本相关的内部序列化布局**(子 blob 的顺序与结构随 Dart 版本变化),**不要当稳定格式承诺**:要精确解析就对着目标 SDK 版本的 `runtime/vm/snapshot.h`、序列化器与 image snapshot 实现来读,而不是套一份写死的分区表
- 机器码本体在**独立的指令段**(`_kDartIsolateSnapshotInstructions`),与数据段分开;`kind` 为 `full-aot` 时即 AOT 快照
- `_kDartVmSnapshot*` 是 VM 快照(dart:core/io 等标准库,所有 Flutter App 共用),业务只在 `_kDartIsolateSnapshot*`
- strings blob 提取业务字符串:
```sh
strings -n 6 -t x snap.rodata | grep -E 'https?://|token|secret|api/|MethodChannel'
```
- 指令段导入 Ghidra:自动分析常认不出 AOT 代码对象,需按目标版本的代码对象元数据(子 blob 布局随版本变化,见上条)划分函数边界;反编译后按字符串交叉引用定位业务函数
- 自动替代:blutter(工具准备)还原 asm/对象池/类名;手动解析用于核对与版本兜底(坑 5)
3. **符号还原**:
- 构建侧留了映射(`--obfuscate --split-debug-info`):
```sh
# 构建命令: flutter build apk --obfuscate --split-debug-info=symbols/
ls symbols/ # app.android-arm64.symbols 等(每 ABI 一个)
python3 -c "import json; d=json.load(open('symbols/app.android-arm64.symbols')); print(len(d['data']))"
# JSON 结构: {"data": {"混淆名": "原名"}} —— 反向映射表,查表即得原名
```
- 无映射时字符串交叉引用反推:strings blob 里的 UI 文案 / 错误提示 / URL → Ghidra 交叉引用找引用指令 → 沿调用链上下游展开(方法对应 [[re-binary-core]] 交叉引用)
- debug kernel_blob.bin:kernel 格式含类/函数名与 AST,strings + 结构解析直接还原调用关系,无需符号表
4. **引擎与业务区分**:
- `libflutter.so` / `Flutter.framework`:Flutter 引擎(C++:Dart VM、Skia、通道实现),各 App 基本一致——不分析,仅用于取 VM 导出符号(步骤 5)
- `libapp.so` / `App.framework/App`:Dart 业务代码 AOT 快照——分析目标
- `_kDartVmSnapshot*`(VM 快照 / 标准库)≠ `_kDartIsolateSnapshot*`(业务):业务字符串与类名只在 isolate 快照
- 原生插件与 MethodChannel 原生端在业务 so 之外,走 [[re-binary-core]]
5. **动态侧(受控设备 / 模拟器快照内,[[re-analyze/platform-tips]] 默认沙箱)**:
- hook Dart VM 类库注册点——libflutter.so 导出的 `Dart_CreateRootLibrary`(root library 注册入口,onEnter 读库 URI):
```js
var f = Process.getModuleByName("libflutter.so").findExportByName("Dart_CreateRootLibrary");
if (f) Interceptor.attach(f, {
onEnter: function (args) {
var uri = args[1].readCString(); // 库 URI(C 字符串)
console.log("root library: " + uri);
}
});
```
- release AOT 下该 C API 可能不触发(库直接由快照反序列化)——改 hook VM 内部 `dart::Library::New`(libflutter.so 内符号,随版本裁剪)或直接走下面的 VM Service 枚举
- Dart VM Service 枚举 isolate / 库(release 需先开启 VM service:hook `Dart_SetVMFlags` / `Dart_InitIsolate` 注入 `--enable-vm-service`,或改用 debug 构建验证动态行为):
```sh
adb forward tcp:8181 tcp:8181
curl http://127.0.0.1:8181/getVM
curl "http://127.0.0.1:8181/getIsolate?isolateId=isolates/1"
curl "http://127.0.0.1:8181/getInstalledLibraries?isolateId=isolates/1"
```
- `getInstalledLibraries` 列出全部业务库(库名 = 包名 / 路径);`getObject` 按 objectId 枚举类/字段;VM service 元数据里类名与字符串仍可读
- 定位到目标函数后按地址 hook(`_kDartIsolateSnapshotInstructions` 基址 + 代码对象偏移),插桩底座见 [[re-frida]]
## 跨域联合
- [[re-mobile]]:移动 App 分析网关——Flutter 分支由 [[re-hybrid-app]] 与本技能承接(容器侧静态与动态跑通走网关)
- [[re-hybrid-app]]:Flutter 快照自动还原(blutter / reFlutter)与 MethodChannel 拦截——与本技能(手动快照解析 + VM 层动态)互补
- [[re-apk]] / [[re-ios]]:Android / iOS 容器侧静态(解包、manifest、签名、DEX/ObjC 原生桥接)
- [[re-frida]]:动态插桩底座(版本匹配、spawn/attach、反检测、脚本模板)
- [[re-binary-core]]:libapp.so / libflutter.so 的原生层([[re-format-elf]] + [[re-ghidra]] 反编译引擎与 JNI 桥)
## 常见坑与陷阱
- **release 没有 kernel_blob.bin**:现象——按 debug 教程找 `assets/flutter_assets/kernel_blob.bin` 找不到;原因——构建模式决定产物:**Debug → JIT(kernel 二进制,业务代码在 kernel_blob)**,**Profile 与 Release 都是 AOT**(业务代码在 libapp.so 的快照段)——**Profile 不靠 kernel_blob 执行业务代码**,它与 Release 的差别是 profiling/service/优化配置而非执行模型;对策——先按步骤 1 判模式:无 kernel_blob → 走步骤 2 快照分区解析,别在 assets 里死找
- **obfuscate/tree-shaking 后短名**:现象——快照里函数/类名全是 `a`/`b` 等短名或直接缺失;原因——`--obfuscate` 混淆名称 + tree-shaking 删除未引用代码;对策——构建侧有 `--split-debug-info` 映射文件就直接查表(步骤 3 JSON);没有映射则以 strings blob 的文案/URL 为锚交叉引用反推调用链;动态侧 hook 关键函数观察行为互补
- **引擎与业务代码混淆**:现象——分析深陷 libflutter.so 的 `dart::` 内部函数或 `_kDartVmSnapshot*` 段(dart:core/io 标准库),时间空耗;原因——引擎层与 VM 快照是所有 Flutter App 共用代码,不是业务;对策——业务只分析 libapp.so 的 `_kDartIsolateSnapshot*`;libflutter.so 只取 VM 导出(步骤 4/5)
- **frida 版本不匹配**:现象——`frida-ps -U` 报协议错误 / unable to communicate;原因——主机 frida-tools 与设备 frida-server 版本号不一致,或架构不符;对策——严格按 [[re-frida]] 工具准备:`frida --version` 对照下载同版本 frida-server,选对架构(arm64/arm/x86_64)
- **快照版本与解析脚本不匹配**:现象——分区偏移错位、strings blob 内容乱码或解析中断;原因——Dart 版本间快照布局有差异(版本串在头部可见),手工/第三方脚本只覆盖特定版本;对策——先读头部版本串;解析按版本分支;版本过新时以通用方法兜底(magic 扫描 + 分区语义推演)
- **hook 读 Dart 字符串读出乱码**:现象——hook 到 Dart 对象指针但打印乱码;原因——Dart 字符串不是 C 字符串(对象指针 +7 偏移处 4 字节 Smi 编码长度,右移 1 位为真实长度,+15 起为 UTF-8 数据,该偏移为社区实测值);对策——写 readDartString 工具函数按此布局读取并加长度上限(布局细节见 [[re-hybrid-app]] 坑条目);**偏移随 Dart 版本浮动**——以 `gen_snapshot --print-object-layout-to` 导出的权威偏移或实测为准,别在异版本上照抄死偏移
- **对象池反序列化当定宽读(VLE)**:现象——blutter 对 custom engine / 结构性重排的快照失败(`No cluster defined for cid N` 或空输出)后,自写解析器读出的 ref 错位、字符串全乱;原因——Dart AOT 对象池是带 tag 的条目数组(X27 持池基址,`LDR Xn,[X27,#off]` 加载第 off/8 号槽),填充区每条目 1 字节 type(旧版 `TypeBits = eb & 0x7f`,`PatchableBit = 0x80`),type 0(kTaggedObject)带 VLE ref(变长)、**type 1(kImmediate)载荷是定宽整型(intptr_t,非 VLE)**(常见坑);无符号 VLE 每字节 7 数据位、末段 bit7 置位(5→`0x85`,27673→`19 58 81`);**注意上述 type 编号/位宽为 Dart 2.9 时代布局,现行版本 TypeBits 位数与枚举序已变——解析前先按快照版本取布局表,别跨版本套用**;对策——VLE 读 length 开头后逐条按 type 解码,slot_offset = idx × 8;先用 2–4 个已知 ref→offset 锚点验证再全量信任,锚点不符时收窄扫描窗口重试;对象池即无符号二进制的符号表——`pool_offset → {加载它的所有 PC} → {所属函数}`,让「哪些函数触及字符串 ref N」可静态回答(来源:reverse-skills(inliver233),MIT)
- **strings 找不到中文文案**:现象——对 libapp.so 跑 `strings`/grep 找不到中文/CJK/emoji 业务文案,线索断掉;原因——Dart 快照字符串两种存储:OneByteString(Latin-1/UTF-8,可普通 grep)与 TwoByteString(**UTF-16LE**);中文/CJK/emoji 基本都在 TwoByteString,按 UTF-8 字节序列去匹配 UTF-16LE 存储当然不中;**CID 编号随 Dart 版本漂移**(旧版 85/86,现行 3.x 为 93/94/95 等),别写死 CID——从快照类表动态读;对策——对目标串同时搜 UTF-8 与 UTF-16LE 两种编码,记录 offset + 编码 + 出现次数;`--dump-utf16` 全量导出 CJK 串再 grep;对象池内函数 Code 与其加载的字符串相邻,无符号时可凭池内邻近性猜字符串↔函数归属(来源:reverse-skills(inliver233),MIT)
- **池槽字符串查不到交叉引用**:现象——已知字符串在池中的 offset,但反编译器的交叉引用列表为空,定位不到引用函数;原因——取池只有两种标准 idiom:`ADD xR,x27,#page,LSL#12` 后跟 `LDR xD,[xR,#off]`(offset=(page<<12)+off),以及 page==0 时的直接 `LDR xD,[x27,#off]`;通用 xref 分析对这类间接取址常漏报;对策——写模式扫描器用原始字节匹配两种 idiom(不需要反编译器),函数归属用 prologue 特征(`fd 79 bf a9 fd 03 0f aa`)反向扫描定位函数起点;先用一个已知加载点校准再信全量;**「扫描器没找到」≠「无加载者」**——非标准编码的加载会被漏掉,阴性结果需第二方法交叉验证;恰好只被一个函数加载的字符串是 feature-dedicated,是功能 UI builder 的强线索(来源:reverse-skills(inliver233),MIT)
- **build/虚方法 callers=0 误判死代码**:现象——对 build 或虚方法做 BL xref 扫描得到 0 个调用者,被误判为死代码放弃分析;原因——Dart build/虚方法不经 BL 调用,而是经分派 stub:读对象 cid → 查分派表 → `blr x30` 虚拟分派,静态 BL 扫描天然看不到;对策——callers=0 是常态,不作死代码证据;找调用者必须动态:在分派 stub 或目标函数 onEnter 捕获 `lr` + `Thread.backtrace(Backtracer.ACCURATE)` 定位分派点(「while rebuilding dirty elements」帧是框架渲染路径,业务 builder 已返回);stub 把 cid 放 x0(低 16 位)、对象指针在 x1,对象字段 @1 也存 cid 可兜底;拿到 cid 后静态搜 `movk xR,#<cid>,lsl#16` 找构造器,其 BL 调用者即父 build;X21 = 分派表基址,需覆盖全表时配合 cid→构造器静态映射(来源:reverse-skills(inliver233),MIT)
- **Dart bool 当 0/1 处理**:现象——反编译 AOT 代码时把 bool 当普通 0/1 整数读,逻辑翻来覆去对不上;原因——Dart AOT 固定寄存器约定:X15=shadow-stack、X26=Thread、X27=Pool、X22=null 哨兵、X28=堆基(压缩指针)、X21=分派表基;**Dart bool 是 canonical 对象的 tagged pointer,不是 0/1**——True 在 null 低位偏移、False 在更高偏移(非压缩指针下 True = null+0x20(低 16 位常形如 …8061)、False = null+0x30(…8071);**压缩指针(Flutter release 默认)为 True = null+0x10(…8051)、False = null+0x18(…8059)**;消费端 `tbz w0,#4`(压缩指针为 `#3`)**bit=0 进 true 分支**——判定位为 0 的是 True;对策——按固定寄存器语义读反汇编(`mov x0,x22` 返回 Dart null;标准 prologue `stp x29,x30,[x15,#-16]!; mov x29,x15`,字节 `fd 79 bf a9 fd 03 0f aa`);bool 编码可用指令频度破解:`add xR,x22,#0x20`(压缩指针 `#0x10`)出现数千次(True)、`add xR,x22,#0x30`(压缩 `#0x18`)同理(False);偏移/判定位随指针压缩与版本浮动,以实测或 `--print-object-layout-to` 为准;Thread 全局 `G = Thread[x26+0x80]`,字段按偏移读;分配 thunk 模式 `mov x2,#imm; movk x2,#cid,lsl16; ldr x4,[x26,#0x228]; br x4`(来源:reverse-skills(inliver233),MIT)
- **patch 物化 false 用 movz w0,#0 崩溃(SIGILL)**:现象——静态 patch 或 Frida 运行时内存写用 `movz w0,#0`(字节 52800000)物化「假」,运行时崩溃信号 4(SIGILL);原因——Dart bool 是 tagged pointer,0 是非法的 immediate tag;对策——物化 True 用 `add x0,x22,#0x20`(字节 c0820091)、False 用 `+0x30`(**压缩指针下为 True `+0x10`、False `+0x18`**);该规则同时适用于静态 patch 与 Frida 运行时内存写,写 Dart bool 一律写 canonical 地址,编码随指针压缩模式与版本浮动,以实测为准(来源:reverse-skills(inliver233),MIT)
- **弹窗 patch 无效(三源分诊)**:现象——NOP 掉所有 showDialog 调用后弹窗照旧,或弹窗约 60 秒定时出现,反复重试成时间黑洞;原因——Flutter 弹窗只有三个来源,误分诊是最大浪费:(a) 显式 showDialog BL;(b) 未捕获异常——MissingPluginException → FlutterError.onError/zone onError → **异步弹窗,不经任何 showDialog BL**;(c) 完整性聚合门——一个聚合函数 OR 约 7 个检查,任一为真就弹窗;对策——logcat + Dart 栈帧见 MissingPluginException/Unhandled Exception 且定时出现 → 源 (b),正解是截断 fault-body 构造器(把 body 构建的条件分支改为无条件跳过),全局 handler 无内容可显示,**停止 NOP 弹窗**;弹窗文案 → 池 ref → body builder → 调用者追踪,callers=0(cid 分派)且无 showDialog BL → 也是源 (b);源 (c) 找聚合函数(引用大量 check*/get* 通道方法字符串 + 条件 → 弹窗构建),NOP 其聚合失败分支(`CMP W0,W22; B.EQ fail`)或 entry-null 弹窗构造 helper;patch 在某设备生效另一设备不生效时,用文案内容定位新触发的检查类别(如校验提示文案对应环境/模拟器检查)(来源:reverse-skills(inliver233),MIT)
- **把 kernel 格式的 magic 当 ASCII `KERNEL`**:现象——按"文件头是 KERNEL"去 grep/xxd 判断 `kernel_blob.bin`,或按该串在容器里搜 kernel 文件,命中不了或命中的是别的东西;原因——Dart kernel 二进制的 magic 是**数值 `0x90abcdef`**(源码 `runtime/vm/kernel_binary.h` 的 `kMagicProgramFile`),文件里显示为小端字节 `ef cd ab 90`,**根本不是 ASCII 串**;header 共 8 字节(magic + formatVersion),其后还有格式版本号,可作为版本判别;对策——按 4 字节魔数判断,不要用字符串搜索
- **按 ASCII `SNAPSHOT` 找 AOT 快照**:现象——`grep SNAPSHOT`/`data.find(b'SNAPSHOT')` 在真实 libapp.so 上零命中,于是判"没有快照"或改用字符串猜偏移;原因——Dart VM snapshot 的头部是**数值 magic `0xdcdcf5f5`**(4B)+ length(8B) + kind(8B) 的 **20 字节基础头**,**不存在 ASCII `SNAPSHOT`,也没有固定的 12 字节版本串**;头之后的子 blob 布局是**版本相关的内部序列化实现**,不能当稳定格式套;对策——按 `0xdcdcf5f5`(小端 `f5 f5 dc dc`)定位并读 length/kind(`kind` 为 `full-aot` 即 AOT 快照),要精确解析就对着目标 SDK 版本的 `runtime/vm/snapshot.h` 与序列化器实现;机器码本体在独立指令段
- **无符号万级函数不知从哪下手**:现象——libapp.so 无符号、约两万函数,直接翻反汇编大海捞针;原因——无符号时函数边界、字符串归属、调用关系全靠静态重建,缺乏优先级信号;对策——用 prologue 特征字节(`fd 79 bf a9 fd 03 0f aa`)在 .text 内反向扫描(窗口约 0x6000–0x8000 字节)先恢复函数边界;每函数建特征向量:访问的字符串集(主导信号)、BL 目标/调用者 fan-in(`(word>>26)==0b100101` 解码 imm26)、gate 后分支点(TBZ/TBNZ/B.EQ)、null 比较(`CMP W0,W22`/`MOV X0,X22`)、bool 返回标志;四排序器把 2 万 → 短名单:关键字串短名单 → 共访问数(关键字类字符串 ≥2 个 → orchestrator)→ 双锚点集合交集(标题串+正文串的访问函数集相交 → 精确 builder,0 或 1 个函数)→ BL 调用 fan-in 向上迭代;**caller 计数 = 风险度量**——高计数(共享状态函数)避免作 patch 点,计数 1 的专用 handler 是安全目标;高 cmp + 条件分支数聚合得分用于浮现完整性门/聚合谓词(来源:reverse-skills(inliver233),MIT)
Attribution
Comments
Loading comments…