通过重编译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上是一个独立的框架。我们的思路很简单:
- 下载所需版本的引擎
- 修改引擎以打印类名、方法等信息,而不是编写自己的快照解析器
- 用补丁版本替换原始libflutter.so库
- 完成
第一步就很困难:如何找到对应的快照版本?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()来打印更多信息
例如打印函数名:
|
|
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函数。