以 SingPass 应用为例分析 iOS RASP 应用自保护的实现以及绕过方法(上) - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

以 SingPass 应用为例分析 iOS RASP 应用自保护的实现以及绕过方法(上)

gejigeji 技术 2023-04-08 12:00:00
79862
收藏

导语:以 SingPass 应用为例分析 iOS RASP 应用自保护的实现以及绕过方法(上)

1.png

通过在应用程序的安装目录中搜索一些关键字,我们实际上得到了两个结果,它们含有混淆器名称的信息:

2.png

NuDetectSDK 二进制文件也使用相同的混淆器,但它似乎没有参与上图所示的早期越狱检测。另一方面,SingPass 是应用程序的主要二进制文件,我们可以观察到与威胁检测相关的字符串:

3.png

混淆器的名称已被编辑,但不会影响代码的内容。

不幸的是,二进制文件没有泄漏其他字符串,这些字符串可以帮助识别应用程序检测越狱设备的位置和方式,但幸运的是,应用程序没有崩溃。

如果我们假设混淆器在运行时解密字符串,则可以尝试在显示错误消息时转储 __data 部分的内容。在执行时,用于检测越狱设备的字符串可能已被解码并清楚地存在于内存中。

1.我们运行应用程序并等待越狱消息;

2.我们使用 Frida 附加到 SingPass,并注入一个库:

2.1在内存中解析 SingPass 二进制文件;

2.2转储 __data 部分的内容;

2.3 将转储写入 iPhone 的 /tmp 目录;

一旦数据区被转储,__data部分会发生以下变化:

4.png

转储前后的 __data 部分

此外,我们可以观察到以下字符串,它们似乎与混淆器的RASP功能有关:

5.png

与 RASP 功能相关的字符串

所有的EVT_*字符串都由一个且只有一个我命名为on_rasp_detection的函数引用。这个函数是应用程序开发者在触发RASP事件时用来执行操作的威胁检测回调函数。

为了更好地理解这些字符串背后的检查逻辑,让我们从用于检测挂钩函数的 EVT_CODE_PROLOGUE 开始。

EVT_CODE_PROLOGUE:挂钩检测

当通过汇编代码接近 on_rasp_detection 的交叉引用时,我们可以多次发现这种模式:

6.png

为了检测给定函数是否被钩住,混淆器加载函数的第一个字节,并将该字节与值0xFF进行比较。乍一看,0xFF似乎是任意的,但事实并非如此。实际上,常规函数以一个序言开始,该序言在堆栈上分配空间,以保存由调用约定定义的寄存器和函数所需的堆栈变量。在AArch64中,这个分配可以通过两种方式执行:

7.png

这些指令是不相等的,如果偏移量存在,它们可能会导致相同的结果。在第二种情况下,指令 sub SP、SP、#CST 用以下字节编码:

8.png

正如我们所看到的,该指令的编码从0xFF开始。如果不是这样,那么该函数要么以不同的堆栈分配序言开始,要么可能以一个挂钩的蹦床开始。由于应用程序的代码是通过混淆器的编译器编译的,因此编译器能够区分这两种情况,并为正确的函数的序言插入正确的检查。

如果函数指令的第一个字节没有通过检查,则跳转到红色基本块。这个基本块的目的是触发一个用户定义的回调,它将根据应用程序的设计和开发人员的选择来处理检测:

打印错误

应用程序崩溃

破坏内部数据

……

从上图中,我们可以观察到检测回调是从位于 #hook_detect_cbk_ptr 的静态变量加载的。调用此检测回调时,混淆器会向回调提供以下信息:

1.检测码:EVT_CODE_PROLOGUE 为 0x400;

2.可能导致应用程序崩溃的受攻击指针;

现在让我们仔细看看检测回调的整体设计。

检测回调

如上一节所述,当混淆器检测到篡改时,它会通过调用存储在地址的静态变量中的检测回调来做出反应:0x10109D760

9.png

通过静态分析 hook_detect_cbk,实现似乎破坏了回调参数中提供的指针。另一方面,在运行应用程序时,我们观察到越狱检测消息,而不是应用程序崩溃。

如果我们查看在该地址读取或写入的交叉引用,我们会得到以下指令列表:

10.png

所以实际上只有一条指令,init_and_check_rasp+01BC,用另一个函数覆盖默认的检测回调:

11.png

与默认回调相比:hook_detect_cbk(被覆盖的函数)相比,hook_detect_cbk_user_def不会损坏一个会导致应用程序崩溃的指针。相反,它调用on_rasp_detection函数,该函数引用上图中列出的所有字符串EVT_CODE_TRACING、EVT_CODE_SYSTEM_LIB等。

通过整体查看init_and_check_rasp函数,我们可以注意到X23寄存器也用于初始化其他静态变量:

13.png

X23写入指令

这些内存写入意味着回调 hook_detect_cbk_user_def 用于初始化其他静态变量。特别是,这些其他静态变量很可能用于其他 RASP 检查。通过查看这些静态变量#EVT_CODE_TRACING_cbk_ptr、#EVT_ENV_JAILBREAK_cbk_ptr 等的交叉引用,我们可以找到执行其他 RASP 检查的位置以及触发它们的条件。

EVT_CODE_SYSTEM_LIB

14.png

EVT_ENV_DEBUGGER

15.png

EVT_ENV_JAILBREAK

16.png

多亏了#EVT_*交叉引用,我们可以静态地通过使用这些#EVT_*变量的所有基本块,并突出显示可能触发RASP回调的底层检查。在详细检查之前,需要注意以下几点:

1.虽然应用程序使用了一个商业混淆器,除了RASP之外,还提供了本地代码混淆,但代码是轻度混淆的,这使得静态汇编代码分析非常容易。

2.应用程序为所有 RASP 事件设置相同的回调。因此,它简化了 RASP 绕过和应用程序的动态分析。

反调试

SingPass 使用的混淆器版本实现了两种调试检查。首先,它检查父进程 id (ppid) 是否与 /sbin/launchd 相同,后者应该为 1。

17.png

getppid 通过函数或系统调用调用。

如果不是这种情况,它会触发 EVT_ENV_DEBUGGER 事件。第二个检查基于用于访问 extern_proc.p_flag 值的 sysctl。如果此标志包含 P_TRACED 值,则 RASP 例程会触发 EVT_ENV_DEBUGGER 事件。

18.png

在 SingPass 二进制中,我们可以在以下地址范围内找到这两个检查的实例:

19.png

越狱检测

对于大多数越狱检测,混淆器会通过检查设备上是否存在(或不存在)某些文件来尝试检测设备是否已越狱。

借助以下帮助程序,可以使用系统调用或常规函数检查文件或目录:

20.png

如上所述,我提到 __data 部分的转储显示与越狱检测相关的字符串,但转储并未显示混淆器使用的所有字符串。

通过仔细研究字符串编码机制,可以发现有些字符串是在临时变量中即时解码的。我将在本文的第二部分解释字符串编码机制,这样,我们可以通过在fopen、utimes等函数上设置钩子,并在这些调用之后立即转储__data部分来揭示字符串。然后,我们可以遍历不同的转储,查看是否出现了新的字符串。

21.png

最后,该方法无法对所有字符串进行解码,但可以实现良好的覆盖。用于检测越狱的文件列表在附件中给出。

还有一个检测 unc0ver 越狱的特殊检查,包括尝试卸载 /.installed_unc0ver:

0x100E4D814: _unmount("/.installed_unc0ver")

环境

混淆器还会检查触发 EVT_ENV_JAILBREAK 事件的环境变量。其中一些检查似乎与代码提升检测有关,但仍会触发 EVT_ENV_JAILBREAK 事件。

22.png

startswith()

从逆向工程的角度来看,startswith()实际上是作为一个“or-ed”的xor序列来实现的,以得到一个布尔值。这可能是编译器优化的结果。你可以在位于地址0x100015684的基本块中观察这个模式。

高级检测

除了常规检查之外,混淆器还执行高级检查,比如验证SIP(系统完整性保护)的当前状态,更准确地说,是KEXTS代码签名状态。

根据我在iOS越狱方面的经验,我认为没有越狱会禁用CSR_ALLOW_UNTRUSTED_KEXTS标志。相反,我猜它是用来检测应用程序是否在允许这种停用的 Apple M1 上运行。

23.png

 Assembly range: 0x100004640 – 0x1000046B8

混淆器还使用 Sandbox API 来验证是否存在某些路径:

24.png

通过这个 API 检查的路径是 OSX 相关的目录,所以我猜它也被用来验证当前代码没有在 Apple Silicon 上被解除。例如,下面是使用 Sandbox API 检查的目录列表:

25.png

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 等来执行的。此手动解析填充一个包含已解析符号的绝对地址的表。

此外,初始化例程设置遵循以下布局的元数据结构:

26.png

symbols_index 是一种转换表,它将混淆器已知的索引转换为 __got 或 __la_symbol_ptr 部分中的索引。索引的来源(即 __got 或 __la_symbol_ptr)由包含类枚举整数的 origins 表确定:

27.png

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 和具有固定值的起源:

28.png

由于symbols_index表包含uint32_t值,#0xCA8匹配#0x32A(起源表的索引)当除以sizeof(uint32_t): 0xCA8 = 0x32A * sizeof(uint32_t)。

换句话说,我们有以下操作:

29.png

然后,给定 sym_idx 值并根据符号的来源,该函数访问已解析的 __got 表或已解析的 __la_symbol_ptr 表。此访问是通过位于 sub_100ED6CC0 的辅助函数完成的。可以用下面的伪代码来概括:

30.png

比较 section_ptr 和 manual_resolved 的索引 sym_idx 处的条目,如果它们不匹配,则触发事件 #EVT_CODE_SYMBOL_TABLE。

实际上,比较涵盖了不同的情况。首先,混淆器处理 sym_idx 处的符号尚未解析的情况。在这种情况下,section_ptr[sym_idx] 指向位于 __stub_helper 部分中的符号解析存根。这就是元数据结构包含本节的内存范围的原因:

31.png

另外,如果两个指针不匹配,函数会使用dladdr来验证它们的位置:

32.png

例如,如果导入的函数与Frida挂钩,则两个指针可能不匹配。

在origin[sym_idx]被设置为SYM_ORIGINS::NONE的情况下,函数跳过检查。因此,我们可以通过用0填充原始表来禁用这个RASP检查。符号的数量接近元数据结构,元数据结构的地址是由___atomic_load和___atomic_store函数泄露的。

33.png

代码跟踪检查

代码跟踪检查旨在验证当前没有被跟踪。通过查看#EVT_CODE_TRACING_cbk_ptr 的交叉引用,我们可以识别出两种验证。

GumExecCtx

EVT_CODE_TRACING 似乎能够检测 Frida 的跟踪检查是否正在运行。这是我第一次观察到这种检查,非常聪明。对于那些想用原始汇编代码进行分析的人,我将使用 SingPass 二进制文件中的这个地址范围:0x10019B6FC – 0x10019B82C。

这是执行 Frida Stalker 检查的函数图:

34.png

与 Frida Stalker 检测相关的代码

是的,此代码能够检测到 Stalker。让我们从第一个基本块开始。 _pthread_mach_thread_np(_pthread_self()) 旨在获取调用此检查的函数的线程 ID。

然后更巧妙的是,MRS(TPIDRRO_EL0) & #-8 用于手动访问线程本地存储区。在 ARM64 上,苹果使用 TPIDRRO_EL0 的最低有效字节来存储 CPU 的数量,而 MSB 包含 TLS 基地址。

35.png

然后,第二个基本块(循环的入口)使用键tlv_idx访问线程本地变量,在循环中取值范围为0x100到0x200:

36.png

以下调用 _vm_region_64(...) 的基本块用于验证 tlv_addr 变量是否包含具有正确大小(即大于 0x30)的有效地址。在这些情况下,它会通过这些奇怪的内存访问跳转到以下基本块:

37.png

触发 EVT_CODE_TRACING的条件

为了弄清楚这些内存访问的含义,我们有必要知道这个函数与 EVT_CODE_TRACING 事件相关联。哪些知名的公共工具可以与代码跟踪相关联?没有太大的风险,我们可以假设存在Frida Stalker。

如果我们查看 Stalker 的实现,我们会注意到在 gumstalker-arm64.c 中的这种初始化:

38.png

所以跟踪者创建了一个线程局部变量,用于存储GumExecCtx结构的指针,该结构具有以下布局:

39.png

如果我们添加这个布局的偏移量并且如果我们实际上内联 GumArm64Writer 结构,我们可以得到这个表示:

40.png

由于编译器强制对齐,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 中定义的枚举,其值如下:

41.png

GumOS 包含 6 个条目,从 GUM_OS_WINDOWS = 0 到 GUM_OS_QNX = 5,这类似于GUM_PTRAUTH_INVALID = 0,而最后一个条目与 GUM_PTRAUTH_SUPPORTED = 2 相关联。

因此,前面的奇怪条件被用来对GumExecCtx结构进行指纹识别:

42.png

防止这种 Stalker 检测的一种方法是使用 _GumExecCtx 结构中的交换字段重新编译 Frida。


本文翻译自:https://www.romainthomas.fr/post/22-08-singpass-rasp-analysis/如若转载,请注明原文地址
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务