Flutter 逆向工程和安全分析

admin 2024年4月10日10:40:01评论7 views字数 12775阅读42分35秒阅读模式

介绍

Flutter 由 Google 开发,是一个广泛使用的跨平台移动应用程序开发框架,还支持 Web 和桌面应用程序。它出现了显着增长,在 Android 和 iOS 市场分别增长了 340% 和 270%。

该框架因其高性能而受到认可,这要归功于 Skia 渲染引擎。它还为复杂的 UI 设计提供了灵活的 UI 系统。

  • Flutter 逆向工程和安全分析

Flutter 逆向工程和安全分析

Flutter 能够与现有应用程序集成,并从头开始设计应用程序。

该平台拥有强大而活跃的社区,有助于其快速增长。由于其新颖性,Flutter 目前在安全性方面的研究不足,在移动应用程序安全分析方面所做的工作有限。

尽管 Flutter 很受欢迎,但它也不能幸免于安全风险,这凸显了对更好的工具和框架知识的需求。这包括学习如何分解和研究 Flutter 应用程序,以发现和理解潜在的安全问题。

工具和技术

要分析 Flutter 应用程序,可以使用两类工具:静态分析工具和动态分析工具。

静态分析工具(如 Doldrums)通过重新实现文件格式来解析文件。Doldrums 仅部分支持旧版本的 Flutter(2.10 和 2.12,撰写本文时的最新版本是 3.10),并且由于运行时快速和持续的演进所面临的挑战,不再维护。

另一方面,动态分析工具,如 reFlutter 修补 Flutter 运行时,以在执行过程中收集信息。这些工具通常需要补充实用程序(如 Frida)或调试器(如 LLDB)来检测动态识别的函数。

尽管 reFlutter 具有强大的方法来分析 Flutter 应用程序,但它不再支持最新版本的运行时。对 Flutter 运行时的更改导致了无法工作的补丁。

结构颤振应用

所有平台上的 Flutter 应用程序都由包装器应用程序、Flutter 运行时和 Flutter 应用程序组成。

下面是 Linux 应用程序布局的示例。 是应用程序代码,是包装器,是运行时。libapp.socounterlibflutter_linux_gtk.so

├── counter
├── data
│ ├── flutter_assets
│ │ ├── AssetManifest.json
│ │ ├── FontManifest.json
│ │ ├── fonts
│ │ │ └── MaterialIcons-Regular.otf
│ │ ├── NOTICES.Z
│ │ ├── packages
│ │ │ └── cupertino_icons
│ │ │ └── assets
│ │ │ └── CupertinoIcons.ttf
│ │ ├── shaders
│ │ │ └── ink_sparkle.frag
│ │ └── version.json
│ └── icudtl.dat
└── lib
├── libapp.so
└── libflutter_linux_gtk.so

包装应用程序提供了一个入口点,该入口点与底层操作系统协调,用于访问呈现图面、辅助功能和输入等服务,并管理消息事件循环。

Flutter 运行时负责加载 Flutter 应用程序,并为应用程序提供一组功能。它由 Dart 虚拟机 (VM) 和 Flutter 引擎组成,后者主要用 C++ 编写,支持所有 Flutter 应用程序所需的原语。

以下是 Flutter 运行时公开的函数列表:

000000000041a860 g    DF .text  00000000000001e8  Base        fl_binary_messenger_send_response
000000000041f740 g DF .text 0000000000000043 Base fl_json_message_codec_new
000000000042a1f0 g DF .text 000000000000015c Base fl_method_channel_invoke_method
000000000042a390 g DF .text 000000000000012b Base fl_method_channel_invoke_method_finish
000000000042b410 g DF .text 000000000000007c Base fl_method_success_response_get_result
0000000000437a00 g DF .text 0000000000000050 Base fl_view_new
000000000041b890 g DF .text 000000000000007c Base fl_dart_project_get_icu_data_path
0000000000e8f8d8 g D .bss 0000000000000000 Base _end
000000000041b990 g DF .text 00000000000000a6 Base fl_dart_project_set_dart_entrypoint_arguments
00000000004364c0 g DF .text 000000000000003d Base fl_value_get_int
00000000004365f0 g DF .text 000000000000003d Base fl_value_get_int32_list
00000000004133d0 w DF .text 0000000000000005 Base operator delete(void*, std::align_val_t)
00000000004192c0 g DF .text 00000000000001c1 Base fl_basic_message_channel_new
00000000004198d0 g DF .text 000000000000015d Base fl_basic_message_channel_send
0000000000419a70 g DF .text 000000000000012b Base fl_basic_message_channel_send_finish
000000000042d980 g DF .text 0000000000000080 Base fl_plugin_registry_get_type
000000000041be60 g DF .text 0000000000000045 Base fl_engine_new_headless
0000000000436480 g DF .text 000000000000003f Base fl_value_get_bool
00000000004297b0 g DF .text 000000000000007c Base fl_method_call_get_args
000000000042ae40 g DF .text 000000000000003b Base fl_method_success_response_get_type
0000000000434f70 g DF .text 0000000000000160 Base fl_texture_registrar_mark_texture_frame_available
000000000041f790 g DF .text 0000000000000176 Base fl_json_message_codec_encode
000000000042d320 g DF .text 0000000000000154 Base fl_plugin_registrar_get_messenger
00000000004368c0 g DF .text 0000000000000073 Base fl_value_set
00000000004293f0 g DF .text 00000000000000be Base fl_message_codec_decode_message

该引擎负责光栅化合成场景、提供 Flutter 核心 API 的低级实现、处理文件和网络 I/O、可访问性支持、插件架构以及 Dart 运行时和编译工具链等任务。运行时是固定的,它不包含任何特定于应用程序的代码。

Flutter 应用程序是一个独立的库,但不会作为标准库加载。相反,它遵循包含应用程序布局、代码和对象的自定义格式。

readelf -Ws libapp.so 

Symbol table '.dynsym' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000001ec000 19904 OBJECT GLOBAL DEFAULT 7 _kDartVmSnapshotInstructions
2: 00000000001f0dc0 0x2e0420 OBJECT GLOBAL DEFAULT 7 _kDartIsolateSnapshotInstructions
3: 0000000000000200 33728 OBJECT GLOBAL DEFAULT 2 _kDartVmSnapshotData
4: 00000000000085c0 0x1dfff0 OBJECT GLOBAL DEFAULT 2 _kDartIsolateSnapshotData
5: 00000000000001c8 32 OBJECT GLOBAL DEFAULT 1 _kDartSnapshotBuildId

应用文件包含两个快照:一个用于 VM 隔离,另一个用于具有实际物质的隔离。这些隔离中的每一个都分为数据部分(包含隔离的堆)和说明部分(包含本机编译的代码)。

Flutter 使用 Dart 的隔离模型,其中每段 Dart 代码都在隔离中运行,隔离隔离是由称为堆的内存块组成的结构。在 Flutter 中,多个隔离不会被利用,除了始终存在的 VM 隔离之外,只使用一个隔离。

VM 隔离是一种特殊的隔离,它只管理不可变的对象,并且可供其他隔离区访问。

在 Flutter 应用的上下文中,Dart 快照表示 Dart VM 在其执行的特定时间点的序列化状态,包括所有原生编译的代码。在 Flutter 中,隔离快照对应于 Dart VM 在调用 main 之前的状态。

安装程序和运行时修补

为了分析 Flutter 应用程序,我们的目标是依靠一种健壮可维护的方法。我们将修改 Dart SDK 以在运行时从 Flutter 应用程序中提取必要的信息,而不是重新实现文件格式。

编译 Flutter 应用程序时,会在应用程序目录下生成两个文件:

Flutter 逆向工程和安全分析

  • libFlutter.so:此文件作为包含 Flutter 引擎的共享库,负责渲染 UI、处理输入事件和管理应用程序生命周期。

  • libapp.so:此文件包含构成 Flutter 应用程序的实际应用程序代码和逻辑。

这两个文件都包含一个以区分生成版本。用作生成版本的唯一标识符。如果 和 文件之间存在差异,则会发生版本不匹配错误,从而阻止应用程序启动。snapshot_hashsnapshot_hashsnapshot_hashlibFlutter.solibapp.so

补丁和构建特定版本的 Flutter 可以按照以下步骤完成:

1. 为特定的 Flutter 版本创建补丁。

要为 Flutter 版本创建补丁,我们必须确定该版本中使用的 Dart SDK 的特定版本。您可以使用以下链接列出所有 Flutter 官方版本: https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json

例如,Flutter 使用的是 Dart SDK:v3.10.4v3.0.3

Flutter 逆向工程和安全分析

第一步是为特定版本的 Dart SDK 创建补丁文件:

git clone https://github.com/dart-lang/sdk
cd sdk
git checkout 2.13.4

现在我们编辑 dart SDK 源代码以在运行时从 Flutter 应用程序转储信息,在下一节中,我们将详细讨论这些变化。完成所有更改后,通过运行以下命令创建包含更改的补丁文件:

git diff > patch_2_13_4.patch

并保留补丁文件以备后用。

2. 使用我们修补的 dart SDK 构建 libFlutter.so。

克隆包含所需工具的存储库。比如 Ninja 构建系统和 gclient 依赖管理工具,并将其添加到您的 .PATH

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
cd depot_tools
export PATH=$PATH:$(pwd)/depot_tools

为了确保我们拥有特定 Flutter 发行版的正确 Flutter 引擎版本,我们在 Flutter 代码库中找到该文件,该文件与发行版的提交哈希相同。打开文件并识别其中提到的提交哈希。这个提交哈希代表该 Flutter 发行版使用的 Flutter 引擎的特定版本。/bin/internal/engine.version

例如,让我们考虑 Flutter 版本,其关联的提交哈希值为 。v3.10.4682aa387cfe4fbd71ccd5418b2c2a075729a1c66

访问以下 URL 获取用于 Flutter 版本的引擎,即 。Engine.version3.10.42a3401c9bbb5a9a9aec74d4f735d18a9dd3ebf2d

Flutter 逆向工程和安全分析

克隆 Flutter Engine 存储库并签出到所需 Flutter 版本中使用的相同提交哈希。

git clone https://github.com/flutter/engine.git
cd engine
git fetch origin 2a3401c9bbb5a9a9aec74d4f735d18a9dd3ebf2d
git reset --hard

准备一个使用 gclient 的目录,并创建一个配置文件。.gclient

mkdir costum_engine
cd customEngine
echo 'solutions = [{"managed": False,"name": "src/flutter","url": "PATH_TO_ENGINE'","custom_deps": {},"deps_file": "DEPS","safesync_url": "",},]' > .gclient

运行并等待它完成:gclient

cd customEngine
gclient sync

gclient将 flutter 引擎内文件中的所有依赖项(包括 Dart SDK 仓库中的 dart SDK)克隆到我们的文件夹中。DEPScostum_engine/src/third_party/dart/

对 dart SDK 源码的任何更改、编译都会产生不同的版本,这可能会导致无法与任何 dart 一起使用的损坏。snapshot_hashlibflutter.solibapp.so

在编译时使用脚本中的方法生成。snapshot_hashMakeSnapshotHashString()make_version.py

在对源代码进行任何更改之前,我们需要确保即使我们更改了源代码,也始终会生成正确的代码。make_version.pysnapshot_hash

使用 make_version.py 脚本生成原始文件。snapshot_hash

cd costum_engine/src/third_party/dart/tool
ipython
In [1]: import make_version
In [2]: make_version.MakeSnapshotHashString()
Out[2]: 'e4a09dbf2bb120fe4674e0576617a0dc'

在文本编辑器中打开脚本并更改它以返回原始脚本。make_version.pysnapshot_hash

code make_version.py

Flutter 逆向工程和安全分析

现在我们可以更改源代码,而不必担心不匹配。将补丁文件复制到 dart SDK 文件夹中并应用它:snapshot_hash

git apply patch_2_13_4.patch

您现在可以为任何受支持的操作系统/架构构建 Flutter:

costum_engine/src/flutter/tools/gn --no-goma --android --android-cpu=arm64 --runtime-mode=release 

ninja -C costum_engine/src/out/android_release_arm64

完成后,您可以在文件夹中找到修补的libFlutter.socustom_engine/src/out/android_release_arm64/lib.stripped/libflutter.so.

提取函数、类和对象

为了转储有关 Flutter 应用程序结构、库列表、函数及其位置的信息,reFlutter 用于修补类表对象以列出它们。此对象不能再使用,因为在较新的版本中修剪了函数列表。

在该类中修剪信息时,该信息在加载时仍存在于应用程序中。上面有一个星号,但我们将在另一篇文章中回到这一点。

在正常情况下,我们仍然可以列出 Flutter 函数、类以及它们与 Flutter 解析器的偏移量。为此,我们必须修补类中的函数。PostLoadFunctionDeserializationCluster

下面是用于以 JSON 格式转储函数偏移量的修补程序示例。

void PostLoad(Deserializer* d, const Array& refs, bool primary) {
+ OS::Print("Patch: Function List STARTn");
if (d->kind() == Snapshot::kFullAOT) {
Function& func = Function::Handle(d->zone());
for (intptr_t i = start_index_, n = stop_index_; i < n; i++) {
func ^= refs.At(i);
auto const code = func.ptr()->untag()->code();
ASSERT(code->IsCode());
+
+ auto& rCode = Code::Handle(code);
+ auto& rClass = Class::Handle(func.Owner());
+ auto& rLib = Library::Handle(rClass.library());
+ auto& rlibName = String::Handle(rLib.url());
+
+ JSONWriter js;
+ // Open empty object so output is valid/parsable JSON.
+ js.OpenObject();
+
+ js.PrintProperty("method_name", func.UserVisibleNameCString());
+ js.PrintProperty("offset", offset);
+
+ auto& sig = String::Handle(func.InternalSignature());
+ js.PrintProperty("library_url", rlibName.ToCString());
+ js.PrintProperty("class_name", rClass.UserVisibleNameCString());
+ js.CloseObject();
+
+ char* buffer = nullptr;
+ intptr_t buffer_length = 0;
+ js.Steal(&buffer, &buffer_length);

修补程序读取库 URL、类名和方法名。也可以提取其他信息,例如方法签名,但它并不总是存在。

返回的偏移量将是一个绝对地址,该地址因库的加载地址而异。

要在二进制文件中仅显示相关的偏移量,可以应用以下修补程序:

code->untag()->monomorphic_entry_point_ = monomorphic_entry_point;
- code->untag()->monomorphic_unchecked_entry_point_ =
- monomorphic_entry_point + unchecked_offset;
+
+ auto& offset =
+ instructions_table_.rodata()
+ ->entries()[instructions_table_.rodata()->first_entry_with_code +
+ instructions_index_ - 1]
+ .pc_offset;
+ code->untag()->monomorphic_unchecked_entry_point_ = offset;
+ // OS::Print("Patch: Offset 0x%016lxn", monomorphic_entry_point + unchecked_offset);

一旦在开始时提取了偏移量,就可以拦截它们。偏移量可以打印在日志中,但这可能会有问题,因为日志有大小限制,这通常会导致数据被截断。

将文件写入磁盘是一种更稳定的解决方案。要写入外部文件,您可以附加以下修补程序,该修补程序尝试在不同的文件位置转储,以适应不同手机的文件系统布局。

+        for (const auto& path : PATHS) {
+ OS::Print("Using Path %sn", path);
+ std::FILE* file;
+ // Write to the file
+ file = std::fopen(path, "a");
+ if (file != NULL) {
+ std::fwrite(buffer, sizeof(char), buffer_length, file);
+ std::fwrite("n", sizeof(char), 1, file);
+ std::fclose(file);
+ OS::Print("Successfully wrote to the file '%s'.n", path);
+ } else {
+ OS::Print("Failed to open the file '%s' for writing.n", path);
+ }
+ }
+

一旦收集了函数列表及其偏移量,我们就可以开始拦截它们了。

动态分析和 Flutter 自定义 ABI

要拦截 Flutter 上的函数调用,一个主要障碍是它使用自定义 ABI 实现。

ABI 规定了二进制兼容性,并涵盖调用约定、数据类型、大小和对齐方式、系统调用接口、名称修改、异常处理和文件格式。

Dart 使用自定义调用约定。例如,arm64 的调用约定在文件中定义。sdk/runtime/vm/constants_arm64.h

// Register aliases.
const Register TMP = R16; // Used as scratch register by assembler.
const Register TMP2 = R17;
const Register PP = R27; // Caches object pool pointer in generated code.
const Register DISPATCH_TABLE_REG = R21; // Dispatch table register.
const Register CODE_REG = R24;
const Register FPREG = FP; // Frame pointer register.
const Register SPREG = R15; // Stack pointer register.
const Register ARGS_DESC_REG = R4; // Arguments descriptor register.
const Register THR = R26; // Caches current thread in generated code.
const Register CALLEE_SAVED_TEMP = R19;
const Register CALLEE_SAVED_TEMP2 = R20;
const Register BARRIER_MASK = R28;
const Register NULL_REG = R22; // Caches NullObject() value.
const Register HEAP_BASE = R23;

要拦截一个函数并使用 Frida 或像 LLDB 这样的调试器读取其参数,我们必须首先获取参数指针,并基于参数类型,我们必须事先知道,或者有时可以从函数签名元数据中提取,我们将确定要读取的参数对象的内存布局。

下面是 Frida 的 JS 代码,它支持提取参数并读取字符串值。Bool 和 Int 是简单的值,可以很容易地以相同的方式检索:

...
const argPointer = dartGetArguments(this.context, index)
const stringValue = getDartStringData(argPointer)

...

/**
* Get Dart Arguments.
*
* Arguments are passed to the custom function stack pointed at by register X15 on ARM.
*
* @param context Frida context giving access to register values.
* @param argIndex Argument index to determine which offset is the arg pointer.
* @returns {*} Argument pointer.
*/

function dartGetArguments(context, argIndex) {
// RSP on x64, see constants_x64.h at Dart VM repo SPREG value.
const x15 = context.x15;
return x15.add(8 * argIndex).readPointer();
}

/**
* Read SMI (Small Integer) from pointer.
* @param smiPtr SMI Pointer,
* @returns {*|null} Value.
*/

function readSMI(smiPtr) {
let smi_data = smiPtr.readU64();
if (parseInt(smi_data & 0x1, 10) === 0) {
return smi_data >> 1;
}
console.log(
`Invalid SMI pointer ${smiPtr} -> 0x${smi_data.toString(16)}: Smi LSB should be 0`)
return null
}

/**
* Parse String Dart object to extract value.
* @param dartStringPtr Dart String pointer.
* @returns {null|*[]} Tuple of parsed data.
*/

function parseDartString(dartStringPtr) {
if (dartStringPtr.and(0x1).toInt32() === 1) {
dartStringPtr = dartStringPtr.sub(1)
}
const tag = dartStringPtr.readU32();
const classId = (tag >> 16) & 0xffff;
if (classId === 0x5 || classId === 0x55) {
let stringLength = readSMI(dartStringPtr.add(8));
let stringDataStr = dartStringPtr.add(16)
let stringData = stringDataStr.readCString(stringLength);
return [stringDataStr, stringLength, stringData]
}
return null
}

/**
* Read Dart string at pointer.
* @param dartStringPtr Dart string pointer.
* @returns {*|null} String value.
*/

function getDartStringData(dartStringPtr) {
let dartStringInfo = parseDartString(dartStringPtr);
if (dartStringInfo != null) {
return dartStringInfo[2];
}
return null;
}

现在我们已经创建了一个补丁,构建了一个运行时的补丁版本,并有一个可以读取函数及其参数的脚本,最后一步是重新打包我们的目标应用程序,无论是 Android 还是 iOS,启动它并附加我们最喜欢的检测工具。

了解要检测哪些功能是一个很大的话题,将在后面的文章中介绍。然后,我们将讨论 Dart SDK 及其超过 120k 的方法,以及其前 1000 个流行软件包的一些风险。

流量拦截

颤振流量是一个需要定制护理的怪事。

在普通的 Android 或 iOS 应用程序中,可以使用多种方法拦截流量并绕过 TLS 固定,例如更改网络安全策略以接受自定义证书、在加密之前挂钩和拦截流量或转储 TLS 会话密钥以解密流量。SSL_readSSL_write

Flutter 不使用原生 TLS 堆栈,因此无法设置代理或拦截流量。TLS 库是在 Flutter 运行时静态编译的,因此很难识别它并动态修补它。

要拦截 Flutter 上的流量,最强大的解决方案是再次修补 Flutter 运行时以禁用 TLS 证书验证并设置自定义代理。

这种方法已经在 reFlutter 工具中看到,并且在当前版本的 Flutter 运行时中继续工作。

在 中,我们强制将 IP 地址和端口设置为用于拦截流量的端口:Socket.cc

void FUNCTION_NAME(Socket_CreateConnect)(Dart_NativeArguments args) {
RawAddr addr;
SocketAddress::GetSockAddr(Dart_GetNativeArgument(args, 1), &addr);
Dart_Handle port_arg = Dart_GetNativeArgument(args, 2);
int64_t port = DartUtils::GetInt64ValueCheckRange(port_arg, 0, 65535);
+ if (port > 50) {
+ port = 8083;
+ addr.addr.sa_family = AF_INET;
+ addr.in.sin_family = AF_INET;
+ inet_aton("192.168.10.5", &addr.in.sin_addr);
+ }
+ ...

然后我们禁用证书签入:ssl_crypto_x509_session_verify_cert_chain

static bool ssl_crypto_x509_session_verify_cert_chain(SSL_SESSION *session,
SSL_HANDSHAKE *hs,
uint8_t *out_alert) {
+ return true;

在这些更改之后,我们使用代理拦截流量,而无需在设备上安装任何证书。但是,我们需要确保使用不可见的代理模式

结论

在本文的第一部分,我们介绍了 Flutter 应用程序布局,如何修补运行时,如何拦截函数调用并读取它们的参数,最后如何修补运行时以拦截流量。

  • Flutter 逆向工程和安全分析

  • windows

  • Flutter 逆向工程和安全分析

  • windows()

  • Flutter 逆向工程和安全分析

  • USB()

  • Flutter 逆向工程和安全分析

  • ()

  • Flutter 逆向工程和安全分析

  • ios

  • Flutter 逆向工程和安全分析

  • windbg

  • Flutter 逆向工程和安全分析

  • ()

  • Flutter 逆向工程和安全分析Flutter 逆向工程和安全分析Flutter 逆向工程和安全分析

  • Flutter 逆向工程和安全分析

  • Flutter 逆向工程和安全分析

  • Flutter 逆向工程和安全分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年4月10日10:40:01
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Flutter 逆向工程和安全分析https://cn-sec.com/archives/2639503.html

发表评论

匿名网友 填写信息