type
status
date
slug
summary
tags
category
icon
password
 
 

0x01 引言

因为WMCTF2023的一道用Flutter写的Android游戏题而引出这篇文章,刚好一直想研究一下Flutter,这次趁这个机会研究一下,并且介绍一种基于Patch Flutter动态链接库实现辅助解析Flutter逆向工程符号的方法。这个方法理论上支持任意Flutter版本,而且具有高度定制化的特点,缺点是需要根据特定的版本重新编译,并且需要替换动态链接库,存在被检测到的可能。

0x02 Flutter

Flutter是谷歌使用Dart语言开发的高性能、跨端UI框架,可以通过一套代码,支持iOSAndroidWindows/MAC/Linux等多个平台,且能达到原生性能。 Flutter也可以与平台原生代码进行混合开发。
在开发中,Flutter应用会在一个 VM(程序虚拟机)中运行,从而可以在保留状态且无需重新编译的情况下,热重载相关的更新。对于发行版 (Release) ,Flutter 应用程序会直接编译为机器代码,即AOT(Ahead Of Time)。

# Flutter体系结构

Flutter体系结构是分层设计的,类似于Android的体系结构。
  • 最上层是Framework层,即Flutter框架层,开发者可以通过 Flutter框架层 与 Flutter 交互,该框架提供了以 Dart 语言编写的现代响应式框架。Flutter框架相对较小,因为一些开发者可能会使用到的更高层级的功能已经被拆分到不同的软件包中,使用 DartFlutter的核心库实现,
  • Flutter引擎是一个用于高质量跨平台应用的可移植运行时,由C/C++编写。它实现了Flutter的核心库,包括动画和图形、文件和网络I/O、辅助功能支持、插件架构,以及用于开发、编译和运行Flutter应用程序的Dart运行时和工具链。引擎将底层C++代码包装成 Dart代码,通过dart:ui暴露给 Flutter框架层。
  • Flutter可以通过一套代码在多个平台使用依靠着嵌入层,嵌入层采用了适合当前平台的语言编写,例如 Android使用的是 Java和 C++, iOS 和 macOS 使用的是 Objective-C 和 Objective-C++,Windows 和 Linux 使用的是 C++。嵌入层提供一个程序入口,程序由此可以与底层操作系统进行协调。
notion image
 

# Flutter App架构

notion image
 

#Flutter编译模式

 
Flutter使用Dart作为应用程序开发编程语言,因此Flutter的编译模式与Dart的编译模式相关。下面这张表总结了Dart的编译模式。
notion image
  • Script::最常见的JIT模式。就像Node.js一样,可以通过Dart VM命令行工具直接执行Dart源代码。
  • Script SnapshotJIT模式。与Script模式不同,Script Snapshot会将源代码打包成代码的Token形式,这可以节省了在编译时词法分析器所花费的时间。
  • Application SnapshotJIT模式。Dart的Application Snapshot有点像运行时的转储。它包含了从源代码解析的类和函数,所以运行时可以更快地进行加载和执行。但是这种快照与架构相关,在IA_32上生成的快照无法在X64平台上运行。
  • AOTAOT模式。在这种模式下,Dart源代码会被翻译成汇编文件,然后汇编文件由汇编器为不同架构编译成二进制代码。
对于Flutter,其在上述编译模式的基础上进行了调整
  • ScriptScript Snapshot:与Dart的模式一样,但Flutter从未使用过。
  • Kernel Snapshot:对应用代码进行中间字节码(Dart kernel格式)快照。通过避免Dart代码重新编译来实现移动端的快速启动,类似于Java字节码与JVM,核心快照是不依赖于体系架构的。
  • Core JITDart编译代码的一种二进制格式。程序数据和指令打包成特定的二进制格式,供 Dart运行时加载。实际上该模式 是一种 AOT 模式。
  • AOT Assembly:即Dart的AOT模式,完全AOT预编译的本地代码。
 
在开发阶段,开发Android App时,为了实现热重载技术加速UI的开发,Flutter在这个阶段使用Kernel Snapshot 模式,即核心快照模式。在编译生成的app-deug.apk 中的资源目录下存在isolate_snapshot_data vm_snapshot_data 以及kernel_blob.bin ,前两个文件分别用于加速isolate启动,加速dart_vm启动,最后一个文件为业务代码的字节码。在lib目录中还存在libflutter.so,即flutter动态链接库,与实际业务代码无关。
notion image
 
在发布阶段,在生产模式下应用程序需要更快地执行。因此,Flutter 在编译应用程序代码时选择了 AOT 模式。但是,由于平台特性不同,编译模式也有很大不同。我们可以将这些总结为一个表格。
notion image
在Android上,Core JITAOT Assembly 两种编译模式都支持。默认使用AOT Assembly 模式,将会生成libapp.so放入apk包中的lib目录下,由dart代码编译而来,除此之外还有libflutter.so,也就是flutter动态链接库,与业务代码无关。

0x03 Flutter逆向

 

# 快照

使用readelf -s命令读取保存快照信息libapp.so将会输出下面的内容
  • _kDartVmSnapshotData: 代表 isolate 之间共享的 Dart 堆 (heap) 的初始状态。有助于更快地启动 Dart isolate,但不包含任何 isolate 专属的信息。
  • _kDartVmSnapshotInstructions:包含 VM 中所有 Dart isolate 之间共享的通用例程的 AOT 指令。这种快照的体积通常非常小,并且大多会包含程序桩 (stub)。
  • _kDartIsolateSnapshotData:代表 Dart 堆的初始状态,并包含 isolate 专属的信息。
  • _kDartIsolateSnapshotInstructions包含由 Dart isolate 执行的 AOT 代码。
 
其中_kDartIsolateSnapshotInstructions 是最为重要的,因为包含了所有要执行的AOT代码,即业务相关的代码。
Dart VM中所有的代码都运行在一些isolate内,isolate可以看作是一个隔离的Dart执行环境,有自己的全局状态和通常自己的执行线程(mutator线程)isolate被组织成isolate group同一个组内的isolate共享同一个垃圾回收堆,用于存储该isolate组分配的对象。
 
在Flutter 中,不会使用多个isolate,除了始终存在的 VM isolate之外,只使用一个isolate
notion image
Isolate中维护了堆栈变量,函数调用栈帧,用于GC、JIT等辅助任务的子线程等, 而这里的堆栈变量就是要被序列化到磁盘上的东西,即IsolateSnapshot。此外像dart预置的全局对象,比如null,true,false等等等是由VMIsolate管理的,这些东西需序列化后即VmSnapshot最初快照不包括机器代码,但是后来在开发AOT编译器时添加了此功能。开发 AOT 编译器和带代码的快照的动机是允许在由于平台级别限制而无法进行 JIT 的平台上使用 VM。带代码的快照的工作方式与普通快照几乎相同,但略有不同:它们包含一个代码部分,与快照的其余部分不同,它不需要反序列化。此代码段的铺设方式允许它在映射到内存后直接成为堆的一部分dart源码中runtime/vm/app_snapshot.cc处理快照的序列化和反序列化
notion image
 

#一般逆向方法

一般情况下要想获取更多关于业务代码相关的信息,暂时只有两种方法
  • 编译修改过的libflutter.so并且重新打包到APK中,在启动APP的过程中,由修改过的引擎动态链接库将快照数据获取并且保存。本文主要采用这种方法。
上面的两种方法,分别对应着静态和动态,但是有着同样的缺点,高度依赖于引擎版本,不同版本的Dart引擎其快照格式不同,所以静态的方法就需要频繁跟着版本更新迭代,成本极高,而动态也需要重新编译对应版本的链接库。同时如果APP作者抹除版本信息和hash信息,则无从下手,而且重打包APK极易被检测到。
 
对于第一种方法相关项目有:
  • 更新:Blutter工具,挺好用,根据不同编码的引擎编译不同版本的工具静态dump出源码信息,无需运行时dumphttps://github.com/worawit/blutter
对于第二种方法相关项目有:
 

#编译Flutter动态链接库

以上两种方法有着各自的缺点,但是要选择肯定选择动态的,但是如果对应的Flutter版本reFlutter还未更新那就只能自己尝试Patch。
以下编译基于已经明确Flutter引擎使用的dart版本的前提,即APP并未抹除或修改snapshotHash的条件下。使用Ubuntu20.04 + Python3.8.8作为编译环境。
  • 要为特定 Flutter版本创建补丁,我们必须确定该版本中使用的 Dart SDK的特定版本。可以从下面的链接中找到对应的版本,在这里访问https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json 即可获取对应版本的DartSDK,但是在这之前需要知道Flutter版本。
    • 对于未抹除hash信息的libapp.so而言,可以在其二进制文件中找到关于快照版本的hash,即snapshotHash在这里是7dbbeeb8ef7b91338640dca3927636然后通过
      notion image
      通过搜索libflutter.so,找到flutter引擎的hash ,在这里是1ac611c64eadbd93c5f5aba5494b8fc3b35ee952
      notion image
      完成后就可以在reFlutter/scripts/enginehash.tmp.csv at main · Impact-I/reFlutter (github.com)中根据两个hash值准确定位到Flutter版本为3.13.0 对应的dart版本为3.1.0
  • 拉取depot_tools 工具并配置环境变量,其含有编译过程中必需的gclient
    • 创建准备放置自定义flutter引擎源码的文件夹并创建.gclient
      • 使用gclient sync 让他自己拉取相关源码
        • 完成后需要将源码切换到特定版本分支
          • notion image
        • 安装编译必需的依赖ninja-build
          • 这里只编译Android arm64 release 的版本,等待编译完成即可在myengine/src/out/android_release_unopt_arm64/lib.stripped 下找到去符号的libflutter.so
             
            💡
            可能出现的问题

            #Patch libflutter.so辅助逆向工程

            在这里给出两种Patch方案:分别来自于
            在这里给出两种方案的patch文件,可以根据下面的命令直接将patch应用到对应版本或者手动修改。
            首先是 Ostorlab的Patch方案,部分代码存在问题,无法在实际测试中工作,所以进行了部分调整。关键部分在于修改code->untag()->monomorphic_unchecked_entry_point_ 将其改为instructions_table_.rodata()->entries()[instructions_table_.rodata()->first_entry_with_code +instructions_index_ - 1].pc_offset; 即为了获取具体类中方法相对于_kDartIsolateSnapshotInstructions的偏移,否则会拿到在实际运行过程中内存地址。在reFlutter的Patch方案中也需要这样patch。
            reFlutter的Patch方案更加复杂(进行部分调整,否则会crash),但是Dump出的数据较为美观规范,但是会出现Dump不完整的情况。

            #更新:Blutter工具使用

            具体使用方法参见https://github.com/worawit/blutter,下面说明可能存在的问题及解决方案,下面的图为使用该工具静态dump出的相关信息导入IDA中的分析结果,较为直观。
            notion image
            环境需求:g++ ≥ 13 cmake>= 3.19 最好使用Kali 2022及以上或Ubuntu22.04及以上
            💡
            git工具无法稀疏clone --sparse 参数,提示error: failed to initialize sparse-checkout 解决方案:升级git到高版本,不使用稀疏clone将会导致一辈子都拉不下dart SDK源码
            💡
            CMake Error at CMakeLists.txt:3 (cmake_minimum_required): CMake 3.19 or higher is required. You are running version 3.16.3 解决:CMake 安装升级更高版本,CMake 3.19.2 or higher is required. You are running version 3.10.2

            0x04 IDA符号恢复

            对于第一种patch方案,编写了IDApython脚本利用dump出的数据(改名格式为json)进行符号恢复,对于第二种reFlutter的Patch方案则存在现成的脚本
            flutter-re-demo
            GuardsquareUpdated May 25, 2024
             

            0x05 参考

             
            1. Flutter Reverse Engineering and Security Analysis | by Ostorlab | Jun, 2023 | Medium | Medium
            1. The Engine architecture · flutter/flutter Wiki
            1. Impact-I/reFlutter: Flutter Reverse Engineering Framework
            1. flutter/flutter: Flutter makes it easy and fast to build beautiful apps for mobile and beyond
            1. dart-lang/sdk: The Dart SDK, including the VM, dart2js, core libraries, and more.
            1. Guardsquare/flutter-re-demo: Experiments on the feasibility of Flutter application reverse engineering
            1. Flutter 引擎编译、运行与调试 | Sunmoon的博客
            1. Flutter’s Compilation Patterns. People who built App with Flutter must… | by stephenwzl | ProAndroidDev
            1. Dart VM
            1. Flutter 架构概览 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter
            1. [原创]Flutter概述和逆向技术发展时间线,带你快速了解
            1. [原创]手把手教大家如何编译flutter的引擎文件libflutter.so(重置版本)
            1. Reverse engineering Flutter for Android – A Moment of Insanity
            1. 如何逆向Flutter应用(反编译) - 简书
            1. [译]Android环境下的Flutter逆向工程
            1. Reverse engineering Flutter apps (Part 1)
            1. Reverse engineering Flutter apps (Part 2)
             
            动态链接库与内存Dump结果修复CVE-2021-0928漏洞分析
            • Twikoo