本文来自 MSRC 的 Saar Amar(Twitter:@AmarSaar),同时也是 pasten CTF 的成员。他发布过一些关于 ARM 最新安全特性的研究,如 MTE 和 CHERI ISA,也对一些 iOS 内核漏洞有做分析。这篇文章是他对 iBoot 安全机制 firebloom 的逆向分析。
英文原文 saaramar.github.io/iBoot_firebloom/
本来想直接插入原文链接,但微信说这是一个非法域名,有点可怕。
(众所周知的 2019 年的 checkm8 漏洞之后)在 2021 年 2 月,Apple 在官网发布了关于 iBoot 内存安全的新内容,作为 Apple Security Platform 的一部分。文档中提到,Apple 修改了用于生成 iBoot 的 bootloader 代码的 C 编译器工具链,以提升安全性;对具体的实现还有一些笼统的描述。以下翻译自官方文档:
内存安全的 iBoot 实现 在 iOS 14 和 iPadOS 14 上,Apple 修改了用于生成 iBoot 的 bootloader 代码的编译器工具链来提升安全性。这些改动用于防止 C 语言程序常见的内存安全和类型安全问题。例如,它可以帮助预防大多数如下列出的漏洞类型:
- 缓冲区溢出:确保所有的指针都引入边界信息,并在访问时验证
- 堆利用:将堆数据和元数据进行隔离,并加强对程序错误(例如双重释放)的检测
- 类型混淆:确保所有指针都包含运行时的类型信息,并在类型转换时校验
- 释放后重用导致的类型混淆:基于静态类型将内存分配在不同的堆
这项技术在搭载了 A13 或更新芯片的 iPhone ,以及搭载 A14 或更新芯片的 iPad 上可用
我认为写一些文字来分析具体实现、格式以及 Apple 所做的令人振奋的工作再好不过了。除此之外,在 iBoot 的二进制文件上可以直接看到一些非常有用的字符串信息——有两位研究人员很早就在推上发过了。
我对这项工作很着迷,因为前文的描述让我觉得这就是一个“轻量级的纯软件实现的 CHERI”(译者注:CHERI 全称 Capability Hardware Enhanced RISC Instructions,是一种通过对指令集架构进行扩展,在非内存安全的程序语言上实现更安全的内存访问的技术)。根据 Apple 的描述,在新版的 iBoot 上,指针除了地址还包含了更多信息——包括边界和类型,从而让编译器可以在代码中引入新的内存安全检查。
我喜欢打破沙锅问到底,所以让我们深入分析,看看能学到什么。
这份分析在 iBoot.d53g.RELEASE.im4p 上完成,对应版本是 iPhone 12 的 iOS 14.4 (18D52)。
开始逆向
首先,让我们看看检测到内存安全问题时会如何处理。当内存访问违例发生时是有必要触发 panic 的,理所应当二进制里就会有一个 __firebloom_panic 字符串。根据这个特征,我们可以找到对应的函数。
iBoot:00000001FC1AA5A0 firebloom_panic
iBoot:00000001FC1AA5A0
iBoot:00000001FC1AA5A0 var_B8= -0xB8
iBoot:00000001FC1AA5A0 var_B0= -0xB0
iBoot:00000001FC1AA5A0 var_18= -0x18
iBoot:00000001FC1AA5A0 var_10= -0x10
iBoot:00000001FC1AA5A0 var_s0= 0
iBoot:00000001FC1AA5A0
iBoot:00000001FC1AA5A0 PACIBSP
iBoot:00000001FC1AA5A4 SUB SP, SP, #0xD0
iBoot:00000001FC1AA5A8 STP X20, X19, [SP,#0xC0+var_10]
iBoot:00000001FC1AA5AC STP X29, X30, [SP,#0xC0+var_s0]
iBoot:00000001FC1AA5B0 ADD X29, SP, #0xC0
iBoot:00000001FC1AA5B4 MOV X19, X0
iBoot:00000001FC1AA5B8 ADD X0, SP, #0xC0+var_B8
iBoot:00000001FC1AA5BC BL sub_1FC1A9A08
iBoot:00000001FC1AA5C0 ADD X8, X29, #0x10
iBoot:00000001FC1AA5C4 STUR X8, [X29,#var_18]
iBoot:00000001FC1AA5C8 ADR X1, aPasPanic ; "pas panic: "
iBoot:00000001FC1AA5CC NOP
iBoot:00000001FC1AA5D0 ADD X0, SP, #0xC0+var_B8
iBoot:00000001FC1AA5D4 BL do_trace
iBoot:00000001FC1AA5D8 LDUR X2, [X29,#var_18]
iBoot:00000001FC1AA5DC ADD X0, SP, #0xC0+var_B8
iBoot:00000001FC1AA5E0 MOV X1, X19
iBoot:00000001FC1AA5E4 BL sub_1FC1A9A48
iBoot:00000001FC1AA5E8 LDR X0, [SP,#0xC0+var_B0]
iBoot:00000001FC1AA5EC BL __firebloom_panic
这个函数有 11 处交叉引用。我将其中一个命名为 do_firebloom_panic,而这个调用者有另外 11 处交叉引用,每一处都负责处理一种不同的访问违例类型:
不错,现在我们有了 firebloom 所要检测的程序错误的(一部分)列表,这些错误都会引发 panic。因为一些新的检查专门针对注入 memset 和 memcpy 这类特定的函数,我们可以期待看到这些函数将被改写为新的封装函数(wrapper),而异常检测就在这些调用里。根据交叉引用不断回溯程序流程,很容易就能找到这些封装函数。
然而我很好奇其他的访问违例是怎么样的——例如,我们将如何/在哪里找到 ptr_under 和 ptr_over(分别对应两种越界访问)?函数 panic_ptr_over
有 179 处调用,其中大部分都是简单的加入 hash 参数的包装。这些封装函数的一部分交叉引用指向真正的错误检测(panic)逻辑。继续跟下去,我们能看到一些不错的示例来展示 iBoot 是如何使用这些函数的。
多说无益,没有什么比代码更清晰,所以我们直接看这段程序:
iBoot:00000001FC05C5AC loop ; CODE XREF: sub_1FC05C548+94↓j
iBoot:00000001FC05C5AC CMP X10, X9
iBoot:00000001FC05C5B0 B.EQ return
iBoot:00000001FC05C5B4 ; 获取指针和下界
iBoot:00000001FC05C5B4 LDP X11, X13, [X0]
iBoot:00000001FC05C5B8 ; 将 ptr 循环递增到 ptr+offset
iBoot:00000001FC05C5B8 ADD X12, X11, X9
iBoot:00000001FC05C5BC CMP X12, X13
iBoot:00000001FC05C5C0 B.CC detected_ptr_under
iBoot:00000001FC05C5C4 ; 获取数组上界
iBoot:00000001FC05C5C4 LDR X13, [X0,#0x10]
iBoot:00000001FC05C5C8 CMP X12, X13
iBoot:00000001FC05C5CC B.CS detected_ptr_over
iBoot:00000001FC05C5D0 ; 真正解引用指针的地方
iBoot:00000001FC05C5D0 LDR W11, [X11,X9]
iBoot:00000001FC05C5D4 STR W11, [X8,#0x1DC]
iBoot:00000001FC05C5D8 ADD X9, X9, #4
iBoot:00000001FC05C5DC B loop
iBoot:00000001FC05C5E0 ; ---------------------------------------------------------------------------
iBoot:00000001FC05C5E0
iBoot:00000001FC05C5E0 return ; CODE XREF: sub_1FC05C548+68↑j
iBoot:00000001FC05C5E0 LDUR X8, [X29,#var_8]
iBoot:00000001FC05C5E4 ADRP X9, #a160d@PAGE ; "160D"
iBoot:00000001FC05C5E8 NOP
iBoot:00000001FC05C5EC LDR X9, [X9,#a160d@PAGEOFF] ; "160D"
iBoot:00000001FC05C5F0 CMP X9, X8
iBoot:00000001FC05C5F4 B.NE do_panic
iBoot:00000001FC05C5F8 LDP X29, X30, [SP,#0x70+var_s0]
iBoot:00000001FC05C5FC ADD SP, SP, #0x80
iBoot:00000001FC05C600 RETAB
iBoot:00000001FC05C604 ; ---------------------------------------------------------------------------
iBoot:00000001FC05C604
iBoot:00000001FC05C604 do_panic ; CODE XREF: sub_1FC05C548+AC↑j
iBoot:00000001FC05C604 BL call_panic
iBoot:00000001FC05C608 ; ---------------------------------------------------------------------------
iBoot:00000001FC05C608
iBoot:00000001FC05C608 detected_ptr_under ; CODE XREF: sub_1FC05C548+78↑j
iBoot:00000001FC05C608 BL call_panic_ptr_under_5383366e236c433
iBoot:00000001FC05C60C ; ---------------------------------------------------------------------------
iBoot:00000001FC05C60C
iBoot:00000001FC05C60C detected_ptr_over ; CODE XREF: sub_1FC05C548+84↑j
iBoot:00000001FC05C60C BL call_panic_ptr_over_5383366e236c433
iBoot:00000001FC05C610 ; ---------------------------------------------------------------------------
代码很有意思。在使用 x9 寄存器提供的偏移量访问指针(对应 0x01FC05C5D0 的指令)之前,代码先检查 ptr+offset 是否越界。原始指针和数组的边界(上界、下界)信息从特定的结构体(稍后解释)中取出。在解释这一步之前,为了给读者感受一下这些函数是如何调用的,我们来看这个 panic 的封装:
iBoot:00000001FC05D384 call_panic_ptr_over_5383366e236c433 ; CODE XREF: sub_1FC05C548:detected_ptr_over↑p
iBoot:00000001FC05D384 ; DATA XREF: call_panic_ptr_over_5383366e236c433+24↓o
iBoot:00000001FC05D384
iBoot:00000001FC05D384 var_8 = -8
iBoot:00000001FC05D384 var_s0 = 0
iBoot:00000001FC05D384
iBoot:00000001FC05D384 PACIBSP
iBoot:00000001FC05D388 SUB SP, SP, #0x20
iBoot:00000001FC05D38C STP X29, X30, [SP,#0x10+var_s0]
iBoot:00000001FC05D390 ADD X29, SP, #0x10
iBoot:00000001FC05D394 ADRL X8, a5383366e236c43 ; "5383366e236c433"
iBoot:00000001FC05D39C STR X8, [SP,#0x10+var_8]
iBoot:00000001FC05D3A0 MOV X8, X30
iBoot:00000001FC05D3A4 XPACI X8
iBoot:00000001FC05D3A8 ADR X16, call_panic_ptr_over_5383366e236c433
iBoot:00000001FC05D3AC NOP
iBoot:00000001FC05D3B0 PACIZA X16
iBoot:00000001FC05D3B4 SUB X2, X8, X16
iBoot:00000001FC05D3B8 ADD X0, SP, #0x10+var_8
iBoot:00000001FC05D3BC MOV W1, #1
iBoot:00000001FC05D3C0 BL panic_ptr_over
iBoot:00000001FC05D3C0 ; End of function call_panic_ptr_over_5383366e236c433
还有这里:
iBoot:00000001FC1AA980 panic_ptr_over ; CODE XREF: sub_1FC04CBD0+3C↑p
iBoot:00000001FC1AA980 ; sub_1FC04EC2C+3C↑p ...
iBoot:00000001FC1AA980
iBoot:00000001FC1AA980 var_20 = -0x20
iBoot:00000001FC1AA980 var_10 = -0x10
iBoot:00000001FC1AA980 var_s0 = 0
iBoot:00000001FC1AA980
iBoot:00000001FC1AA980 PACIBSP
iBoot:00000001FC1AA984 STP X22, X21, [SP,#-0x10+var_20]!
iBoot:00000001FC1AA988 STP X20, X19, [SP,#0x20+var_10]
iBoot:00000001FC1AA98C STP X29, X30, [SP,#0x20+var_s0]
iBoot:00000001FC1AA990 ADD X29, SP, #0x20
iBoot:00000001FC1AA994 MOV X19, X2
iBoot:00000001FC1AA998 MOV X20, X1
iBoot:00000001FC1AA99C MOV X21, X0
iBoot:00000001FC1AA9A0 ADRP X8, #0x1FC2F2270@PAGE
iBoot:00000001FC1AA9A4 LDR X8, [X8,#0x1FC2F2270@PAGEOFF]
iBoot:00000001FC1AA9A8 CBZ X8, do_panic
iBoot:00000001FC1AA9AC BLRAAZ X8
iBoot:00000001FC1AA9B0
iBoot:00000001FC1AA9B0 do_panic ; CODE XREF: panic_ptr_over+28↑j
iBoot:00000001FC1AA9B0 ADR X0, aPtrOver ; "ptr_over"
iBoot:00000001FC1AA9B4 NOP
iBoot:00000001FC1AA9B8 MOV X1, X21
iBoot:00000001FC1AA9BC MOV X2, X20
iBoot:00000001FC1AA9C0 MOV X3, X19
iBoot:00000001FC1AA9C4 BL do_firebloom_panic
iBoot:00000001FC1AA9C4 ; End of function panic_ptr_over
好,很简单。
让我们看看同样的模式有没有出现在别的地方。例如这一处:
在这段示例中,你可以看到一个循环遍历一个数组的所有元素(每一个的大小是 0x20),将每个元素作为参数调用一些函数。而且,不出我所料,所谓的“指针结构体”在这里被同样的方式使用了。
格式化和帮助函数
结合前文的分析,我们推测内存分配所使用的结构如下所示:
00000000 safe_allocation struc ; (sizeof=0x20, mappedto_1)
00000000 raw_ptr DCQ ? ; offset
00000008 lower_bound_ptr DCQ ? ; offset
00000010 upper_bound_ptr DCQ ? ; offset
00000018 field_18 DCQ ?
00000020 safe_allocation ends
不错。我们可以将其视作“胖指针”(原文 Fat/Bounded Pointer,指带有访问边界信息的指针)。iBoot 并没有直接使用 64 位原始指针作为内存地址,而是使用一个结构体代表指针,并保存额外的元数据。
显而易见地,使用 32 字节(4 个 64bit 值)来表示一个指针,会对许多操作产生影响。考虑一个简单的指针赋值操作,不能再直接用 p2 = p; ,而是需要读写一个四元组(通常编译为两个 LDP 和两个 LDP 指令)。
我很想找到新的内存分配函数,也就是那些分配一个块并初始化元数据结构体的函数。我很轻松地通过继续向上查找调用栈发现了目标。
如果你查看 do_firebloom_panic 函数的交叉引用,可以看到一个很有意思的封装函数 call_panic_allocation_size_error。这个函数被调用得不多(不到五处),调用者是一组非常相似的函数。最简单的一处调用如下:
iBoot:00000001FC1A1CF0 do_safe_allocation ; CODE XREF: sub_1FC0523D8+8↑j
iBoot:00000001FC1A1CF0 ; sub_1FC05259C+70↑p ...
iBoot:00000001FC1A1CF0
iBoot:00000001FC1A1CF0 var_20 = -0x20
iBoot:00000001FC1A1CF0 var_18 = -0x18
iBoot:00000001FC1A1CF0 var_10 = -0x10
iBoot:00000001FC1A1CF0 var_s0 = 0
iBoot:00000001FC1A1CF0
iBoot:00000001FC1A1CF0 PACIBSP
iBoot:00000001FC1A1CF4 SUB SP, SP, #0x30
iBoot:00000001FC1A1CF8 STP X20, X19, [SP,#0x20+var_10]
iBoot:00000001FC1A1CFC STP X29, X30, [SP,#0x20+var_s0]
iBoot:00000001FC1A1D00 ADD X29, SP, #0x20
iBoot:00000001FC1A1D04 ; X8:需要初始化的结构体指针
iBoot:00000001FC1A1D04 MOV X19, X8
iBoot:00000001FC1A1D08 ; X0 和 X1 可能对应大小和数据指针
iBoot:00000001FC1A1D08 UMULH X8, X1, X0
iBoot:00000001FC1A1D0C CBNZ X8, allocation_size_error_detected
iBoot:00000001FC1A1D10 ; X20:分配的大小
iBoot:00000001FC1A1D10 MUL X20, X1, X0
iBoot:00000001FC1A1D14 ADRP X8, #0x1FC2F50B8@PAGE
iBoot:00000001FC1A1D18 ADD X8, X8, #0x1FC2F50B8@PAGEOFF
iBoot:00000001FC1A1D1C STR X8, [SP,#0x20+var_18]
iBoot:00000001FC1A1D20 STR WZR, [SP,#0x20+var_20]
iBoot:00000001FC1A1D24 ADRP X2, #0x1FC2F50B0@PAGE
iBoot:00000001FC1A1D28 ADD X2, X2, #0x1FC2F50B0@PAGEOFF
iBoot:00000001FC1A1D2C ADRL X3, off_1FC2D6EC0
iBoot:00000001FC1A1D34 ADRL X1, qword_1FC2D6E80
iBoot:00000001FC1A1D3C ; 对分配 API 函数指针加 PAC 签名
iBoot:00000001FC1A1D3C ADR X16, do_allocation
iBoot:00000001FC1A1D40 NOP
iBoot:00000001FC1A1D44 PACIZA X16
iBoot:00000001FC1A1D48 MOV X6, X16
iBoot:00000001FC1A1D4C MOV X0, #0
iBoot:00000001FC1A1D50 MOV X4, X20
iBoot:00000001FC1A1D54 MOV W5, #1
iBoot:00000001FC1A1D58 MOV X7, X1
iBoot:00000001FC1A1D5C ; 调用 API 分配一块内存
iBoot:00000001FC1A1D5C ; X19 就是返回值(X0)(X0)
iBoot:00000001FC1A1D5C ; 这个函数内部在返回时会 "MOV X0, X19"
iBoot:00000001FC1A1D5C BL wrap_do_allocation
iBoot:00000001FC1A1D60 ADRL X8, off_1FC2D6EF8
iBoot:00000001FC1A1D68 STR X8, [X19,#0x18]
iBoot:00000001FC1A1D6C STR X0, [X19]
iBoot:00000001FC1A1D70 ; 检查是否分配成功
iBoot:00000001FC1A1D70 CBZ X0, allocation_failed
iBoot:00000001FC1A1D74 ; X0:分配内存块的基址
iBoot:00000001FC1A1D74 ; X8:X0 + allocation_size,也就是上界
iBoot:00000001FC1A1D74 ADD X8, X0, X20
iBoot:00000001FC1A1D78 ; 保存上界和下界到结构体,分别对应
iBoot:00000001FC1A1D78 ; +0x8, +0x10 的成员
iBoot:00000001FC1A1D78 STP X0, X8, [X19,#8]
iBoot:00000001FC1A1D7C LDP X29, X30, [SP,#0x20+var_s0]
iBoot:00000001FC1A1D80 LDP X20, X19, [SP,#0x20+var_10]
iBoot:00000001FC1A1D84 ADD SP, SP, #0x30 ; '0'
iBoot:00000001FC1A1D88 RETAB
iBoot:00000001FC1A1D8C ; ---------------------------------------------------------------------------
iBoot:00000001FC1A1D8C
iBoot:00000001FC1A1D8C allocation_failed ; CODE XREF: do_safe_allocation+80↑j
iBoot:00000001FC1A1D8C ADD X8, X19, #8
iBoot:00000001FC1A1D90 ; 分配失败,上界和下界置零
iBoot:00000001FC1A1D90 STP XZR, XZR, [X8]
iBoot:00000001FC1A1D94 LDP X29, X30, [SP,#0x20+var_s0]
iBoot:00000001FC1A1D98 LDP X20, X19, [SP,#0x20+var_10]
iBoot:00000001FC1A1D9C ADD SP, SP, #0x30 ; '0'
iBoot:00000001FC1A1DA0 RETAB
iBoot:00000001FC1A1DA4 ; ---------------------------------------------------------------------------
iBoot:00000001FC1A1DA4
iBoot:00000001FC1A1DA4 allocation_size_error_detected ; CODE XREF: do_safe_allocation+1C↑j
iBoot:00000001FC1A1DA4 BL call_panic_allocation_size_error
iBoot:00000001FC1A1DA4 ; End of function do_safe_allocation
美妙!这就是我要的滑板鞋找的函数。这个函数分配内存块并设置对应的元数据,具体的结构在前文已经分析过。
你可能想知道其余的分配函数长什么样。其实,可能正如你假设的那样,有一个很相似的函数,唯一的区别是在结尾使用 memset0(也就是 calloc 调用的)初始化数据。
iBoot:00000001FC1AA58C memset_0 ; CODE XREF: sub_1FC1A0890+3CC↑p
iBoot:00000001FC1AA58C ; do_safe_allocation_and_zeroing:zero_the_allocation↑j ...
iBoot:00000001FC1AA58C CBZ X1, return
iBoot:00000001FC1AA590
iBoot:00000001FC1AA590 loop ; CODE XREF: memset_0+C↓j
iBoot:00000001FC1AA590 STRB WZR, [X0],#1
iBoot:00000001FC1AA594 SUBS X1, X1, #1
iBoot:00000001FC1AA598 B.NE loop
iBoot:00000001FC1AA59C
iBoot:00000001FC1AA59C return ; CODE XREF: memset_0↑j
iBoot:00000001FC1AA59C RET
iBoot:00000001FC1AA59C ; End of function memset_0
以上三个新的内存分配 API 加起来有超过 100 处调用。看上去八*不离十了。
malloc
除了有超过100 处代码直接调用 do_safe_allocation
函数之外,我还看到一些旧版本见过的 malloc
调用被保留。有很多种办法可以识别 iBoot 中的 malloc
,一个简单的方式就是查找出现字符串 %s malloc failed
的函数。接下来可以看到,实际上新的 malloc
内部直接调用了 do_allocation
。
如下是 malloc 的代码:
iBoot:00000001FC15ABF8 malloc ; CODE XREF: sub_1FC19F50C+58↓p
iBoot:00000001FC15ABF8 ; sub_1FC19F77C+464↓p ...
iBoot:00000001FC15ABF8 B call_do_allocation
iBoot:00000001FC15ABF8 ; End of function malloc
以及 call_do_allocation:
iBoot:00000001FC1A1B30 call_do_allocation
iBoot:00000001FC1A1B30
iBoot:00000001FC1A1B30 var_10= -0x10
iBoot:00000001FC1A1B30 var_8= -8
iBoot:00000001FC1A1B30 var_s0= 0
iBoot:00000001FC1A1B30
iBoot:00000001FC1A1B30 PACIBSP
iBoot:00000001FC1A1B34 SUB SP, SP, #0x20
iBoot:00000001FC1A1B38 STP X29, X30, [SP,#0x10+var_s0]
iBoot:00000001FC1A1B3C ADD X29, SP, #0x10
iBoot:00000001FC1A1B40 MOV X4, X0
iBoot:00000001FC1A1B44 ADRP X8, #0x1FC2F50B8@PAGE
iBoot:00000001FC1A1B48 ADD X8, X8, #0x1FC2F50B8@PAGEOFF
iBoot:00000001FC1A1B4C STR X8, [SP,#0x10+var_8]
iBoot:00000001FC1A1B50 STR WZR, [SP,#0x10+var_10]
iBoot:00000001FC1A1B54 ADRP X2, #0x1FC2F50B0@PAGE
iBoot:00000001FC1A1B58 ADD X2, X2, #0x1FC2F50B0@PAGEOFF
iBoot:00000001FC1A1B5C ADRL X3, off_1FC2D6EC0
iBoot:00000001FC1A1B64 ADRL X1, qword_1FC2D6E80
iBoot:00000001FC1A1B6C ADR X16, do_allocation
iBoot:00000001FC1A1B70 NOP
iBoot:00000001FC1A1B74 PACIZA X16
iBoot:00000001FC1A1B78 MOV X6, X16
iBoot:00000001FC1A1B7C MOV X0, #0
iBoot:00000001FC1A1B80 MOV W5, #1
iBoot:00000001FC1A1B84 MOV X7, X1
iBoot:00000001FC1A1B88 BL wrap_do_allocation
iBoot:00000001FC1A1B8C LDP X29, X30, [SP,#0x10+var_s0]
iBoot:00000001FC1A1B90 ADD SP, SP, #0x20 ; ' '
iBoot:00000001FC1A1B94 RETAB
iBoot:00000001FC1A1B94 ; End of function call_do_allocation
这段对我很重要,有两个原因:
- 这从另一方面证明我们之前对内存分配 API 的理解是正确的
- 那些直接调用
malloc
而不是do_safe_allocation
的代码,分配到的内存并不会保存 firebloom 的元数据
一个有趣的问题是,为什么这些代码没有更新到新的内存分配机制?一些可能的答案:
- 也许 Apple 使用静态分析证明了这些这些地方可以不用 firebloom 的保护
- 也许一些依赖库还没有适配 firebloom?可能这个缓解措施还在逐步推进中?
类型安全
好了,我们现在知道有这个带有原始指针和数组边界的新结构体。我们在二进制里识别到了相关代码,并分析了其工作原理。
然而,我本来期望能在这个结构体里看到类型信息,因为:
- 首先,我们有一些诸如 panic_memset_bad_type 的函数
- Apple 明确地在文档里提到了这点
好吧,如果你还有印象,前文的内存分配函数确实将一个值保存到结构体的 0x18 偏移上(所以我把结构体的尺寸定为 0x20)。如果我们跟进函数 panic_memset_bad_type 的调用,我们可以找到例如这样的代码:
iBoot:00000001FC15A9CC LDR X23, [X20,#0x18]
iBoot:00000001FC15A9D0 MOV X0, X23
iBoot:00000001FC15A9D4 BL check_type
iBoot:00000001FC15A9D8 TBNZ W0, #0, call_memset
iBoot:00000001FC15A9DC CBNZ W22, detected_memset_bad_type
对的!看上去 0x18 偏移就是用来存储类型信息的。有机会我将详细解释更多类型安全的实现细节。在这里只需要知道一点就够了,firebloom 的类型信息由 do_safe_allocation 的调用方传入并保存,这在代码里不难发现。
小结和思考
很高兴能看到更多内存安全技术上的工作,也很高兴能有新的东西可以折腾。
这项改动非常有趣。它显然能帮助缓解一些内存安全漏洞,但代价也相当昂贵,体现在一些不同的方面:
- 额外的内存开支:新的指针格式使用 0x20 个字节的内存,而不是 8 个字节。受保护的指针所占用内存是原来的 4 倍。
- 生成代码的长度:显而易见,采用了 firebloom 的二进制需要更多的代码。更多的指令来管理新的元数据,更多的条件分支,更多的检查等。
- 性能损耗:大量的解引用操作被替换成更多的指令(用以从内存中加载数据),对性能产生影响。
显然我没有实际测算过新老版本 iBoot 之间的开销差异,所以上面的分析都只是基于理论。但我有把握相信代价存在,而 Apple 找到了一个折中的方式让其顺利运行。
我知道这么说听上去很糟糕,但老实说,iBoot 正好是一个适合引入此类机制的地方。如果 Apple(或其他厂商)在内核中启用如此开销昂贵的保护,我将会感到非常诧异。iBoot 是一个非常轻量、可控的环境。它具有整个 DRAM 的访问权限,其设计具有有限的和非常明确的目的。这项机制对保护第二阶段的 bootloader 很有帮助,而后者是整个安全引导过程中至关重要的一环。
对于 Apple 而言这是一个非常不错的例子,表明其对大量的一阶原语开发针对性的缓解措施,从而提升软件的安全性。
原文始发于微信公众号(非尝咸鱼贩):【翻译】iBoot 安全机制 firebloom 简介
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论