通过重编译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逆向工程的文章和代码库包括:

  • 逆向工程Flutter for Android(讲解快照格式基础,介绍Doldrums工具,目前仅支持快照版本8ee4ef7a67df9845fba331734198a953)
  • 逆向工程Flutter应用(第一部分)详细讲解Dart内部机制,但未提供代码,第二部分尚未发布
  • darter:Dart快照解析器,支持快照版本c8562f0ee0ebc38ba217c7955956d1cb

主要代码包含两个库:libflutter.so(Flutter引擎)和libapp.so(应用代码)。如果你尝试用标准反汇编器打开libapp.so(AOT编译的Dart代码),会发现这只是一堆原生代码。使用IDA最初只能看到一堆字节,而使用Binary Ninja等工具进行线性扫描时,虽然能看到很多方法,但所有方法都未命名,找不到字符串引用,也没有外部函数引用(无论是libc还是其他库),更没有直接调用内核的系统调用。

使用Darter和Doldrums等工具可以提取类名和方法名,并找到函数实现的地址。这大大方便了应用逆向工作,你还可以使用Frida在这些地址上进行挂钩来提取内存或方法参数。

快照格式问题

特定工具只能处理特定版本快照的原因是:快照格式不稳定,且设计为只能在特定版本的运行时中运行。快照格式非常严格,如果无法解析某部分,就无法继续解析后续部分。

快照格式基本结构为:<标签> <数据字节> <标签> <数据字节>...。每个块没有明确长度,标签头部也没有特定格式,因此无法通过模式匹配来确定块的起始位置。所有内容都是数字,除了源代码本身,没有相关文档。

事实上,这种格式甚至没有版本号,而是通过快照版本字符串来标识。该字符串是通过对快照相关文件的源代码进行哈希生成的。理论上,如果文件发生变化,格式就会改变,但这并不绝对(例如编辑注释也会改变快照版本字符串)。

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

重编译Flutter引擎

Flutter引擎(libflutter.so)是与libapp.so(主应用逻辑代码)分离的库,在iOS上是一个独立的框架。我们的思路很简单:

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

第一步就很困难:如何找到对应的快照版本?darter提供的表格有帮助,但未更新到最新版本。对于其他版本,我们需要搜索并测试是否匹配快照号。

重编译Flutter引擎的说明在此提供,但编译过程中会遇到问题,需要为快照版本修改Python脚本。而且Dart内部本身也不容易处理。我测试的多数旧版本无法正确编译,需要编辑DEPS文件。

修改引擎代码

如果我们修改快照文件,快照哈希值会改变,因此需要在third_party/dart/tools/make_version.py中返回静态版本号来修复这个问题。如果不修补版本,应用将无法启动。

修补后(从打印"Hello World"开始),使用OS::PrintErr(“Hello World”)并重新编译代码,我们可以测试替换.so文件并运行。

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

  • 在runtime/vm/clustered_snapshot.cc中修改Deserializer::ReadProgramSnapshot(ObjectStore* object_store)来打印类表
  • 在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());
}

SSL证书问题

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 设计