Flutter / Dart AOT 逆向(快照解析 / 符号还原 / VM 动态分析)

SkillDev tools

Flutter/Dart AOT 逆向:libapp.so 快照分区解析、符号还原、Dart VM 动态分析。触发词:Flutter、Dart、libapp.so、AOT snapshot、kernel_blob、flutter_assets、Dart VM

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 / Dart AOT 逆向(快照解析 / 符号还原 / VM 动态分析) skill

What this skill tells your AI

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

何时使用 / 何时不用

  • 用: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 --versionfrida-ps -U 列出设备进程

blutter(AOT 快照自动还原,可选)

  • release libapp.so 的类/函数/对象池自动还原,安装与用法见 [[re-hybrid-app]] 工具准备;本技能步骤 2 以手动解析为核心方法,blutter 作自动化替代与核对
  • 验证: 输出目录出现 pp.txt 且含目标 App 类名

操作步骤

按顺序执行;每步产物(段提取 / 解析脚本 / hook 脚本 + sha256)存档。

  1. 识别构建模式(debug / release)

    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)

    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),与数据段分开;kindfull-aot 时即 AOT 快照
    • _kDartVmSnapshot* 是 VM 快照(dart:core/io 等标准库,所有 Flutter App 共用),业务只在 _kDartIsolateSnapshot*
    • strings blob 提取业务字符串:
      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):
      # 构建命令: 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):
      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 构建验证动态行为):
      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 & 0x7fPatchableBit = 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(压缩指针为 #3bit=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.hkMagicProgramFile),文件里显示为小端字节 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(kindfull-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)

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-flutter
Source
github.com/dslsdzc/rev-skills