通过重编译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在这些地址上挂钩以转储内存或方法参数。
快照格式问题
特定工具只能转储特定版本快照的原因是:快照格式不稳定,且设计为由特定版本的运行时运行。与其他可以跳过未知或不支持格式的格式不同,快照格式非常严格。如果无法解析某部分,就无法解析下一部分。
基本来说,快照格式如下:<tag> <data bytes> <tag> <data bytes> …
每个块没有显式长度,标签头也没有特定格式(因此无法通过模式匹配知道块的开始)。所有内容都是数字。除了源代码本身,没有关于此快照的文档。
实际上,甚至没有此格式的版本号。格式由快照版本字符串标识。该字符串通过哈希快照相关文件的源代码生成。假设如果文件更改,格式就会更改。这在大多数情况下成立,但并非总是如此(例如:编辑注释会更改快照版本字符串)。
最初的想法是通过查看Dart源代码的差异来修改Doldrums或Darter以适应所需版本。但事实证明这并不容易:枚举有时插入在中间(意味着需要将所有常量移动一个数字),且Dart广泛使用C++模板进行位操作。例如,在Doldrums代码中看到如下内容:
|
|
本以为可以快速检查代码中的此常量(在新版本中是否更改),但类型并非简单整数。
|
|
可以看到此位字段实现为BitField模板类。这个特定位易于阅读,但如果看到kNextBit,需要回顾所有先前的位定义。对于经验丰富的C++开发人员来说,这并不难,但跟踪版本之间的这些变化需要大量手动检查。
结论是:我不想维护Python代码,下次应用更新重新测试时,他们可能使用更新版本的Flutter SDK和另一个快照版本。对于当前工作,需要测试两个使用不同Flutter版本的应用:一个是应用商店已发布的,另一个是即将发布的。
重编译Flutter引擎
Flutter引擎(libflutter.so)是与libapp.so(主应用逻辑代码)分离的库,在iOS上是一个单独的框架。想法非常简单:
- 下载所需版本的引擎
- 修改它以打印类名、方法等,而不是编写自己的快照解析器
- 用修补后的版本替换原始libflutter.so库
- 收益
第一步已经很困难:如何找到相应的快照版本?darter的表格有帮助,但未更新到最新版本。对于其他版本,需要搜索并测试是否有匹配的快照号。重编译Flutter引擎的说明在此,但编译过程中有一些问题,需要修改Python脚本以适应快照版本。此外,Dart内部本身并不容易处理。
测试的大多数旧版本无法正确编译。需要编辑DEPS文件以使其编译。在我的情况下,差异很小,但需要在网上搜索才能找到。不知何故,特定提交不可用,需要使用其他版本。注意:不要盲目应用此补丁,基本检查以下两点:
- 如果提交不可用,从发布日期找到最接近的提交
- 如果某物引用_internal,可能应删除_internal部分
|
|
现在可以开始编辑快照文件以了解其工作原理。但如前所述:如果修改快照文件,快照哈希将更改,因此需要在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()
以打印更多信息
例如,打印函数名:
|
|
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。