GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

应用安全 --- 反向工程 之 Flutter exe app.so

 Dart 代码的本质就是html+js+css为一种代码风格并编译到apk和exe

当 Flutter 应用以 Release 模式 编译后,Dart 源码被 AOT(Ahead-Of-Time)编译成原生机器码:

安装包/
├── data/
│   └── app.so              ← 所有 Dart 业务代码(AOT 产物)

⚠️ 注意:虽然 Windows 上叫 .so,但它实际上是 ELF 格式(Linux shared object),只是 Flutter 团队没改扩展名。

用 aotopsy doctor 检测

aotopsy.exe doctor app.so

输出:
  ELF:        OK (13697968 bytes)
  Snapshot:    OK
  Dart:        3.10.7              Dart SDK 版本
  Pointers:    uncompressed (8 bytes)
  Support:     OK                   工具支持该版本
  Hash:        1ce86630892e2dca9a8543fdb8ed8e22  Snapshot 版本哈希
  Features:    product no-code_comments dwarf_stack_traces_mode 
               dedup_instructions no-tsan no-msan no-shared_data 
               x64 windows no-compressed-pointers

关键信息解读:

  • Dart 3.10.7 — 2024 年的稳定版
  • product — Release 模式编译
  • no-code_comments — 注释被移除
  • dedup_instructions — 指令去重
  • x64 windows — Windows x64 平台

2.3 ELF Section Headers

SectionOffsetSize内容
.rodata 0x340 4,800,656 B Dart Snapshot Data(对象池)
.text 0x498000 8,868,608 B Dart Snapshot Instructions(机器码)

两个段加起来 13.06 MB —— 这就是全部的 Dart 业务代码。

2.4 Dart Snapshot 统计

code:       8,840,856 bytes at VA 0x49EBE8
instructions: 39,594 entries (32,578 stubs + 7,016 actual code)
classes:    5,739 layouts          ← 类定义
scripts:    1,984 entries           ← Dart 源文件(含原始路径)
instances:  4,345 entries            ← 常量实例
type_arguments: 2,574 entries       ← 泛型参数
closure_data: 5,273 entries         ← 闭包
library_functions: 1,654 entries     ← 库函数
ffi_bridges: 1 entry                ← FFI 桥接

三、反编译工具选择

3.1 主流工具对比

工具语言平台Dart 版本功能推荐度
aotopsy Go Win/Mac/Linux 2.10 ~ 3.13 完整反编译 + 导出 Dart 源码 ⭐⭐⭐⭐⭐
darter C++ Windows (x64dbg 插件) Dart 2.x 仅提取函数名/结构 ⭐⭐⭐
Blutter C++ Windows/Mac Dart 2.x ~ 3.x 需要编译 Dart VM ⭐⭐⭐
unpackFlutter Python 跨平台 特定版本 静态解析 Class/Offset ⭐⭐
ddcov Go 跨平台 Dart 2.x 基础反编译 ⭐⭐
objdump + 手动 工具链 跨平台 所有 看汇编 + 字符串

3.2 最终选择:aotopsy v1.4.0

选择理由:

  1. 原生支持 Dart 3.10.7(正好匹配目标)
  2. 支持 x86_64(Windows 平台)
  3. export-dart --max 0 可以导出完整 Dart 源码
  4. -app-only 过滤掉 Flutter/Dart 框架代码
  5. 纯 Go 实现,无依赖,单文件 7 MB
  6. 准确率宣称 89.8% 函数名恢复 + 100% 有效 Dart 语法

3.3 下载地址

https://github.com/BroNils/aotopsy/releases
→ aotopsy_v1.4.0_windows_amd64.zip

四、完整反编译步骤

Step 1:下载并解压工具

# 下载
Invoke-WebRequest -Uri "https://github.com/BroNils/aotopsy/releases/download/v1.4.0/aotopsy_v1.4.0_windows_amd64.zip" `
                  -OutFile "aotopsy.zip"

# 解压
Expand-Archive -Path "aotopsy.zip" -DestinationPath "aotopsy"

Step 2:诊断扫描

aotopsy.exe doctor nthucc\data\app.so

必须成功才能继续。如果 Support 不是 OK,说明 Dart 版本太新/太旧,需要换工具或等 aotopsy 更新。

Step 3:导出完整 Dart 源码(核心步骤)

aotopsy.exe export-dart `
  -lib "c:\path\to\nthucc\data\app.so" `       # 输入文件
  -out "decompile_output\lib_full" `            # 输出目录
  -max 0 `                                       # 0 = 不限制数量(默认 500)
  -app-only                                      # 只导出业务代码(跳过 dart:* 和 package:flutter*)

参数详解:

参数作用不设的后果
-max 0 无限制导出所有函数 默认只导出 500 个(第一次就卡在这里了!)
-app-only 只导出用户代码 会混入 Flutter 框架的代码,体积大 10 倍

输出结果:

[export-dart] Loaded app.so (Dart 3.10.7, x86_64)
[export-dart] Total code entries: 39594
[export-dart] Successfully exported 11621 methods across 1478 classes into 202 .dart files

Step 4:(可选)完整信号分析

如果磁盘空间足够(> 500 MB),可以跑完整分析:

PowerShell
aotopsy.exe app.so                   # 全管线分析

这会生成:

  • signal.html — 交互式信号分析页面
  • signal_graph.json — 调用图
  • aotopsy.sarif — 静态分析结果
  • functions.jsonl — 所有函数列表
  • classes.jsonl — 所有类列表
  • call_edges.jsonl — 调用边
  • string_refs.jsonl — 字符串引用
  • disasm/ — 反汇编文件
  • 等等...

Step 5:(可选)直接分析 JSONL 数据

如果导出的 Dart 源码混淆严重,可以直接分析原始 JSONL:

PowerShell
# 找所有业务类
Select-String -Path "classes.jsonl" -Pattern "atlas|service|proxy" -CaseSensitive:$false

# 找所有 API 字符串
Select-String -Path "string_refs.jsonl" -Pattern "https?://"

# 找方法调用关系
Get-Content "call_edges.jsonl" | ConvertFrom-Json | Where-Object { $_.caller -match "MihomeCore" }

五、反编译结果解读

5.1 混淆现象

反编译后的类名变成了 3 字母混淆名:

混淆名真实身份恢复方式
idm.dart MihomeCoreService ✅ 日志字符串 "[MihomeCoreService]"
jcm.dart AppConfigService ✅ 日志字符串 "[AppConfigService]"
tam.dart UserProvider ✅ 日志字符串 "[UserProvider]"
Xcm.dart *****ConfigService ✅ 日志字符串 "[*****ConfigService]"
LZl.dart Main ✅ 日志字符串 "[Main]"

恢复策略:搜索 [ServiceName] 格式的日志字符串 —— 这是 Dart 开发者常写的 print("[ServiceName] xxx") 调试输出,而 aotopsy 会完整保留字符串常量。

5.2 反编译出的 Dart 代码结构

Dart
 
// 原始 Dart 代码(猜测)
class MihomeCoreService {
  Future<void> start() async {
    print("[MihomeCoreService] 启动核心...");
    final exitCode = await process.exitCode;
    print("[MihomeCoreService] 进程退出,退出码: $exitCode");
    if (consecutiveCrashes >= 5) {
      print("[MihomeCoreService] ⛔ 核心已连续崩溃 $consecutiveCrashes 次");
    }
  }
}
Dart
 
// aotopsy 反编译出的代码
class WB {  // ← 混淆后的类名
  dynamic _anonymous_closure__750708() {
    push(framePointer);
    while (true) {
      block_1:;
      final t1 = sub_71eb1c(arg0, arg1, local_16, local_24.f39);
      // ... SSA 寄存器分配 ...
      t5.f23 = "[MihomeCoreService] 进程退出,退出码: ";
      // ... 控制流恢复 ...
      if (unresolved_cond) {
        t10.f23 = "[MihomeCoreService] ⚠️ 核心崩溃 (连续第 ";
      }
    }
  }
}

差异说明:

原始 Dart反编译后原因
Future<void> start() dynamic _anonymous_closure__750708() async 编译成状态机
consecutiveCrashes >= 5 if (unresolved_cond) 复杂条件被编译器优化
process.exitCode local_16.f39 字段偏移恢复但语义丢失
await 语法 SSA 寄存器分配 async/await 编译成状态机闭包

5.3 能恢复的完整程度

✅ 完整恢复:

  • 所有函数体的控制流(if/else/for/while/try/catch)
  • 所有字符串常量
  • 所有类名、方法名(但被混淆)
  • 所有字段访问偏移
  • 异常处理逻辑
  • 闭包结构

⚠️ 部分恢复:

  • 变量类型(需要从类型推断恢复)
  • 泛型参数(会变成 <dynamic>
  • async/await 结构(编译成状态机,但控制流完整)

❌ 无法恢复:

  • 原始变量名(编译器会做寄存器分配)
  • 注释
  • 完整 Widget 树(需要 Flutter Inspector 配合)

5.4 项目原始路径

dart_plugin_registrant.dart 中恢复出开发者本地路径:

Dart
 
// Library: file:///D:/a/atlas-app/atlas-app/.dart_tool/flutter_build/dart_plugin_registrant.dart

→ 项目名叫 atlas-app,在 D:\a\atlas-app\atlas-app\ 下开发


六、aotopsy 工作原理(深入)

6.1 处理管线

 
app.so (ELF)
  │
  ▼
┌─────────────────────────────────────┐
│ 1. ELF Parser                        │
│    解析 .rodata / .text section      │
└──────────────┬──────────────────────┘
               ▼
┌─────────────────────────────────────┐
│ 2. Snapshot Extractor                │
│    找到 _kDartIsolateSnapshotData    │
│    和 _kDartIsolateSnapshotInstr     │
└──────────────┬──────────────────────┘
               ▼
┌─────────────────────────────────────┐
│ 3. Cluster Allocator                 │
│    解析对象池(字符串、类、常量)      │
│    重建 CID → 对象映射表              │
└──────────────┬──────────────────────┘
               ▼
┌─────────────────────────────────────┐
│ 4. Instructions Table                │
│    解析代码段,提取所有函数范围        │
│    恢复 method name / offset 映射     │
└──────────────┬──────────────────────┘
               ▼
┌─────────────────────────────────────┐
│ 5. x86_64 Disassembler               │
│    反汇编每个函数的机器码             │
└──────────────┬──────────────────────┘
               ▼
┌─────────────────────────────────────┐
│ 6. Control Flow Graph (CFG)          │
│    恢复分支/跳转/异常处理结构         │
└──────────────┬──────────────────────┘
               ▼
┌─────────────────────────────────────┐
│ 7. SSA Register Value Graph          │
│    寄存器分配恢复为 SSA 形式          │
│    每个物理寄存器 → 唯一 SSA 值槽     │
└──────────────┬──────────────────────┘
               ▼
┌─────────────────────────────────────┐
│ 8. Type Inference                    │
│    从常量实例推断字段类型             │
│    解析间接调用目标                   │
└──────────────┬──────────────────────┘
               ▼
┌─────────────────────────────────────┐
│ 9. Dart Synthesizer                  │
│    SSA → Dart 伪代码                 │
│    恢复 async/await / closure 结构   │
│    尝试恢复惯用模式                   │
└──────────────┬──────────────────────┘
               ▼
         *.dart 文件

6.2 Snapshot 版本哈希的作用

每个 Dart SDK 版本的 Snapshot 格式略有不同,版本哈希用于识别格式:

 
Dart 3.10.7 → hash 1ce86630892e2dca9a8543fdb8ed8e22
aotopsy 对照这个哈希选择正确的解析器版本

6.3 为什么 x86_64 反汇编效果好于 ARM64

Dart AOT 在 ARM64 上使用自定义寄存器约定:

  • R15 = Dart VM 栈指针
  • R27 = 对象池指针
  • R21 = 分发表寄存器

而 x86_64 上 Dart AOT 的寄存器使用更接近标准调用约定,反汇编分析器更容易理解。


七、完整命令速查表

PowerShell
# === 诊断 ===
aotopsy.exe doctor app.so

# === 完整分析(生成 HTML/JSONL/反汇编) ===
aotopsy.exe app.so --quiet

# === 导出 Dart 源码 ===
aotopsy.exe export-dart -lib app.so -out ./lib_full -max 0 -app-only

# === 导出所有(含 Flutter 框架) ===
aotopsy.exe export-dart -lib app.so -out ./lib_all -max 0

# === 按关键字过滤 ===
aotopsy.exe export-dart -lib app.so -out ./lib_filtered -filter "Mihome" -max 0

# === Ghidra/IDA 集成 ===
aotopsy.exe ghidra app.so
aotopsy.exe ida app.so

# === 生成 Flutter meta ===
aotopsy.exe meta app.so

八、常见问题

问题原因解决
export-dart 只导出 500 个 默认 -max 500 -max 0
导出的类名是 3 字母 Dart AOT 编译时会混淆 搜索字符串恢复:grep -r "$$ServiceName$$"
support: NO Dart 版本太新/太旧 换匹配版本的工具(查 aotopsy README)
磁盘空间不够写 signal_cfg.dot 完整分析会生成大量中间文件 清理磁盘或只跑 export-dart
反编译出 unresolved_cond 条件表达式被编译器优化 查看汇编或类型推断
async/await 变成状态机闭包 Dart 编译特性 aotopsy 尝试线性化,但不完美

九、输出文件清单

完整反编译结果在 decompile_output/

 
decompile_output/
├── lib_full/                          ← ✅ 反编译出的 Dart 源码(完整)
│   ├── app/app.dart                   ← 核心业务(MihomeCoreService, UserProvider 等)
│   ├── D/a/atlas-app/atlas-app/...   ← 插件注册(含项目路径)
│   ├── idm.dart                       ← MihomeCoreService
│   ├── jcm.dart                       ← AppConfigService + SSPanelConfig
│   ├── tam.dart                       ← UserProvider
│   ├── Xcm.dart                       ← *****ConfigService
│   ├── LZl.dart                       ← Main 主入口
│   ├── Rem.dart                       ← WindowsTitleBar
│   ├── mam.dart                       ← NodeSelection
│   ├── Ocm.dart                       ← PpanelSubscriptionState
│   ├── Rcm.dart                       ← PreProxyService
│   ├── Uam.dart                       ← IPv6
│   └── ~195 个其他 .dart 文件          ← 页面/Widget/模型
│
├── app.aotopsy/                       ← 完整分析输出(因磁盘空间只生成了部分)
│   ├── signal.html                    ← 交互式信号分析
│   ├── signal_graph.json              ← 调用图
│   ├── functions.jsonl                ← 39,594 个函数列表
│   ├── classes.jsonl                  ← 5,739 个类定义
│   ├── call_edges.jsonl               ← 调用边
│   ├── string_refs.jsonl              ← 字符串引用
│   ├── network_endpoints.jsonl        ← 网络端点
│   ├── plugins.jsonl                  ← Flutter 插件
│   ├── deobfuscation.jsonl            ← 混淆映射
│   ├── dispatch_table.jsonl           ← 分发表
│   └── asm/                           ← 反汇编(每个函数一个文件)
│
└── 早期提取的辅助文件
    ├── all_strings.txt                ← 34,773 个可读字符串
    ├── all_urls.txt                   ← URL/API 端点
    ├── business_classes.txt            ← 1,079 个业务类名
    └── packages.txt                   ← Dart package 引用


 

为什么其他反编译工具需要 Dart VM

这是一个很好的架构问题。答案分两层:格式问题和工程选择。


1. Dart AOT 快照格式没有公开规范

Dart 的 AOT 快照(libapp.so 里的 snapshot region)不是一个文档化的二进制格式。它本质上是 Dart VM 堆的序列化内存镜像:

text
snapshot_region
├── cluster alloc phase   ← 每个 Cluster 对应一种 VM 内部 CID(Class ID)
│   └── 每个 CID 的对象数量、字段布局...
└── cluster fill phase    ← 字段值、字符串、引用关系...

这个格式:

  • 随每个 Dart 版本变化(有时每个小版本都变)
  • 没有版本号字段,只有一个 git-hash 派生的 magic,需要逆向对应
  • 字段布局依赖 VM 内部 RawObject 结构体,这些结构体在 C++ 头文件里,不是公开 API

Blutter 的思路是:既然格式由 VM 定义,就直接用 VM 来解析。编译匹配版本的 Dart SDK,把 VM 嵌进去,让 VM 自己反序列化快照。这样格式变化自动跟进,不需要手动建模。

text
Blutter 的路径:
libapp.so → 找到 Dart 版本 → 编译对应 SDK → 嵌入 VM → VM.ReadSnapshot() → 完美还原

代价很明显:

  • 每个目标版本都要编译一次 Dart SDK(几十分钟)
  • 只支持 ARM64(VM 只在 ARM64 模式下能跑对应快照)
  • 需要本地有 Dart SDK 工具链

2. AOTopsy 的选择:直接建模格式

AOTopsy 选择不依赖 VM,而是把格式作为"确定性二进制语法"来逆向建模:

Go
// 例:cluster alloc 阶段,按 CID 分派到对应的结构体
func (r *Reader) allocCluster(cid ClassID) (Cluster, error) {
    switch cid {
    case CIDFunction:
        return r.allocFunctionCluster()
    case CIDClass:
        return r.allocClassCluster()
    // ... 每个 CID 单独建模
    }
}

每个 Dart 版本需要维护:

  • CID 表(哪个数字对应哪个 VM 内部类)
  • THR 字段偏移(internal/vmtables/
  • 字段布局变更(internal/snapshot/ 版本 profile)

这样的好处:

  • 纯静态,零依赖,不需要编译任何 SDK
  • 支持 x86_64(VM 路径做不到)
  • 速度快,整个分析是一次 pass

代价就是 README 里诚实说明的:每次 Dart 格式变化都要手动建模,没有 VM 兜底。


3. 为什么 VM 路径是"正确但昂贵"的

 Blutter(VM 路径)AOTopsy(直接解析)
格式保真度 100%(VM 自己解析) ~90%(建模覆盖范围内)
新版本适配 编译新 SDK 即可 需要手动更新模型
架构支持 ARM64 only ARM64 + x86_64
运行依赖 Dart SDK 工具链
分析速度 慢(SDK 编译)
反编译输出 无(依赖 Ghidra) 原生 Dart 伪代码

一句话总结

其他工具用 VM,是因为 Dart AOT 快照格式本质上就是 VM 的内部序列化格式,没有独立规范。用 VM 解析是最省力的正确做法。AOTopsy 选择直接建模这个格式,换取零依赖和跨架构能力,代价是必须跟踪每个版本的格式变化。

 

posted on 2026-09-08 11:10  GKLBB  阅读(18)  评论(0)    收藏  举报