在第一篇文章中,我们介绍了 Dart 和 Flutter 之间关系,以及开发人员在应用程序构建阶段容易犯的一些低级错误。本篇主要讲解对某公司采用Release模式的Flutter APK进行黑盒测试。
在 4-5 天的时间内,通过黑盒测试发现了一处独特的漏洞,其原因不是他们的设计,而是他们使用的一个公共包。该漏洞允许攻击者进入开发者模式并在 APK 中发现大量 PII 。结论是硬编码存储的“超级用户”凭据绝非安全做法,即使它是加密的。
先验概念:
假设我们是一个未经授权的用户,没有权限从某些用户甚至特权用户那里访问那些私有资产,那么我们首先需要知道应用程序是如何工作的,然后站在开发人员构建 APK 的角度,设法通过安全边界的缺失来达到我们的目的。
Flutter构建APK:
flutter build apk --release
该命令明确以Release模式而非Debug模式来构建 APK,那么作为渗透测试人员,我们有什么办法可以帮助了解应用程序在做什么呢?
在 Gradle (gradle.properties) 中,开发者可能会因为自己的喜好或是对于暴露函数命名并不那么关心而忘记添加下面这段代码:
extra-gen-snapshot-options=--obfuscate
开发者也可以在应用程序处于构建时通过以下方式添加:
flutter build apk --release --obfuscate
为什么我们关心应用程序混淆?因为如果从开发人员的角度来看,应用程序的大小可以通过混淆来减小,但开发者还需要启用 --split-debug-info ,这样从函数符号调试堆栈跟踪的话,对于后续分析 APK 时才不会产生分歧。
对于开发人员来说,这可能是他们的第一个失误,因为他们没有充分考虑到逆向工程师的存在。
在下面的场景中,我们将使用不带混淆标志的Flutter Release APK,目的是检索存储在APK(硬编码)中的秘密字符串,APK使用了已知的对称块加密方式,具体参见:
https://www.cryptomathic.com/news-events/blog/symmetric-key-encryption-why-where-and-how-its-used-in-banking#:~:text=Symmetric%20encryption%20is%20a%20type,used%20in%20the%20decryption%20process.
命名分析:
我们知道 Flutter Snapshots(在支持的库中)会采用不同的语法格式,这使得逆向工程变得更加困难,但是由于没有启用混淆,它可能会使事情变得稍微容易一些。
对于APK安装与运行,网上有很多模拟器可供免费使用:
NoxPlayer
LDPlayer
Genymotion (with ARM Translation Packages)
BlueStacks
AVD Manager from Android Studio
QEMU-Based Android Emulator
下载完成后,我们可以使用 adb 命令来安装 APK:
adb install -g <path-to-the-apk.apk>
APK 的设计非常简单,只提供了一个接受字符串的界面,这些字符串将在用户输入后进行验证。
我们做的第一件事是使用 IDA 分析libapp.so,首先使用 apktool 反编译 APK,接着,可以查看 lib/ 文件夹中是否有两个so文件,分别是 libflutter.so 和 libapp.so。
apktool d <your-path-to-flutter.apk>
我们将使用 _kDartIsolateSnapshotInstructions (0x1707f0) 的地址偏移,然后使用 reFlutter 工具从 APK 本身提取了每个功能和小部件偏移量,这非常有用,该工具将帮助我们进一步逆向该 APK。
为了使用 reFlutter,首先将其作为 Python 包安装,然后找到 APK 路径,将有两个选项可供选择。此工具将修补 APK 的共享对象库 (ELF),并最终绑定到所有接口(包括不可见代理模式)上的端口 8083。我们需要寻找的是 dump.dart 文件,该文件将在修补后的 APK 运行后生成,文件中将包含我们可利用的地址偏移量。
if ver>27:
replaceFileText('src/third_party/dart/runtime/vm/class_table.cc','::Print() {','::Print() { OS::PrintErr("reFlutter");\n char pushArr[60000]="";\n')
replaceFileText('src/flutter/BUILD.gn',' if (is_android) {\n public_deps +=\n [ "//flutter/shell/platform/android:flutter_shell_native_unittests" ]\n }','')
replaceFileText('src/third_party/dart/runtime/vm/class_table.cc','OS::PrintErr("%" Pd ": %s\\n", i, name.ToCString());','\n auto& funcs = Array::Handle(cls.functions()); if (funcs.Length()>1000) { continue; } char classText[100000]=""; String& supname = String::Handle(); name = cls.Name(); strcat(classText,cls.ToCString()); Class& supcls = Class::Handle(); supcls = cls.SuperClass(); if (!supcls.IsNull()) { supname = supcls.Name(); strcat(classText," extends "); strcat(classText,supname.ToCString()); } const auto& interfaces = Array::Handle(cls.interfaces()); auto& interface = Instance::Handle(); for (intptr_t in = 0;in < interfaces.Length(); in++) { interface^=interfaces.At(in); if(in==0){strcat(classText," implements ");} if(in>0){strcat(classText," , ");} strcat(classText,interface.ToCString()); } strcat(classText," {\\n"); const auto& fields = Array::Handle(cls.fields()); auto& field = Field::Handle(); auto& fieldType = AbstractType::Handle(); String& fieldTypeName = String::Handle(); String& finame = String::Handle(); Instance& instance2 = Instance::Handle(); for (intptr_t f = 0; f < fields.Length(); f++) { field ^= fields.At(f); finame = field.name(); fieldType = field.type(); fieldTypeName = fieldType.Name(); strcat(classText," "); strcat(classText,fieldTypeName.ToCString()); strcat(classText," "); strcat(classText,finame.ToCString()); if(field.is_static()){ instance2 ^= field.StaticValue(); strcat(classText," = "); strcat(classText,instance2.ToCString()); strcat(classText," ;\\n"); } else { strcat(classText," = "); strcat(classText," nonstatic;\\n"); } } for (intptr_t c = 0; c < funcs.Length(); c++) { auto& func = Function::Handle(); func = cls.FunctionFromIndex(c); String& signature = String::Handle(); signature = func.InternalSignature();auto& codee = Code::Handle(func.CurrentCode()); if(!func.IsLocalFunction()) { strcat(classText," \\n "); strcat(classText,func.ToCString()); strcat(classText," "); strcat(classText,signature.ToCString()); strcat(classText," { \\n\\n "); char append[70]; sprintf(append," Code Offset: _kDartIsolateSnapshotInstructions + 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint())); strcat(classText,append); strcat(classText," \\n }\\n"); } else { auto& parf = Function::Handle(); parf=func.parent_function(); String& signParent = String::Handle(); signParent = parf.InternalSignature(); strcat(classText," \\n "); strcat(classText,parf.ToCString()); strcat(classText," "); strcat(classText,signParent.ToCString()); strcat(classText," { \\n\\n "); char append[80]; sprintf(append," Code Offset: _kDartIsolateSnapshotInstructions + 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint())); strcat(classText,append); strcat(classText," \\n }\\n"); } } strcat(classText," \\n }\\n\\n"); const Library& libr = Library::Handle(cls.library());if (!libr.IsNull()) { auto& owner_class = Class::Handle(); owner_class = libr.toplevel_class(); auto& funcsTopLevel = Array::Handle(owner_class.functions()); char pushTmp[1000]; String& owner_name = String::Handle(); owner_name = libr.url(); sprintf(pushTmp,"\'%s\',",owner_name.ToCString()); if (funcsTopLevel.Length()>0&&strstr(pushArr, pushTmp) == NULL) { strcat(pushArr,pushTmp); strcat(classText,"Library:"); strcat(classText,pushTmp); strcat(classText," {\\n"); for (intptr_t c = 0; c < funcsTopLevel.Length(); c++) { auto& func = Function::Handle(); func = owner_class.FunctionFromIndex(c); String& signature = String::Handle(); signature = func.InternalSignature(); auto& codee = Code::Handle(func.CurrentCode()); if(!func.IsLocalFunction()) { strcat(classText," \\n "); strcat(classText,func.ToCString()); strcat(classText," "); strcat(classText,signature.ToCString()); strcat(classText," { \\n\\n "); char append[70]; sprintf(append," Code Offset: _kDartIsolateSnapshotInstructions + 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint())); strcat(classText,append); strcat(classText," \\n }\\n"); } else { auto& parf = Function::Handle(); parf=func.parent_function(); String& signParent = String::Handle(); signParent = parf.InternalSignature(); strcat(classText," \\n "); strcat(classText,parf.ToCString()); strcat(classText," "); strcat(classText,signParent.ToCString()); strcat(classText," { \\n\\n "); char append[80]; sprintf(append," Code Offset: _kDartIsolateSnapshotInstructions + 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint())); strcat(classText,append); strcat(classText," \\n }\\n"); } } strcat(classText," \\n }\\n\\n");}} struct stat entry_info; int exists = 0; if (stat("/data/data/", &entry_info)==0 && S_ISDIR(entry_info.st_mode)){ exists=1; } if(exists==1){ pid_t pid = getpid(); char path[64] = { 0 }; sprintf(path, "/proc/%d/cmdline", pid); FILE *cmdline = fopen(path, "r"); if (cmdline) { char chm[264] = { 0 }; char pat[264] = { 0 }; char application_id[64] = { 0 }; fread(application_id, sizeof(application_id), 1, cmdline); sprintf(pat, "/data/data/%s/dump.dart", application_id); do { FILE *f = fopen(pat, "a+"); fprintf(f, "%s",classText); fflush(f); fclose(f); sprintf(chm,"/data/data/%s",application_id); chmod(chm, S_IRWXU|S_IRWXG|S_IRWXO); chmod(pat, S_IRWXU|S_IRWXG|S_IRWXO); } while (0); fclose(cmdline); } } if(exists==0){ char pat[264] = { 0 }; sprintf(pat, "%s/Documents/dump.dart", getenv("HOME")); OS::PrintErr("reFlutter dump file: %s",pat); do { FILE *f = fopen(pat, "a+"); fprintf(f, "%s",classText); fflush(f); fclose(f); } while (0); }')
#replaceFileText('src/third_party/dart/runtime/vm/class_table.cc','OS::PrintErr("%" Pd ": %s\\n", i, name.ToCString());','auto& funcs = Array::Handle(cls.functions()); if (funcs.Length()>1000) { continue; } char classText[65000]=""; String& supname = String::Handle(); name = cls.Name(); strcat(classText," "); strcat(classText,cls.ToCString()); Class& supcls = Class::Handle(); supcls = cls.SuperClass(); if (!supcls.IsNull()) { supname = supcls.Name(); strcat(classText," extends "); strcat(classText,supname.ToCString()); } const auto& interfaces = Array::Handle(cls.interfaces()); auto& interface = Instance::Handle(); for (intptr_t in = 0;in < interfaces.Length(); in++) { interface^=interfaces.At(in); if(in==0){strcat(classText," implements ");} if(in>0){strcat(classText," , ");} strcat(classText,interface.ToCString()); } strcat(classText," {\\n"); const auto& fields = Array::Handle(cls.fields()); auto& field = Field::Handle(); auto& fieldType = AbstractType::Handle(); String& fieldTypeName = String::Handle(); String& finame = String::Handle(); Instance& instance2 = Instance::Handle(); for (intptr_t f = 0; f < fields.Length(); f++) { field ^= fields.At(f); finame = field.name(); fieldType = field.type(); fieldTypeName = fieldType.Name(); strcat(classText," "); strcat(classText,fieldTypeName.ToCString()); strcat(classText," "); strcat(classText,finame.ToCString()); if(field.is_static()){ instance2 ^= field.StaticValue(); strcat(classText," = "); strcat(classText,instance2.ToCString()); strcat(classText," ;\\n"); } else { strcat(classText," = "); strcat(classText," nonstatic;\\n"); } } for (intptr_t c = 0; c < funcs.Length(); c++) { auto& func = Function::Handle(); func = cls.FunctionFromIndex(c); String& signature = String::Handle(); signature = func.InternalSignature(); if(!func.IsLocalFunction()) { strcat(classText," \\n"); strcat(classText,func.ToCString()); strcat(classText," "); strcat(classText,signature.ToCString()); strcat(classText," { \\n\\n }\\n"); } else { auto& parf = Function::Handle(); parf=func.parent_function(); String& signParent = String::Handle(); signParent = parf.InternalSignature(); strcat(classText," \\n"); strcat(classText,parf.ToCString()); strcat(classText," "); strcat(classText,signParent.ToCString()); strcat(classText," { \\n\\n }\\n"); } } OS::PrintErr("reflutter:\\n %s \\n }\\n",classText);')
else:
replaceFileText('src/third_party/dart/runtime/vm/class_table.cc','::Print() {','::Print() { OS::PrintErr("reFlutter");\n char pushArr[60000]="";\n')
replaceFileText('src/third_party/dart/runtime/vm/class_table.cc','OS::PrintErr("%" Pd ": %s\\n", i, name.ToCString());','\n auto& funcs = Array::Handle(cls.functions()); if (funcs.Length()>1000) { continue; } char classText[100000]=""; String& supname = String::Handle(); name = cls.Name(); strcat(classText,cls.ToCString()); Class& supcls = Class::Handle(); supcls = cls.SuperClass(); if (!supcls.IsNull()) { supname = supcls.Name(); strcat(classText," extends "); strcat(classText,supname.ToCString()); } const auto& interfaces = Array::Handle(cls.interfaces()); auto& interface = Instance::Handle(); for (intptr_t in = 0;in < interfaces.Length(); in++) { interface^=interfaces.At(in); if(in==0){strcat(classText," implements ");} if(in>0){strcat(classText," , ");} strcat(classText,interface.ToCString()); } strcat(classText," {\\n"); const auto& fields = Array::Handle(cls.fields()); auto& field = Field::Handle(); auto& fieldType = AbstractType::Handle(); String& fieldTypeName = String::Handle(); String& finame = String::Handle(); Instance& instance2 = Instance::Handle(); for (intptr_t f = 0; f < fields.Length(); f++) { field ^= fields.At(f); finame = field.name(); fieldType = field.type(); fieldTypeName = fieldType.Name(); strcat(classText," "); strcat(classText,fieldTypeName.ToCString()); strcat(classText," "); strcat(classText,finame.ToCString()); if(field.is_static()){ instance2 = field.StaticValue(); strcat(classText," = "); strcat(classText,instance2.ToCString()); strcat(classText," ;\\n"); } else { strcat(classText," = "); strcat(classText," nonstatic;\\n"); } } for (intptr_t c = 0; c < funcs.Length(); c++) { auto& func = Function::Handle(); func = cls.FunctionFromIndex(c); String& signature = String::Handle(); signature = func.Signature();auto& codee = Code::Handle(func.CurrentCode()); if(!func.IsLocalFunction()) { strcat(classText," \\n "); strcat(classText,func.ToCString()); strcat(classText," "); strcat(classText,signature.ToCString()); strcat(classText," { \\n\\n "); char append[70]; sprintf(append," Code Offset: _kDartIsolateSnapshotInstructions + 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint())); strcat(classText,append); strcat(classText," \\n }\\n"); } else { auto& parf = Function::Handle(); parf=func.parent_function(); String& signParent = String::Handle(); signParent = parf.Signature(); strcat(classText," \\n "); strcat(classText,parf.ToCString()); strcat(classText," "); strcat(classText,signParent.ToCString()); strcat(classText," { \\n\\n "); char append[80]; sprintf(append," Code Offset: _kDartIsolateSnapshotInstructions + 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint())); strcat(classText,append); strcat(classText," \\n }\\n"); } } strcat(classText," \\n }\\n\\n"); const Library& libr = Library::Handle(cls.library());if (!libr.IsNull()) { auto& owner_class = Class::Handle(); owner_class = libr.toplevel_class(); auto& funcsTopLevel = Array::Handle(owner_class.functions()); char pushTmp[1000]; String& owner_name = String::Handle(); owner_name = libr.url(); sprintf(pushTmp,"\'%s\',",owner_name.ToCString()); if (funcsTopLevel.Length()>0&&strstr(pushArr, pushTmp) == NULL) { strcat(pushArr,pushTmp); strcat(classText,"Library:"); strcat(classText,pushTmp); strcat(classText," {\\n"); for (intptr_t c = 0; c < funcsTopLevel.Length(); c++) { auto& func = Function::Handle(); func = owner_class.FunctionFromIndex(c); String& signature = String::Handle(); signature = func.Signature(); auto& codee = Code::Handle(func.CurrentCode()); if(!func.IsLocalFunction()) { strcat(classText," \\n "); strcat(classText,func.ToCString()); strcat(classText," "); strcat(classText,signature.ToCString()); strcat(classText," { \\n\\n "); char append[70]; sprintf(append," Code Offset: _kDartIsolateSnapshotInstructions + 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint())); strcat(classText,append); strcat(classText," \\n }\\n"); } else { auto& parf = Function::Handle(); parf=func.parent_function(); String& signParent = String::Handle(); signParent = parf.Signature(); strcat(classText," \\n "); strcat(classText,parf.ToCString()); strcat(classText," "); strcat(classText,signParent.ToCString()); strcat(classText," { \\n\\n "); char append[80]; sprintf(append," Code Offset: _kDartIsolateSnapshotInstructions + 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint())); strcat(classText,append); strcat(classText," \\n }\\n"); } } strcat(classText," \\n }\\n\\n");}} struct stat entry_info; int exists = 0; if (stat("/data/data/", &entry_info)==0 && S_ISDIR(entry_info.st_mode)){ exists=1; } if(exists==1){ pid_t pid = getpid(); char path[64] = { 0 }; sprintf(path, "/proc/%d/cmdline", pid); FILE *cmdline = fopen(path, "r"); if (cmdline) { char chm[264] = { 0 }; char pat[264] = { 0 }; char application_id[64] = { 0 }; fread(application_id, sizeof(application_id), 1, cmdline); sprintf(pat, "/data/data/%s/dump.dart", application_id); do { FILE *f = fopen(pat, "a+"); fprintf(f, "%s",classText); fflush(f); fclose(f); sprintf(chm,"/data/data/%s",application_id); chmod(chm, S_IRWXU|S_IRWXG|S_IRWXO); chmod(pat, S_IRWXU|S_IRWXG|S_IRWXO); } while (0); fclose(cmdline); } } if(exists==0){ char pat[264] = { 0 }; sprintf(pat, "%s/Documents/dump.dart", getenv("HOME")); OS::PrintErr("reFlutter dump file: %s",pat); do { FILE *f = fopen(pat, "a+"); fprintf(f, "%s",classText); fflush(f); fclose(f); } while (0); }')
#replaceFileText('src/third_party/dart/runtime/vm/class_table.cc','OS::PrintErr("%" Pd ": %s\\n", i, name.ToCString());','#if defined(HOST_ARCH_X64) uintptr_t instrArch = 0xE000;#elif defined(HOST_ARCH_ARM64) uintptr_t instrArch = 0xF000;#else uintptr_t instrArch = 0xB000;#endif auto& funcs = Array::Handle(cls.functions()); if (funcs.Length()>1000) { continue; } char classText[100000]=""; String& supname = String::Handle(); name = cls.Name(); strcat(classText,cls.ToCString()); Class& supcls = Class::Handle(); supcls = cls.SuperClass(); if (!supcls.IsNull()) { supname = supcls.Name(); strcat(classText," extends "); strcat(classText,supname.ToCString()); } const auto& interfaces = Array::Handle(cls.interfaces()); auto& interface = Instance::Handle(); for (intptr_t in = 0;in < interfaces.Length(); in++) { interface^=interfaces.At(in); if(in==0){strcat(classText," implements ");} if(in>0){strcat(classText," , ");} strcat(classText,interface.ToCString()); } strcat(classText," {\\n"); const auto& fields = Array::Handle(cls.fields()); auto& field = Field::Handle(); auto& fieldType = AbstractType::Handle(); String& fieldTypeName = String::Handle(); String& finame = String::Handle(); Instance& instance2 = Instance::Handle(); for (intptr_t f = 0; f < fields.Length(); f++) { field ^= fields.At(f); finame = field.name(); fieldType = field.type(); fieldTypeName = fieldType.Name(); strcat(classText," "); strcat(classText,fieldTypeName.ToCString()); strcat(classText," "); strcat(classText,finame.ToCString()); if(field.is_static()){ instance2 = field.StaticValue(); strcat(classText," = "); strcat(classText,instance2.ToCString()); strcat(classText," ;\\n"); } else { strcat(classText," = "); strcat(classText," nonstatic;\\n"); } } for (intptr_t c = 0; c < funcs.Length(); c++) { auto& func = Function::Handle(); func = cls.FunctionFromIndex(c); String& signature = String::Handle(); signature = func.Signature();auto& codee = Code::Handle(func.CurrentCode()); if(!func.IsLocalFunction()) { strcat(classText," \\n "); strcat(classText,func.ToCString()); strcat(classText," "); strcat(classText,signature.ToCString()); strcat(classText," { \\n\\n "); char append[70]; sprintf(append," Code Offset: 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint())+ instrArch); strcat(classText,append); strcat(classText," \\n }\\n"); } else { auto& parf = Function::Handle(); parf=func.parent_function(); String& signParent = String::Handle(); signParent = parf.Signature(); strcat(classText," \\n "); strcat(classText,parf.ToCString()); strcat(classText," "); strcat(classText,signParent.ToCString()); strcat(classText," { \\n\\n "); char append[50]; sprintf(append," Code Offset: 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint()) + instrArch); strcat(classText,append); strcat(classText," \\n }\\n"); } } strcat(classText," \\n }\\n\\n"); const Library& libr = Library::Handle(cls.library());if (!libr.IsNull()) { auto& owner_class = Class::Handle(); owner_class = libr.toplevel_class(); auto& funcsTopLevel = Array::Handle(owner_class.functions()); char pushTmp[1000]; String& owner_name = String::Handle(); owner_name = libr.url(); sprintf(pushTmp,"\'%s\',",owner_name.ToCString()); if (funcsTopLevel.Length()>0&&strstr(pushArr, pushTmp) == NULL) { strcat(pushArr,pushTmp); strcat(classText,"Library:"); strcat(classText,pushTmp); strcat(classText," {\\n"); for (intptr_t c = 0; c < funcsTopLevel.Length(); c++) { auto& func = Function::Handle(); func = owner_class.FunctionFromIndex(c); String& signature = String::Handle(); signature = func.Signature(); auto& codee = Code::Handle(func.CurrentCode()); if(!func.IsLocalFunction()) { strcat(classText," \\n "); strcat(classText,func.ToCString()); strcat(classText," "); strcat(classText,signature.ToCString()); strcat(classText," { \\n\\n "); char append[70]; sprintf(append," Code Offset: 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint())+ instrArch); strcat(classText,append); strcat(classText," \\n }\\n"); } else { auto& parf = Function::Handle(); parf=func.parent_function(); String& signParent = String::Handle(); signParent = parf.Signature(); strcat(classText," \\n "); strcat(classText,parf.ToCString()); strcat(classText," "); strcat(classText,signParent.ToCString()); strcat(classText," { \\n\\n "); char append[50]; sprintf(append," Code Offset: 0x%016" PRIxPTR "\\n",static_cast<uintptr_t>(codee.MonomorphicUncheckedEntryPoint()) + instrArch); strcat(classText,append); strcat(classText," \\n }\\n"); } } strcat(classText," \\n }\\n\\n");}} struct stat entry_info; int exists = 0; if (stat("/data/data/", &entry_info)==0 && S_ISDIR(entry_info.st_mode)){ exists=1; } if(exists==1){ pid_t pid = getpid(); char path[64] = { 0 }; sprintf(path, "/proc/%d/cmdline", pid); FILE *cmdline = fopen(path, "r"); if (cmdline) { char chm[264] = { 0 }; char pat[264] = { 0 }; char application_id[64] = { 0 }; fread(application_id, sizeof(application_id), 1, cmdline); sprintf(pat, "/data/data/%s/dump.dart", application_id); do { FILE *f = fopen(pat, "a+"); fprintf(f, "%s",classText); fflush(f); fclose(f); sprintf(chm,"/data/data/%s",application_id); chmod(chm, S_IRWXU|S_IRWXG|S_IRWXO); chmod(pat, S_IRWXU|S_IRWXG|S_IRWXO); } while (0); fclose(cmdline); } } if(exists==0){ char pat[264] = "/tmp/dump.dart"; do { FILE *f = fopen(pat, "a+"); fprintf(f, "%s",classText); fflush(f); fclose(f); } while (0); }')
replaceFileText('src/third_party/dart/tools/make_version.py','snapshot_hash = MakeSnapshotHashString()', 'snapshot_hash = \''+hashS+'\'')
replaceFileText('src/third_party/dart/runtime/bin/socket.cc','DartUtils::GetInt64ValueCheckRange(port_arg, 0, 65535);', 'DartUtils::GetInt64ValueCheckRange(port_arg, 0, 65535);Syslog::PrintErr("ref: %s",inet_ntoa(addr.in.sin_addr));if(port>50){port=8083;addr.addr.sa_family=AF_INET;addr.in.sin_family=AF_INET;inet_aton("192.168.133.104", &addr.in.sin_addr);}')
replaceFileText('src/third_party/boringssl/src/ssl/ssl_x509.cc','static bool ssl_crypto_x509_session_verify_cert_chain(SSL_SESSION *session,\n SSL_HANDSHAKE *hs,\n uint8_t *out_alert) {', 'static bool ssl_crypto_x509_session_verify_cert_chain(SSL_SESSION *session,\n SSL_HANDSHAKE *hs,\n uint8_t *out_alert) {return true;')
replaceFileText('src/third_party/boringssl/src/ssl/ssl_x509.cc','static int ssl_crypto_x509_session_verify_cert_chain(SSL_SESSION *session,\n
生成和修补的 APK 需要重新签名,我们可以使用 uber-jar-APKSigner 来加快 APK 签名过程。然后从模拟器/真实设备中删除了原始 APK,并安装了打了补丁后的 APK。然后检查 /data/data/<name-of-the-flutter-package>路径下, dump.dart 文件是否已经存在。一旦存在,我们就可以使用 adb pull 命令来提取该文件。
我们可以查看 dump.dart 文件中的主要 Dart 函数,该函数负责保存 Flutter 应用程序的逻辑。对于入口点的上下文,就好比 C 中经典的 int main() 或默认 APK 中的 MainActivity。
Library:'package:pwndroid/main.dart' Class: [email protected] extends State {
Function 'compare':. String: null {
Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000188164
}
Function 'encrypt':. String: null {
Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000185418
}
Function 'prepare':. String: null {
Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000184cd0
}
Function 'build':. String: null {
Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000184a78
}
}
Library:'package:pwndroid/main.dart', {
Function 'main': static. () => void {
Code Offset: _kDartIsolateSnapshotInstructions + 0x000000000024b0f0
}
}
应用程序的主逻辑中有四个用户定义的函数,build和prepare
可能是它的第一次初始化,encrypt函数可能与涉及用户输入或硬编码内容的某些加密有关,compare函数可能会在两个对象/变量/值之间进行一些比较。
这里需要注意的是,偏移值都是基于 _kDartIsolateSnapshotInstructions。这意味着,假如我们想要定位compare函数,0x1707f0 + 0x0000000000188164 才是该函数的地址。
另一个需要记下的重要事项是 Flutter 应用程序使用的包,这个包名称在二进制 ELF 共享对象中可以作为字符串读取,dump.dart 也可以识别它,以 package:[flutter|dart]/.* 前缀为开头。
我们可以回到 IDA 并查看这些使用的包(View -> Open Subviews -> Strings):
向下滚动,我们会看到使用了一个加密包(encrypt packages)。
Library:'package:encrypt/encrypt.dart' Class: Encrypter extends Object {
Function 'encryptBytes':. String: null {
Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000185544
}
Function 'encrypt':. String: null {
Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000188210
}
}
Library:'package:encrypt/encrypt.dart' Class: Encrypted extends Object {
Function 'Encrypted.fromUtf8': constructor. String: null {
Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000002664
}
Function 'Encrypted.fromLength': constructor. String: null {
Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000002664
}
Function 'get:base64':. String: null {
Code Offset: _kDartIsolateSnapshotInstructions + 0x00000000001881cc
}
Function '==':. String: null {
Code Offset: _kDartIsolateSnapshotInstructions + 0x00000000001ea96c
}
}
Library:'package:encrypt/encrypt.dart' Class: Key extends Encrypted {
Function 'Key.fromLength': constructor. String: null {
Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000002664
}
}
Library:'package:encrypt/encrypt.dart' Class: IV extends Encrypted {
Function 'IV.fromUtf8': constructor. String: null {
Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000002664
}
}
Library:'package:encrypt/encrypt.dart' Class: Salsa20 extends Object implements Type: Algorithm {
Function 'Salsa20.': constructor. String: null {
Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000002664
}
Function 'encrypt':. String: null {
Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000185590
}
Function '[email protected]':. String: null {
Code Offset: _kDartIsolateSnapshotInstructions + 0x0000000000188064
}
}
Library:'package:encrypt/encrypt.dart' Class: Algorithm extends Object {
}
你可能会注意到 Salsa20 是应用程序内部使用的对称加密,它需要 16/32 字节的密钥和 8 字节的 IV(初始化向量),密钥派生自 Key.fromLength 函数,IV 派生自 IV.fromUtf8 函数, IV 可能是随机的,但更令人惊讶的是密钥本身尽管由 16/32 字节组成,但却是固定的并且可以很容易地预测。
通过对这个包进行一段时间的试验后,在当前版本的 Flutter
(https://pub.dev/packages/encrypt) 的加密包中可以发现这个漏洞。
如果我们从它的官方 GitHub 上查看之前的问题和修复,他们实际上已经说过要为 fromLength 方法实现一个安全的随机生成字节,但事实证明,一旦我们尝试直接在 Flutter 项目中使用它,它并没有这样做。
让我们尝试使用 Zapp(https://zapp.run/)沙盒来运行 Flutter 项目,首先,我们在保存Flutter项目依赖信息的pubspec.yaml文件中添加最新版本为5.0.1的加密包,并将加密包导入到main.dart文件中:
完成后,我们编译构建 Flutter Apps 项目并检查日志。
fromLength 返回了一个尾随的0字节数组,这意味着它仍然不安全,因为它产生了一个空字节。我们可以利用密钥的这个弱生成字节稍后解密一些相关对象。
Hooking大法:
现在到了最后一步,我们通常会使用 IDA 和 Frida 进行进一步分析。dump.dart 文件包含了每个函数的特定偏移量,因此可以根据dump来重命名剥离的函数,也可以按照 Guardsquare 指南(https://www.guardsquare.com/blog/current-state-and-future-of-reversing-flutter-apps)根据交叉引用的dump函数来立即重命名函数。
main.dart
===========================================================
main() = sub_3bb8e0 [Widgets and Function Calls]
compare() = sub_2f8954 [comparing s1 & s2]
encrypt() = sub_2f5c08
prepare() = sub_2f54c0
build() = sub_2f5268
encrypt.dart [Encrypter]
===========================================================
encryptBytes() = sub_2f5d34
encrypt() = sub_2f8a00
encrypt.dart [Encrypted]
============================================================
Encrypted.fromUtf8() = sub_172e54 [derivate from Salsa20]
Encrypted.fromLength() = sub_172e54 [derivate from Salsa20]
Key.fromLength() = sub_172e54 [derivate from Encrypted.fromLength]
IV.fromUtf8() = sub_172e54 [derivate from Encrypted.fromUtf8]
get:base64() = sub_2f89bc
Salsa20
============================================================
Salsa20 = sub_172e54
encrypt() = sub_2f5d80
我们可以使用 Frida 脚本来hook这些特定函数,以查看内部存储和处理的对象以及返回值,假设我们要hook的函数是 encrypt():
function hookFunc() {
var isolate = 0x00000000001707f0;
// var target = 0x000000000017c240;
var target = 0x0000000000185418; //encrypt
var dumpOffset = isolate + target;
var argBufferSize = 300
var address = Module.findBaseAddress('libapp.so') // libapp.so (Android) or App (IOS)
console.log('\n\nbaseAddress: ' + address.toString())
var codeOffset = address.add(dumpOffset)
console.log('codeOffset: ' + codeOffset.toString())
console.log('')
console.log('Wait..... ')
Interceptor.attach(codeOffset, {
onEnter: function(args) {
console.log('')
console.log('--------------------------------------------|')
console.log('\n Hook Function: ' + dumpOffset);
console.log('')
console.log('--------------------------------------------|')
console.log('')
// for (var argStep = 0; argStep < 20; argStep++) {
// try {
// dumpArgs(argStep, args[argStep], argBufferSize);
// } catch (e) {
// break;
// }
// }
for(let i = 0; i < 8; i++) {
try {
console.log("addr ",i,args[i]);
console.log(hexdump(args[i]));
console.log("Value")
console.log(Memory.readCString(ptr(args[i])));
console.log("Pointer address hexdump")
console.log(hexdump(ptr(args[i])));
} catch (error) {
console.log("fail",i,(args[i]));
}
}
},
onLeave: function(retval) {
console.log('RETURN : ' + retval)
// console.log(hexdump(retval))
// dumpArgs(0, retval, 300);
// for (var argStep = 0; argStep < 50; argStep++) {
// try {
// dumpArgs(argStep, retval[argStep], argBufferSize);
// } catch (e) {
// break;
// }
// }
}
});
}
function dumpArgs(step, address, bufSize) {
var buf = Memory.readByteArray(address, bufSize)
console.log('Argument ' + step + ' address ' + address.toString() + ' ' + 'buffer: ' + bufSize.toString() + '\n\n Value:\n' +hexdump(buf, {
offset: 0,
length: bufSize,
header: false,
ansi: false
}));
console.log("Trying interpret that arg is pointer")
console.log("=====================================")
try{
console.log(Memory.readCString(ptr(address)));
console.log(ptr(address).readCString());
console.log(hexdump(ptr(address)));
}catch(e){
console.log(e);
}
console.log('')
console.log('----------------------------------------------------')
console.log('')
}
setTimeout(hookFunc, 1000)
一旦完成hook,我们就可以查看从 Frida 脚本生成的 hexdump:
当前截获的内存hook中有很多0,我们可以假设加密过程从这里开始,使用 Salsa20 算法及其密钥和 IV,然而我们并不知道 IV,但由于 Salsa20 仅使用 8 个字节的 IV,这意味着我们可以暴力破解或在内存 hexdump 中每 8 个字节使用一个已知字节。
接着我们hook compare() ,因为我们想知道它在比较哪些值:
只有一个 Base64 编码的字符串,无法成功解码。那么假设,Base64 编码的字符串是我们的输入,最终已经在 Base64 中加密和编码,那么应该有一个硬编码值,在二进制共享对象中进行比较。
回到 IDA中,我们重新检查里面是否有硬编码的 Base64 值:
事实证明确实有一个硬编码的 Base64 值,现在我们需要做的就是用空字节密钥和未知 IV 重构 Salsa20 算法的解密过程,我们可以暴力破解这 8 个字节,但实际上我们已经在第一个 hexdump 中看到了实际的 IV(0x10、0x18、0x5a、0x22、0x07、0x7e、0x62、0x41)。
根据 PyCryptodome 包的最终 Python 脚本:
from Crypto.Cipher import Salsa20
enc = b"cmc2UbeRkpDnZdyfGoiMEtwgf3n9wug4Gd3SB8EouUM4R7c2tBCVJeOmygQqjE5LNy6DmaDRkqEzG0nrkXkYHG77ooISZ23vLqR+LQ=="
key = b"\x00"*32
nonce_iv = b''.join([chr(i).encode() for i in [0x10,0x18,0x5a,0x22,0x7,0x7e,0x62,0x41]])
cip = Salsa20.new(key=key,nonce=nonce_iv)
print(cip.decrypt(base64.b64decode(enc)))
思考:
有人说,分析以Release模式构建的 Flutter 应用程序是不可能的,然而,实际上并不是不可能!不要害怕尝试通过静/动态方法再次分析它们。如果遇到困难,建议可以检查他们使用的一些软件包,说不定能够幸运地发现其中的漏洞。
参考:
https://docs.flutter.dev/testing/build-modes
https://www.oreilly.com/library/view/flutter-for-beginners/9781788996082/8927b6a6-3e37-4c8c-b277-2dcc96a71119.xhtml
https://cryptax.medium.com/reversing-an-android-sample-which-uses-flutter-23c3ff04b847
https://github.com/ptswarm/reFlutter
https://morioh.com/p/37f016ffe381
====正文结束====