通过在应用程序的安装目录中搜索一些关键字,我们实际上得到了两个结果,它们含有混淆器名称的信息:
NuDetectSDK 二进制文件也使用相同的混淆器,但它似乎没有参与上图所示的早期越狱检测。另一方面,SingPass 是应用程序的主要二进制文件,我们可以观察到与威胁检测相关的字符串:
混淆器的名称已被编辑,但不会影响代码的内容。
不幸的是,二进制文件没有泄漏其他字符串,这些字符串可以帮助识别应用程序检测越狱设备的位置和方式,但幸运的是,应用程序没有崩溃。
如果我们假设混淆器在运行时解密字符串,则可以尝试在显示错误消息时转储 __data 部分的内容。在执行时,用于检测越狱设备的字符串可能已被解码并清楚地存在于内存中。
1.我们运行应用程序并等待越狱消息;
2.我们使用 Frida 附加到 SingPass,并注入一个库:
2.1在内存中解析 SingPass 二进制文件;
2.2转储 __data 部分的内容;
2.3 将转储写入 iPhone 的 /tmp 目录;
一旦数据区被转储,__data部分会发生以下变化:
转储前后的 __data 部分
此外,我们可以观察到以下字符串,它们似乎与混淆器的RASP功能有关:
与 RASP 功能相关的字符串
所有的EVT_*字符串都由一个且只有一个我命名为on_rasp_detection的函数引用。这个函数是应用程序开发者在触发RASP事件时用来执行操作的威胁检测回调函数。
为了更好地理解这些字符串背后的检查逻辑,让我们从用于检测挂钩函数的 EVT_CODE_PROLOGUE 开始。
当通过汇编代码接近 on_rasp_detection 的交叉引用时,我们可以多次发现这种模式:
为了检测给定函数是否被钩住,混淆器加载函数的第一个字节,并将该字节与值0xFF进行比较。乍一看,0xFF似乎是任意的,但事实并非如此。实际上,常规函数以一个序言开始,该序言在堆栈上分配空间,以保存由调用约定定义的寄存器和函数所需的堆栈变量。在AArch64中,这个分配可以通过两种方式执行:
这些指令是不相等的,如果偏移量存在,它们可能会导致相同的结果。在第二种情况下,指令 sub SP、SP、#CST 用以下字节编码:
正如我们所看到的,该指令的编码从0xFF开始。如果不是这样,那么该函数要么以不同的堆栈分配序言开始,要么可能以一个挂钩的蹦床开始。由于应用程序的代码是通过混淆器的编译器编译的,因此编译器能够区分这两种情况,并为正确的函数的序言插入正确的检查。
如果函数指令的第一个字节没有通过检查,则跳转到红色基本块。这个基本块的目的是触发一个用户定义的回调,它将根据应用程序的设计和开发人员的选择来处理检测:
打印错误
应用程序崩溃
破坏内部数据
……
从上图中,我们可以观察到检测回调是从位于 #hook_detect_cbk_ptr 的静态变量加载的。调用此检测回调时,混淆器会向回调提供以下信息:
1.检测码:EVT_CODE_PROLOGUE 为 0x400;
2.可能导致应用程序崩溃的受攻击指针;
现在让我们仔细看看检测回调的整体设计。
如上一节所述,当混淆器检测到篡改时,它会通过调用存储在地址的静态变量中的检测回调来做出反应:0x10109D760
通过静态分析 hook_detect_cbk,实现似乎破坏了回调参数中提供的指针。另一方面,在运行应用程序时,我们观察到越狱检测消息,而不是应用程序崩溃。
如果我们查看在该地址读取或写入的交叉引用,我们会得到以下指令列表:
所以实际上只有一条指令,init_and_check_rasp+01BC,用另一个函数覆盖默认的检测回调:
与默认回调相比:hook_detect_cbk(被覆盖的函数)相比,hook_detect_cbk_user_def不会损坏一个会导致应用程序崩溃的指针。相反,它调用on_rasp_detection函数,该函数引用上图中列出的所有字符串EVT_CODE_TRACING、EVT_CODE_SYSTEM_LIB等。
通过整体查看init_and_check_rasp函数,我们可以注意到X23寄存器也用于初始化其他静态变量:
X23写入指令
这些内存写入意味着回调 hook_detect_cbk_user_def 用于初始化其他静态变量。特别是,这些其他静态变量很可能用于其他 RASP 检查。通过查看这些静态变量#EVT_CODE_TRACING_cbk_ptr、#EVT_ENV_JAILBREAK_cbk_ptr 等的交叉引用,我们可以找到执行其他 RASP 检查的位置以及触发它们的条件。
多亏了#EVT_*交叉引用,我们可以静态地通过使用这些#EVT_*变量的所有基本块,并突出显示可能触发RASP回调的底层检查。在详细检查之前,需要注意以下几点:
1.虽然应用程序使用了一个商业混淆器,除了RASP之外,还提供了本地代码混淆,但代码是轻度混淆的,这使得静态汇编代码分析非常容易。
2.应用程序为所有 RASP 事件设置相同的回调。因此,它简化了 RASP 绕过和应用程序的动态分析。
SingPass 使用的混淆器版本实现了两种调试检查。首先,它检查父进程 id (ppid) 是否与 /sbin/launchd 相同,后者应该为 1。
getppid 通过函数或系统调用调用。
如果不是这种情况,它会触发 EVT_ENV_DEBUGGER 事件。第二个检查基于用于访问 extern_proc.p_flag 值的 sysctl。如果此标志包含 P_TRACED 值,则 RASP 例程会触发 EVT_ENV_DEBUGGER 事件。
在 SingPass 二进制中,我们可以在以下地址范围内找到这两个检查的实例:
对于大多数越狱检测,混淆器会通过检查设备上是否存在(或不存在)某些文件来尝试检测设备是否已越狱。
借助以下帮助程序,可以使用系统调用或常规函数检查文件或目录:
如上所述,我提到 __data 部分的转储显示与越狱检测相关的字符串,但转储并未显示混淆器使用的所有字符串。
通过仔细研究字符串编码机制,可以发现有些字符串是在临时变量中即时解码的。我将在本文的第二部分解释字符串编码机制,这样,我们可以通过在fopen、utimes等函数上设置钩子,并在这些调用之后立即转储__data部分来揭示字符串。然后,我们可以遍历不同的转储,查看是否出现了新的字符串。
最后,该方法无法对所有字符串进行解码,但可以实现良好的覆盖。用于检测越狱的文件列表在附件中给出。
还有一个检测 unc0ver 越狱的特殊检查,包括尝试卸载 /.installed_unc0ver:
0x100E4D814: _unmount("/.installed_unc0ver")
混淆器还会检查触发 EVT_ENV_JAILBREAK 事件的环境变量。其中一些检查似乎与代码提升检测有关,但仍会触发 EVT_ENV_JAILBREAK 事件。
从逆向工程的角度来看,startswith()实际上是作为一个“or-ed”的xor序列来实现的,以得到一个布尔值。这可能是编译器优化的结果。你可以在位于地址0x100015684的基本块中观察这个模式。
除了常规检查之外,混淆器还执行高级检查,比如验证SIP(系统完整性保护)的当前状态,更准确地说,是KEXTS代码签名状态。
根据我在iOS越狱方面的经验,我认为没有越狱会禁用CSR_ALLOW_UNTRUSTED_KEXTS标志。相反,我猜它是用来检测应用程序是否在允许这种停用的 Apple M1 上运行。
Assembly range: 0x100004640 – 0x1000046B8
混淆器还使用 Sandbox API 来验证是否存在某些路径:
通过这个 API 检查的路径是 OSX 相关的目录,所以我猜它也被用来验证当前代码没有在 Apple Silicon 上被解除。例如,下面是使用 Sandbox API 检查的目录列表:
Assembly range: 0x100ED7684 (function)
此外,它使用沙盒属性 file-read-metadata 作为 stat() 函数的替代方案。
Assembly range: 0x1000ECA5C – 0x1000ECE54
该应用程序通过私有系统调用使用沙盒 API 来确定是否存在一些越狱工件。这是非常明智的做法,但我想这并不符合苹果的安全政策。
此检查的目的是验证已解析导入的地址是否指向正确的库。换句话说,此检查验证导入表没有被可用于挂钩导入函数的指针篡改。
Initialization: part of sub_100E544E8
Assembly range: 0x100016FC4 – 0x100017024
在 RASP 检查初始化 (sub_100E544E8) 期间,混淆器会手动解析导入的函数。此手动解析是通过迭代 SingPass 二进制文件中的符号、检查导入符号的库、访问(在内存中)此库的 __LINKEDIT 段、解析导出 trie 等来执行的。此手动解析填充一个包含已解析符号的绝对地址的表。
此外,初始化例程设置遵循以下布局的元数据结构:
symbols_index 是一种转换表,它将混淆器已知的索引转换为 __got 或 __la_symbol_ptr 部分中的索引。索引的来源(即 __got 或 __la_symbol_ptr)由包含类枚举整数的 origins 表确定:
symbols_index和origins这两个表的长度都是由静态变量nb_symbols定义的,它被设置为0x399。元数据结构后面跟着两个指针:resolved_la_syms 和 resolved_got_syms,它们指向混淆器手动填充的导入地址表。
每个部分都有一个专用表:__got 和 __la_symbol_ptr。
然后,macho_la_syms 指向 __la_symbol_ptr 部分的开头,而 macho_got_syms 指向 __got 部分。
最后,stub_helper_start / stub_helper_end 保存了 __stub_helper 部分的内存范围。稍后我将介绍这些值的用途。
这个元数据结构的所有值都是在函数sub_100E544E8中进行初始化时设置的。
在 SingPass 二进制文件的不同位置,混淆器使用此元数据信息来验证已解析导入的完整性。它首先访问 symbols_index 和具有固定值的起源:
由于symbols_index表包含uint32_t值,#0xCA8匹配#0x32A(起源表的索引)当除以sizeof(uint32_t): 0xCA8 = 0x32A * sizeof(uint32_t)。
换句话说,我们有以下操作:
然后,给定 sym_idx 值并根据符号的来源,该函数访问已解析的 __got 表或已解析的 __la_symbol_ptr 表。此访问是通过位于 sub_100ED6CC0 的辅助函数完成的。可以用下面的伪代码来概括:
比较 section_ptr 和 manual_resolved 的索引 sym_idx 处的条目,如果它们不匹配,则触发事件 #EVT_CODE_SYMBOL_TABLE。
实际上,比较涵盖了不同的情况。首先,混淆器处理 sym_idx 处的符号尚未解析的情况。在这种情况下,section_ptr[sym_idx] 指向位于 __stub_helper 部分中的符号解析存根。这就是元数据结构包含本节的内存范围的原因:
另外,如果两个指针不匹配,函数会使用dladdr来验证它们的位置:
例如,如果导入的函数与Frida挂钩,则两个指针可能不匹配。
在origin[sym_idx]被设置为SYM_ORIGINS::NONE的情况下,函数跳过检查。因此,我们可以通过用0填充原始表来禁用这个RASP检查。符号的数量接近元数据结构,元数据结构的地址是由___atomic_load和___atomic_store函数泄露的。
代码跟踪检查旨在验证当前没有被跟踪。通过查看#EVT_CODE_TRACING_cbk_ptr 的交叉引用,我们可以识别出两种验证。
EVT_CODE_TRACING 似乎能够检测 Frida 的跟踪检查是否正在运行。这是我第一次观察到这种检查,非常聪明。对于那些想用原始汇编代码进行分析的人,我将使用 SingPass 二进制文件中的这个地址范围:0x10019B6FC – 0x10019B82C。
这是执行 Frida Stalker 检查的函数图:
与 Frida Stalker 检测相关的代码
是的,此代码能够检测到 Stalker。让我们从第一个基本块开始。_pthread_mach_thread_np(_pthread_self()) 旨在获取调用此检查的函数的线程 ID。
然后更巧妙的是,MRS(TPIDRRO_EL0) & #-8 用于手动访问线程本地存储区。在 ARM64 上,苹果使用 TPIDRRO_EL0 的最低有效字节来存储 CPU 的数量,而 MSB 包含 TLS 基地址。
然后,第二个基本块(循环的入口)使用键tlv_idx访问线程本地变量,在循环中取值范围为0x100到0x200:
以下调用 _vm_region_64(...) 的基本块用于验证 tlv_addr 变量是否包含具有正确大小(即大于 0x30)的有效地址。在这些情况下,它会通过这些奇怪的内存访问跳转到以下基本块:
触发 EVT_CODE_TRACING的条件
为了弄清楚这些内存访问的含义,我们有必要知道这个函数与 EVT_CODE_TRACING 事件相关联。哪些知名的公共工具可以与代码跟踪相关联?没有太大的风险,我们可以假设存在Frida Stalker。
如果我们查看 Stalker 的实现,我们会注意到在 gumstalker-arm64.c 中的这种初始化:
所以跟踪者创建了一个线程局部变量,用于存储GumExecCtx结构的指针,该结构具有以下布局:
如果我们添加这个布局的偏移量并且如果我们实际上内联 GumArm64Writer 结构,我们可以得到这个表示:
由于编译器强制对齐,destroy_pending_since 位于偏移量 0x08 而不是 0x04处。
这样一来,我们可以观察到:
*(tlv_table + 0x18) 有效匹配 GumThreadId thread_id 属性;
*(tlv_table + 0x24) 匹配 GumOS target_os;
*(tlv_table + 0x28) 匹配 GumPtrauthSupport ptrauth_support;
GumOS 和 GumPtrauthSupport 是在 gumdefs.h 和 gummemory.h 中定义的枚举,其值如下:
GumOS 包含 6 个条目,从 GUM_OS_WINDOWS = 0 到 GUM_OS_QNX = 5,这类似于GUM_PTRAUTH_INVALID = 0,而最后一个条目与 GUM_PTRAUTH_SUPPORTED = 2 相关联。
因此,前面的奇怪条件被用来对GumExecCtx结构进行指纹识别:
防止这种 Stalker 检测的一种方法是使用 _GumExecCtx 结构中的交换字段重新编译 Frida。
参考及来源:https://www.romainthomas.fr/post/22-08-singpass-rasp-analysis/