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/