逆向 Flutter 应用

Posted 程序员大咖

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了逆向 Flutter 应用相关的知识,希望对你有一定的参考价值。


Python实战社群

Java实战社群

长按识别下方二维码,按需求添加

扫码关注添加客服

进Python社群▲

逆向 Flutter 应用

扫码关注添加客服

进Java社群


逆向 Flutter 应用


作者 | Andre Lipke 
来源 | PIXELTOAST Blog

https://blog.tst.sh/reverse-engineering-flutter-apps-part-1/


在开始之前,我先介绍Flutter堆栈的一些背景知识及其工作原理。

您可能已经知道:Flutter是从头开始构建的,有自己的渲染管线和 widget 库,从而做到了真正的跨平台,并保证了设计的一致性,无论在什么设备上运行,体验都是一样的。

与大多数平台不同,flutter 框架中的所有渲染相关的组件(包括动画,布局和绘画)都包含在 package:flutter 中。

您可以在官方的架构图中看到这些组件:

逆向 Flutter 应用

从逆向工程的角度来看,最有趣的部分是 Dart 层,因为这是应用程序所有业务逻辑所在的位置。

那 Dart 层是什么样的呢?

Flutter 将 Dart 编译为本机汇编代码,而使用的格式尚未公开,更不用说完全反编译和重新编译了。

相比较而言,React Native 使用的是容易检查和修改的 javascript,而 android 使用的 Java 有详细的字节码说明,并且有许多免费的反编译器。

尽管没有混淆(默认情况下)或加密,但是 Flutter 应用程序目前仍然很难逆向,因为需要深入了解 Dart 内部知识才能了解到皮毛。

从安全的角度来看,这让 Flutter 变得非常出色,几乎可以防止别人窥探你的代码。

接下来,我将向您展示 Flutter 应用程序的构建过程,并详细说明如何对它产生的代码进行逆向工程。

快照


Dart SDK 具有高度的通用性,您可以在许多不同的平台上以不同的配置嵌入 Dart 代码。

运行Dart的最简单方法是使用 dart 可执行文件,该可执行文件可以像读取脚本语言一样直接读取 dart 源文件。它包括我们称为前端的主要组件(解析 Dart 代码),运行时(提供在其中运行代码的环境)以及 JIT 编译器。

您还可以使用 dart 创建和执行快照,这是 Dart 的预编译形式,通常用于加速常用的命令行工具(如 pub)。

ping@debian:~/Desktop$ time dart hello.dart
Hello, World!

real 0m0.656s
user 0m0.920s
sys 0m0.084s

ping@debian:~/
Desktop$ dart --snapshot=hello.snapshot hello.dart
ping@debian:~/Desktop$ time dart hello.snapshot
Hello, World!

real 0m0.105s
user 0m0.208s
sys 0m0.016s

如您所见,使用快照时,启动时间大大缩短。

默认的快照格式是 kernel,它是等效于 AST 的 Dart 代码的中间表示形式。

在调试模式下运行Flutter应用程序时,Flutter工具会创建 kernal 快照,并使用 调试运行时+JIT 在您的android应用程序中运行该快照。这让你能够在运行时使用热重载实时调试应用程序和修改代码。

不幸的是,由于对RCE的关注日益增加,在移动行业中,使用自己的JIT编译器已不受欢迎。ios实际上阻止你执行像这样的动态生成的代码。

但是,还有两种快照类型,即 app-jit 和 app-aot ,它们包含编译后的机器代码,这些代码可以比 kernel 快照更快地初始化,但它们不是跨平台的。

快照的最终类型为 app-aot ,仅包含机器代码,且没有内核。这些快照是使用 flutter/bin/cache/artifacts/engine/<arch>/<target>/ 中的 gen_snapshots 工具生成的,稍后会对此进行更多介绍。

但是,它们不仅仅是 Dart 代码的编译版本,实际上,它们是在调用main之前VM堆栈的完整“快照”。这是Dart的一项独特功能,也是与其他运行时相比,其初始化速度如此之快的原因之一。

Flutter 使用这些AOT快照构建发布版本,您可以在文件树中查看包含它们的文件,该文件树包含使用 flutter build apk 构建的 Android APK

ping@debian:~/Desktop/app/lib$ tree .
.
├── arm64-v8a
│ ├── libapp.so
│ └── libflutter.so
└── armeabi-v7a
├── libapp.so
└── libflutter.so

在这里,您可以看到两个libapp.so文件,它们分别是作为 ELF 二进制文件的 a64 和 a32 快照。

gen_snapshots在此处输出ELF/共享对象可能会引起误解,它不会将 dart 方法公开为可以在外部调用的符号。相反,这些文件是“cluster 化快照”格式的容器,但在单独的可执行部分中包含编译的代码,以下是它们的结构:

ping@debian:~/Desktop/app/lib/arm64-v8a$ aarch64-linux-gnu-objdump -T libapp.so

libapp.so: file format elf64-littleaarch64

DYNAMIC SYMBOL TABLE:
0000000000001000 g DF .text 0000000000004ba0 _kDartVmSnapshotInstructions
0000000000006000 g DF .text 00000000002d0de0 _kDartIsolateSnapshotInstructions
00000000002d7000 g DO .rodata 0000000000007f10 _kDartVmSnapshotData
00000000002df000 g DO .rodata 000000000021ad10 _kDartIsolateSnapshotData

AOT快照采用共享对象形式而不是常规快照文件的原因是因为 gen_snapshots 生成的机器代码需要在应用程序启动时加载到可执行内存中,而最好的方法是通过ELF文件。

使用此共享对象,链接器会将 .text 部分中的所有内容加载到可执行内存中,从而允许 Dart 运行时随时调用它。

您可能已经注意到有两个快照:VM 快照和 Isolate 快照。

DartVM 有一个执行后台任务的 isolate,称为 vm isolate,它是 app-aot 快照所必需的,因为运行时无法像dart可执行文件那样动态加载它。

Dart SDK


幸运的是,Dart是完全开源的,因此在对快照格式进行逆向工程时,我们不是两眼摸黑。

在创建用于生成和分解快照的测试平台之前,您必须设置Dart SDK,这里有有关如何构建它的文档:https://github.com/dart-lang/sdk/wiki/Building。

您想生成通常由flutter工具编排的 libapp.so 文件,但是似乎没有任何有关如何执行此操作的文档。

flutter sdk 附带了 gen_snapshot 的二进制文件,该文件不属于构建 dart 时通常使用的标准 create_sdk 构建目标。

尽管 gen_snapshot 确实是作为SDK中的一个单独目标存在,但是你可以使用以下命令为构建 arm  版本的 gen_snapshot :

./tools/build.py -m product -a simarm gen_snapshot

通常,您只能根据架构来生成快照,以解决它们已经创建了模拟目标的情况,该模拟目标可模拟目标平台的快照生成。这里有一些限制,例如无法在 32 位系统上制作 aarch64 或 x86_64 快照。

在制作共享库之前,您必须使用前端编译一个 dill 文件:

~/flutter/bin/cache/dart-sdk/bin/dart ~/flutter/bin/cache/artifacts/engine/linux-x64/frontend_server.dart.snapshot --sdk-root ~/flutter/bin/cache/artifacts/engine/common/flutter_patched_sdk_product/ --strong --target=flutter --aot --tfa -Ddart.vm.product=true --packages .packages --output-dill app.dill package:foo/main.dart

Dill文件实际上与 kernel 快照的格式相同,其格式可以参考:https://github.com/dart-lang/sdk/blob/master/pkg/kernel/binary.md

这是用作 gen_snapshot 和 analyzer 之类的工具之间的 Dart 代码的通用表示形式的格式。

有了 app.dill ,我们最终可以使用以下命令生成 libapp.so :

gen_snapshot --causal_async_stacks --deterministic --snapshot_kind=app-aot-elf --elf=libapp.so --strip app.dill

一旦能够手动生成libapp.so,就可以轻松修改SDK,以打印出对 AOT 快照格式进行逆向工程所需的所有调试信息。

附带说明一下,Dart 实际上是由创建 JavaScript 的 V8 的一些人设计的,V8 可以说是有史以来最先进的解释器。DartVM 的设计令人难以置信,我认为人们没有给予 DartVM 创造者足够的荣誉。

快照剖析


AOT快照本身非常复杂,目前是没有详细文档的自定义二进制格式。你可能会被迫在调试器中手动完成序列化过程,以实现可以读取其格式的工具。

与快照生成相关的源文件可以在这里找到:

  • Cluster serialization / deserialization

vm/clustered_snapshot.h
vm/clustered_snapshot.cc
  • ROData serialization

vm/image_snapshot.h
vm/image_snapshot.cc
  • ReadStream / WriteStream

vm/datastream.h
  • Object definitions

vm/object.h
  • ClassId enum

vm/class_id.h

我花了大约两周的时间来实现一个命令行工具,该工具能够解析快照,使我们能够完全访问已编译应用程序的堆。

我们来快速浏览一下快照数据的布局:

逆向 Flutter 应用

Isolate 中的每个 RawObject * 根据其 class ID 由相应的 SerializationCluster 实例进行序列化。这些对象可以包含代码,实例,类型,原语,闭包,常量等任何内容。稍后将进一步介绍。

对 VM isolate 快照进行反序列化之后,其堆中的每个对象都会添加到 isolate 快照对象池中,从而可以在同一上下文中对其进行引用。

Cluster 分三个阶段进行序列化:跟踪,分配和填充。

在跟踪阶段,根对象与它们在广度优先搜索中引用的对象一起添加到队列中。同时,创建与每个类类型相对应的SerializationCluster实例。

根对象是虚拟机在 isolate 的ObjectStore中使用的一组静态对象,稍后我们将使用这些 ObjectStore 来查找库和类。VM快照包括StubCode基础对象,这些对象在所有 isolate 之间共享。

stub 基本上是 Dart 代码所调用的汇编的手写部分,从而使其可以与运行时安全地通信。

跟踪之后,将写入 cluster 信息,其中包含有关 cluster 的基本信息,最重要的是要分配的对象数。

在分配阶段,将调用每个 cluster 的 WriteAlloc 方法,该方法将写入分配原始对象所需的任何信息。在大多数情况下,此方法所做的全部工作就是写入 class ID 和属于该 cluster 的对象的数量。

属于每个 cluster 的对象还按分配顺序分配了一个递增的对象ID,稍后在填充阶段解析对象引用时会用到这个 ID。

您可能已经注意到还缺少任何关于索引和 cluster 大小的信息,必须完全读取整个快照才能从中获取任何有意义的数据。因此,要真正进行任何逆向工程,您必须为31种以上的 cluster 类型实现反序列化例程(我已经完成),或者通过将其加载到经过修改的运行时中来提取信息(这很难做到跨体系结构)。

以下是一个数组 [123,42] 的 cluster 结构的简化示例:

逆向 Flutter 应用

如果一个对象引用了另一个对象(如数组元素),则序列化器将在分配阶段写入最初分配的对象ID,如上所示。

对于像 Mints 和 Smis 这样的简单对象,它们完全是在分配阶段构造的,因为它们没有引用任何其他对象。

之后,将编写大约 107 个根引用,包括核心类型,库,类,缓存,静态异常和其他几个其他对象的对象ID。

最后,写入 ROData 对象,将其直接映射到内存中的 RawObject * ,以避免额外的反序列化步骤。

ROData最重要的类型是 RawOneByteString ,它用于库/类/函数名。 ROData 也通过偏移量引用,偏移量是快照数据中唯一可选解码的位置。

与ROData相似, RawInstruction 对象是直接指向快照数据的指针,但存储在可执行指令符号中,而不是主快照数据中。

以下是序列化 cluster 的转储,通常在编译应用程序时编写:

idx | cid | ClassId enum        | Cluster name
----|-----|---------------------|----------------------------------------
0 | 5 | Class | ClassSerializationCluster
1 | 6 | PatchClass | PatchClassSerializationCluster
2 | 7 | Function | FunctionSerializationCluster
3 | 8 | ClosureData | ClosureDataSerializationCluster
4 | 9 | SignatureData | SignatureDataSerializationCluster
5 | 12 | Field | FieldSerializationCluster
6 | 13 | Script | ScriptSerializationCluster
7 | 14 | Library | LibrarySerializationCluster
8 | 17 | Code | CodeSerializationCluster
9 | 20 | ObjectPool | ObjectPoolSerializationCluster
10 | 21 | PcDescriptors | RODataSerializationCluster
11 | 22 | CodeSourceMap | RODataSerializationCluster
12 | 23 | StackMap | RODataSerializationCluster
13 | 25 | ExceptionHandlers | ExceptionHandlersSerializationCluster
14 | 29 | UnlinkedCall | UnlinkedCallSerializationCluster
15 | 31 | MegamorphicCache | MegamorphicCacheSerializationCluster
16 | 32 | SubtypeTestCache | SubtypeTestCacheSerializationCluster
17 | 36 | UnhandledException | UnhandledExceptionSerializationCluster
18 | 40 | TypeArguments | TypeArgumentsSerializationCluster
19 | 42 | Type | TypeSerializationCluster
20 | 43 | TypeRef | TypeRefSerializationCluster
21 | 44 | TypeParameter | TypeParameterSerializationCluster
22 | 45 | Closure | ClosureSerializationCluster
23 | 49 | Mint | MintSerializationCluster
24 | 50 | Double | DoubleSerializationCluster
25 | 52 | GrowableObjectArray | GrowableObjectArraySerializationCluster
26 | 65 | StackTrace | StackTraceSerializationCluster
27 | 72 | Array | ArraySerializationCluster
28 | 73 | ImmutableArray | ArraySerializationCluster
29 | 75 | OneByteString | RODataSerializationCluster
30 | 95 | TypedDataInt8Array | TypedDataSerializationCluster
31 | 143 | <instance> | InstanceSerializationCluster
...
54 | 463 | <instance> | InstanceSerializationCluster

快照中可能还有其他几个 cluster ,但是到目前为止,这是我在Flutter应用程序中唯一看到的 cluster。

在DartVM中,在 ClassId 枚举中定义了一组静态的预定义类 ID,确切地说,从Dart 2.4.0 起 有 142 个ID。之外的ID(或没有关联的 cluster)用单独的 InstanceSerializationClusters 编写。

最后,将解析器组合在一起,我可以从根对象表中的库列表开始,从头开始查看快照的结构。

使用对象树(这里是找到顶级函数的方法),在这种情况下是 packge:ftest/main.dart :

逆向 Flutter 应用

如上所示,在release快照中包含库,类和函数的名称。

Dart在不混淆堆栈跟踪的情况下无法真正删除它们,请参见:https://github.com/flutter/flutter/wiki/Obfuscating-Dart-Code

混淆可能不值得去花大力气,但是这很可能会在将来有所改变,并且变得更加简单,类似于Android上的proguard或web 中的 sourcemap。

实际的机器代码存储在 Code 对象指向的 Instructions 对象中,到指令数据的开头有一定的偏移量。

RawObject


DartVM中的所有托管对象都称为RawObjects,按照真正的DartVM方式,这些类都在一个有 3000 行代码的文件 vm/raw_object.h 中定义。

在生成的代码中,您可以访问 RawObject * 并在其中切换,GC似乎能够仅通过被动扫描来跟踪引用。

这是类树:

逆向 Flutter 应用

RawInstances 是在 Dart 代码中传递并调用方法的传统对象,它们在 dart 都有等效的类型。但是,非实例对象是内部的,仅存在于利用引用跟踪和垃圾回收的情况下,它们没有等效的dart类型。

每个对象都以包含以下标记的uint32_t开头:

逆向 Flutter 应用

此处的 class ID 与 cluster 序列化之前的 class ID 相同,它们在 vm/class_id.h 中定义,但也包括从 kNumPredefinedCids 开始的用户定义。

Size和GC数据标签用于垃圾回收,大多数时候可以忽略它们。

如果设置了规范位,则意味着该对象是唯一的,并且没有其他对象等于它,例如 Symbol 和 Type。

对象非常轻,RawInstance 的大小通常只有 4 个字节,令人惊讶的是它们根本都不使用虚拟方法。

所有这些意味着分配一个对象并填充其字段的成本非常低,这在Flutter中可以做很多。

Hello World!


不错吧,我们可以按名称查找函数,但是如何确定它们的实际作用呢?

正如预期的那样,从这里开始进行逆向工程要困难一些,因为我们正在挖掘包含在 Instructions 对象中的汇编代码。

Dart实际上没有使用clang等现代的编译器后端,而是使用JIT编译器来生成代码,同时有一些针对AOT的优化。

如果您从未使用过JIT代码,那么与等效的C代码相比,它在某些地方会有点臃肿。并不是说 Dart 做得不好,而是它的设计目的是在运行时快速生成,并且针对常见指令的手写汇编在性能方面常常胜过 clang/gcc 。

经过微优化的生成代码实际上发挥了巨大作用,因为它更类似于用于生成代码的更高级别的IR。

大多数相关的代码生成可以在以下位置找到:

  • vm/compiler/backend/il_<arch>.cc 

  • vm/compiler/assembler/assembler_<arch>.cc 

  • vm/compiler/asm_intrinsifier_<arch>.cc 

  • vm/compiler/graph_intrinsifier_<arch>.cc 

这是dart A64汇编程序的寄存器布局和调用约定:

       r0 |     | Returns
r0 - r7 | | Arguments
r0 - r14 | | General purpose
r15 | sp | Dart stack pointer
r16 | ip0 | Scratch register
r17 | ip1 | Scratch register
r18 | | Platform register
r19 - r25 | | General purpose
r19 - r28 | | Callee saved registers
r26 | thr | Current thread
r27 | pp | Object pool
r28 | brm | Barrier mask
r29 | fp | Frame pointer
r30 | lr | Link register
r31 | zr | Zero / CSP

这个ABI遵循标准AArch64调用约定,但带有一些全局寄存器:

  • R26/THR :指向正在运行的vm线程的指针,请参阅 vm/thread.h 

  • R27/PP :指向当前上下文的 ObjectPool 的指针,请参阅 vm/object.h 

  • R28/BRM :barrier mask,用于增量垃圾收集

同样,这是A32的寄存器布局:

r0 -  r1 |     | Returns
r0 - r9 | | General purpose
r4 - r10 | | Callee saved registers
r5 | pp | Object pool
r10 | thr | Current thread
r11 | fp | Frame pointer
r12 | ip | Scratch register
r13 | sp | Stack pointer
r14 | lr | Link register
r15 | pc | Program counter

尽管A64是更常见,但由于A32更易于阅读和反汇编,因此我将主要介绍A32。

您可以通过将 --disassemble-optimized 传递给 gen_snapshot 来查看IR和反汇编,但请注意,这仅适用于调试/发布 target,不适用于最终的产品。

例如,在编译hello world时:

void hello() {
print("Hello, World!");
}

在反汇编代码中向下滚动,您会发现:

Code for optimized function 'package:dectest/hello_world.dart_::_hello' {
;; B0
;; B1
;; Enter frame
0xf69ace60 e92d4800 stmdb sp!, {fp, lr}
0xf69ace64 e28db000 add fp, sp, #0
;; CheckStackOverflow:8(stack=0, loop=0)
0xf69ace68 e59ac024 ldr ip, [thr, #+36]
0xf69ace6c e15d000c cmp sp, ip
0xf69ace70 9bfffffe blls +0 ; 0xf69ace70
;; PushArgument(v3)
0xf69ace74 e285ca01 add ip, pp, #4096
0xf69ace78 e59ccfa7 ldr ip, [ip, #+4007]
0xf69ace7c e52dc004 str ip, [sp, #-4]!
;; StaticCall:12( print<0> v3)
0xf69ace80 ebfffffe bl +0 ; 0xf69ace80
0xf69ace84 e28dd004 add sp, sp, #4
;; ParallelMove r0 <- C
0xf69ace88 e59a0060 ldr r0, [thr, #+96]
;; Return:16(v0)
0xf69ace8c e24bd000 sub sp, fp, #0
0xf69ace90 e8bd8800 ldmia sp!, {fp, pc}
0xf69ace94 e1200070 bkpt #0x0
}

此处打印的内容与内置产品的快照略有不同,但重要是我们可以在汇编代码旁看到IR指令。

来分析一下

       ;; Enter frame
0xf6a6ce60 e92d4800 stmdb sp!, {fp, lr}
0xf6a6ce64 e28db000 add fp, sp, #0

这是一个标准的函数,将调用者的帧指针和链接寄存器推入堆栈,然后将帧指针设置为函数堆栈帧的底部。

与标准ARM ABI一样,它使用全降序堆栈,这意味着它在内存中向后增长。

        ;; CheckStackOverflow:8(stack=0, loop=0)
0xf6a6ce68 e59ac024 ldr ip, [thr, #+36]
0xf6a6ce6c e15d000c cmp sp, ip
0xf6a6ce70 9bfffffe blls +0 ; 0xf6a6ce70

这是一个简单的例程,它会执行你所想的事情,检查堆栈是否溢出。

遗憾的是,它们的反汇编程序不会注释线程字段或分支目标,因此必须进行一些挖掘。

可以在 vm/compiler/runtime_offsets_extracted.h 中找到字段偏移量列表,该列表定义的 Thread_stack_limit_offset = 36 告诉我们访问的字段是线程堆栈限制。

比较堆栈指针后,如果溢出,将调用 stackOverflowStubWithoutFpuRegsStub 。反汇编中的分支目标似乎未打补丁,但之后我们仍然可以检查二进制文件进行确认。

        ;; PushArgument(v3)
0xf6a6ce74 e285ca01 add ip, pp, #4096
0xf6a6ce78 e59ccfa7 ldr ip, [ip, #+4007]
0xf6a6ce7c e52dc004 str ip, [sp, #-4]!

这里,来自对象池的对象被压入堆栈。由于偏移量太大而无法适合ldr偏移量编码,因此它使用了额外的 add 指令。

这个对象实际上就是我们的“Hello World!”字符串,并作为 RawOneByteString * 存储在我们的 isolate 的 globalObjectPool 中,偏移量为8103。

您可能已经注意到偏移量未对齐,这是因为对象指针是使用 vm/pointer_tagging.h 中的 kHeapObjectTag 标记的,在这种情况下,已编译代码中指向 RawObjects 的所有指针都偏移了1。

        ;; StaticCall:12( print<0> v3)
0xf6a6ce80 ebfffffe bl +0 ; 0xf6a6ce80
0xf6a6ce84 e28dd004 add sp, sp, #4

在这里,先调用print,然后再从堆栈中弹出字符串参数。

就像分支尚未解析之前一样,它是 dart:core 中用于 print 入口点的相关分支。

        ;; ParallelMove r0 <- C
0xf69ace88 e59a0060 ldr r0, [thr, #+96]

空值被加载到返回寄存器中,96 是 Thread 中空对象字段的偏移量。

        ;; Return:16(v0)
0xf69ace8c e24bd000 sub sp, fp, #0
0xf69ace90 e8bd8800 ldmia sp!, {fp, pc}
0xf69ace94 e1200070 bkpt #0x0

最后是函数结尾,栈帧和所有被调用者保存的寄存器一起恢复。由于 lr 被最后推入,将其弹出到 pc 中将导致该函数返回。

从现在开始,我将使用自己的反汇编程序中的代码片段,该代码片段的问题少于内置的问题。


【未完待续】

逆向 Flutter 应用
  
    
    
  
程序员专栏
 扫码关注填加客服 
长按识别下方二维码进群
逆向 Flutter 应用

逆向 Flutter 应用

近期精彩内容推荐:  

逆向 Flutter 应用 

逆向 Flutter 应用 

逆向 Flutter 应用 

逆向 Flutter 应用 


逆向 Flutter 应用



在看点这里好文分享给更多人↓↓

以上是关于逆向 Flutter 应用的主要内容,如果未能解决你的问题,请参考以下文章

如何逆向Flutter应用

Android 逆向使用 DB Browser 查看并修改 SQLite 数据库 ( 从 Android 应用数据目录中拷贝数据库文件 | 使用 DB Browser 工具查看数据块文件 )(代码片段

Android 逆向使用 DB Browser 查看并修改 SQLite 数据库 ( 从 Android 应用数据目录中拷贝数据库文件 | 使用 DB Browser 工具查看数据块文件 )(代码片段

Android 逆向类加载器 ClassLoader ( 类加载器源码简介 | BaseDexClassLoader | DexClassLoader | PathClassLoader )(代码片段

Android 逆向Android 逆向通用工具开发 ( Android 平台运行的 cmd 程序类型 | Android 平台运行的 cmd 程序编译选项 | 编译 cmd 可执行程序 )(代码片段

Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段