executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
re-flutterFlutter/Dart AOT 逆向:libapp.so 快照分区解析、符号还原、Dart VM 动态分析。触发词:Flutter、Dart、libapp.so、AOT snapshot、kernel_blob、flutter_assets、Dart VM
Independent GitHub snapshot · Automated classification · Original documentation governs
lib/<abi>/libapp.so(Dart AOT 快照)与 libflutter.so(引擎)——业务逻辑在 libapp.soApp.framework/App(AOT 快照嵌入 Mach-O,Flutter.framework 为引擎)assets/flutter_assets/kernel_blob.bin(Dart kernel 二进制,含类/函数名与 AST)参考 [[re-analyze/platform-tips]]——Flutter 产物大(libapp.so 数十 MB 起),遵循「静态优先(大型样本)」:先静态定位、动态按需补充;动态(运行 / 重打包 App)默认在受控设备 / 模拟器快照内。
apt install python3 / dnf install python3 / pacman -S pythonchoco install pythonpython3 --versionapt install binutils;Fedora: dnf install binutils;Arch: pacman -S binutilsxcode-select --install)readelf --version && strings --version_kDart* 符号frida --version;frida-ps -U 列出设备进程pp.txt 且含目标 App 类名按顺序执行;每步产物(段提取 / 解析脚本 / hook 脚本 + sha256)存档。
识别构建模式(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 串搜可靠
assets/flutter_assets/kernel_blob.bin 存在(header 共 8 字节 = magic 0x90abcdef + formatVersion,Dart kernel 二进制,类/函数名与 AST 保留,还原成本低)→ 直接步骤 3_kDartIsolateSnapshotData(数据)/ _kDartIsolateSnapshotInstructions(指令)→ 步骤 2Payload/<App>.app/Frameworks/App.framework/App(业务)与 Flutter.framework/Flutter(引擎),Mach-O 内同段名,otool -l / strings 定位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 样本上找不到任何东西runtime/vm/snapshot.h、序列化器与 image snapshot 实现来读,而不是套一份写死的分区表_kDartIsolateSnapshotInstructions),与数据段分开;kind 为 full-aot 时即 AOT 快照_kDartVmSnapshot* 是 VM 快照(dart:core/io 等标准库,所有 Flutter App 共用),业务只在 _kDartIsolateSnapshot*strings -n 6 -t x snap.rodata | grep -E 'https?://|token|secret|api/|MethodChannel'
符号还原:
--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": {"混淆名": "原名"}} —— 反向映射表,查表即得原名
引擎与业务区分:
libflutter.so / Flutter.framework:Flutter 引擎(C++:Dart VM、Skia、通道实现),各 App 基本一致——不分析,仅用于取 VM 导出符号(步骤 5)libapp.so / App.framework/App:Dart 业务代码 AOT 快照——分析目标_kDartVmSnapshot*(VM 快照 / 标准库)≠ _kDartIsolateSnapshot*(业务):业务字符串与类名只在 isolate 快照动态侧(受控设备 / 模拟器快照内,[[re-analyze/platform-tips]] 默认沙箱):
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);
}
});
dart::Library::New(libflutter.so 内符号,随版本裁剪)或直接走下面的 VM Service 枚举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 元数据里类名与字符串仍可读_kDartIsolateSnapshotInstructions 基址 + 代码对象偏移),插桩底座见 [[re-frida]]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 里死找a/b 等短名或直接缺失;原因——--obfuscate 混淆名称 + tree-shaking 删除未引用代码;对策——构建侧有 --split-debug-info 映射文件就直接查表(步骤 3 JSON);没有映射则以 strings blob 的文案/URL 为锚交叉引用反推调用链;动态侧 hook 关键函数观察行为互补dart:: 内部函数或 _kDartVmSnapshot* 段(dart:core/io 标准库),时间空耗;原因——引擎层与 VM 快照是所有 Flutter App 共用代码,不是业务;对策——业务只分析 libapp.so 的 _kDartIsolateSnapshot*;libflutter.so 只取 VM 导出(步骤 4/5)frida-ps -U 报协议错误 / unable to communicate;原因——主机 frida-tools 与设备 frida-server 版本号不一致,或架构不符;对策——严格按 [[re-frida]] 工具准备:frida --version 对照下载同版本 frida-server,选对架构(arm64/arm/x86_64)gen_snapshot --print-object-layout-to 导出的权威偏移或实测为准,别在异版本上照抄死偏移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/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)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)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)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)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)CMP W0,W22; B.EQ fail)或 entry-null 弹窗构造 helper;patch 在某设备生效另一设备不生效时,用文案内容定位新触发的检查类别(如校验提示文案对应环境/模拟器检查)(来源:reverse-skills(inliver233),MIT)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 字节魔数判断,不要用字符串搜索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 与序列化器实现;机器码本体在独立指令段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)