逆向工程Flutter应用程序Part 1

本文探讨了Flutter应用程序的逆向工程,重点放在Dart层。Flutter将Dart代码编译为AOT快照,这使得逆向工程更具挑战性。文章详细介绍了Dart SDK、快照类型和生成过程,以及如何对快照进行反向工程。虽然Dart是开源的,但AOT快照的复杂格式使得解析和理解代码变得困难。作者还分享了如何设置Dart SDK以生成和解析快照,为深入研究Flutter应用的逆向工程奠定了基础。
摘要由CSDN通过智能技术生成

第1章:掉进兔子洞

首先,我将介绍Flutter堆栈的一些背景知识及其工作原理。

您可能已经知道:Flutter是从头开始构建的,具有自己的渲染管道和小部件库,从而使其真正跨平台,并具有一致的设计,无论在什么设备上运行都可以感觉到。

与大多数平台不同,flutter框架的所有基本渲染组件(包括动画,布局和绘画)都在中完全向您公开package:flutter

您可以从Wiki / The-Engine-architecture的官方架构图中看到这些组件

从逆向工程的角度来看,最有趣的部分是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工具会创建内核快照,并使用调试运行时+ JIT在您的android应用中运行该快照。这使您能够通过热重载在运行时调试应用程序并实时修改代码。

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

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

快照的最终类型app-aot,仅包含机器代码,不包含内核。这些快照是使用中提供的gen_snapshots工具生成的flutter/bin/cache/artifacts/engine/<arch>/<target>/,稍后再介绍。

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

Flutter将这些AOT快照用于发行版本,您可以在使用以下内容构建的Android APK的文件树中看到包含它们的文件flutter build 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方法公开为可以在外部调用的符号。而是,这些文件是“群集快照”格式的容器,但在单独的可执行部分中包含编译的代码,以下是它们的结构:

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_snapshot在应用启动时需要将生成的机器代码加载到可执行内存中,而最好的方法是通过ELF文件。

有了这个共享.text库,链接器将把该节中的所有内容加载到可执行内存中,从而允许Dart运行时随时调用它。

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

DartVM具有执行后台任务的第二个隔离,称为vm隔离。app-aot快照是必需的,因为运行时无法像dart可执行文件那样动态地将其加载。

Dart SDK

幸运的是,Dart是完全开源的,因此在对快照格式进行反向工程时,我们不必盲目。

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

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

flutter sdk附带的二进制文件gen_snapshot不是create_sdk构建飞镖时通常使用的标准构建目标的一部分。

虽然它确实作为SDK中的一个单独目标存在,但是您可以gen_snapshot使用以下命令为arm 构建工具:

./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文件实际上与内核快照的格式相同,其格式在此处指定:https : //github.com/dart-lang/sdk/blob/master/pkg/kernel/binary.md

这是用作工具(包括gen_snapshot和)之间的飞镖代码的通用表示形式的格式analyzer

有了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的一些人设计的,它可以说是有史以来最先进的解释器。DartVM的设计令人难以置信,我认为人们没有给予DartVM足够的信誉。

快照剖析

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

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

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

作为概述,这是群集快照数据的布局:

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

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

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

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

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

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

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

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

每个群集中的对象也按分配顺序分配了一个递增的对象ID,稍后在填充阶段解析对象引用时使用此ID。

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

这是一个数组的簇结构的简化示例[123, 42]

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

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

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

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

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

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

这是序列化集群的转储,通常在编译应用程序时编写:

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

快照中可能还有更多的群集,但是到目前为止,这是我在Flutter应用程序中唯一看到的群集。

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

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

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

如您所见,版本快照中包含库,类和函数的名称。

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

混淆可能不值得付出努力,但是这种情况将来很有可能会改变,并且变得更加简化,类似于Android上的proguard或网络上的源地图。

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


原始对象

DartVM中的所有托管对象都被称为RawObjects,以真正的DartVM方式,这些类都在位于的3,000行文件中定义vm/raw_object.h

在生成的代码中,您可以访问RawObject*s 并在其中移动,但是只要您根据增量写屏障屏蔽进行操作,GC似乎就能够仅通过被动扫描来跟踪引用。

这是类树:

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

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

此处的类ID与集群序列化之前的类ID相同,它们在中定义,vm/class_id.h但也包含用户定义的开头kNumPredefinedCids

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

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

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

所有这些意味着分配一个对象并填充其字段几乎可以免费完成,这在Flutter中可以做很多。

 

你好,世界!

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

正如预期的那样,从现在开始进行逆向工程要困难一些,因为我们正在挖掘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的指针Thread,请参阅vm / thread.h
  • R27 / PP:指向ObjectPool当前上下文的的指针,请参见vm / object.h
  • R28 / BRM:防毒面具,用于增量垃圾收集

同样,这是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。

您可以将传递--disassemble-optimizedgen_snapshot,以查看IR和反汇编,但请注意,这仅适用于调试/发布目标,不适用于产品。

例如,在编译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偏移量编码,因此它使用了额外的添加指令。

这个对象实际上就是我们的“世界你好!” 字符串作为RawOneByteString*存储在globalObjectPool我们的隔离中的偏移量8103处。

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

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

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

就像分支尚未解析之前一样,它是printdart:core 的入口点的相对分支。

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

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

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

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

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

 

 

 

 

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值