跳过内容

MASTG-TECH-0112: 逆向工程 Flutter 应用

Flutter 是 Google 的一个开源 UI SDK,用于从单个代码库构建原生编译的移动、Web 和桌面应用程序。Dart 是 Flutter 中使用的编程语言,是其功能的关键,它提供了语言特性和性能优化,从而能够高效地开发高质量的跨平台应用程序。

Dart 快照是 Dart 程序的预编译表示,可以加快启动速度和高效执行。Flutter 应用程序开发重点在于 AOT(预先编译)快照,该快照用于所有 Flutter 移动应用程序。

由于以下几个因素,逆向工程 Dart AOT 快照存在重大挑战

  1. 独特的汇编代码:生成的汇编代码使用独特的寄存器、调用约定和整数编码,从而使分析变得复杂。
  2. 顺序类信息:必须按顺序读取 Dart AOT 快照中有关每个类的信息,从而阻止随机访问并使查找特定类非常耗时。
  3. 缺乏文档:Dart 快照格式缺乏全面的文档,并且随着时间的推移而不断发展,这增加了复杂性。
  4. 混淆和优化:Flutter 的构建过程可能包括混淆和优化技术,这些技术会阻碍逆向工程工作。

由于这些挑战,有效分析 Flutter 应用程序需要专门的工具和方法。

使用 Blutter

要使用 Blutter,您需要

  1. 提取 APK:解压缩 APK 文件并找到 libapp.so 文件。
  2. 执行 Blutter:使用 libapp.so 文件的路径运行 Blutter,并指定一个输出目录。
python3 blutter.py path/to/app/lib/arm64-v8a out_dir

Blutter 生成多个文件

  • asm/*:带有符号的汇编文件。
  • blutter_frida.js:用于检测应用程序的 Frida 脚本模板。
  • objs.txt:对象池中对象的完整嵌套转储。
  • pp.txt:对象池中的所有 Dart 对象。

asm/* 中的汇编文件包含具有名称的重建函数,从而可以更轻松地跟踪应用程序的逻辑。以下是 main 函数的摘录

  static _ main(/* No info */) async {
    // ** addr: 0x5961e0, size: 0x230
    // 0x5961e0: EnterFrame
    //     0x5961e0: stp             fp, lr, [SP, #-0x10]!
    //     0x5961e4: mov             fp, SP
    // 0x5961e8: AllocStack(0x28)
    //     0x5961e8: sub             SP, SP, #0x28
    // 0x5961ec: SetupParameters()
    //     0x5961ec: stur            NULL, [fp, #-8]
    // 0x5961f0: CheckStackOverflow
    //     0x5961f0: ldr             x16, [THR, #0x38]  ; THR::stack_limit
    //     0x5961f4: cmp             SP, x16
    //     0x5961f8: b.ls            #0x596400
    // 0x5961fc: InitAsync() -> Future<void?>
    //     0x5961fc: ldr             x0, [PP, #0x80]  ; [pp+0x80] TypeArguments: <void?>
    //     0x596200: bl              #0x3a5d48
    // 0x596204: r0 = ensureInitialized()
    //     0x596204: bl              #0x570d8c  ; [package:flutter/src/widgets/binding.dart] WidgetsFlutterBinding::ensureInitialized
    // 0x596208: r0 = init()
    //     0x596208: bl              #0x59a98c  ; [package:get_secure_storage/src/storage_impl.dart] GetSecureStorage::init
    // 0x59620c: mov             x1, x0
    // 0x596210: stur            x1, [fp, #-0x10]
    // 0x596214: r0 = Await()

虽然此代码不如典型的反编译 Java 代码那样容易理解,但仍然可以获得大量信息。在顶部,我们可以看到函数的名称 (main),以及函数在原始 libapp.so 二进制文件中的位置。不同的跳转指令 (bl) 附带符号信息,从而更容易理解代码正在做什么。例如,我们可以看到该应用程序首先确保正确初始化 Flutter 绑定 (WidgetsFlutterBinding::ensureInitialized),然后初始化 get_secure_storage 插件 (GetSecureStorage::init)。