flutter逆向工程一

前言

几个星期前, 出于某些原因我想逆向某个APK文件, 和平常一样, 我使用apktool/dex2jar/jd-gui三件套开始工作. 出乎意料的是, 我没有在classes.dex里面找到任何程序相关的逻辑, 取而代之的是一些”flutter”的代码段.

初探

flutter是谷歌开发的跨平台框架, 初略的探索之后, 我得知程序的逻辑都存在于”libapp.so”文件中.

1
2
3
4
5
6
7
8
9
10
11
12
13
➜ tree lib
lib
├── arm64-v8a
│   ├── libapp.so
│   └── libflutter.so
├── armeabi-v7a
│   ├── libapp.so
│   └── libflutter.so
└── x86_64
├── libapp.so
└── libflutter.so

3 directories, 6 files

APK解开后, lib目录下面会有两个库文件, “libflutter.so”是框架的运行时支持, “libapp.so”则是逻辑层编译而成的产物.

1
2
3
4
5
6
7
8
9
10
➜ readelf --symbols --wide libapp.so

Symbol table '.dynsym' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00001000 12 FUNC GLOBAL DEFAULT 1 _kDartBSSData
2: 00002000 9616 FUNC GLOBAL DEFAULT 2 _kDartVmSnapshotInstructions
3: 00005000 24400 FUNC GLOBAL DEFAULT 3 _kDartVmSnapshotData
4: 0000b000 0x1edf30 FUNC GLOBAL DEFAULT 4 _kDartIsolateSnapshotInstructions
5: 001f9000 0x175bb0 FUNC GLOBAL DEFAULT 5 _kDartIsolateSnapshotData

查看导出符号, 只有上面四个, 但从名称上分析并不像函数. 接下来开始使用IDA分析:

符号”_kDartVmSnapshotData”看起来的确不像是导出函数, 更像是序列化之后的数据, 应该是提供给dart虚拟机解释执行的.

探索

我搜索了很久, 几乎没有关于flutter逆向相关的文章, 只有reverse-engineering-flutter-apps-part-1这篇文章进行了剖析.

快照

“libapp.so”实际上是dart编译生成的快照文件, 这种快照格式称为”AOT”, 上面提到的四个导出符号就是快照数据的起始偏移:

  • “_kDartVmSnapshotData”: 虚拟机运行所需的Object等相关数据
  • “_kDartVmSnapshotInstructions”: 虚拟机运行所需指令相关数据
  • “_kDartIsolateSnapshotData”: 逻辑层运行所需的Object等相关数据
  • “_kDartIsolateSnapshotInstructions”: 逻辑层运行所需指令相关数据

官方没有文档公布快照AOT快照的数据格式, 不过幸好flutter和dart都是开源的, 我们可以调试AOT生成/加载的全过程, 就能知道快照里存了什么数据, 在逆向过程中又能如何利用这些数据.

编译

文章中提到了如何单纯的将dart文件转变成”libapp.so”:

1
2
~/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 --output-dill app.dill main.dart
gen_snapshot --causal_async_stacks --deterministic --snapshot_kind=app-aot-elf --elf=libapp.so --strip app.dill

上面使用的是flutter sdk中包含的dart相关工具, 我测试之后发现只需要dart sdk就可以完成AOT的生成. 那么接下来, 我们需要编译一个带有调试信息的dart sdk:

1
2
3
4
5
6
7
8
9
10
11
# dev tool
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH="$PATH:$PWD/depot_tools"

# fetch source
mkdir dart-sdk; cd dart-sdk
fetch dart

# build
cd sdk
./tools/build.py -m debug --no-goma -a x64 --debug-opt-level 0 all

查看编译生成的产物:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
➜ tree -L 1
.
├── -
├── app.dill
├── args.gn
├── build.ninja
├── build.ninja.d
├── compressed_observatory_archive.tar
├── dart
├── dart2js_nnbd_strong_outline.dill
├── dart2js_nnbd_strong_platform.dill
├── dart2js_nnbd_strong_platform.dill.d
├── dart2js_outline.dill
├── dart2js_platform.dill
├── dart2js_platform.dill.d
├── dart2js_server_nnbd_strong_outline.dill
├── dart2js_server_nnbd_strong_platform.dill
├── dart2js_server_nnbd_strong_platform.dill.d
├── dart2js_server_outline.dill
├── dart2js_server_platform.dill
├── dart2js_server_platform.dill.d
├── dartdevc.dill
├── dartdevc.js
├── dartdevc.js.map
├── dartdev.dill
├── dart_precompiled_runtime
├── dart_precompiled_runtime_product
├── dart-sdk
├── ddc_outline.dill
├── ddc_outline_sound.dill
├── ddc_platform.dill
├── ddc_platform.dill.d
├── ddc_platform_sound.dill
├── ddc_platform_sound.dill.d
├── dds.dart.snapshot
├── dev_compiler
├── exe.stripped
├── gen
├── gen_kernel_bytecode.dill
├── gen_snapshot
├── gen_snapshot_fuchsia
├── gen_snapshot_host_targeting_host
├── gen_snapshot_product
├── gen_snapshot_product_fuchsia
├── gen_snapshot_product_host_targeting_host
├── icudtl.dat
├── icudtl_extra.dat
├── kernel-service.dart.snapshot
├── libapp.so
├── libentrypoints_verification_test_extension.so
├── libentrypoints_verification_test_extension.so.TOC
├── libffi_test_dynamic_library.so
├── libffi_test_dynamic_library.so.TOC
├── libffi_test_functions.so
├── libffi_test_functions.so.TOC
├── libsample_extension.so
├── libsample_extension.so.TOC
├── libtest_extension.so
├── libtest_extension.so.TOC
├── obj
├── observatory_archive.tar
├── offsets_extractor
├── offsets_extractor_precompiled_runtime
├── out.js
├── out.js.deps
├── out.js.map
├── process_test
├── run_vm_tests
├── toolchain.ninja
├── vm_outline_strong.dill
├── vm_outline_strong_product.dill
├── vm_outline_strong_stripped.dill
├── vm_platform_strong.dill
├── vm_platform_strong.dill.d
├── vm_platform_strong.dill.S
├── vm_platform_strong_product.dill
├── vm_platform_strong_product.dill.d
├── vm_platform_strong_stripped.dill
├── vm_platform_strong_stripped.dill.d
└── zlib_bench

5 directories, 73 files

dart-sdk子目录就和我们在官网下载到的版本一样, 但是它里面的程序符号表已被去除, 我们无法基于源码调试, 我们只能使用顶层那些散乱的产物. 根据官网的描述, 使用dart2native就能生成AOT快照:

1
dart2native bin/main.dart -k aot

在我们的目标产物中, 查看dart2native内容, 发现它只是个脚本文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env bash
# Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.

# Run dart2native.dart.snapshot on the Dart VM

function follow_links() {
file="$1"
while [ -h "$file" ]; do
# On Mac OS, readlink -f doesn't work.
file="$(readlink "$file")"
done
echo "$file"
}

# Unlike $0, $BASH_SOURCE points to the absolute path of this file.
PROG_NAME="$(follow_links "$BASH_SOURCE")"

# Handle the case where dart-sdk/bin has been symlinked to.
BIN_DIR="$(cd "${PROG_NAME%/*}" ; pwd -P)"
SNAPSHOTS_DIR="${BIN_DIR}/snapshots"
DART="$BIN_DIR/dart"

exec "$DART" "${SNAPSHOTS_DIR}/dart2native.dart.snapshot" $*

脚本使用dart执行dart2native.dart.snapshot, 说明dart2native.dart.snapshot是由dart编写而成的, 具体的源码可以查看github仓库. 通过阅读源码, 发现它其实是通过调用gen_snapshot进行的快照生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
final String kernelFile = path.join(tempDir.path, 'kernel.dill');
final kernelResult = await generateAotKernel(Platform.executable, genKernel,
productPlatformDill, sourcePath, kernelFile, packages, defines,
enableExperiment: enableExperiment);
if (kernelResult.exitCode != 0) {
stderr.writeln(kernelResult.stdout);
stderr.writeln(kernelResult.stderr);
await stderr.flush();
throw 'Generating AOT kernel dill failed!';
}

if (verbose) {
print('Generating AOT snapshot.');
}

final String snapshotFile = (outputKind == Kind.aot
? outputPath
: path.join(tempDir.path, 'snapshot.aot'));
final snapshotResult = await generateAotSnapshot(genSnapshot, kernelFile,
snapshotFile, debugPath, enableAsserts, extraOptions);
if (snapshotResult.exitCode != 0) {
stderr.writeln(snapshotResult.stdout);
stderr.writeln(snapshotResult.stderr);
await stderr.flush();
throw 'Generating AOT snapshot failed!';
}

先使用dart生成kernel.dill, 然后使用gen_snapshot将kernel.dill转成AOT, 这也与文章中提到的方法一致.

生成

进入dart-sdk子目录, 编辑hello.dart:

1
2
3
void main() {
print('Hello, World!');
}

接着生成kernel.dill:

1
./bin/dart bin/snapshots/gen_kernel.dart.snapshot --platform lib/_internal/vm_platform_strong_product.dill --aot -Ddart.vm.product=true -o kernel.dill hello.dart

使用gen_snapshot生成AOT快照:

1
./bin/utils/gen_snapshot --snapshot_kind=app-aot-elf --elf=libapp.so kernel.dill

经历重重关卡, 我们成功将dart源码转成了AOT快照, 那么接下来, 我们只需要使用gdb一步步跟踪快照的生成.