通过重编译Flutter引擎实现逆向工程

本文详细介绍了如何通过重编译Flutter引擎来逆向发布版Flutter应用的技术方法,包括快照格式分析、引擎修改步骤和SSL证书绕过技巧,为移动应用安全测试提供实用解决方案。

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

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

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

在本文中,我将仅提供Android平台的示例,但这里写的一切都是通用的,适用于其他平台。简而言之:我们不更新或创建快照解析器,而是重新编译Flutter引擎并将其替换到目标应用中。

Flutter编译应用

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

  • 逆向工程Android版Flutter(解释快照格式基础,介绍Doldrums,截至撰写时仅支持快照版本8ee4ef7a67df9845fba331734198a953)
  • 逆向工程Flutter应用(第1部分)一篇很好的解释Dart内部机制的文章,遗憾的是没有提供代码,且截至撰写时第2部分尚未发布
  • darter:Dart快照解析器,用于转储快照版本c8562f0ee0ebc38ba217c7955956d1cb的工具

主要代码包括两个库:libflutter.so(Flutter引擎)和libapp.so(你的代码)。你可能想知道:如果你尝试使用标准反汇编器打开libapp.so(AOT编译的Dart代码)会发生什么?它只是原生代码,对吧?如果你使用IDA,最初你只会看到这一堆字节。

如果你使用其他工具,如Binary Ninja(它会尝试进行线性扫描),你可以看到很多方法。所有这些方法都没有名称,你也找不到任何字符串引用。也没有外部函数(libc或其他库)的引用,也没有直接调用内核的系统调用(如Go)。

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

快照格式问题

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

基本上,快照格式是这样的:<标签> <数据字节> <标签> <数据字节>… 每个块没有明确的长度,标签的头部也没有特定格式(因此你不能仅通过模式匹配就知道块的开始)。一切都只是数字。除了源代码本身,没有关于此快照的文档。

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

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

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

我以为可以快速检查代码中的这个常量(在新版本中是否已更改),结果发现类型不是一个简单的整数。

1
2
3
4
5
6
7
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()更改为你的特定版本的静态字符串。

如果我们不修补版本会发生什么?应用将无法启动。因此,在修补(仅从使用OS::PrintErr(“Hello World”)打印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)来解决此问题。引用此博客文章的TLDR:

  • Flutter使用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 设计