有事鸽子了几天,估计已经有翻译了。转了一篇就烂尾有点怪怪的,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↑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, #0x1FC2F2248@PAGE
iBoot:00000001FC1AA83C LDR X8, [X8,#0x1FC2F2248@PAGEOFF]
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 位的值组成的四元组。通常哦我们会看到两条 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, #0xFFFFFFFFFFFFFFF8
iBoot:00000001FC1A0D00 LDRB W12, [X11]
iBoot:00000001FC1A0D04 TST W12, #7
iBoot:00000001FC1A0D08 ; 最低有效位的 3 bit 非 0,表示非基本类型
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 位屏蔽掉(这 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 ; 检测 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 ; 检查 dst 是一个基本类型
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 ; 根据类型信息检查长度,检测不完整或者未对齐的内存写
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
。
当类型不满足条件的时候,我们还有更多的信息可以使用!事实证明,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↑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 ; 初始化如下寄存器:
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 ; 检查目标是不是基本类型
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 ; 检查源是不是基本类型
iBoot:00000001FC15A81C BL is_primitive_type
iBoot:00000001FC15A820 TBNZ W0, #0, loc_1FC15A854
iBoot:00000001FC15A824 ; 至少有一项参数不是基本类型,检查是否满足类型相同
iBoot:00000001FC15A824
iBoot:00000001FC15A824 loc_1FC15A824 ; CODE XREF: wrap_memcpy_safe_allocation+34↑j
iBoot:00000001FC15A824 MOV X8, SP
iBoot:00000001FC15A828 ; dst 的类型描述符指针
iBoot:00000001FC15A828 MOV X0, X23
iBoot:00000001FC15A82C ; src 的类型描述符指针
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 ; 对长度参数做乘除法操作,检测不完整或者未对齐的写入
iBoot:00000001FC15A848 UDIV X8, X21, X0
iBoot:00000001FC15A84C MSUB X8, X8, X0, X21
iBoot:00000001FC15A850 CBNZ X8, detect_memcpy_bad_n
iBoot:00000001FC15A854 ; 有两种可能的情况:
iBoot:00000001FC15A854 ; A) 要么都是基本类型
iBoot:00000001FC15A854 ; B) 类型相匹配,目标长度也满足要求,可以直接复制原始内存
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 ; 最低 3 位有非 0 值,返回 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 ; 检查指针元素的数量是否为 0
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
在如下三个条件全部满足的时候才会执行内存复制:
-
两者都是基本类型,或者类型相同
-
长度参数不会造成 dst 类型的越界访问(通过
safe_allocation
结构检查) -
长度参数对齐类型的大小,不会产生不完整的初始化。
请注意,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, 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 ; 元素的一个实例
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 ; 读取值的低 32 位,也就是表示类型的长度
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
这里的 AND 0xFFFFFFFFFFFFFFF8
看起来是不是很眼熟?如果是的话,很有可能是因为你在文章开头见过,在我解释 cast_impl 如何检测基本类型的时候。类型描述符的第一个成员是一个指针,其最低 3 位似乎编码了某种信息,所以每次我们解引用的时候都需要将其低位屏蔽。
而确实如此,这个函数返回 0x20 偏移处的 32 位整数来表示类型的长度。
Firebloom 里的基本类型
等等,偏移 0x20 处的 64 位值有一些非常有意思的东西。我们知道:
-
低 32 位表示类型的长度
-
中间有 3 位用途未知
-
最高的 29 位表示指针元素的个数
我们在之前看到过这个值。重新看一遍 cast_impl
和 is_primitive_type 的实现代码。这几处代码检查了指针元素的数量是否为 0——只有在等于 0 的时候才表示基本类型。很有道理!
现在我们把注意力转到 is_primitive_type。这个函数的逻辑如下:
-
屏蔽 type 指针的 3 位最低有效位,然后解引用这个指针
-
如果 3 个最低有效位的任意一位不为 0,返回 false
-
读取 0x20 偏移处的 64 位值
-
提取最高的 29 位,也就是“指针元素的数量”
-
如果这个值是 0 则返回 true,反之返回 false
换句话说:
-
只要 3 位最低有效位设置了任意一位,类型就不是基本类型——返回 false
-
如果 3 位都是 0,代码检查类型里是否不包含指针元素。一个含有指针的结构不可能是基本类型
所以函数 is_primitive_type
只有在 3 位最低有效位都是 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
期望初始化的值)是否为 0,也就是字符 x00。如果参数非 0,则执行 panic_memset_bad_type。所以 iBoot 拒绝使用
memset
在基本类型之外使用非 0 来初始化结构。 -
函数 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_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, #qword_1FC2339C8@PAGE
iBoot:00000001FC10E4FC LDR X8, [X8,#qword_1FC2339C8@PAGEOFF]
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 是一些内存分配的基址
iBoot:00000001FC10E51C ; 设置 X8 为 raw_ptr+0x50,也就是 upper_bound_ptr
iBoot: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 ; 注意:类型的大小在 +0x50
iBoot:00000001FC10E524 ADRL X9, off_1FC2D09E8
iBoot: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 处,我期望看到:
-
类型的长度(低 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,不包含指针,以及标记为基本类型。来看如下的二进制:
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
-
不包含指针元素(
0x100000001 >> 0x23
) -
以及,理所当然的,这是一个基本类型(最低 3 位都是 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: %sn"
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 %zun"
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 如何实现 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
原文始发于微信公众号(非尝咸鱼贩):【翻译】iBoot Firebloom 之类型描述符
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论