Flutter 做为⾕歌推出的⼀个跨平台移动应⽤开发框架,能够帮助开发者快速在移动 iOS 、Android 上构建⾼质量的原⽣⽤户界⾯,同时还支持开发Web和桌面应用。自2018 年 12 ⽉ Flutter 1.0 版本发布以来,Flutter受到越累越多的开发者的追捧。截⾄⽬前, Flutter 在 GitHub 上已经得到了 93.1K 的 Star 和 12.6K的 Fork ,发展速度至关惊⼈。前端
目前,使用Flutter进行工程化开发的有阿⾥、腾讯、字节跳动、美团等知名⼤⼚,固然,除此以外,还有一些我的个中小企业,详细能够查看Flutter开发现状。众所周知,不一样意React Native和Weex等跨平台技术方案,Flutter是一款自带渲染引擎的跨平台开发框架,它有本身的渲染管线和 Widget 库,于是开发出的应用体验更好。以下图所示,是Flutter官方给出的架构示意图。
能够看到,Flutter框架主要分为Framework、Engine和 Embedder三层。
其中,Framework使用Dart语言实现,包括UI、文本、图片、按钮等Widgets,渲染,动画,手势等,与开发者直接交互的就是这一层。Engine使用C++实现,主要包括Skia、Dart 和 Text。linux
Embedder则是一个嵌入层,该层的主要做用是把Flutter嵌入到各个平台上去,它的主要工做包括渲染Surface设置, 线程设置,以及插件等。平台(如iOS)只是提供一个画布,剩余的全部渲染相关的逻辑都在Flutter内部,这就使得它具备了很好的跨端一致性。git
因为平时进行应用开发时和咱们打交道最多的就是Framework层,而且该层主要使用 Dart 语言进行编写,也是应用程序全部业务逻辑所在的位置,所以,逆向Flutter应用主要的工做就在这一层。github
因为Flutter 将 Dart 编译为本机汇编代码使用的格式还没有公开,所以尽管没有混淆或加密,但 Flutter 应用程序目前仍然很难逆向,由于须要深刻了解 Dart 内部知识才能了解到皮毛。而比较其余应用而言,React Native 使用的是容易检查和修改的 Javascript,而 Android 使用的 Java 有详细的字节码说明,而且有许多免费的反编译器,所以逆向要容易许多。web
接下来,咱们经过Flutter 应用程序的构建过程,来详细说明如何对它产生的代码进行逆向工程。首先须要说明的就是【快照】编程
Dart SDK 具备高度的通用性,咱们能够在许多不一样的平台上以不一样的配置嵌入 Dart 代码。运行Dart的最简单方法是使用 dart 可执行文件,该可执行文件能够像读取脚本语言同样直接读取 dart 源文件。它包括咱们称为前端的主要组件(解析 Dart 代码),运行时(提供在其中运行代码的环境)以及 JIT 编译器。后端
您还可使用 dart 建立和执行快照,这是 Dart 的预编译形式,一般用于加速经常使用的命令行工具(如 pub)。例如,咱们新建一个main.dart文件,而后添加以下源码。数组
void main() { print('Hello, World!'); //输出Hello, World! }
而后,咱们在控制台输入以下的“time dart main.dart”命令,会获得以下打印信息。缓存
Flutter xiangzhihong$ time dart main.dart Hello, World! real 0m0.775s user 0m0.691s sys 0m0.191s xiangzhihong:Flutter xiangzhihong$ dart --snapshot=main.snapshot main.dart xiangzhihong:Flutter xiangzhihong$ time dart main.snapshot Hello, World! real 0m0.100s user 0m0.093s sys 0m0.028s
能够发现,使用快照后启动时间大大缩短。默认的快照格式是 kernel,它是等效于 AST 的 Dart 代码的中间表示形式。安全
在调试模式下运行Flutter应用程序时,Flutter工具会建立 kernal 快照,并使用调试运行时+JIT 在您的Android应用程序中运行该快照。这让你可以在运行时使用热重载实时调试应用程序和修改代码。不幸的是,因为对RCE的关注日益增长,在移动行业中,使用本身的JIT编译器已不受欢迎,而且iOS已经开始阻止执行这样的动态生成的代码。
除了kernel快照外,还有两种快照类型,即 app-jit 和 app-aot ,它们包含编译后的机器代码,这些代码能够比 kernel 快照更快地初始化,但它们不是跨平台的,是平台编译后的产物。
在Flutter开发中,快照的最终类型为 app-aot ,仅包含机器代码,且没有内核。这些快照使用的是flutter/bin/cache/artifacts/engine/<arch>/<target>/
中的 gen_snapshots 工具生成的。app-jit不只仅是 Dart 代码的编译版本,实际上,它们是在调用main以前VM堆栈的完整【快照】。这是Dart的一项独特功能,也是与其余运行时相比,其初始化速度如此之快的缘由之一。
Flutter 使用这些AOT快照构建发布版本,您能够在文件树中查看包含它们的文件,该文件树包含使用 flutter build apk 构建的 Android APK。须要说明的是,使用下面的命令都须要在linux环境下进行。
Flutter xiangzhihong$ ~/Desktop/app/lib$ tree . . ├── arm64-v8a │ ├── libapp.so │ └── libflutter.so └── armeabi-v7a ├── libapp.so └── libflutter.so
能够看到, Android apk包中两个libapp.so文件,它们分别是做为 ELF 二进制文件的 a64 和 a32 快照。gen_snapshots在此处输出ELF/共享对象可能会引发误解,它不会将 dart 方法公开为能够在外部调用的符号。相反,这些文件是“cluster 化快照”格式的容器,但在单独的可执行部分中包含编译的代码,如下是它们的结构:
Flutter xiangzhihong$~/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 快照。其中,Dart VM 有一个执行后台任务的 isolate,称为 vm isolate,它是 app-aot 快照所必需的,由于运行时没法像dart可执行文件那样动态加载它。
幸运的是,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
ROData serialization
ReadStream / WriteStream
Object definitions
ClassId enum
我花了两周时间实现了一个能解析快照的命令行工具,能够帮助咱们查看应用的数据构成。下面是快照数据块的布局总览:
Isolate 中的每一个 RawObject* 对象的序列化由对应的 SerializationCluster 完成,索引是其 class id。这些对象囊括了代码、实例、类型、原语、闭包、常量等等。Isolate 序列化完成后,每一个对象被加入 Isolate 对象池里,便于在同一上下文中引用。
Clusters 序列化分三个步骤:Trace、Alloc 和 Fill 。
在 trace 阶段,根节点们和在广度优先搜索时它们引用的对象被添加到一个队列里,同时生成每一个对象的 SerializationCluster
。根节点是虚拟机用到的对象的集合,位于 isolate 的 ObjectStore
中,咱们用它定位库和类。VM 快照中的 StubCode
基对象在 isolates 中是共享的。Stubs
基本都是手写的汇编代码,dart 代码能够调用进去,实现和运行时的安全通讯。
tracing 完成后,cluster 的基本信息就写入完成了,最重要的是知道了待分配对象的数量。在 alloc 阶段,会调用每一个 cluster 的 WriteAlloc
函数来写入分配原始对象须要的全部信息,大部分是该 cluster 的 class id 和对象的数量。每一个 cluster 中的对象的 object id 是按照分配顺序递增赋值的,以后在 fill 阶段解析对象引用时会用到。
可能你注意到缺了索引和 cluster 大小相关的信息,要获得咱们须要的信息必须完整读取整个快照。如今要进行逆向有两条路可选:一是给 31+ cluster 类型实现反序列化,二是把快照加载到修改过的运行时里提取信息。例如,下面是一个 cluster 中数组的例子 [123, 42],它的数据快照块以下:
若是一个对象引用了另外一个对象(好比数组元素),serializer 在 alloc 阶段把 object id 初始化(如上图所示)。
简单对象如 Mint 和 Smi 类型的对象建立在 alloc 阶段就已经完成,由于它们不须要引用其余对象。以后写入根引用的值,包括核心类型的对象 id、库、类、缓存、静态异常和其余对象。
最后是 ROData 的写入,ROData 直接映射进 RawObject*
的内存,这样反序列化过程就能少一步。ROData 最重要的类型是 RawOneByteString
,做为库/类/函数名称的类型。ROData 是以偏移引用的,也是快照数据中惟一能够不用解码的地方。
和 ROData 相似,RawInstruction 对象是指向快照数据的指针,存储在可执行指令区而不是快照主数据区。下面是编译 app 时常见的 SerializationCluster:
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 应用里见过,就再也不列举。ClassId 枚举对象里预约义了 class ID 集合,在 Dart 2.4.0 版本中有 142 个 ID,此范围以外或没有相关联 cluster 的 ID 单独写在 InstanceSerializationCluster
中。
终于到了能够能完全地查看快照结构的解析器部分了,从根对象表中的库开始。经过对象树能够定位函数,以 package:ftest/main.dart 的 main 函数为例:
如你所见 ,release 版本的快照是包含库名、类名和函数名的。若是不混淆 Dart 是没办法移除这些符号的,见 https://github.com/flutter/flutter/wiki/Obfuscating-Dart-Code。
目前这种混淆可能不值得,但将来这种状况极可能会改善,变得更合理易用,就像 Android 的 proguard 和 web 的 sourcemaps 。机器码以 Instruction
对象存储,Code
对象以指定数据起始偏移指向 Instruction
对象。
Dart 虚拟机中全部的对象都是 RawObject
,这些类的定义能够在 vm/raw_object.h 中找到。
根据递增的写屏障标志,只要你在生成的代码中声明,就能够随意读取、移动 RawObject*
,GC 能经过标志被动地扫描追踪引用。下面是类的树形图:
RawInstance 在 dart 世界中的类型都是 Object,在 dart 代码和方法调用时都能看到。非实例对象是内部的,只存在于引用跟踪、垃圾回收时,它们没有相同的 dart 类型。而且,每一个对象都以一个 uint32_t 类型的标志位开头,结构以下所示。
这里的 Class ID 和以前 cluster 序列化的 class id 同样,定义在 vm/class_id.h ,也包括用户定义的开头,在 kNumPredefinedCids。
Size 和 GC data 垃圾回收时使用,基本能够忽略。若是 canonical 位有值,表明这个对象是惟一的,没有对象和它相等,如 Symbol 和 Type 的实例。通常来讲,对象都很小,RawInstance
一般只有 4 字节,也不须要使用虚拟方法,这些都意味着分配一个对象并填充字段基本没有消耗。
Dart 并无用流行的编译后端(好比 Clang),而是用针对 AOT 优化了的 JIT 编译器进行代码生成。
若是你没有研究过 JIT 代码,那么你能够看看C 代码的等比产物,相比C 代码的产物,JIT 的产物在某些地方有些庞大。并非说 Dart 作得很差,而是设计的目的在于在运行时可以快速地生成代码,因此性能上可能比不上C代码,硬说性能的话手写的汇编指令速度但是完胜 clang/gcc 。实际上生成代码优化越少咱们的优点越大,和生成它的高级中间语言更接近。
其中,代码生成相关的源码,均可以在如下文件中找到,文件路径为vm/compiler/:
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
A64 采用了 AArch64 的调用约定 但多了几个全局寄存器:
相似的, 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
例如,下面是一个简单的Hello Word的例子。
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 架构是递减栈,倒序增加。
;; 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
代表线程栈可访问的字段个数限制在 36 个。若是检测到栈溢出,调用 stackOverflowStubWithoutFpuRegsStub
处理。汇编中的分支不能打补丁,但能够经过观察二进制来进行确认。接下来,看下一段:
;; PushArgument(v3) 0xf6a6ce74 e285ca01 add ip, pp, #4096 0xf6a6ce78 e59ccfa7 ldr ip, [ip, #+4007] 0xf6a6ce7c e52dc004 str ip, [sp, #-4]!
上面的代码用于将对象入栈,若是对象的偏移太大,ldr 就处理不了,须要使用基址寻址。这个对象其实是 RawOneByteString 类型的 “Hello, World!”,位于 isolate 偏移 8103 处的 globalObjectPool 中。
注意到这里的偏移没有对齐,这是由于对象指针都被 `vm/pointer_tagging.h 定义的 kHeapObjectTag 标记了,本例中全部的 RawObject 指针以 1 对齐。
;; StaticCall:12( print<0> v3) 0xf6a6ce80 ebfffffe bl +0 ; 0xf6a6ce80 0xf6a6ce84 e28dd004 add sp, sp, #4
上面的代码表示字符串参数出栈以后的调用,最后调用 dart:core 中 print 函数进行打印操做。
;; ParallelMove r0 <- C 0xf69ace88 e59a0060 ldr r0, [thr, #+96]
返回值是 Null,96 是 Thread 中 null 对象的偏移。
;; Return:16(v0) 0xf69ace8c e24bd000 sub sp, fp, #0 0xf69ace90 e8bd8800 ldmia sp!, {fp, pc} 0xf69ace94 e1200070 bkpt #0x0
最后是函数结语,写回调用者保存的寄存器,恢复栈帧。lr 是最后入栈的,把它 pop 给 pc 后函数返回。
原文连接:https://blog.tst.sh/reverse-engineering-flutter-apps-part-1/