通过重编译Flutter引擎逆向工程Flutter应用

本文详细介绍了如何通过重编译Flutter引擎来逆向工程发布版的Flutter应用,包括快照格式问题、引擎修改步骤以及SSL证书处理等关键技术点。

通过重编译Flutter引擎逆向工程Flutter应用

逆向工程发布版的Flutter应用并不容易,因为工具不可用且Flutter引擎本身变化迅速。目前,如果运气好,可以使用darter或Doldrums等工具转储Flutter应用的类名和方法名,但前提是应用使用特定版本的Flutter SDK构建。

如果非常幸运(就像我第一次测试Flutter应用时那样),甚至不需要逆向工程应用。如果应用非常简单且使用简单的HTTPS连接,可以使用拦截代理(如Burp或Zed Attack Proxy)测试所有功能。但最近测试的应用在HTTPS之上增加了额外的加密层,因此需要进行实际的逆向工程。

本文仅以Android平台为例,但所有内容均通用且适用于其他平台。简而言之:我们不更新或创建快照解析器,而是重编译Flutter引擎并将其替换到目标应用中。

Flutter编译应用

目前找到的关于Flutter逆向工程的文章和仓库包括:

主要代码包括两个库:libflutter.so(Flutter引擎)和libapp.so(应用代码)。如果尝试使用标准反汇编器打开libapp.so(AOT编译的Dart代码),会发现它只是本地代码。在IDA中,最初只能看到一堆字节。

使用Binary Ninja等工具进行线性扫描时,可以看到许多方法,但所有方法均未命名,且找不到字符串引用。也没有外部函数(如libc或其他库)的引用,更没有直接调用内核的系统调用(如Go)。

使用Darter和Doldrums等工具可以转储类名和方法名,并找到函数实现的地址。以下是使用Doldrums转储的示例,这对逆向应用非常有帮助。还可以使用Frida在这些地址上挂钩以转储内存或方法参数。

快照格式问题

特定工具只能转储特定版本快照的原因是:快照格式不稳定,且设计为由特定版本的运行时运行。与其他可以跳过未知或不支持格式的格式不同,快照格式非常严格。如果无法解析某部分,就无法解析下一部分。

基本来说,快照格式如下:<tag> <data bytes> <tag> <data bytes> … 每个块没有显式长度,标签头也没有特定格式(因此无法通过模式匹配知道块的开始)。所有内容都是数字。除了源代码本身,没有关于此快照的文档。

实际上,甚至没有此格式的版本号。格式由快照版本字符串标识。该字符串通过哈希快照相关文件的源代码生成。假设如果文件更改,格式就会更改。这在大多数情况下成立,但并非总是如此(例如:编辑注释会更改快照版本字符串)。

最初的想法是通过查看Dart源代码的差异来修改Doldrums或Darter以适应所需版本。但事实证明这并不容易:枚举有时插入在中间(意味着需要将所有常量移动一个数字),且Dart广泛使用C++模板进行位操作。例如,在Doldrums代码中看到如下内容:

1
def decodeTypeBits(value): return value & 0x7f

本以为可以快速检查代码中的此常量(在新版本中是否更改),但类型并非简单整数。

1
2
class ObjectPool : public Object { using TypeBits = compiler::ObjectPoolBuilderEntry::TypeBits; }
struct ObjectPoolBuilderEntry { using TypeBits = BitField<uint8_t, EntryType, 0, 7>; }

可以看到此位字段实现为BitField模板类。这个特定位易于阅读,但如果看到kNextBit,需要回顾所有先前的位定义。对于经验丰富的C++开发人员来说,这并不难,但跟踪版本之间的这些变化需要大量手动检查。

结论是:我不想维护Python代码,下次应用更新重新测试时,他们可能使用更新版本的Flutter SDK和另一个快照版本。对于当前工作,需要测试两个使用不同Flutter版本的应用:一个是应用商店已发布的,另一个是即将发布的。

重编译Flutter引擎

Flutter引擎(libflutter.so)是与libapp.so(主应用逻辑代码)分离的库,在iOS上是一个单独的框架。想法非常简单:

  1. 下载所需版本的引擎
  2. 修改它以打印类名、方法等,而不是编写自己的快照解析器
  3. 用修补后的版本替换原始libflutter.so库
  4. 收益

第一步已经很困难:如何找到相应的快照版本?darter的表格有帮助,但未更新到最新版本。对于其他版本,需要搜索并测试是否有匹配的快照号。重编译Flutter引擎的说明在此,但编译过程中有一些问题,需要修改Python脚本以适应快照版本。此外,Dart内部本身并不容易处理。

测试的大多数旧版本无法正确编译。需要编辑DEPS文件以使其编译。在我的情况下,差异很小,但需要在网上搜索才能找到。不知何故,特定提交不可用,需要使用其他版本。注意:不要盲目应用此补丁,基本检查以下两点:

  • 如果提交不可用,从发布日期找到最接近的提交
  • 如果某物引用_internal,可能应删除_internal部分
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
diff --git a/DEPS b/DEPS
index e173af55a..54ee961ec 100644
--- a/DEPS
+++ b/DEPS
@@ -196,7 +196,7 @@ deps = {
    Var('dart_git') + '/dartdoc.git@b039e21a7226b61ca2de7bd6c7a07fc77d4f64a9',
    'src/third_party/dart/third_party/pkg/ffi':
-   Var('dart_git') + '/ffi.git@454ab0f9ea6bd06942a983238d8a6818b1357edb',
+   Var('dart_git') + '/ffi.git@5a3b3f64b30c3eaf293a06ddd967f86fd60cb0f6',
    'src/third_party/dart/third_party/pkg/fixnum':
    Var('dart_git') + '/fixnum.git@16d3890c6dc82ca629659da1934e412292508bba',
@@ -468,7 +468,7 @@ deps = {
   'src/third_party/android_tools/sdk/licenses': {
       'packages': [
         {
-        'package': 'flutter_internal/android/sdk/licenses',
+        'package': 'flutter/android/sdk/licenses',
         'version': 'latest',
         }
       ],

现在可以开始编辑快照文件以了解其工作原理。但如前所述:如果修改快照文件,快照哈希将更改,因此需要在third_party/dart/tools/make_version.py中返回静态版本号来修复。如果修改VM_SNAPSHOT_FILES中的任何文件,将snapshot_hash = MakeSnapshotHashString()更改为特定版本的静态字符串。

如果不修补版本会怎样?应用将无法启动。因此,在修补(从打印“Hello World”开始)使用OS::PrintErr("Hello World")并重新编译代码后,可以测试替换.so文件并运行。

我进行了许多实验(例如尝试FORCE_INCLUDE_DISASSEMBLER),因此没有干净的修改可以分享,但可以提供一些修改提示:

  • 在runtime/vm/clustered_snapshot.cc中,可以修改Deserializer::ReadProgramSnapshot(ObjectStore* object_store)以打印类表isolate->class_table()->Print()
  • 在runtime/vm/class_table.cc中,可以修改void ClassTable::Print()以打印更多信息

例如,打印函数名:

1
2
3
4
5
6
const Array& funcs = Array::Handle(cls.functions());
for (intptr_t j = 0; j < funcs.Length(); j++) {
    Function& func = Function::Handle();
    func = cls.FunctionFromIndex(j);
    OS::PrintErr("Function: %s", func.ToCString());
}

Flutter应用的另一个问题是:不信任用户安装的根证书。这对渗透测试是个问题,有人记录了如何修补二进制文件(直接或使用Frida)以解决此问题。引用该博客文章的简而言之:

  • Flutter使用Dart,Dart不使用系统CA存储
  • Dart使用编译到应用中的CA列表
  • Dart在Android上不感知代理,因此使用ProxyDroid和iptables
  • 挂钩x509.cc中的session_verify_cert_chain函数以禁用链验证

通过重编译Flutter引擎,可以轻松完成此操作。只需修改源代码(third_party/boringssl/src/ssl/handshake.cc),无需在编译代码中查找汇编字节模式。

混淆Flutter

可以使用此处提供的说明混淆Flutter/Dart应用。这将使逆向工程变得稍微困难。注意,只有名称被混淆,没有执行高级控制流混淆。

结论

我很懒,重编译Flutter引擎是我采取的捷径,而不是编写适当的快照解析器。当然,其他人在逆向其他技术时也有类似的黑客运行时引擎的想法,例如,逆向工程混淆的PHP脚本时,可以使用PHP模块挂钩eval。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计