【翻译】iBoot Firebloom 之类型描述符

Saar Amar 非尝咸鱼贩 2022-05-13 17:26

有事鸽子了几天,估计已经有翻译了。转了一篇就烂尾有点怪怪的,better late than never。译者水平限制,如有错漏,诚请斧正。


英文原文地址

saaramar[.]github.io/iBoot_firebloom_type_desc/


本文是 【翻译】iBoot 安全机制 firebloom 简介 的第二篇,转载翻译自 MSRC 的 Saar Amar。




简介


这是 Firebloom 系列文章的第二篇。在第一篇文章中介绍了 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 指针有关的有趣的逻辑(类型转换,使用安全 API 分配的内存之间复制等),逆向起来很有意思。我觉得这样自下而上逐步分析是最好的。


指针和内存内容的复制


从示例入手。我们从 panic_memcpy_bad_type 开始逆向——如下是 do_firebloom_panic 函数的 11 处交叉引用之一:


iBoot:00000001FC1AA818 panic_memcpy_bad_type                   ; CODE XREF: call_panic_memcpy_bad_type+3C↑piBoot:00000001FC1AA818iBoot:00000001FC1AA818 var_20          = -0x20iBoot:00000001FC1AA818 var_10          = -0x10iBoot:00000001FC1AA818 var_s0          =  0iBoot:00000001FC1AA818iBoot:00000001FC1AA818                 PACIBSPiBoot: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, #0x20iBoot:00000001FC1AA82C                 MOV             X19, X2iBoot:00000001FC1AA830                 MOV             X20, X1iBoot:00000001FC1AA834                 MOV             X21, X0iBoot:00000001FC1AA838                 ADRP            X8, #0x1FC2F2248@PAGEiBoot:00000001FC1AA83C                 LDR             X8, [X8,#0x1FC2F2248@PAGEOFF]iBoot:00000001FC1AA840                 CBZ             X8, loc_1FC1AA848iBoot:00000001FC1AA844                 BLRAAZ          X8iBoot:00000001FC1AA848iBoot:00000001FC1AA848 loc_1FC1AA848                           ; CODE XREF: panic_memcpy_bad_type+28↑jiBoot:00000001FC1AA848                 ADR             X0, aMemcpyBadType ; "memcpy_bad_type"iBoot:00000001FC1AA84C                 NOPiBoot:00000001FC1AA850                 MOV             X1, X21iBoot:00000001FC1AA854                 MOV             X2, X20iBoot:00000001FC1AA858                 MOV             X3, X19iBoot:00000001FC1AA85C                 BL              do_firebloom_paniciBoot:00000001FC1AA85C ; End of function panic_memcpy_bad_type


我们先从简单的入手——指针的复制。


如果你还记得,在之前的文章中,我特别提到现在复制一个指针(也就是指针的赋值)现在需要移动 4 个 64 位的值组成的四元组。通常哦我们会看到两条 LDP 和 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


像这样两条 LDP 和 STP 指令的模式现在在 iBoot 代码里很常见(很容易理解,指针的赋值使用得很频繁),所以你可以在很多地方看到类似的内联代码。虽然这对复制指针本身很有效,在很多情况下我们还需要将内存内容一并复制——例如调用 memcpy。到这里就开始有趣起来。我们先问自己,是不是可以直接在两个“安全分配的内存块”上直接用 memcpy


理论上的代码如下:


memcpy(dst->raw_ptr, src->raw_ptr, length);


然而,需要记住每一次 safe_allocation 都会包含一个类型信息。这个 type  指针指向某种特定的结构,可能会给我们关于正在处理的类型的更多信息。这些信息会用来做更多的检查和校验。例如,我们会希望看到一些逻辑来检查 dst src 是否都是基本类型(primitive types,例如不包括对其他类型的引用,也不包含嵌套的类型,如 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 是类型指针iBoot:00000001FC1A0CF8                 LDR             X11, [X21]iBoot:00000001FC1A0CFC                 AND             X11, X11, #0xFFFFFFFFFFFFFFF8iBoot:00000001FC1A0D00                 LDRB            W12, [X11]iBoot:00000001FC1A0D04                 TST             W12, #7iBoot:00000001FC1A0D08 ; 最低有效位的 3 bit 非 0,表示非基本类型iBoot:00000001FC1A0D08                 B.NE            cannot_cast_primitive_to_non_primitive_typeiBoot: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_typeiBoot:00000001FC1A0E70                                         ; CODE XREF: cast_impl+478↑jiBoot:00000001FC1A0E70                                         ; cast_impl+484↑jiBoot:00000001FC1A0E70                 ADR             X11, aCannotCastPrim ; "Cannot cast primitive type to non-primi"...


好的,现在我们知道 Firebloom 如何标记和检查基本类型。这份代码来自一个复杂的,用来处理类型转换的功能,其中 x21 寄存器就是需要转换的目标的 safe_allocation 结构中的 type 指针。所以这份代码需要检查目标也满足基本类型要求(否则就 panic)。


为了实现检查,代码解引用 type 指针,得到一个新的指针,指向的内容我们姑且称之为 type_descriptor。我们将最低 3 位屏蔽掉(这 3 位数据中很可能包含一个编码格式,所以所有用到这个指针的地方都会先用掩码处理),然后再解引用。


现在,如果如下两个属性都满足,这个类型就被视作“基本类型”:


  1. 第一个 qword 成员指针的最低 3 位都是 0

  2. 偏移量 0x20 上的最高的 29 位是 0


很好,我们刚刚了解了基本类型是如何表示的。在这篇文章中,我们将理解这些值具体是什么意思,请耐心阅读。


有了这些知识,我相信我们可以开始分析 Firebloom 如何封装 iBoot 里的 memset 和 memcpy 了。我们从 memset 开始:


iBoot:00000001FC15A99C wrap_memset_safe_allocation             ; CODE XREF: sub_1FC04E5D0+124↑piBoot:00000001FC15A99C                                         ; sub_1FC04ED68+8↑j ...iBoot:00000001FC15A99CiBoot:00000001FC15A99C var_30          = -0x30iBoot:00000001FC15A99C var_20          = -0x20iBoot:00000001FC15A99C var_10          = -0x10iBoot:00000001FC15A99C var_s0          =  0iBoot:00000001FC15A99CiBoot:00000001FC15A99C                 PACIBSPiBoot:00000001FC15A9A0                 SUB             SP, SP, #0x60iBoot: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, #0x50iBoot: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, X2iBoot:00000001FC15A9BC                 MOV             X22, X1iBoot:00000001FC15A9C0                 MOV             X20, X0iBoot:00000001FC15A9C4                 MOV             X19, X8iBoot:00000001FC15A9C8 ; 检测 upper_bound - raw_ptr >= x2 (length)iBoot:00000001FC15A9C8                 BL              check_ptr_boundsiBoot:00000001FC15A9CC                 LDR             X23, [X20,#safe_allocation.type]iBoot:00000001FC15A9D0                 MOV             X0, X23iBoot:00000001FC15A9D4 ; 检查 dst 是一个基本类型iBoot:00000001FC15A9D4                 BL              is_primitive_typeiBoot:00000001FC15A9D8                 TBNZ            W0, #0, call_memsetiBoot:00000001FC15A9DC                 CBNZ            W22, detected_memset_bad_typeiBoot:00000001FC15A9E0                 MOV             X0, X23iBoot:00000001FC15A9E4                 BL              get_type_lengthiBoot:00000001FC15A9E8 ; 根据类型信息检查长度,检测不完整或者未对齐的内存写iBoot:00000001FC15A9E8                 UDIV            X8, X21, X0iBoot:00000001FC15A9EC                 MSUB            X8, X8, X0, X21iBoot:00000001FC15A9F0                 CBNZ            X8, detected_memset_bad_niBoot:00000001FC15A9F4iBoot:00000001FC15A9F4 call_memset                             ; CODE XREF: wrap_memset_safe_allocation+3C↑jiBoot:00000001FC15A9F4                 LDR             X0, [X20,#safe_allocation]iBoot:00000001FC15A9F8                 MOV             X1, X22iBoot:00000001FC15A9FC                 MOV             X2, X21iBoot:00000001FC15AA00                 BL              _memsetiBoot:00000001FC15AA04                 BL              move_safe_allocation_x20_to_x19iBoot: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                 RETABiBoot:00000001FC15AA20 ; ---------------------------------------------------------------------------iBoot:00000001FC15AA20iBoot:00000001FC15AA20 detected_memset_bad_type                ; CODE XREF: wrap_memset_safe_allocation+40↑jiBoot:00000001FC15AA20                 BL              call_panic_memset_bad_typeiBoot:00000001FC15AA24 ; ---------------------------------------------------------------------------iBoot:00000001FC15AA24iBoot:00000001FC15AA24 detected_memset_bad_n                   ; CODE XREF: wrap_memset_safe_allocation+54↑jiBoot:00000001FC15AA24                 BL              call_panic_memset_bad_niBoot:00000001FC15AA24 ; End of function wrap_memset_safe_allocation


很好,所以这个 wrap_memset_safe_allocation 函数会检查 dst 是不是一个基本类型。如果是,就直接调用 memset


当类型不满足条件的时候,我们还有更多的信息可以使用!事实证明,Apple 在 type 结构里存储了更多的信息(其中有一个指向新结构体的指针,可以做很多事情)。例如,对于具有可变长度的非基本类型,Apple 将长度信息编码到类型结构的第一个指针指向的内存中。如果 memset 的长度参数没有对齐类型的长度,iBoot 就会执行 panic_memset_bad_n


需要注意在这个函数的起始部分有常规的边界检查(使用 safe_allocation 结构的边界指针)来检查越界访问,检测到即 panic。而函数 panic_memset_bad_n 则更进一步加固了不完整初始化/内存复制的场景,检测到就 panic。很酷!


可以预见到在 memcpy 里会有类似的行为,也正是如此:


iBoot:00000001FC15A7E0 wrap_memcpy_safe_allocation             ; CODE XREF: sub_1FC052C08+21C↑piBoot:00000001FC15A7E0                                         ; sub_1FC054C94+538↑p ...iBoot:00000001FC15A7E0iBoot:00000001FC15A7E0 var_70          = -0x70iBoot:00000001FC15A7E0 var_30          = -0x30iBoot:00000001FC15A7E0 var_20          = -0x20iBoot:00000001FC15A7E0 var_10          = -0x10iBoot:00000001FC15A7E0 var_s0          =  0iBoot:00000001FC15A7E0iBoot:00000001FC15A7E0                 PACIBSPiBoot:00000001FC15A7E4                 SUB             SP, SP, #0x80iBoot: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, #0x70iBoot:00000001FC15A7FC ; 初始化如下寄存器:iBoot:00000001FC15A7FC ; MOV             X21, X2 (length)iBoot:00000001FC15A7FC ; MOV             X22, X1 (src)iBoot:00000001FC15A7FC ; MOV             X20, X0 (dst)iBoot:00000001FC15A7FC ; MOV             X19, X8iBoot:00000001FC15A7FC                 BL              call_check_ptr_bounds_iBoot:00000001FC15A800                 BL              do_check_ptr_bounds_x22iBoot:00000001FC15A804                 LDR             X23, [X20,#safe_allocation.type]iBoot:00000001FC15A808                 MOV             X0, X23iBoot:00000001FC15A80C ; 检查目标是不是基本类型iBoot:00000001FC15A80C                 BL              is_primitive_typeiBoot:00000001FC15A810                 LDR             X24, [X22,#safe_allocation.type]iBoot:00000001FC15A814                 CBZ             W0, loc_1FC15A824iBoot:00000001FC15A818                 MOV             X0, X24iBoot:00000001FC15A81C ; 检查源是不是基本类型iBoot:00000001FC15A81C                 BL              is_primitive_typeiBoot:00000001FC15A820                 TBNZ            W0, #0, loc_1FC15A854iBoot:00000001FC15A824 ; 至少有一项参数不是基本类型,检查是否满足类型相同iBoot:00000001FC15A824iBoot:00000001FC15A824 loc_1FC15A824                           ; CODE XREF: wrap_memcpy_safe_allocation+34↑jiBoot:00000001FC15A824                 MOV             X8, SPiBoot:00000001FC15A828 ; dst 的类型描述符指针iBoot:00000001FC15A828                 MOV             X0, X23iBoot:00000001FC15A82C ; src 的类型描述符指针iBoot:00000001FC15A82C                 MOV             X1, X24iBoot:00000001FC15A830                 BL              compare_typesiBoot:00000001FC15A834                 LDR             W8, [SP,#0x70+var_70]iBoot:00000001FC15A838                 CMP             W8, #1iBoot:00000001FC15A83C                 B.NE            detect_memcpy_bad_typeiBoot:00000001FC15A840                 LDR             X0, [X20,#safe_allocation.type]iBoot:00000001FC15A844                 BL              get_type_lengthiBoot:00000001FC15A848 ; 对长度参数做乘除法操作,检测不完整或者未对齐的写入iBoot:00000001FC15A848                 UDIV            X8, X21, X0iBoot:00000001FC15A84C                 MSUB            X8, X8, X0, X21iBoot:00000001FC15A850                 CBNZ            X8, detect_memcpy_bad_niBoot:00000001FC15A854 ; 有两种可能的情况:iBoot:00000001FC15A854 ; A) 要么都是基本类型iBoot:00000001FC15A854 ; B) 类型相匹配,目标长度也满足要求,可以直接复制原始内存iBoot:00000001FC15A854iBoot:00000001FC15A854 loc_1FC15A854                           ; CODE XREF: wrap_memcpy_safe_allocation+40↑jiBoot:00000001FC15A854                 BL              memcpy_safe_allocations_x22_to_x20iBoot:00000001FC15A858                 BL              move_safe_allocation_x20_to_x19iBoot: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, #0x80iBoot:00000001FC15A870                 RETABiBoot:00000001FC15A874 ; ---------------------------------------------------------------------------iBoot:00000001FC15A874iBoot:00000001FC15A874 detect_memcpy_bad_type                  ; CODE XREF: wrap_memcpy_safe_allocation+5C↑jiBoot:00000001FC15A874                 BL              call_panic_memcpy_bad_typeiBoot:00000001FC15A878 ; ---------------------------------------------------------------------------iBoot:00000001FC15A878iBoot:00000001FC15A878 detect_memcpy_bad_n                     ; CODE XREF: wrap_memcpy_safe_allocation+70↑jiBoot:00000001FC15A878                 BL              call_panic_memcpy_bad_niBoot:00000001FC15A878 ; End of function wrap_memcpy_safe_allocation


确实如此,函数 is_primitive_type 和我们之前看到的一致:


iBoot:00000001FC15A8C0 is_primitive_type                       ; CODE XREF: wrap_memcpy_safe_allocation+2C↑piBoot:00000001FC15A8C0                                         ; wrap_memcpy_safe_allocation+3C↑p ...iBoot:00000001FC15A8C0                 LDR             X8, [X0]iBoot:00000001FC15A8C4                 AND             X8, X8, #0xFFFFFFFFFFFFFFF8iBoot:00000001FC15A8C8                 LDRB            W9, [X8]iBoot:00000001FC15A8CC                 TST             W9, #7iBoot:00000001FC15A8D0                 B.EQ            check_number_of_pointer_elementsiBoot:00000001FC15A8D4 ; 最低 3 位有非 0 值,返回 falseiBoot:00000001FC15A8D4                 MOV             W0, #0iBoot:00000001FC15A8D8                 RETiBoot:00000001FC15A8DC ; ---------------------------------------------------------------------------iBoot:00000001FC15A8DCiBoot:00000001FC15A8DC check_number_of_pointer_elements        ; CODE XREF: do_check_ptr_bounds+54↑jiBoot:00000001FC15A8DC                 LDR             X8, [X8,#0x20]iBoot:00000001FC15A8E0 ; 检查指针元素的数量是否为 0iBoot:00000001FC15A8E0                 LSR             X8, X8, #0x23 ; '#'iBoot:00000001FC15A8E4                 CMP             X8, #0iBoot:00000001FC15A8E8                 CSET            W0, EQiBoot:00000001FC15A8EC                 RETiBoot: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, X21iBoot:00000001FC15AD94                 B               _memcpyiBoot:00000001FC15AD94 ; End of function memcpy_safe_allocations_x22_to_x20


实际上就是:


memcpy(dst->raw_ptr, src->raw_ptr, length);


简直漂亮。函数 wrap_memcpy_safe_allocation  在如下三个条件全部满足的时候才会执行内存复制:


  1. 两者都是基本类型,或者类型相同

  2. 长度参数不会造成 dst 类型的越界访问(通过 safe_allocation 结构检查)

  3. 长度参数对齐类型的大小,不会产生不完整的初始化。


请注意,Apple 在这里存储了类型的具体长度信息,所以 memset 和 memcpy 的限制就不光是防止越界访问(通过 safe_allocation  实现)。除此之外,Apple 会检查对结构体的写入不会产生局部的初始化(从内存对齐的角度考虑)。


有一种情况下这种检查非常重要,考虑在一个数组 struct A* arr上执行 memset 。有了这个 panic_memset_bad_n 检查,iBoot 可以确保数组当中不会有非初始化的元素。


还差一个部分没有解释,就是 get_type_length。我们现在开始分析 :)


编码和格式


我首先要做的事情就是证明 get_type_length 确实是我认为的那样。看起来显然是正确的(在这里有一处非常明显的 panic 调用),而且它对 memcpy 的长度参数做了比对。当然,我认为我们还是可以阅读它的实现来理解其功能。


逆向类型转换的实现的时候,我发现一个有趣的函数 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, W8iBoot:00000001FC1A3FB8                 CBZ             W11, bne_size_mismatchiBoot:00000001FC1A3FBC                 B.CC            left_has_smaller_size_than_rightiBoot:00000001FC1A3FC0                 CMP             W10, W23iBoot:00000001FC1A3FC4                 B.CC            left_has_fewer_pointer_elements_than_right


单从上面的片段来看,我们可以知道:


  1. 类型描述符的 0x20 偏移处保存了类型的长度(32 位)

  2. 这个值的高 29 位保存了另一处有用的信息,指针元素的数量


现在我可以展示 get_type_length 的代码,而它由 wrap_memset_safe_allocation  和  wrap_memcpy_safe_allocation 调用。


iBoot:00000001FC15A964 get_type_length                         ; CODE XREF: wrap_memcpy_safe_allocation+64↑piBoot:00000001FC15A964                                         ; wrap_memset_safe_allocation+48↓p ...iBoot:00000001FC15A964                 LDR             X8, [X0]iBoot:00000001FC15A968                 AND             X8, X8, #0xFFFFFFFFFFFFFFF8iBoot:00000001FC15A96C                 LDR             W9, [X8]iBoot:00000001FC15A970                 AND             W9, W9, #7iBoot:00000001FC15A974                 CMP             W9, #5iBoot:00000001FC15A978                 CCMP            W9, #1, #4, NEiBoot:00000001FC15A97C                 B.NE            loc_1FC15A988iBoot:00000001FC15A980 ; 元素的一个实例iBoot:00000001FC15A980                 MOV             W0, #1iBoot:00000001FC15A984                 RETiBoot:00000001FC15A988 ; ---------------------------------------------------------------------------iBoot:00000001FC15A988iBoot:00000001FC15A988 loc_1FC15A988                           ; CODE XREF: call_panic_memcpy_bad_type+58↑jiBoot:00000001FC15A988                 CBNZ            W9, return_0iBoot:00000001FC15A98C ; 读取值的低 32 位,也就是表示类型的长度iBoot:00000001FC15A98C                 LDR             W0, [X8,#0x20]iBoot:00000001FC15A990                 RETiBoot:00000001FC15A994 ; ---------------------------------------------------------------------------iBoot:00000001FC15A994iBoot:00000001FC15A994 return_0                                ; CODE XREF: call_panic_memcpy_bad_type:loc_1FC15A988jiBoot:00000001FC15A994                 MOV             X0, #0iBoot:00000001FC15A998                 RET


这里的 AND 0xFFFFFFFFFFFFFFF8  看起来是不是很眼熟?如果是的话,很有可能是因为你在文章开头见过,在我解释 cast_impl 如何检测基本类型的时候。类型描述符的第一个成员是一个指针,其最低 3 位似乎编码了某种信息,所以每次我们解引用的时候都需要将其低位屏蔽。


而确实如此,这个函数返回 0x20 偏移处的 32 位整数来表示类型的长度。


Firebloom 里的基本类型


等等,偏移 0x20 处的 64 位值有一些非常有意思的东西。我们知道:


  1. 低 32 位表示类型的长度

  2. 中间有 3 位用途未知

  3. 最高的 29 位表示指针元素的个数


我们在之前看到过这个值。重新看一遍 cast_impl 和 is_primitive_type 的实现代码。这几处代码检查了指针元素的数量是否为 0——只有在等于 0 的时候才表示基本类型。很有道理!


现在我们把注意力转到 is_primitive_type。这个函数的逻辑如下:


  1. 屏蔽 type 指针的 3 位最低有效位,然后解引用这个指针

  2. 如果 3 个最低有效位的任意一位不为 0,返回 false

  3. 读取 0x20 偏移处的 64 位值

  4. 提取最高的 29 位,也就是“指针元素的数量”

  5. 如果这个值是 0 则返回 true,反之返回 false


换句话说:


  1. 只要 3 位最低有效位设置了任意一位,类型就不是基本类型——返回 false

  2. 如果 3 位都是 0,代码检查类型里是否不包含指针元素。一个含有指针的结构不可能是基本类型


所以函数 is_primitive_type 只有在 3 位最低有效位都是 0 和不包含指针元素(译者注:作者行文的信息重复率有点高)的情况下才会返回 true。这正如我们设想的一样,因为你不应该在非基本类型的元素之间按照原始字节直接复制,除非他们的结构(或多或少)相同。


为了更好的理解代码,我们来看看 is_primitive_type 的交叉引用。这个函数只被 wrap_memset_safe_allocation 和 wrap_memcpy_safe_allocation 调用,来判断是否可以简单直接地使用 memset  和 memcpy 而无需更多检查。


我们来验证一下:


  1. 函数 wrap_memset_safe_allocation 调用了 is_primitive_type,并检查返回值(0 或 1)。如果返回 1,直接走 memset。反之则检查 c 参数(memset 期望初始化的值)是否为 0,也就是字符 \x00。如果参数非 0,则执行 panic_memset_bad_type

    所以 iBoot 拒绝使用 memset 在基本类型之外使用非 0 来初始化结构。

  2. 函数 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 ; 需要初始化的 `safe_allocation` 结构iBoot:00000001FC10E4E0                 ADD             X8, SP, #0xD0+v_safe_allocationiBoot:00000001FC10E4E4                 MOV             W0, #1iBoot:00000001FC10E4E8                 BL              do_safe_allocation_callociBoot: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_1FC10E1C4iBoot:00000001FC10E4F8                 ADRP            X8, #qword_1FC2339C8@PAGEiBoot:00000001FC10E4FC                 LDR             X8, [X8,#qword_1FC2339C8@PAGEOFF]iBoot:00000001FC10E500                 CBZ             X8, detect_ptr_nulliBoot:00000001FC10E504                 CMP             X23, X19iBoot:00000001FC10E508                 B.HI            detected_ptr_underiBoot:00000001FC10E50C                 CMP             X28, X19iBoot:00000001FC10E510                 B.LS            detected_ptr_overiBoot:00000001FC10E514                 MOV             X20, X0iBoot:00000001FC10E518                 MOV             X27, X1iBoot:00000001FC10E51C ; 此处的 X19 是一些内存分配的基址iBoot:00000001FC10E51C ; 设置 X8 为 raw_ptr+0x50,也就是 upper_bound_ptriBoot:00000001FC10E51C                 ADD             X8, X19, #0x50 ; 'P'iBoot:00000001FC10E520 ; 再次初始化 safe_allocation:iBoot:00000001FC10E520 ; 设置 x19 为 raw_ptr(同时也是 lower_bound_ptr)iBoot:00000001FC10E520                 STP             X19, X19, [SP,#0xD0+v_safe_allocation]iBoot:00000001FC10E524 ; 获取对应的l欸行指针,赋值到iBoot:00000001FC10E524 ; safe_allocation->type(偏移 +0x18,也就是 upper_bound_ptr 之后的下一个 qword).iBoot:00000001FC10E524 ;iBoot:00000001FC10E524 ; 注意:类型的大小在 +0x50iBoot:00000001FC10E524                 ADRL            X9, off_1FC2D09E8iBoot:00000001FC10E52C                 STP             X8, X9, [SP,#0xD0+v_safe_allocation.upper_bound_ptr]


很有意思。我们找到了对 do_safe_allocation_calloc 的调用,然后代码在 off_1FC2D09E8 处设置了 type  指针。来看看这有什么:


iBoot:00000001FC2D09E8 off_1FC2D09E8   DCQ off_1FC2D0760+2     ; DATA XREF: sub_1FC1071C0+33C↑o
iBoot:00000001FC2D09E8 ; sub_1FC107D90+188↑o ...


很棒!这个指针指向的值却是是某个地址 +2 的结果(还记得掩码 0xFFFFFFFFFFFFFFF8 吗?:P)我们来解引用这个指针,在偏移 +0x20 处,我期望看到:


  1. 类型的长度(低 32 位)

  2. 类型中有多少个指针(高 29 位)


确实如此:


iBoot:00000001FC2D0760 off_1FC2D0760   DCQ off_1FC2D0760       ; DATA XREF: iBoot:off_1FC2D0760oiBoot:00000001FC2D0760                                         ; iBoot:00000001FC2D0A98o ...iBoot:00000001FC2D0768                 ALIGN 0x20iBoot:00000001FC2D0780                 DCQ 0x1300000050, 0x100000000


妙极了!偏移 +0x20 处的值是 0x1300000050,正如我们推测的那样。


  • 结构的大小 = 0x50(和分析完全一致!)

  • 有 2 个指针成员(0x1300000050 >> 0x23


不错,值都对上了!


示例 2


我们不能忽视掉默认的类型,对吧?正如你在之前的文章看到的那样,所有的 do_safe_allocation* 函数都会在偏移 +0x18 处设置一个默认的类型指针,而不同的调用者在需要的时候可以传入其他的类型(就像前两个例子一样)。


如下是对 default_type_ptr 函数的交叉引用:


图片


我期望看到这里会提供一些“默认值”,也就是类型的长度为 1,不包含指针,以及标记为基本类型。来看如下的二进制:


iBoot:00000001FC2D6EF8 default_type_ptr DCQ default_type_ptr   ; DATA XREF: __firebloom_panic+2CoiBoot:00000001FC2D6EF8                                         ; sub_1FC15AD98+1FCo ...iBoot:00000001FC2D6F00                 DCQ 0, 0, 0iBoot:00000001FC2D6F18                 DCQ 0x100000001


完美!这个 default_type_ptr 指针指向自己(很好),在偏移 +0x20 处值为 0x0000000100000001,意思是:


  • 类型的长度 = 0x1

  • 不包含指针元素(0x100000001 >> 0x23

  • 以及,理所当然的,这是一个基本类型(最低 3 位都是 0,指针元素的数量为 0)


太棒了!


类型转换


类型转换的实现很不错。要解释它的工作原理需要长篇大论,所以我这次就节省些篇幅。然而我希望能激励更多人去动手分析这个二进制,来看看这个非常厉害的 cast_failed 函数,有很多有用的字符串并调用了 wrap_firebloom_type_kind_dump


iBoot:00000001FC1A18A8 cast_failed                             ; CODE XREF: cast_impl+D00↑piBoot:00000001FC1A18A8                                         ; sub_1FC1A1594+C8↑piBoot:00000001FC1A18A8iBoot:00000001FC1A18A8 var_D0          = -0xD0iBoot:00000001FC1A18A8 var_C0          = -0xC0iBoot:00000001FC1A18A8 var_B8          = -0xB8iBoot:00000001FC1A18A8 var_20          = -0x20iBoot:00000001FC1A18A8 var_10          = -0x10iBoot:00000001FC1A18A8 var_s0          =  0iBoot:00000001FC1A18A8iBoot:00000001FC1A18A8                 PACIBSPiBoot:00000001FC1A18AC                 SUB             SP, SP, #0xE0iBoot: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, #0xD0iBoot:00000001FC1A18C0                 MOV             X19, X3iBoot:00000001FC1A18C4                 MOV             X20, X2iBoot:00000001FC1A18C8                 MOV             X21, X1iBoot:00000001FC1A18CC                 MOV             X22, X0iBoot:00000001FC1A18D0                 ADD             X0, SP, #0xD0+var_C0iBoot:00000001FC1A18D4                 BL              sub_1FC1A9A08iBoot:00000001FC1A18D8                 LDR             X8, [X22,#0x30]iBoot:00000001FC1A18DC                 STR             X8, [SP,#0xD0+var_D0]iBoot:00000001FC1A18E0                 ADR             X1, aCastFailedS ; "cast failed: %s\n"iBoot:00000001FC1A18E4                 NOPiBoot:00000001FC1A18E8                 ADD             X0, SP, #0xD0+var_C0iBoot:00000001FC1A18EC                 BL              do_traceiBoot:00000001FC1A18F0                 LDR             X8, [X22,#0x38]iBoot:00000001FC1A18F4                 CBZ             X8, loc_1FC1A1948iBoot:00000001FC1A18F8                 LDR             X8, [X22,#0x40]iBoot:00000001FC1A18FC                 CBZ             X8, loc_1FC1A1948iBoot:00000001FC1A1900                 ADR             X1, aTypesNotEqual ; "types not equal: "iBoot:00000001FC1A1904                 NOPiBoot:00000001FC1A1908                 ADD             X0, SP, #0xD0+var_C0iBoot:00000001FC1A190C                 BL              do_traceiBoot:00000001FC1A1910                 LDR             X0, [X22,#0x38]iBoot:00000001FC1A1914                 ADD             X1, SP, #0xD0+var_C0iBoot:00000001FC1A1918                 BL              wrap_firebloom_type_kind_dumpiBoot:00000001FC1A191C                 ADR             X1, aAnd ; " and "iBoot:00000001FC1A1920                 NOPiBoot:00000001FC1A1924                 ADD             X0, SP, #0xD0+var_C0iBoot:00000001FC1A1928                 BL              do_traceiBoot:00000001FC1A192C                 LDR             X0, [X22,#0x40]iBoot:00000001FC1A1930                 ADD             X1, SP, #0xD0+var_C0iBoot:00000001FC1A1934                 BL              wrap_firebloom_type_kind_dumpiBoot:00000001FC1A1938                 ADR             X1, asc_1FC1C481F ; "\n"iBoot:00000001FC1A193C                 NOPiBoot:00000001FC1A1940                 ADD             X0, SP, #0xD0+var_C0iBoot:00000001FC1A1944                 BL              do_traceiBoot:00000001FC1A1948iBoot:00000001FC1A1948 loc_1FC1A1948                           ; CODE XREF: cast_failed+4C↑jiBoot:00000001FC1A1948                                         ; cast_failed+54↑jiBoot:00000001FC1A1948                 ADR             X1, aWhenTestingPtr ; "when testing ptr type "iBoot:00000001FC1A194C                 NOPiBoot:00000001FC1A1950                 ADD             X0, SP, #0xD0+var_C0iBoot:00000001FC1A1954                 BL              do_traceiBoot:00000001FC1A1958                 ADD             X1, SP, #0xD0+var_C0iBoot:00000001FC1A195C                 MOV             X0, X21iBoot:00000001FC1A1960                 BL              wrap_firebloom_type_kind_dumpiBoot:00000001FC1A1964                 ADR             X1, aAndCastType ; " and cast type "iBoot:00000001FC1A1968                 NOPiBoot:00000001FC1A196C                 ADD             X0, SP, #0xD0+var_C0iBoot:00000001FC1A1970                 BL              do_traceiBoot:00000001FC1A1974                 ADD             X1, SP, #0xD0+var_C0iBoot:00000001FC1A1978                 MOV             X0, X20iBoot:00000001FC1A197C                 BL              wrap_firebloom_type_kind_dumpiBoot:00000001FC1A1980                 STR             X19, [SP,#0xD0+var_D0]iBoot:00000001FC1A1984                 ADR             X1, aWithSizeZu ; " with size %zu\n"iBoot:00000001FC1A1988                 NOPiBoot:00000001FC1A198C                 ADD             X0, SP, #0xD0+var_C0iBoot:00000001FC1A1990                 BL              do_traceiBoot:00000001FC1A1994                 LDR             X0, [SP,#0xD0+var_B8]iBoot:00000001FC1A1998                 BL              call_firebloom_paniciBoot: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 如何实现 Apple Platform Security 当中 Memory safe iBoot implementation[1] 一章提到的这些美妙的安全特性。


我觉得 Apple 在这里做了很有建设性的工作,在 Fireboom 里实现了了不起的效果。强制使用安全特性并不是一件简单的事情,Apple 做到了。确实,我之前的文章也提到 Firebloom 的开销非常大。但再次重申,对于 iBoot 而言很有用(前文也提到了原因)。我必须承认这很棒 :)


希望你喜欢这篇文章。


[1]. Memory safe iBoot implementation
https://support.apple.com/en-il/guide/security/sec30d8d9ec1/web

发表于上海

微信扫一扫
关注该公众号