[Flutter翻译]通过重新编译Flutter引擎对Flutter应用进行逆向工程。

1,733 阅读8分钟

原文地址:tinyhack.com/2021/03/07/…

原文作者:tinyhack.com

发布时间:2021年3月7日

要对一个flutter应用的发布版本进行逆向工程并不容易,因为没有工具,而且flutter引擎本身变化很快。截至目前,如果你很幸运,如果一个flutter应用是用特定版本的Flutter SDK构建的,你可以使用darterDoldrums转储该应用的类和方法名。

如果你非常幸运,这就是我第一次需要测试Flutter App时发生的事情:你甚至不需要对App进行反向工程。如果应用非常简单,使用简单的HTTPS连接,你可以使用拦截代理(如Burp或Zed Attack Proxy)来测试所有功能。我刚刚测试的应用在HTTPS的基础上使用了额外的一层加密,这也是我需要进行实际逆向工程的原因。

在这篇文章中,我将只给出Android平台的例子,但这里写的所有内容都是通用的,适用于其他平台。TLDR是:我们不更新或创建快照解析器,而只是重新编译flutter引擎,并将其替换到我们的目标应用中。

Flutter编译后的应用

目前我找到的关于Flutter逆向工程的几篇文章和资料库是。

主要代码由两个库libflutter.so(flutter引擎)和libapp.so(你的代码)组成。你可能会好奇:如果你试图用标准的反汇编器打开一个libapp.so(被AOT编译的Dart代码),实际上会发生什么。这只是本地代码,对吗?如果你使用IDA,最初,你只会看到这一堆字节。

image.png

如果你使用其他工具,比如二进制ninja会尝试做一些线性扫描,你可以看到很多方法。所有的方法都没有命名,也没有你能找到的字符串引用。也没有外部函数的引用(无论是libc还是其他库),也没有直接调用内核的syscall(比如Go)。

用Darter dan Doldrums这样的工具,你可以转储类名和方法名,你可以找到函数的实现地址。下面是一个使用Doldrums进行转储的例子。这对反转应用有极大的帮助。你也可以使用Frida在这些地址处进行钩子,转储内存或方法参数。

image.png

快照格式问题

特定的工具只能转储特定版本的快照,原因是:快照格式不稳定,它是为了特定版本的运行而设计的。与其他一些格式可以跳过未知或不支持的格式不同,快照格式是非常不宽容的。如果你不能解析一个部分,你可以解析下一个部分。

基本上,快照格式是这样的。<tag> <data bytes> <tag> <data bytes> ......每一个分块都没有明确的长度,标签的头也没有特定的格式(所以你不能只做一个模式匹配就想知道一个分块的开始)。一切都只是数字。除了源代码本身,没有任何关于这个快照的文档。

事实上,这种格式甚至没有版本号。这个格式是由快照版本串来标识的。版本字符串是由快照相关文件的源代码哈希生成的。我们假设如果文件被改变,那么格式也会被改变。这在大多数情况下是正确的,但并不总是如此(例如:如果你编辑了一条评论,快照版本串就会改变)。

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

def decodeTypeBits(value):
       return value &amp; 0x7f

dart.googlesource.com/sdk//+/2beb… 我想我可以快速检查一下代码中的这个常量(新版本中是否有变化),类型原来不是一个简单的整数。

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

你可以看到这个Bitfield是作为BitField模板类来实现的。这个特殊的位很容易读懂,但是如果你看到kNextBit,你需要回溯之前所有的位定义。我知道对于经验丰富的C++开发者来说,这并不难,但还是:要跟踪这些版本之间的变化,你需要做大量的手工检查。

我的结论是。我不想维护Python代码,下次更新应用进行复测的时候,他们可以使用较新版本的Flutter SDK,再来一个快照版本。而对于我正在做的具体工作。我需要用两个不同的Flutter版本测试两个应用:一个是已经在应用商店发布的东西,另一个是即将发布的应用。

重建Flutter引擎

flutter引擎(libflutter.so)是一个独立于libapp.so(主应用逻辑代码)的库,在iOS上,这是一个独立的框架。思路非常简单。

  • 下载我们想要的引擎版本
  • 修改它来打印类名、方法等,而不是编写我们自己的快照分析器。
  • 用我们的补丁版本替换原来的libflutter.so库。
  • 盈利

第一步已经很困难了:如何才能找到相应的快照版本?darter的这张表可以帮助我们,但是没有更新最新的版本。对于其他版本,我们需要猎取并测试它是否有匹配的快照号。这里有重新编译Flutter引擎的指令,但是编译过程中出现了一些小插曲,我们需要修改快照版本的python脚本。还有:Dart内部本身就不是那么容易操作的。

我测试过的大部分旧版本都不能正确编译。你需要编辑DEPS文件来让它编译。在我的情况下:差异很小,但我需要在网上搜到这个。不知何故,特定的提交不可用,我需要使用不同的版本。注意:不要盲目应用这个补丁,基本上要检查这两点。

  • 如果一个提交没有了,就找离发布日期最近的一个。
  • 如果某件事情指的是_internal的,你可能应该把_internal的部分去掉。
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()这一行改为你的特定版本。

image.png

如果我们不给版本打补丁会怎么样呢,应用无法启动。所以,在使用OS::PrintErr("Hello World")打上补丁(就从打印hello world开始)并重新编译代码后,我们可以测试替换.so文件,然后运行它。

我做了很多实验(比如尝试FORCE_INCLUDE_DISASSEMBLER),所以我没有一个干净的修改方法分享给大家,但我可以提供一些修改的提示。

  • 在runtime/vm/clustered_snapshot.cc中,我们可以修改Deserializer::ReadProgramSnapshot(ObjectStore* object_store)来打印类表隔离->class_table()->Print()
  • 在runtime/vm/class_table.cc中,我们可以修改void ClassTable::Print()来打印更多的信息。 例如,打印函数名称。
const Array&amp; funcs = Array::Handle(cls.functions());  
 for (intptr_t j = 0; j < funcs.Length(); j++) {
      Function&amp; func = Function::Handle();
      func = cls.FunctionFromIndex(j);
      OS::PrintErr("Function: %s", func.ToCString());
}

旁注:SSL证书

Flutter应用的另一个问题是:它不会信任用户安装的root cert。这对于pentesting来说是一个问题,有人做了一个关于如何给二进制打补丁的说明(直接或使用Frida)来解决这个问题。引用这个博文的TLDR。

  • Flutter使用的是Dart,而Dart并没有使用系统的CA商店
  • Dart使用的CA列表被编译到应用程序中。
  • Dart在安卓系统上是没有代理意识的,所以使用ProxyDroid与iptables。
  • 勾选 x509.cc 中的 session_verify_cert_chain 函数来禁用链式验证。

通过重新编译Flutter引擎,可以很容易地实现这一点。我们只需按原样修改源代码(third_party/boringssl/src/ssl/handshake.cc),而不需要在编译后的代码中找到汇编字节模式。

混淆Flutter

使用这里提供的说明可以混淆Flutter/Dart应用程序。这将使反转变得有点困难。请注意,只有名称被混淆,没有执行高级控制流混淆。

结束语

我很懒,重新编译flutter引擎是我采取的捷径,而不是写一个合适的快照解析器。当然,其他人在反转其他技术时,也有类似的想法,即黑掉运行时引擎,例如,要反转一个混淆的PHP脚本,可以用PHP模块钩住eval。


通过www.DeepL.com/Translator(免费版)翻译