介绍
Flutter 由 Google 开发,是一个广泛使用的跨平台移动应用程序开发框架,还支持 Web 和桌面应用程序。它出现了显着增长,在 Android 和 iOS 市场分别增长了 340% 和 270%。
该框架因其高性能而受到认可,这要归功于 Skia 渲染引擎。它还为复杂的 UI 设计提供了灵活的 UI 系统。
-
恶意软件开发
-
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.so
counter
libflutter_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 应用程序时,会在应用程序目录下生成两个文件:
-
libFlutter.so
:此文件作为包含 Flutter 引擎的共享库,负责渲染 UI、处理输入事件和管理应用程序生命周期。 -
libapp.so
:此文件包含构成 Flutter 应用程序的实际应用程序代码和逻辑。
这两个文件都包含一个以区分生成版本。用作生成版本的唯一标识符。如果 和 文件之间存在差异,则会发生版本不匹配错误,从而阻止应用程序启动。snapshot_hash
snapshot_hash
snapshot_hash
libFlutter.so
libapp.so
补丁和构建特定版本的 Flutter 可以按照以下步骤完成:
1. 为特定的 Flutter 版本创建补丁。
要为 Flutter 版本创建补丁,我们必须确定该版本中使用的 Dart SDK 的特定版本。您可以使用以下链接列出所有 Flutter 官方版本: https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json
例如,Flutter 使用的是 Dart SDK:v3.10.4
v3.0.3
第一步是为特定版本的 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.4
682aa387cfe4fbd71ccd5418b2c2a075729a1c66
访问以下 URL 获取用于 Flutter 版本的引擎,即 。Engine.version
3.10.4
2a3401c9bbb5a9a9aec74d4f735d18a9dd3ebf2d
克隆 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)克隆到我们的文件夹中。DEPS
costum_engine/src/third_party/dart/
对 dart SDK 源码的任何更改、编译都会产生不同的版本,这可能会导致无法与任何 dart 一起使用的损坏。snapshot_hash
libflutter.so
libapp.so
在编译时使用脚本中的方法生成。snapshot_hash
MakeSnapshotHashString()
make_version.py
在对源代码进行任何更改之前,我们需要确保即使我们更改了源代码,也始终会生成正确的代码。make_version.py
snapshot_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.py
snapshot_hash
code make_version.py
现在我们可以更改源代码,而不必担心不匹配。将补丁文件复制到 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.so
custom_engine/src/out/android_release_arm64/lib.stripped/libflutter.so.
提取函数、类和对象
为了转储有关 Flutter 应用程序结构、库列表、函数及其位置的信息,reFlutter 用于修补类表对象以列出它们。此对象不能再使用,因为在较新的版本中修剪了函数列表。
在该类中修剪信息时,该信息在加载时仍存在于应用程序中。上面有一个星号,但我们将在另一篇文章中回到这一点。
在正常情况下,我们仍然可以列出 Flutter 函数、类以及它们与 Flutter 解析器的偏移量。为此,我们必须修补类中的函数。PostLoad
FunctionDeserializationCluster
下面是用于以 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_read
SSL_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 应用程序布局,如何修补运行时,如何拦截函数调用并读取它们的参数,最后如何修补运行时以拦截流量。
-
-
windows网络安全防火墙与虚拟网卡(更新完成)
-
-
windows文件过滤(更新完成)
-
-
USB过滤(更新完成)
-
-
游戏安全(更新中)
-
-
ios逆向
-
-
windbg
-
-
还有很多免费教程(限学员)
-
-
-
更多详细内容添加作者微信
-
-
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论