简介
欢迎来到Firebloom iBoot系列文章的第二篇。在上一篇文章中,我们为读者详细介绍了Firebloom在iBoot中的实现方式,内存分配的表现形式,以及编译器是如何使用它们的。现在,让我们首先回顾一下用于描述内存分配的结构体:
00000000 safe_allocation struc ; (sizeof=0x20, mappedto_1) 00000000 raw_ptr DCQ ? ; 00000008 lower_bound_ptr DCQ ? ; 00000010 upper_bound_ptr DCQ ? ; 00000018 type DCQ ? ; 00000020 safe_allocation ends
在上一篇文章中,我们重点介绍了Firebloom是如何使用lower_bound_ptr和upper_bound_ptr指针的,它们主要用于为内存空间提供安全保护,即防止任何形式的后向/前向越界访问。在这篇文章中,我们将重点介绍type指针。
我们强烈建议读者在阅读这篇博文之前,首先阅读我们的第一篇文章,以了解相关的背景知识。和上一篇文章一样,我们的研究工作是在iBoot.d53g.RELEASE.im4p上进行的,对应于安装了ios 14.4(18D52)系统的iPhone 12机器。
继续我们的逆向之旅
在之前的文章中,我们为读者介绍过do_safe_allocation函数,其所用是封装内存分配API并对safe_allocation结构体进行初始化,其中偏移量+0x18处的指针指向了一些描述类型信息的结构体。我们还通过一个例子表明,在使用分配的内存之前,系统会通过该指针进行相应的类型检查。现在,是时候看看这种功能是如何工作的,以及这个type指针起到了那些作用。
我发现许多与type指针有关的逻辑(类型转换、在安全分配的内存之间进行复制,等等),这些逻辑真的值得我们逆向研究一番。我想最好是从构件开始,然后逐级而上。
复制指针与内存
为了帮助大家理解,我们决定从一些例子入手。为此,让我们从panic_memcpy_bad_type开始进行逆向——它是do_firebloom_panic的11个交叉引用之一。
iBoot:00000001FC1AA818 panic_memcpy_bad_type ; CODE XREF: call_panic_memcpy_bad_type+3C↑p iBoot:00000001FC1AA818 iBoot:00000001FC1AA818 var_20 = -0x20 iBoot:00000001FC1AA818 var_10 = -0x10 iBoot:00000001FC1AA818 var_s0 = 0 iBoot:00000001FC1AA818 iBoot:00000001FC1AA818 PACIBSP iBoot:00000001FC1AA81C STP X22, X21, [SP,#-0x10+var_20]! iBoot:00000001FC1AA820 STP X20, X19, [SP,#0x20+var_10] iBoot:00000001FC1AA824 STP X29, X30, [SP,#0x20+var_s0] iBoot:00000001FC1AA828 ADD X29, SP, #0x20 iBoot:00000001FC1AA82C MOV X19, X2 iBoot:00000001FC1AA830 MOV X20, X1 iBoot:00000001FC1AA834 MOV X21, X0 iBoot:00000001FC1AA838 ADRP X8, #[email protected] iBoot:00000001FC1AA83C LDR X8, [X8,#[email protected]] iBoot:00000001FC1AA840 CBZ X8, loc_1FC1AA848 iBoot:00000001FC1AA844 BLRAAZ X8 iBoot:00000001FC1AA848 iBoot:00000001FC1AA848 loc_1FC1AA848 ; CODE XREF: panic_memcpy_bad_type+28↑j iBoot:00000001FC1AA848 ADR X0, aMemcpyBadType ; "memcpy_bad_type" iBoot:00000001FC1AA84C NOP iBoot:00000001FC1AA850 MOV X1, X21 iBoot:00000001FC1AA854 MOV X2, X20 iBoot:00000001FC1AA858 MOV X3, X19 iBoot:00000001FC1AA85C BL do_firebloom_panic iBoot:00000001FC1AA85C ; End of function panic_memcpy_bad_type
我们将从最简单的东西开始,即复制指针的操作。
在我之前的文章中,我特别指出:现在复制一个指针(即进行指针赋值)需要移动一个由4个64位值组成的元组。通常来说,我们可以将其视为是2个LDP与2个STP指令。现在,我们通过下面的函数来举例说明:
iBoot:00000001FC15AD74 move_safe_allocation_x20_to_x19 ; CODE XREF: sub_1FC15A7E0+78↑p iBoot:00000001FC15AD74 ; wrap_memset_type_safe+68↑p ... iBoot:00000001FC15AD74 LDP X8, X9, [X20] iBoot:00000001FC15AD78 LDP X10, X11, [X20,#0x10] iBoot:00000001FC15AD7C STP X10, X11, [X19,#0x10] iBoot:00000001FC15AD80 STP X8, X9, [X19] iBoot:00000001FC15AD84 RET iBoot:00000001FC15AD84 ; End of function move_safe_allocation_x20_to_x19
如今,这种由2个LDP指令和2个STP指令组成的模式,在iBoot中是非常常见的(这是很正常的,因为指针赋值经常发生),所以,我们会在许多地方看到这样的内联代码。虽然这对于指针赋值很有用,但在许多情况下,我们想要做的却是复制内容——例如,调用memcpy的时候。因此,有趣的事情就出现了:是否应该允许在两个“safe_allocations”内存之间调用memcpy?
理论上,我们可以执行下面的代码:
memcpy(dst->raw_ptr, src->raw_ptr, length);
但是,请记住,每段safe_allocation内存都具有相应的type指针,该指针指向某个结构体,以便提供与当前处理的类型有关的更多信息。这些信息可以用于进一步的检查和验证。例如,我们希望看到一些逻辑来检查dst和src的类型是否为基本类型(即这些类型不包含对其他结构体、嵌套结构体等结构体的引用,如short/int/float/double/等类型,就是属于基本类型)。
这很重要,因为如果src或dst是非基本类型,我们就需要确保只有在它们的类型在某种程度上相等时才将src复制到dst。或者,type实际上保存了更多与结构体有关的元数据,因此需要确保更多的安全属性。
因此,我想了解一下Firebloom是如何描述基本类型的。在对类型转换功能,以及其他功能代码进行逆向分析之后,我终于搞清楚了这一点。有趣的是,分析过程再简单不过了——我们在函数cast_impl中找到了很多有用的字符串。例如:
aCannotCastPrim DCB "Cannot cast primitive type to non-primitive type",0
借助于交叉引用,我们在下面的代码中发现,X21寄存器就是来自safe_allocation内存的type指针:
iBoot:00000001FC1A0CF8 ; X21 is the type pointer iBoot:00000001FC1A0CF8 LDR X11, [X21] iBoot:00000001FC1A0CFC AND X11, X11, #0xFFFFFFFFFFFFFFF8 iBoot:00000001FC1A0D00 LDRB W12, [X11] iBoot:00000001FC1A0D04 TST W12, #7 iBoot:00000001FC1A0D08 ; one of the 3 LSB bits is not 0, non-primitive type iBoot:00000001FC1A0D08 B.NE cannot_cast_primitive_to_non_primitive_type iBoot:00000001FC1A0D0C LDR X11, [X11,#0x20] iBoot:00000001FC1A0D10 LSR X11, X11, #0x23 ; '#' iBoot:00000001FC1A0D14 CBNZ X11, cannot_cast_primitive_to_non_primitive_type ... iBoot:00000001FC1A0E70 cannot_cast_primitive_to_non_primitive_type iBoot:00000001FC1A0E70 ; CODE XREF: cast_impl+478↑j iBoot:00000001FC1A0E70 ; cast_impl+484↑j iBoot:00000001FC1A0E70 ADR X11, aCannotCastPrim ; "Cannot cast primitive type to non-primi"...
好了,现在我们知道Firebloom是如何标记和测试基本类型的了。这段代码只是将一种类型转换为另一种类型的功能实现中的一小部分,特别是这里的X21寄存器是我们要转换为的safe_allocation结构体中的type指针。在进行类型转换时,我们验证要转换的类型是基本类型;同时,还要验证转换后的目标类型也是基本类型(否则就会出现panic)。
为了完成这项检查,代码会对type指针进行解引用,进而得到另一个指针(我们称之为type_descriptor)。然后,将其最低的3个二进制位屏蔽掉(它们可能对应于一个编码,这就是所有用到该指针的地方都会在解除其引用之前都屏蔽它的原因),然后对该指针解除引用。
现在,如果以下两个属性都满足要求,那么,该类型被认为是“基本类型”:
第一个qword的低3位都为0。
在偏移量0x20处存储的值的高29位都为0。
太好了,我们刚刚了解了基本类型是如何表示的。在本文的后面部分,我们将详细介绍这些值的具体含义。
有了这些知识,我们就可以着手了解Firebloom到底是如何在iBoot中封装memset和memcpy函数的了。现在,让我们从memset函数开始:
iBoot:00000001FC15A99C wrap_memset_safe_allocation ; CODE XREF: sub_1FC04E5D0+124↑p iBoot:00000001FC15A99C ; sub_1FC04ED68+8↑j ... iBoot:00000001FC15A99C iBoot:00000001FC15A99C var_30 = -0x30 iBoot:00000001FC15A99C var_20 = -0x20 iBoot:00000001FC15A99C var_10 = -0x10 iBoot:00000001FC15A99C var_s0 = 0 iBoot:00000001FC15A99C iBoot:00000001FC15A99C PACIBSP iBoot:00000001FC15A9A0 SUB SP, SP, #0x60 iBoot:00000001FC15A9A4 STP X24, X23, [SP,#0x50+var_30] iBoot:00000001FC15A9A8 STP X22, X21, [SP,#0x50+var_20] iBoot:00000001FC15A9AC STP X20, X19, [SP,#0x50+var_10] iBoot:00000001FC15A9B0 STP X29, X30, [SP,#0x50+var_s0] iBoot:00000001FC15A9B4 ADD X29, SP, #0x50 iBoot:00000001FC15A9B8 ; void *memset(void *s, int c, size_t n); iBoot:00000001FC15A9B8 ; X0 - dst (s) iBoot:00000001FC15A9B8 ; X1 - char (c) iBoot:00000001FC15A9B8 ; X2 - length (n) iBoot:00000001FC15A9B8 MOV X21, X2 iBoot:00000001FC15A9BC MOV X22, X1 iBoot:00000001FC15A9C0 MOV X20, X0 iBoot:00000001FC15A9C4 MOV X19, X8 iBoot:00000001FC15A9C8 ; verify upper_bound - raw_ptr >= x2 (length) iBoot:00000001FC15A9C8 BL check_ptr_bounds iBoot:00000001FC15A9CC LDR X23, [X20,#safe_allocation.type] iBoot:00000001FC15A9D0 MOV X0, X23 iBoot:00000001FC15A9D4 ; check if dst is a primitive type iBoot:00000001FC15A9D4 BL is_primitive_type iBoot:00000001FC15A9D8 TBNZ W0, #0, call_memset iBoot:00000001FC15A9DC CBNZ W22, detected_memset_bad_type iBoot:00000001FC15A9E0 MOV X0, X23 iBoot:00000001FC15A9E4 BL get_type_length iBoot:00000001FC15A9E8 ; divide and multiply the length argument iBoot:00000001FC15A9E8 ; by the type's size, to detect iBoot:00000001FC15A9E8 ; partial/unalignment writes iBoot:00000001FC15A9E8 UDIV X8, X21, X0 iBoot:00000001FC15A9EC MSUB X8, X8, X0, X21 iBoot:00000001FC15A9F0 CBNZ X8, detected_memset_bad_n iBoot:00000001FC15A9F4 iBoot:00000001FC15A9F4 call_memset ; CODE XREF: wrap_memset_safe_allocation+3C↑j iBoot:00000001FC15A9F4 LDR X0, [X20,#safe_allocation] iBoot:00000001FC15A9F8 MOV X1, X22 iBoot:00000001FC15A9FC MOV X2, X21 iBoot:00000001FC15AA00 BL _memset iBoot:00000001FC15AA04 BL move_safe_allocation_x20_to_x19 iBoot:00000001FC15AA08 LDP X29, X30, [SP,#0x50+var_s0] iBoot:00000001FC15AA0C LDP X20, X19, [SP,#0x50+var_10] iBoot:00000001FC15AA10 LDP X22, X21, [SP,#0x50+var_20] iBoot:00000001FC15AA14 LDP X24, X23, [SP,#0x50+var_30] iBoot:00000001FC15AA18 ADD SP, SP, #0x60 ; '`' iBoot:00000001FC15AA1C RETAB iBoot:00000001FC15AA20 ; --------------------------------------------------------------------------- iBoot:00000001FC15AA20 iBoot:00000001FC15AA20 detected_memset_bad_type ; CODE XREF: wrap_memset_safe_allocation+40↑j iBoot:00000001FC15AA20 BL call_panic_memset_bad_type iBoot:00000001FC15AA24 ; --------------------------------------------------------------------------- iBoot:00000001FC15AA24 iBoot:00000001FC15AA24 detected_memset_bad_n ; CODE XREF: wrap_memset_safe_allocation+54↑j iBoot:00000001FC15AA24 BL call_panic_memset_bad_n iBoot:00000001FC15AA24 ; End of function wrap_memset_safe_allocation
所以,函数wrap_memset_safe_allocation会检查dst的类型是否为基本类型。如果是的话,就直接调用memset函数。
然而,如果dst不是基本类型,我们就有更多的信息可以利用了。事实证明,苹果公司在type结构体中编码了大量的信息(其中有一个指向结构体的指针,用处多多)。例如,由于非基本类型的长度是可变的,因此,苹果公司就把相关信息编码到了type结构体的第一个指针所指向的内存中。如果memset函数的参数n与类型的长度不一致,iBoot会调用panic_memset_bad_n。
请注意,在这个函数的开头部分,会进行常规的边界检查(使用safe_allocation中的边界指针),如果检测到OOB,就会立即panic。而函数panic_memset_bad_n则会进行更加严格的检查,一旦发现初始化/复制不彻底的情况,就会立即panic。太酷了!
不难想到,函数memcpy也可能具有类似行为:
iBoot:00000001FC15A7E0 wrap_memcpy_safe_allocation ; CODE XREF: sub_1FC052C08+21C↑p iBoot:00000001FC15A7E0 ; sub_1FC054C94+538↑p ... iBoot:00000001FC15A7E0 iBoot:00000001FC15A7E0 var_70 = -0x70 iBoot:00000001FC15A7E0 var_30 = -0x30 iBoot:00000001FC15A7E0 var_20 = -0x20 iBoot:00000001FC15A7E0 var_10 = -0x10 iBoot:00000001FC15A7E0 var_s0 = 0 iBoot:00000001FC15A7E0 iBoot:00000001FC15A7E0 PACIBSP iBoot:00000001FC15A7E4 SUB SP, SP, #0x80 iBoot:00000001FC15A7E8 STP X24, X23, [SP,#0x70+var_30] iBoot:00000001FC15A7EC STP X22, X21, [SP,#0x70+var_20] iBoot:00000001FC15A7F0 STP X20, X19, [SP,#0x70+var_10] iBoot:00000001FC15A7F4 STP X29, X30, [SP,#0x70+var_s0] iBoot:00000001FC15A7F8 ADD X29, SP, #0x70 iBoot:00000001FC15A7FC ; set the following registers: iBoot:00000001FC15A7FC ; MOV X21, X2 (length) iBoot:00000001FC15A7FC ; MOV X22, X1 (src) iBoot:00000001FC15A7FC ; MOV X20, X0 (dst) iBoot:00000001FC15A7FC ; MOV X19, X8 iBoot:00000001FC15A7FC BL call_check_ptr_bounds_ iBoot:00000001FC15A800 BL do_check_ptr_bounds_x22 iBoot:00000001FC15A804 LDR X23, [X20,#safe_allocation.type] iBoot:00000001FC15A808 MOV X0, X23 iBoot:00000001FC15A80C ; check if dst's type is a primitive type iBoot:00000001FC15A80C BL is_primitive_type iBoot:00000001FC15A810 LDR X24, [X22,#safe_allocation.type] iBoot:00000001FC15A814 CBZ W0, loc_1FC15A824 iBoot:00000001FC15A818 MOV X0, X24 iBoot:00000001FC15A81C ; check if src's type is a primitive type iBoot:00000001FC15A81C BL is_primitive_type iBoot:00000001FC15A820 TBNZ W0, #0, loc_1FC15A854 iBoot:00000001FC15A824 ; at least one of the allocation (src or dst) iBoot:00000001FC15A824 ; are not primitive type. Call the type iBoot:00000001FC15A824 ; equal implementation to see if they are equal iBoot:00000001FC15A824 iBoot:00000001FC15A824 loc_1FC15A824 ; CODE XREF: wrap_memcpy_safe_allocation+34↑j iBoot:00000001FC15A824 MOV X8, SP iBoot:00000001FC15A828 ; dst's type descriptor ptr iBoot:00000001FC15A828 MOV X0, X23 iBoot:00000001FC15A82C ; src's type descriptor ptr iBoot:00000001FC15A82C MOV X1, X24 iBoot:00000001FC15A830 BL compare_types iBoot:00000001FC15A834 LDR W8, [SP,#0x70+var_70] iBoot:00000001FC15A838 CMP W8, #1 iBoot:00000001FC15A83C B.NE detect_memcpy_bad_type iBoot:00000001FC15A840 LDR X0, [X20,#safe_allocation.type] iBoot:00000001FC15A844 BL get_type_length iBoot:00000001FC15A848 ; divide and multiply the length argument iBoot:00000001FC15A848 ; by the type's size, to detect iBoot:00000001FC15A848 ; partial/unalignment writes iBoot:00000001FC15A848 UDIV X8, X21, X0 iBoot:00000001FC15A84C MSUB X8, X8, X0, X21 iBoot:00000001FC15A850 CBNZ X8, detect_memcpy_bad_n iBoot:00000001FC15A854 ; ok, we have one of two possible cases: iBoot:00000001FC15A854 ; A) types are either both primitives iBoot:00000001FC15A854 ; B) type are both equals, iBoot:00000001FC15A854 ; and dst's length is verified w.r.t the len argument iBoot:00000001FC15A854 ; which means we can do a raw copy iBoot:00000001FC15A854 iBoot:00000001FC15A854 loc_1FC15A854 ; CODE XREF: wrap_memcpy_safe_allocation+40↑j iBoot:00000001FC15A854 BL memcpy_safe_allocations_x22_to_x20 iBoot:00000001FC15A858 BL move_safe_allocation_x20_to_x19 iBoot:00000001FC15A85C LDP X29, X30, [SP,#0x70+var_s0] iBoot:00000001FC15A860 LDP X20, X19, [SP,#0x70+var_10] iBoot:00000001FC15A864 LDP X22, X21, [SP,#0x70+var_20] iBoot:00000001FC15A868 LDP X24, X23, [SP,#0x70+var_30] iBoot:00000001FC15A86C ADD SP, SP, #0x80 iBoot:00000001FC15A870 RETAB iBoot:00000001FC15A874 ; --------------------------------------------------------------------------- iBoot:00000001FC15A874 iBoot:00000001FC15A874 detect_memcpy_bad_type ; CODE XREF: wrap_memcpy_safe_allocation+5C↑j iBoot:00000001FC15A874 BL call_panic_memcpy_bad_type iBoot:00000001FC15A878 ; --------------------------------------------------------------------------- iBoot:00000001FC15A878 iBoot:00000001FC15A878 detect_memcpy_bad_n ; CODE XREF: wrap_memcpy_safe_allocation+70↑j iBoot:00000001FC15A878 BL call_panic_memcpy_bad_n iBoot:00000001FC15A878 ; End of function wrap_memcpy_safe_allocation
事实上,函数is_primitive_type的行为与前面看到的如出一辙:
iBoot:00000001FC15A8C0 is_primitive_type ; CODE XREF: wrap_memcpy_safe_allocation+2C↑p iBoot:00000001FC15A8C0 ; wrap_memcpy_safe_allocation+3C↑p ... iBoot:00000001FC15A8C0 LDR X8, [X0] iBoot:00000001FC15A8C4 AND X8, X8, #0xFFFFFFFFFFFFFFF8 iBoot:00000001FC15A8C8 LDRB W9, [X8] iBoot:00000001FC15A8CC TST W9, #7 iBoot:00000001FC15A8D0 B.EQ check_number_of_pointer_elements iBoot:00000001FC15A8D4 ; one of the 3 LSB bits is non-0, therefore return false iBoot:00000001FC15A8D4 MOV W0, #0 iBoot:00000001FC15A8D8 RET iBoot:00000001FC15A8DC ; --------------------------------------------------------------------------- iBoot:00000001FC15A8DC iBoot:00000001FC15A8DC check_number_of_pointer_elements ; CODE XREF: do_check_ptr_bounds+54↑j iBoot:00000001FC15A8DC LDR X8, [X8,#0x20] iBoot:00000001FC15A8E0 ; check if number of pointer elements == 0 iBoot:00000001FC15A8E0 ; return true if it is, otherwise return false iBoot:00000001FC15A8E0 LSR X8, X8, #0x23 ; '#' iBoot:00000001FC15A8E4 CMP X8, #0 iBoot:00000001FC15A8E8 CSET W0, EQ iBoot:00000001FC15A8EC RET iBoot:00000001FC15A8EC ; End of function do_check_ptr_bounds
而函数memcpy_safe_allocations_x22_to_x20就更简单了:
iBoot:00000001FC15AD88 memcpy_safe_allocations_x22_to_x20 iBoot:00000001FC15AD88 iBoot:00000001FC15AD88 LDR X0, [X20,#safe_allocation.raw_ptr] iBoot:00000001FC15AD8C LDR X1, [X22,#safe_allocation.raw_ptr] iBoot:00000001FC15AD90 MOV X2, X21 iBoot:00000001FC15AD94 B _memcpy iBoot:00000001FC15AD94 ; End of function memcpy_safe_allocations_x22_to_x20
即:
memcpy(dst->raw_ptr, src->raw_ptr, length);
简直不要太好。所以,函数wrap_memcpy_safe_allocation只在满足以下所有条件的情况下才会将src复制到dst:
两者都是基本类型,或者其类型是相同的。
长度参数不会因大于dst的长度而导致越界访问,其长度可以从safe_allocation结构体中提取。
长度参数与类型的大小对齐,所以不会导致部分初始化问题。
请注意,由于苹果公司在这里提供了类型的具体长度,因此可以对memset/memcpy施加更多的限制,而非仅限于“不要越界访问”(具体来说,他们是通过safe_allocation的下限/上限指针来实现的)。此外,苹果公司还会确保对结构体的写入操作没有留下“部分未初始化”的区域,并且实际上写入操作就应该是这样的(从对齐的角度来看)。
对于某些情况来说,这种检查是非常重要的,比如对数组struct A* arr调用memset函数的时候。有了这个panic_memset_bad_n检查,iBoot就可以确保永远不会在数组中留下部分未初始化的元素。
到目前为止,我们还没有介绍get_type_length,别急,下面就轮到它了。
编码与格式
我想做的第一件事就是证明get_type_length的行为果然不出我所料。目前来说,我的语料好像都是正确的,它会将返回值与memcpy(n)的长度参数进行比较(出现"panic_memcpy_bad_n"情况时,就会panic)。但是,我仍然认为,我们可以通过考察其实现代码来了解其作用,以及其他方面的信息。
在逆向类型转换功能的实现代码时,我发现了一个有趣的函数:firebloom_type_equalizer。这个函数中含有许多有用的字符串,为我们提供了丰富的信息。例如,请查看以下代码:
iBoot:00000001FC1A3FA4 LDR X9, [X26,#0x20] iBoot:00000001FC1A3FA8 LDR X8, [X20,#0x20] iBoot:00000001FC1A3FAC LSR X10, X9, #0x23 ; '#' iBoot:00000001FC1A3FB0 LSR X23, X8, #0x23 ; '#' iBoot:00000001FC1A3FB4 CMP W9, W8 iBoot:00000001FC1A3FB8 CBZ W11, bne_size_mismatch iBoot:00000001FC1A3FBC B.CC left_has_smaller_size_than_right iBoot:00000001FC1A3FC0 CMP W10, W23 iBoot:00000001FC1A3FC4 B.CC left_has_fewer_pointer_elements_than_right
仅从这段代码中,我们就可以了解到以下内容:
类型描述符结构中,类型的长度存储在偏移量+0x20处。这个值的位宽为32比特。
在这个值的高29位,还提供了一个非常有用的信息:“指针元素的数量”。这正是我们所感兴趣的东西!
下面,我们开始展示get_type_length,它是由wrap_memset_safe_allocation和wrap_memcpy_safe_allocation调用的:
iBoot:00000001FC15A964 get_type_length ; CODE XREF: wrap_memcpy_safe_allocation+64↑p iBoot:00000001FC15A964 ; wrap_memset_safe_allocation+48↓p ... iBoot:00000001FC15A964 LDR X8, [X0] iBoot:00000001FC15A968 AND X8, X8, #0xFFFFFFFFFFFFFFF8 iBoot:00000001FC15A96C LDR W9, [X8] iBoot:00000001FC15A970 AND W9, W9, #7 iBoot:00000001FC15A974 CMP W9, #5 iBoot:00000001FC15A978 CCMP W9, #1, #4, NE iBoot:00000001FC15A97C B.NE loc_1FC15A988 iBoot:00000001FC15A980 ; one instance of the element iBoot:00000001FC15A980 MOV W0, #1 iBoot:00000001FC15A984 RET iBoot:00000001FC15A988 ; --------------------------------------------------------------------------- iBoot:00000001FC15A988 iBoot:00000001FC15A988 loc_1FC15A988 ; CODE XREF: call_panic_memcpy_bad_type+58↑j iBoot:00000001FC15A988 CBNZ W9, return_0 iBoot:00000001FC15A98C ; load the low 32-bit from this value, iBoot:00000001FC15A98C ; which represents the length of this type iBoot:00000001FC15A98C LDR W0, [X8,#0x20] iBoot:00000001FC15A990 RET iBoot:00000001FC15A994 ; --------------------------------------------------------------------------- iBoot:00000001FC15A994 iBoot:00000001FC15A994 return_0 ; CODE XREF: call_panic_memcpy_bad_type:loc_1FC15A988↑j iBoot:00000001FC15A994 MOV X0, #0 iBoot:00000001FC15A998 RET
对于以0xFFFFFFFFFFFFFFF8为操作数的AND指令,是不是有一种熟悉的味道?如果是的话,很可能是因为您在本文开头部分就见过它,当时我们在考察cast_impl是如何检测基本类型的。另外,我们还提过,类型描述符中的第一个指针的最低3个比特中,好像编码了某些信息,因此,每次我们需要解除该指针的引用时,都需要用零屏蔽它们。
实际上,该函数将返回存储在类型描述符中偏移量0x20处的32位值,而这正是类型的长度。
Firebloom中的基本类型
实际上,在偏移量+0x20处存储的64位值中还有一些非常有趣的东西,比如:
其中低32位表示类型的长度。
中间有3位,其用途当前尚不明确。
最高29位表示“指针元素的数量”。
我们以前就见过这个值。大家不妨再看一下cast_impl和is_primitive_type中的代码。在这些地方,代码会检查 "指针元素的数量",并将其与0进行比较——只有当它等于0时,该类型才被认为是基本类型。这是很有道理的!
现在,让我们开始考察is_primitive_type。这个函数的逻辑如下:
解除对type指针的引用时,要先屏蔽掉低3位,然后,才解除对这个指针的引用。
如果低3位中的任何一位被置1,则返回false。
读取存储在+0x20处的64位值。
提取这个值的高29位——我们知道,它表示"指针元素的数量"。
如果这个值为0,返回true;否则,返回false。
换句话说:
如果低3位中的任何一位被置1,那么,这个类型就不是基本类型:这时将返回false。
如果低3位都是0,那么代码会检查指针元素的数量是否等于0。
因此,函数is_primitive_type只有在低3位都是0并且指针元素的数量为0时才返回true。这就是我们所期望的,因为你不应该被允许在非基本类型之间复制字节,除非它们(在某种方式下)是相同的。
为了帮助大家更好的理解,让我们看看is_primitive_type的交叉引用。这个函数(只)被wrap_memset_safe_allocation和wrap_memcpy_safe_allocation调用,以确定它们是否可以直接调用memset和memcpy,而无需进行其他检查。
让我们来仔细看看:
函数wrap_memset_safe_allocation将调用is_primitive_type,并检查返回值(0或1)。如果返回值为1,它就直接调用memset;否则,它会检查参数c(表示memset的模式)是否为零,即字符\x00。如果它不是零,它就会调用panic_memset_bad_type。
所以,参数c不为0的情况下,iBoot就会拒绝调用memset来处理非基本类型数据。
函数wrap_memcpy_safe_allocation会调用两次is_primitive_type函数,一次用于处理dst,另一次用于处理src。如果两次调用都返回1,它就直接调用memcpy。否则,它就会调用compare_types,使用firebloom_type_equalizer来检查类型。
所以,对于memcpy,iBoot拒绝从/向非基本类型复制内容,除非它们的类型是一致的(以一种定义好的方式进行比较)。
这非常有趣,而且很合理。看到这样的类型安全验证,也是非常酷的。
与类型相关的示例
在进行总结之前,我想在这里展示一下iBoot二进制代码中的与类型处理相关的几个例子。正如我在之前的文章中讲过的,调用do_safe_allocation*的函数需要设置相应的类型(如果不想使用do_safe_allocation*函数设置的默认类型的话)。下面,让我们通过do_safe_allocation*函数的调用者,看看我们通过二进制代码中了解到的格式是否正确。
示例1
让我们从下面的代码开始:
iBoot:00000001FC10E4DC LDR W1, [X22,#0x80] iBoot:00000001FC10E4E0 ; the `safe_allocation` structure to initialize iBoot:00000001FC10E4E0 ADD X8, SP, #0xD0+v_safe_allocation iBoot:00000001FC10E4E4 MOV W0, #1 iBoot:00000001FC10E4E8 BL do_safe_allocation_calloc iBoot:00000001FC10E4EC LDP X0, X1, [SP,#0xD0+v_safe_allocation] iBoot:00000001FC10E4F0 LDP X2, X3, [SP,#0xD0+v_safe_allocation.upper_bound_ptr] iBoot:00000001FC10E4F4 BL sub_1FC10E1C4 iBoot:00000001FC10E4F8 ADRP X8, #[email protected] iBoot:00000001FC10E4FC LDR X8, [X8,#[email protected]] iBoot:00000001FC10E500 CBZ X8, detect_ptr_null iBoot:00000001FC10E504 CMP X23, X19 iBoot:00000001FC10E508 B.HI detected_ptr_under iBoot:00000001FC10E50C CMP X28, X19 iBoot:00000001FC10E510 B.LS detected_ptr_over iBoot:00000001FC10E514 MOV X20, X0 iBoot:00000001FC10E518 MOV X27, X1 iBoot:00000001FC10E51C ; X19 here is a base of some allocation, iBoot:00000001FC10E51C ; set X8 to be raw_ptr+0x50, which is iBoot:00000001FC10E51C ; the upper_bound_ptr iBoot:00000001FC10E51C ADD X8, X19, #0x50 ; 'P' iBoot:00000001FC10E520 ; re-initialize the safe_allocation: iBoot:00000001FC10E520 ; set X19 as both raw_ptr and lower_bound_ptr iBoot:00000001FC10E520 STP X19, X19, [SP,#0xD0+v_safe_allocation] iBoot:00000001FC10E524 ; take the relevant type pointer, set it in iBoot:00000001FC10E524 ; safe_allocation->type (offset +0x18, iBoot:00000001FC10E524 ; which is one qword after upper_bound_ptr). iBoot:00000001FC10E524 ; iBoot:00000001FC10E524 ; Note: the size of this type should be +0x50 iBoot:00000001FC10E524 ADRL X9, off_1FC2D09E8 iBoot:00000001FC10E52C STP X8, X9, [SP,#0xD0+v_safe_allocation.upper_bound_ptr]
Ok,有意思。因此,我们调用了do_safe_allocation_calloc,然后代码将type指针设置为off_1fc2d09e8。下面,让我们看看我们那里有什么:
iBoot:00000001FC2D09E8 off_1FC2D09E8 DCQ off_1FC2D0760+2 ; DATA XREF: sub_1FC1071C0+33C↑o iBoot:00000001FC2D09E8 ; sub_1FC107D90+188↑o ...
好极了! 实际上,这个指针所指向的值是某地址+2(还记得掩码0xFFFFFFFFFFF8吗?)。让我们解除对这个指针的引用,在偏移量+0x20处,我期望看到:
类型的长度(低32位)
类型中指针的数量(高29位)
而且确实如此:
iBoot:00000001FC2D0760 off_1FC2D0760 DCQ off_1FC2D0760 ; DATA XREF: iBoot:off_1FC2D0760↓o iBoot:00000001FC2D0760 ; iBoot:00000001FC2D0A98↓o ... iBoot:00000001FC2D0768 ALIGN 0x20 iBoot:00000001FC2D0780 DCQ 0x1300000050, 0x100000000
太棒了! 在偏移量+0x20处的值为0x1300000050,与我们的预期完全一致:
类型的长度 = 0x50(这正是我们所预期的!)
指针的数量 = 2(0x1300000050>>0x23)
很好,与我们的预期完全一致。
示例2
我们不能忽略默认类型,对吧?如前所述,所有do_safe_allocation*函数都在偏移量+0x18处设置了一个默认类型指针,并且如果需要,调用者是可以修改该类型的(详见如上面的两个示例)。下面,让我们看看default_type_ptr的交叉引用:
我们期望在这里看到这样一些“默认”值,即类型的长度 = 1,number_of_pointers = 0,并且被标记为基本类型。好了,让我们来看看实际情况:
iBoot:00000001FC2D6EF8 default_type_ptr DCQ default_type_ptr ; DATA XREF: __firebloom_panic+2C↑o iBoot:00000001FC2D6EF8 ; sub_1FC15AD98+1FC↑o ... iBoot:00000001FC2D6F00 DCQ 0, 0, 0 iBoot:00000001FC2D6F18 DCQ 0x100000001
完全符合我们的预期! 其中,default_type_ptr指向自身(很好),在偏移量+0x20处的值为0x0000000100000001,这意味着:
类型的长度 = 0x1
指针的数量 = 0(0x100000001>>0x23)
当然,这个类型是基本类型(因为低三位的值都是0,指针的数量是0)。
类型转换
实际上,类型转换的实现代码,做的也很好。考虑到篇幅问题,这里就先不介绍类型转换的实现原理了,以后我们抽机会再讲。然而,我确实想鼓励更多的人去考察相应的二进制代码,比如这个非常酷的cast_failed函数,其中含有许多非常有用的字符串,以及对wrap_firebloom_type_kind_dump的调用:
iBoot:00000001FC1A18A8 cast_failed ; CODE XREF: cast_impl+D00↑p iBoot:00000001FC1A18A8 ; sub_1FC1A1594+C8↑p iBoot:00000001FC1A18A8 iBoot:00000001FC1A18A8 var_D0 = -0xD0 iBoot:00000001FC1A18A8 var_C0 = -0xC0 iBoot:00000001FC1A18A8 var_B8 = -0xB8 iBoot:00000001FC1A18A8 var_20 = -0x20 iBoot:00000001FC1A18A8 var_10 = -0x10 iBoot:00000001FC1A18A8 var_s0 = 0 iBoot:00000001FC1A18A8 iBoot:00000001FC1A18A8 PACIBSP iBoot:00000001FC1A18AC SUB SP, SP, #0xE0 iBoot:00000001FC1A18B0 STP X22, X21, [SP,#0xD0+var_20] iBoot:00000001FC1A18B4 STP X20, X19, [SP,#0xD0+var_10] iBoot:00000001FC1A18B8 STP X29, X30, [SP,#0xD0+var_s0] iBoot:00000001FC1A18BC ADD X29, SP, #0xD0 iBoot:00000001FC1A18C0 MOV X19, X3 iBoot:00000001FC1A18C4 MOV X20, X2 iBoot:00000001FC1A18C8 MOV X21, X1 iBoot:00000001FC1A18CC MOV X22, X0 iBoot:00000001FC1A18D0 ADD X0, SP, #0xD0+var_C0 iBoot:00000001FC1A18D4 BL sub_1FC1A9A08 iBoot:00000001FC1A18D8 LDR X8, [X22,#0x30] iBoot:00000001FC1A18DC STR X8, [SP,#0xD0+var_D0] iBoot:00000001FC1A18E0 ADR X1, aCastFailedS ; "cast failed: %s\n" iBoot:00000001FC1A18E4 NOP iBoot:00000001FC1A18E8 ADD X0, SP, #0xD0+var_C0 iBoot:00000001FC1A18EC BL do_trace iBoot:00000001FC1A18F0 LDR X8, [X22,#0x38] iBoot:00000001FC1A18F4 CBZ X8, loc_1FC1A1948 iBoot:00000001FC1A18F8 LDR X8, [X22,#0x40] iBoot:00000001FC1A18FC CBZ X8, loc_1FC1A1948 iBoot:00000001FC1A1900 ADR X1, aTypesNotEqual ; "types not equal: " iBoot:00000001FC1A1904 NOP iBoot:00000001FC1A1908 ADD X0, SP, #0xD0+var_C0 iBoot:00000001FC1A190C BL do_trace iBoot:00000001FC1A1910 LDR X0, [X22,#0x38] iBoot:00000001FC1A1914 ADD X1, SP, #0xD0+var_C0 iBoot:00000001FC1A1918 BL wrap_firebloom_type_kind_dump iBoot:00000001FC1A191C ADR X1, aAnd ; " and " iBoot:00000001FC1A1920 NOP iBoot:00000001FC1A1924 ADD X0, SP, #0xD0+var_C0 iBoot:00000001FC1A1928 BL do_trace iBoot:00000001FC1A192C LDR X0, [X22,#0x40] iBoot:00000001FC1A1930 ADD X1, SP, #0xD0+var_C0 iBoot:00000001FC1A1934 BL wrap_firebloom_type_kind_dump iBoot:00000001FC1A1938 ADR X1, asc_1FC1C481F ; "\n" iBoot:00000001FC1A193C NOP iBoot:00000001FC1A1940 ADD X0, SP, #0xD0+var_C0 iBoot:00000001FC1A1944 BL do_trace iBoot:00000001FC1A1948 iBoot:00000001FC1A1948 loc_1FC1A1948 ; CODE XREF: cast_failed+4C↑j iBoot:00000001FC1A1948 ; cast_failed+54↑j iBoot:00000001FC1A1948 ADR X1, aWhenTestingPtr ; "when testing ptr type " iBoot:00000001FC1A194C NOP iBoot:00000001FC1A1950 ADD X0, SP, #0xD0+var_C0 iBoot:00000001FC1A1954 BL do_trace iBoot:00000001FC1A1958 ADD X1, SP, #0xD0+var_C0 iBoot:00000001FC1A195C MOV X0, X21 iBoot:00000001FC1A1960 BL wrap_firebloom_type_kind_dump iBoot:00000001FC1A1964 ADR X1, aAndCastType ; " and cast type " iBoot:00000001FC1A1968 NOP iBoot:00000001FC1A196C ADD X0, SP, #0xD0+var_C0 iBoot:00000001FC1A1970 BL do_trace iBoot:00000001FC1A1974 ADD X1, SP, #0xD0+var_C0 iBoot:00000001FC1A1978 MOV X0, X20 iBoot:00000001FC1A197C BL wrap_firebloom_type_kind_dump iBoot:00000001FC1A1980 STR X19, [SP,#0xD0+var_D0] iBoot:00000001FC1A1984 ADR X1, aWithSizeZu ; " with size %zu\n" iBoot:00000001FC1A1988 NOP iBoot:00000001FC1A198C ADD X0, SP, #0xD0+var_C0 iBoot:00000001FC1A1990 BL do_trace iBoot:00000001FC1A1994 LDR X0, [SP,#0xD0+var_B8] iBoot:00000001FC1A1998 BL call_firebloom_panic iBoot:00000001FC1A1998 ; End of function cast_failed
这个函数是从cast_impl中调用的,其中的字符串对于我们了解上下文非常有用,比如(这里只显示了一部分):
"Cannot cast dynamic void type to anything" "types not equal" "Pointer is not in bounds" "Cannot cast primitive type to non-primitive type" "Target type has larger size than the bounds of the pointer" "Pointer is not in phase" "Bad subtype result kind"
所有这些字符串,在cast_impl中都用到了。
小结
我希望这两篇文章能帮助您更好地了解iBoot Firebloom的工作原理,以及苹果公司是如何实现Apple Platform Security中Memory safe iBoot implementation部分所描述的各种出色的安全特性的。
我认为苹果公司在这方面做得很棒,用Firebloom实现了一些了不起的事情。当然,强制执行这些安全属性并非易事,但是苹果公司的确做到了。另外,正如我在前面的文章中提到的,Firebloom的这种实现的代价是极其昂贵的。但对于iBoot来说,其效果非常好。而且,我不得不承认这的确很酷。
本文翻译自:https://saaramar.github.io/iBoot_firebloom_type_desc/如若转载,请注明原文地址