当前位置:   article > 正文

逆向工程Flutter应用程序Part 1_flutter反编译

flutter反编译

第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)的速度。

  1. ping@debian:~/Desktop$ time dart hello.dart
  2. Hello, World!
  3. real 0m0.656s
  4. user 0m0.920s
  5. sys 0m0.084s
  6. ping@debian:~/Desktop$ dart --snapshot=hello.snapshot hello.dart
  7. ping@debian:~/Desktop$ time dart hello.snapshot
  8. Hello, World!
  9. real 0m0.105s
  10. user 0m0.208s
  11. 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

  1. ping@debian:~/Desktop/app/lib$ tree .
  2. .
  3. ├── arm64-v8a
  4. │ ├── libapp.so
  5. │ └── libflutter.so
  6. └── armeabi-v7a
  7. ├── libapp.so
  8. └── libflutter.so

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

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

  1. ping@debian:~/Desktop/app/lib/arm64-v8a$ aarch64-linux-gnu-objdump -T libapp.so
  2. libapp.so: file format elf64-littleaarch64
  3. DYNAMIC SYMBOL TABLE:
  4. 0000000000001000 g DF .text 0000000000004ba0 _kDartVmSnapshotInstructions
  5. 0000000000006000 g DF .text 00000000002d0de0 _kDartIsolateSnapshotInstructions
  6. 00000000002d7000 g DO .rodata 0000000000007f10 _kDartVmSnapshotData
  7. 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对象是指向快照数据的直接指针,但存储在可执行指令符号中,而不是主快照数据中。

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

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

  1. r0 | | Returns
  2. r0 - r7 | | Arguments
  3. r0 - r14 | | General purpose
  4. r15 | sp | Dart stack pointer
  5. r16 | ip0 | Scratch register
  6. r17 | ip1 | Scratch register
  7. r18 | | Platform register
  8. r19 - r25 | | General purpose
  9. r19 - r28 | | Callee saved registers
  10. r26 | thr | Current thread
  11. r27 | pp | Object pool
  12. r28 | brm | Barrier mask
  13. r29 | fp | Frame pointer
  14. r30 | lr | Link register
  15. r31 | zr | Zero / CSP

此ABI遵循此处的标准AArch64调用约定但具有一些全局寄存器:

  • R26 / THR:指向正在运行的vm的指针Thread,请参阅vm / thread.h
  • R27 / PP:指向ObjectPool当前上下文的的指针,请参见vm / object.h
  • R28 / BRM:防毒面具,用于增量垃圾收集

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

  1. r0 - r1 | | Returns
  2. r0 - r9 | | General purpose
  3. r4 - r10 | | Callee saved registers
  4. r5 | pp | Object pool
  5. r10 | thr | Current thread
  6. r11 | fp | Frame pointer
  7. r12 | ip | Scratch register
  8. r13 | sp | Stack pointer
  9. r14 | lr | Link register
  10. r15 | pc | Program counter

尽管A64是更常见的目标,但由于它更易于阅读和分解,因此我将主要介绍A32。

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

例如,在编译hello world时:

  1. void hello() {
  2. print("Hello, World!");
  3. }

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

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

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

分解:

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

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

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

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

这是一个简单的例程,它可能会执行您可能猜到的事情,检查堆栈是否溢出。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 

 

 

 

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/article/detail/44237
推荐阅读
相关标签
  

闽ICP备14008679号