type
status
date
slug
summary
tags
category
icon
password
0x01 引言0x02 Flutter# Flutter体系结构# Flutter App架构#Flutter编译模式0x03 Flutter逆向# 快照#一般逆向方法#编译Flutter动态链接库#Patch libflutter.so辅助逆向工程#更新:Blutter工具使用0x04 IDA符号恢复0x05 参考
0x01 引言
因为
WMCTF2023
的一道用Flutter
写的Android
游戏题而引出这篇文章,刚好一直想研究一下Flutter,这次趁这个机会研究一下,并且介绍一种基于Patch
Flutter
动态链接库实现辅助解析Flutter逆向工程符号的方法。这个方法理论上支持任意Flutter版本,而且具有高度定制化的特点,缺点是需要根据特定的版本重新编译,并且需要替换动态链接库,存在被检测到的可能。0x02 Flutter
Flutter
是谷歌使用Dart语言开发的高性能、跨端UI框架,可以通过一套代码,支持iOS
、Android
、Windows/MAC/Linux
等多个平台,且能达到原生性能。 Flutter
也可以与平台原生代码进行混合开发。在开发中,
Flutter
应用会在一个 VM(程序虚拟机)中运行,从而可以在保留状态且无需重新编译的情况下,热重载相关的更新。对于发行版 (Release
) ,Flutter 应用程序会直接编译为机器代码,即AOT(Ahead Of Time)。# Flutter体系结构
Flutter体系结构是分层设计的,类似于Android的体系结构。
- 最上层是
Framework
层,即Flutter框架层,开发者可以通过Flutter
框架层 与Flutter
交互,该框架提供了以 Dart 语言编写的现代响应式框架。Flutter
框架相对较小,因为一些开发者可能会使用到的更高层级的功能已经被拆分到不同的软件包中,使用Dart
和Flutter
的核心库实现,
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++。嵌入层提供一个程序入口,程序由此可以与底层操作系统进行协调。
# Flutter App架构
#Flutter编译模式
Flutter
使用Dart
作为应用程序开发编程语言,因此Flutter
的编译模式与Dart
的编译模式相关。下面这张表总结了Dart的编译模式。- Script::最常见的
JIT
模式。就像Node.js
一样,可以通过Dart VM
命令行工具直接执行Dart源代码。
- Script Snapshot:
JIT
模式。与Script
模式不同,Script Snapshot
会将源代码打包成代码的Token
形式,这可以节省了在编译时词法分析器所花费的时间。
- Application Snapshot:
JIT
模式。Dart的Application Snapshot
有点像运行时的转储。它包含了从源代码解析的类和函数,所以运行时可以更快地进行加载和执行。但是这种快照与架构相关,在IA_32上生成的快照无法在X64平台上运行。
- AOT:
AOT
模式。在这种模式下,Dart源代码会被翻译成汇编文件,然后汇编文件由汇编器为不同架构编译成二进制代码。
对于Flutter,其在上述编译模式的基础上进行了调整
- Script和Script Snapshot:与
Dart
的模式一样,但Flutter从未使用过。
- Kernel Snapshot:对应用代码进行中间字节码(Dart kernel格式)快照。通过避免Dart代码重新编译来实现移动端的快速启动,类似于Java字节码与JVM,核心快照是不依赖于体系架构的。
- Core JIT:
Dart
编译代码的一种二进制格式。程序数据和指令打包成特定的二进制格式,供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动态链接库,与实际业务代码无关。在发布阶段,在生产模式下应用程序需要更快地执行。因此,Flutter 在编译应用程序代码时选择了 AOT 模式。但是,由于平台特性不同,编译模式也有很大不同。我们可以将这些总结为一个表格。
在Android上,
Core JIT
和AOT 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
Isolate
中维护了堆栈变量,函数调用栈帧,用于GC、JIT等辅助任务的子线程等, 而这里的堆栈变量就是要被序列化到磁盘上的东西,即IsolateSnapshot
。此外像dart预置的全局对象,比如null,true,false等等等是由VMIsolate
管理的,这些东西需序列化后即VmSnapshot
。最初快照不包括机器代码,但是后来在开发AOT编译器时添加了此功能。开发 AOT 编译器和带代码的快照的动机是允许在由于平台级别限制而无法进行 JIT 的平台上使用 VM。带代码的快照的工作方式与普通快照几乎相同,但略有不同:它们包含一个代码部分,与快照的其余部分不同,它不需要反序列化。此代码段的铺设方式允许它在映射到内存后直接成为堆的一部分。dart
源码中runtime/vm/app_snapshot.cc
处理快照的序列化和反序列化#一般逆向方法
一般情况下要想获取更多关于业务代码相关的信息,暂时只有两种方法:
- 静态解析
libapp.so
,即写一个解析器,将libapp.so中的快照数据按照其既定格式进行解析,获取业务代码的类的各种信息,包括类的名称、其中方法的偏移等数据,从而辅助逆向工作。
关于Flutter快照的具体刨析只需要看下面引用的两篇文章,因为这不是本文的重点,在这里不再详细展开:
- 编译修改过的
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
。然后通过通过搜索
libflutter.so
,找到flutter
引擎的hash
,在这里是1ac611c64eadbd93c5f5aba5494b8fc3b35ee952
完成后就可以在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
fork
一份flutter
引擎到自己的github,flutter/engine: The Flutter engine (github.com),然后将下面内容填入.gclient
- 使用
gclient sync
让他自己拉取相关源码
- 完成后需要将源码切换到特定版本分支
- 安装编译必需的依赖
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中的分析结果,较为直观。
环境需求:
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.20x04 IDA符号恢复
对于第一种patch方案,编写了IDApython脚本利用dump出的数据(改名格式为json)进行符号恢复,对于第二种reFlutter的Patch方案则存在现成的脚本
flutter-re-demo
Guardsquare • Updated May 25, 2024
0x05 参考
- 作者:LLeaves
- 链接:https://lleavesg.top//article/Flutter-Reverse
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章