初探flutter

# preface: 起因是 WMCTF2023 有个 anticheat2 是基于 flutter 开发的,中间也得用到相关知识。题目是一下子被队里师傅秒了(555 队里师傅秒题太快哩毫无存在感属于是)但是总归还是得来补补功课,啥也不学还是啥也不会。(虽然这会我的安卓环境还是不太彳亍容错很低当时比赛的时候就没有环境跑这个题
# 文档:Flutter 架构概览 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter
# 参考文章:Reverse engineering Flutter apps (Part 1) (tst.sh)
# Reverse engineering Flutter apps (Part 2) (tst.sh)
# TODO:工具
# flutter 逆向初探 -- 2023 国赛 ctf 的 flutterror_1mmorta1 的博客 - CSDN 博客
# Reverse Engineering Flutter Apps | Guardsquare
# Flutter 架构:分层系统,上层组件依赖下层组件,组件层间不可越权且各个部分可选可替代(类似层与层间透明)
# archdiagram
# 对于底层操作系统而言,Flutter 应用程序的包装方式与其他原生应用相同。在每一个平台上,会包含一个特定的嵌入层,从而提供一个程序入口,程序由此可以与底层操作系统进行协调,访问诸如 surface 渲染、辅助功能和输入等服务,并且管理事件循环队列。该嵌入层采用了适合当前平台的语言编写,例如 Android 使用的是 Java 和 C++, iOS 和 macOS 使用的是 Objective-C 和 Objective-C++,Windows 和 Linux 使用的是 C++。 Flutter 代码可以通过嵌入层,以模块方式集成到现有的应用中,也可以作为应用的主体。 Flutter 本身包含了各个常见平台的嵌入层,同时也 存在一些其他的嵌入层
# Flutter 引擎 毫无疑问是 Flutter 的核心,它主要使用 C++ 编写,并提供了 Flutter 应用所需的原语。当需要绘制新一帧的内容时,引擎将负责对需要合成的场景进行栅格化。它提供了 Flutter 核心 API 的底层实现,包括图形(在 iOS 和 Android 上通过 Impeller,在其他平台上通过 Skia)、文本布局、文件及网络 IO、辅助功能支持、插件架构和 Dart 运行环境及编译环境的工具链。
# 引擎将底层 C++ 代码包装成 Dart 代码,通过 dart:ui 暴露给 Flutter 框架层。该库暴露了最底层的原语,包括用于驱动输入、图形、和文本渲染的子系统的类。
# 应用结构:app-anatomy
# flutter 构建的文件结构:
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 化快照” 格式的容器,但在单独的可执行部分中包含编译的代码,以下是它们的结构:

$ 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
# Dart 构建
# Dart 混淆
# dart 64 的寄存器和函数调用约定:
       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
# dart 32 的寄存器和函数调用约定:
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
# example:
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};stmdb sp!存储数据前递减寄存器(Store Multiple Decrement Before)
0xf69ace64    e28db000               add fp, sp, #0
        ;; CheckStackOverflow:8(stack=0, loop=0)将字段偏移表(限制个数为36)加载到ip中并检测栈溢出
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
}
# another example:
// prologue, polymorphic entry
000 | stmdb sp!, {fp, lr}
004 | add fp, sp, #0
008 | sub sp, sp, #4
// optional parameter handling
00c | ldr r0, [r4, #0x13] // arr[2] (positional arg count)
010 | ldr r1, [r4, #0xf]  // arr[1] (argument count)
014 | cmp r0, #0          // check if we have positional args
018 | bgt 0x74            // jump to 08c
// check named args
01c | ldr r0, [r4, #0x17]  // arr[3] (first arg name)
020 | add ip, pp, #0x2000  // 
024 | ldr ip, [ip, #0x4a7] // string "x"
028 | cmp r0, ip           // check if arg present
02c | bne 0x20             // jump to 04c
030 | ldr r0, [r4, #0x1b]    // arr[4] (first arg position)
034 | sub r2, r1, r0         // r2 = arg_count - position
038 | add r0, fp, r2, lsl #1 // r0 = fp + r2 * 2
    |                        // this is really r2 * 4 because it's an smi
03c | ldr r0, [r0, #4]       // read arg
040 | mov r2, r0             // 
044 | mov r0, #2             // 
048 | b 12                   // jump to 054
04c | ldr r2, [thr, #0x68] // thr->objectNull
050 | mov r0, #0           // 
054 | str r2, [fp, #-4] // store arg in local
// done loading args
058 | cmp r1, r0 // check if we have read all args
05c | bne 0x30   // jump to 08c
// continue prologe
060 | ldr ip, [thr, #0x24] // thr->stackLimit
064 | cmp sp, ip           //
068 | blls -0x5af00        // stackOverflowStubWithoutFpuRegsStub
// rest of function
06c | ...
// incompatible args path
08c | ldr r6, [pp, #0x33] // Code* callClosureNoSuchMethod
090 | sub sp, fp, #0      // 
094 | ldmia sp!, {fp, lr} // exit frame
098 | ldr pc, [r6, #3]    // invoke stub

一些普遍做题思路:先 reflutter 然后对着 dump 出来的 offset 进行 hook / 恢复符号表

大概流程懂了,等个环境先

ARM 交叉编译工具链 (32 位): sudo apt-get install gcc-arm-linux-gnueabi binutils-arm-linux-gnueabi (64 位): sudo apt-get install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu

寻找 SVC 指令出现的地址: aarch64-linux-gnu-objdump -D libapp.so | grep -B2 -A2 --color=always "svc"


# leaves 大哥带着在看这个 babyanti2,比赛的时候被他高速非预期了,但是现在复盘感觉预期解难度有点离谱的
# 目前还没有完整复现,但是大概记录一下:
  • 最简单的先把 anti 里面的环境检测 hook 掉

  • 然后合理就去 gg 修改分数,但是发现有疑似内存检查

  • flutter 的 libapp.so 恢复符号表(基本上就是我上面说的方法)发现有对内存进行的操作,进行 hook

  • 然后不够,libapp.so 里面有一个 generateShellcodes ,非常复杂;可以 hook mprotect ,发现有一个传入 0x7(可读、写、执行)的调用,比较异常,对其操作的 0x1000 长度的地址 dump 下来分析,发现好几十个 SVC 调用

  • 后续: 结论是两层 shellcode 调用 mincore,检查是否有内存缺页更改的操作,确实难


# 9.14: BabyAnti2 完整复现:

AntiCheatPlugin 的 Java 层几乎看不到东西,只能判断使用 flutter,那么必须继续看下去,找 dart 层和 native 层的逻辑

image-20230914083329468

直接看 native:

image-20230914083721077

libflutter.so :flutter 预编译的组件库(不会因为开发者的 Dart 而改变),flutter 引擎的主要组成部分,包含了 flutter 运行所需要的核心代码,负责渲染 Flutter widgets、处理事件、与 Dart VM 进行交互以及其他核心功能

libapp.so :Dart 代码编译后的产物,包含所有 Dart 层的 native 逻辑、UI、Flutter plugins,当应用启动时, libflutter.so 会加载 libapp.so 并开始执行 Dart 代码

libanticheat.so(其他) :通过 Android NDK 编写的 native 代码编译产物

尝试通过 rscloura/Doldrums: A Flutter/Dart reverse engineering tool (github.com) 工具提取 libapp.so,失败,DartSDK 版本对不上

image-20230914094329393

寻找版本相关信息,可以得到这段,获取版本号 3.1.0(这里可以猜测这个是 dart 的版本)对应到 Flutter SDK archive | Flutter 可以查找 flutter 的版本号 beta 版本的 3.13.0-0.4.pre(根据发布时间和 Dart version,不过 hash 号没有找到,还得看看)

image-20230914103350070

题目出的时候 reflutter 还没有更新到这个版本,可以认为,在比赛环境下这让做这道题的预期解难度陡然升高

那也只能开始漫长学习,先看看这篇的原理:Fork Bomb for Flutter – PT SWARM (ptsecurity.com)

安装应用: adb push .apk /data/local/tmp

pm install -r .apk

签名工具:Release v1.2.1 · patrickfav/uber-apk-signer (github.com)

java -jar uber-apk-signer.jar --allowResign -**a** release.RE.apk

adb logcat -e reflutter | ForEach-Object { $_ -replace '.*DartVM', '' } >> reflutter.txt