zboralski/unflutter

GitHub: zboralski/unflutter

Flutter/Dart AOT 快照的纯静态分析器,无需 Dart VM 即可从 libapp.so 恢复函数名、类层次、调用图和行为信号。

Stars: 58 | Forks: 8

# unflutter Flutter/Dart AOT 快照的静态分析器。从 `libapp.so` 中恢复函数名、类层次结构、调用图和行为信号,无需嵌入或执行 Dart VM。 ## 为什么不选 Blutter [Blutter](https://github.com/aspect-sec/blutter) 通过嵌入 Dart VM 本身来解决 Flutter 逆向工程问题。它调用 `Dart_Initialize`,从快照创建一个 isolate 组,并使用内部 VM API 遍历反序列化的堆。快照中的任何 Dart 代码都不会被执行。VM 纯粹用于自省。但这仍然意味着 Blutter 必须为每个目标版本编译一个匹配的 Dart SDK 并链接到 VM 内部。 unflutter 采取了不同的路径。没有 VM。无需编译 SDK。快照是一个具有已知语法的字节流。我们直接解析它。 权衡在于:Blutter 因为通过 VM 自身的代码路径进行反序列化而获得了完美的保真度。unflutter 则获得了可移植性、速度,以及无需构建任何特定版本的组件即可分析来自任何 Dart 版本的快照的能力。代价是必须在我们解析器中显式处理跨 Dart 版本的每个格式更改。没有可依赖的运行时。 ## 设计 约束消除。我们将快照视为确定性二进制语法。 ``` Omega = all possible interpretations of the byte stream C = { ELF invariants, snapshot magic (0xf5f5dcdc), version hash (32-byte ASCII), CID table (class ID -> cluster handler), cluster grammar (alloc counts, fill encoding), instruction layout (stubs + code regions) } R = Omega reduced by C ``` 每个约束都缩小了范围。ELF 验证排除了非 ARM64 的二进制文件。快照魔术字节排除了非 Dart 数据。版本哈希精确选择一个 CID 表和标签编码。集群分配计数固定了对象总数。填充解析恢复该固定总数内的字段值。经过所有约束后幸存下来的就是分析结果。 ``` if |R| == 0 → HALT: overconstrained (bug in our model) if |R| > 1 → HALT: underdetermined (missing constraint) if |R| == 1 → COMMIT: the answer ``` 没有启发式方法。没有运行时回退。没有超出约束的推断。 ## 工作原理 ### 快照重构 Dart AOT 快照 = 两阶段序列化:**alloc**(分配)然后 **fill**(填充)。 **Alloc** 按 CID 顺序遍历集群。每个集群声明存在多少该类的对象。这会为每个对象分配顺序引用 ID。目前只读取数量,不读取数据。 **Fill** 再次遍历相同的集群。这一次它读取实际的字段值:字符串字节、指向其他对象的引用 ID、整数标量。填充编码因对象类型和 Dart 版本而异。 我们从原始字节重放这两个阶段。alloc 阶段为我们提供对象普查。fill 阶段为我们提供名称、字符串和交叉引用。结合指令表(它将代码对象映射到其机器码偏移量),我们恢复了完整的函数名到地址的映射,这正是 Blutter 通过 VM API 所获得的。 ### 代码恢复 isolate 指令映像包含两个区域: **Stubs**(索引 0 到 `FirstEntryWithCode-1`):放置在用户代码之前的运行时跳板(类型检查处理器、分配 stub、分派辅助程序)。 **Code**(索引 `FirstEntryWithCode` 及以上):用户函数和框架代码。每个 Code 对象通过指令表映射到一个 PC 偏移量。 我们解析这两个区域,生成一个覆盖整个可执行范围的完整函数映射。 ### ARM64 反汇编和调用边 每个函数的代码字节都使用 `arm64asm.Decode` 逐指令解码。分支检测处理原始 32 位编码的 B、B.cond、CBZ、CBNZ、TBZ、TBNZ、RET。 **CFG 构建** 遵循 3 阶段算法: 1. 收集块首指令:指令 0、分支目标、终结符之后的指令 2. 排序并划分为基本块 3. 遍历块,根据终结指令计算后继边 **调用边提取** 区分两种类型: - **BL (直接调用)**:从 imm26 字段解码目标地址,通过符号映射解析为函数名 - **BLR (间接调用)**:通过 `RegTracker`(滑动窗口 W=8)解析目标寄存器来源 寄存器跟踪器追踪 BLR 目标寄存器如何获取其值: | 来源 | 模式 | 描述 | |------------|---------|-------------| | PP (对象池) | `LDR Xt, [X27, #imm]` | X27 是池指针。池索引 = byte_offset / 8 | | THR (线程) | `LDR Xt, [X26, #imm]` | X26 是线程指针。通过特定版本的偏移映射解析 | | 窥孔优化 PP | `ADD Xd, X27, #hi; LDR Xt, [Xd, #lo]` | 用于大池索引的双指令 PP | | 分派表 | `LDR Xn, [X21, Xm, LSL #3]` | X21 是分派表寄存器 | 每个 BLR 都会注释其来源(例如,`PP[42] Widget.build`、`THR.AllocateArray_ep`、`dispatch_table`)。 ### 图构建 调用边和 CFG 被转换为 [lattice](https://github.com/zboralski/lattice) 类型,这是一种与 SpiderMonkey-dumper(用于 JS 字节码分析)共享的、与架构无关的图 IR。lattice 库提供 DOT 渲染。 ### 反编译 (Ghidra + IDA) 两个反编译器共享一个通用的元数据管道。`flutter-meta` 生成包含函数名、类结构布局、THR 字段、字符串引用和指针大小元数据的 `flutter_meta.json`。每个反编译器的脚本都会使用此文件。 **Ghidra** (`unflutter decompile`) 运行无头管道: 1. 预脚本通过 `SpecExtension` 注册 `__dartcall` 调用约定(将 X15/X26-X28 标记为不受影响,清除临时寄存器) 2. 后脚本应用所有元数据: - 在所有已知函数地址处进行反汇编 - 创建/重命名函数 - 创建具有正确字段大小(压缩指针为 4 字节,否则为 8 字节)的 Dart 类结构类型 - 为 THR (X26) 访问创建 `DartThread` 结构(200 个字段) - 应用类型化函数签名(`this` 指针、参数计数、返回类型) - 为 THR 字段、PP 池引用和字符串字面量设置 EOL 注释 - **寄存器类型重设**:重命名 Dart 特定寄存器的反编译器变量,并将 X26 类型化为 `DartThread*`,从而启用结构字段解析: | 寄存器 | 变量 | 用途 | | -------- | ------------------- | ----------------------------------------------- | | X15 | `SHADOW_SP` | Dart 影子调用栈 | | X21 | `DT` | 分派表指针 | | X22 | `DART_NULL` | Dart null 对象 | | X26 | `THR` (DartThread*) | 线程指针,字段访问解析为名称 | | X27 | `PP` | 对象池指针 | | X28 | `HEAP_BASE` | 压缩指针基址 | | X29 | `FP` | 帧指针 | | X30 | `LR` | 链接寄存器 | **IDA** (`unflutter ida`) 通过 idalib 运行 (无头模式): 1. 生成包含所有结构类型的 C 头文件,通过 `idc_parse_types()` 一次性解析 2. 创建具有 Dart 已检查/未检查入口点拆分的函数(在元数据地址处拆分 IDA 合并的函数) 3. 通过 `apply_type()` 应用函数签名(IL2CppDumper 模式) 4. 设置重复注释(在 Hex-Rays 反编译器中可见) 5. Hex-Rays 寄存器类型重设(与 Ghidra 寄存器表相同) **Ghidra 与 IDA 输出质量对比:** Ghidra 在可读性方面胜出:结构字段解析(`THR->stack_limit` 对比 `THR + 72`),索引访问(`SHADOW_SP[-2]` 对比 `*(_QWORD*)(SHADOW_SP - 16)`),并且没有 `_QWORD`/`_DWORD` 转换。 IDA 在类型整洁性方面胜出:零 `undefined` 类型,零 `unaff_` 寄存器名,零警告。IDA 使用 `__int64` 和 `_QWORD` 转换,虽然冗长但类型正确。 THR 结构字段解析差距是 Hex-Rays 微码的一个限制。`set_lvar_type()` 不会重构反编译器的 AST 以使用结构成员语法。 ### 版本处理 | Dart | 标签样式 | 指针 | 关键变化 | |------|-----------|----------|------------| | 2.10.0 | CID-Int32 | 未压缩 | 4 个头字段,规范拆分前 | | 2.13.0 | CID-Int32 | 未压缩 | 5 个头字段,规范拆分 | | 2.14.0 | CID-Shift1 | 未压缩 | CID 移入 uint64 标签 | | 2.15.0 | CID-Shift1 | 未压缩 | 插入 NativePointer CID | | 2.16.0 | CID-Shift1 | 未压缩 | 添加 ConstMap/ConstSet | | 2.17.6 | CID-Shift1 | 未压缩 | 最后一个无符号引用版本 | | 2.18.0 | CID-Shift1 | 压缩 | 有符号引用,压缩指针 | | 2.19.0 | CID-Shift1 | 压缩 | 64 字节对齐 | | 3.0.5-3.3.0 | CID-Shift1 | 压缩 | 渐进式 CID 表更改 | | 3.4.3-3.10.7 | ObjectHeader | 压缩 | 新标签编码,记录类型 | 没有版本条件化的架构。版本哈希选择一个约束集。运行相同的管道。 ## 构建和安装 需要 Go 1.24+。一个外部依赖:`golang.org/x/arch`(ARM64 指令解码)。 ``` make build # build ./unflutter binary make install # install binary to /usr/local/bin, scripts to ~/.unflutter/ make test # run tests ``` Ghidra 集成需要带有 Jython 支持的 Ghidra 11.x。从 `GHIDRA_HOME`、`PATH` 或通用 brew 位置自动检测。 ## 用法 ### 完整管道 (默认) ``` unflutter libapp.so ``` 一次性运行 ELF 解析、反汇编、信号分析和元数据生成: ``` elf Dart SDK 3.10.7 code 284352 bytes at VA 0x569a8 instructions: 1465 entries (0 stubs + 1465 code) ranges: 1465 (0 stubs + 1465 code) classes: 402 layouts disasm 1465 functions, pool 1511 entries (1318 resolved) functions: 1465 -> samples/evil-patched.unflutter/asm call edges: 5937 (822 BLR: 757 annotated, 65 unannotated) string refs: 620 BLR annotation: 92.1% signal 71 signal + 1076 context, 4178 edges net: 40 url: 4 base64: 1 cloaking: 1 asm snippets: 1142 -> signal_graph.json (900218 bytes) -> signal.html (456296 bytes) -> signal.dot (5809 bytes) -> signal_cfg.dot (51 functions, 50855 bytes) -> signal.svg (18136 bytes) -> signal_cfg.svg (145979 bytes) meta 1465 functions focus: 71 signal functions (use --all for everything) dart: 3.10.7 ptr_size: 4 thr_fields: 272 classes: 402 layouts comments: 1363 from asm files string refs: +461 comments -> flutter_meta.json (577230 bytes) summary output: samples/evil-patched.unflutter dart: 3.10.7 functions: 1465 classes: 402 signal: 71 next open samples/evil-patched.unflutter/signal.html unflutter ghidra libapp.so --from samples/evil-patched.unflutter unflutter ida libapp.so --from samples/evil-patched.unflutter ``` 使用 `--quiet` / `-q` 抑制详细输出。使用 `--out` 设置输出目录 (默认:`.unflutter/`)。 ### 快速扫描 ``` unflutter scan libapp.so # print snapshot info ``` ### 仅信号 (跳过元数据) 默认管道已包含信号分析。使用 `unflutter signal` 运行相同的管道,但跳过元数据生成阶段: ``` unflutter signal libapp.so # default pipeline without meta unflutter signal libapp.so -k 3 # custom context depth (default: 2) unflutter signal libapp.so --from out/target # rerun signal from existing disasm ``` ### Ghidra 反编译 ``` unflutter ghidra libapp.so # full pipeline + Ghidra headless unflutter ghidra libapp.so --from out/target # reuse existing disasm output unflutter ghidra libapp.so --all # decompile ALL functions ``` ### IDA 反编译 ``` unflutter ida libapp.so # full pipeline + IDA idalib unflutter ida libapp.so --from out/target # reuse existing disasm output unflutter ida libapp.so --all # decompile ALL functions ``` ### 仅元数据 ``` unflutter meta libapp.so # full pipeline, produce flutter_meta.json unflutter meta --from out/target # regenerate from existing disasm ``` ### 输出产物 | 文件 | 描述 | |------|-------------| | `functions.jsonl` | 函数记录:名称、地址、大小、所有者、参数数量 | | `call_edges.jsonl` | 调用边:带有已解析目标和来源的 BL/BLR | | `classes.jsonl` | 类布局:字段、偏移量、实例大小 | | `string_refs.jsonl` | 来自 PP 加载的字符串引用 | | `dart_meta.json` | 快照元数据:Dart 版本、指针大小、THR 字段 | | `flutter_meta.json` | Ghidra/IDA 统一元数据:函数、类、THR 字段、注释 | | `asm/*.txt` | 每个函数带注释的 ARM64 反汇编 | | `cfg/*.dot` | 每个函数的控制流图 (使用 `--graph`) | | `callgraph.dot` | 完整调用图 (使用 `--graph`) | | `signal.html` | 行为信号报告 | | `decompiled/*.c` | Ghidra 反编译的 C 输出 | ## 架构 ``` internal/ elfx/ ELF validation, ARM64 symbol extraction snapshot/ Region extraction, header parsing, version profiles dartfmt/ Dart VM stream encoding (variable-length integers) cluster/ Two-phase snapshot deserialization (alloc + fill) disasm/ ARM64 decode, CFG, call edge provenance, register tracking callgraph/ Lattice graph builders (call graph + CFG) signal/ Behavioral string classification render/ HTML/DOT visualization output/ JSONL serialization ``` ### 管道 ``` libapp.so → ELF parse (elfx) → snapshot region extraction (snapshot) → header + version detection (snapshot) → cluster alloc scan (cluster) → cluster fill parse (cluster) → instructions table: stubs + code (cluster) → ARM64 disassembly + CFG (disasm) → call edge extraction with register tracking (disasm) → lattice graph construction (callgraph) → signal classification (signal) → Ghidra metadata + decompilation (ghidra-meta / decompile) → JSON / DOT / HTML artifacts ``` 每个阶段都是一个从字节到结构化数据的纯函数。没有可变的全局状态。没有 VM 运行时。相同的输入,相同的输出。 ## 已知限制 - **仅支持 AOT。** 不支持 JIT 模式。 - **仅支持 ARM64。** 不支持 x86 或 RISC-V。 - **无源码重构。** 输出是函数名、调用边、结构体、字符串,而不是 Dart 源码。 - **BLR 跟踪窗口。** 寄存器来源使用滑动窗口 (W=8)。超出窗口的复杂寄存器链未被解析。 - **Dart 2.12.x 未验证。** 无可用样本。 - **大型信号图。** 超过 1 MB 的 DOT 文件将跳过 SVG 渲染。Graphviz `dot` 使用 O(n²) 层次布局,在包含数千个节点的图上会挂起。对大型图请使用 `sfdp -Tsvg`。 - **必须为每个格式更改建模。** 没有运行时可以自动处理它。
标签:Android安全, AOT快照, ARM64, Call Graph, Dart, DAST, ELF解析, EVTX分析, Flutter, iOS安全, libapp.so, URL提取, 二进制分析, 云安全监控, 云安全运维, 云资产清单, 反编译, 可配置连接, 快照恢复, 恶意软件分析, 文件解析, 无VM分析, 日志审计, 目录枚举, 移动安全, 程序分析, 类层次结构, 跨版本分析, 逆向工程, 静态分析, 静态逆向