--- 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//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/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(',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)