---
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）
