深入学习Firebloom iBoot之类型描述符篇
2022-5-21 11:50:0 Author: www.4hou.com(查看原文) 阅读量:26 收藏

简介

欢迎来到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.png

我们期望在这里看到这样一些“默认”值,即类型的长度 = 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/如若转载,请注明原文地址


文章来源: https://www.4hou.com/posts/vLKL
如有侵权请联系:admin#unsafe.sh