随着内核地址空间布局随机化(KASLR)被现代系统广泛采用,获取信息泄漏是大多数权限升级攻击的必要组成部分。本文我们将介绍XNU(苹果macOS使用的内核)的实现细节,它可以消除许多内核漏洞中对专用信息泄露漏洞的需要。
关键在于内核Mach-O的__HIB段,它包含系统休眠和底层CPU管理的函数子集和数据结构,总是映射在一个已知的地址。
我们将首先介绍__HIB段以及它可能被滥用的各种方式。使用我们的Pwn2Own 2021内核漏洞作为一个真实的例子,展示如何简化该漏洞。
滥用基本操作系统设计可以实现漏洞利用原语
__HIB 段:休眠和内核入口
__HIB段的名字来源于“休眠(hibernation)”,似乎它的主要目的是处理从休眠映像中恢复系统。
它的其他主要功能是处理内核的低级进入和退出,即通过中断或系统调用。它包含中断描述符表 (IDT) 和相应的中断处理程序存根。
它同样包含通过syscall和sysenter指令进入的存根。这些入口点是通过编写相应的 MSR 来设置的:
这个代码片段使用DBLMAP宏引用“DBLMAP”中的每个函数的别名。
XNU dblmap
dblmap是一个虚拟内存区域,别名为组成__HIB段的相同物理页。
dblmap在doublemap_init()中初始化,它将虚拟别名插入到构成__HIB段的所有物理页的页表中。dblmap 的基数是用 8 位熵随机化的。
别名防止了使用sidt指令的琐碎的KASLR绕过,sidt指令将揭示IDT的内核地址。这个指令不是权限指令,如果在 ring3(用户模式)中执行不会导致异常,除非在处理器上启用了用户模式指令保护(UMIP)(不在 XNU 中):
用户模式指令预防(CR4的第11位)设置后,如果CPL > 0: SGDT、SIDT、SLDT、SMSW和STR,则不能执行以下指令,如果试图执行该值,将导致通用保护异常(#GP)。
由于 IDT/GDT/LDT/etc 位于 dblmap 别名中,因此知道它们在 dblmap 中的地址不会显示它们在正常内核映射中的地址。但是,它们确实会泄漏 dblmap 信息。
缓解失败
dblmap 的一个辅助目的是它起到缓解 Meltdown 的作用。各种操作系统采用的常用技术是在执行用户空间代码时从页表中删除内核映射,然后在进行中断/系统调用/等时重新映射内核。Linux 将其实现称为内核页表隔离 (KPTI),在 Microsoft Windows 上,它的对等物被称为 KVA Shadow。
两种实现方式的相似之处在于,在执行用户代码时,只有一小部分内核页面出现在页表中。该子集包含处理中断和系统调用以及在用户和内核页表之间来回转换所需的代码/数据。在 XNU 的情况下,“小子集”正是 dblmap。
为了进行页表转换,中断调度代码需要知道正确的页表地址以进行切换。换句话说,要向cr3写入什么值,即保存顶级分页结构的物理地址的控制寄存器。
这些值驻留在 __HIB 段的数据部分之一中的每个 CPU 数据结构中:
cpshadows[cpu].cpu_ucr3 : 用于userspace + dblmap;
cpshadows[cpu].cpu_shadowtask_cr3 :用于userspace + full kernel;
从用户空间进入后,中断调度代码会将 cr3 切换到 cpu_shadowtask_cr3,然后在返回用户空间之前切换回 cpu_ucr3。
由于在执行用户代码时 dblmap 被映射到页表中,因此理论上 dblmap 中的任何内容都可以使用 Meltdown 读取。这就产生了 dblmap 中是否存在任何“敏感”数据的问题。
值得注意的是,有几个函数指针以数据的形式出现,用于从dblmap跳转到正常映射的内核函数。泄露其中任何一个都足以确定kernel slide(用于描述随机内核基址地址的 XNU 术语)并破坏KASLR。
有趣的是,KASLR 在 Windows 和 Linux 上都可以通过 Meltdown 进行类似的攻击:
同样的策略也适用于运行有漏洞的英特尔硬件的最新版本的macOS。只需使用libkdump从dbblmap读取即可。简而言之,Meltdown 可用于在除最后一代基于 Intel 的 Mac 机型之外的所有设备上破坏 KASLR,该机型附带 Ice Lake 处理器,其中包含针对 Meltdown 的 CPU 级修复。
虽然 KASLR 在基于 Intel 的 Mac 上无疑是一个重要且几乎普遍的突破,但我们现在将讨论 dblmap 提供的其他几个有用的内核利用原语,这些原语适用于包括 Ice Lake 在内的所有版本。
LDT——已知内核地址的受控数据
从利用的角度来看,也许 dblmap 最有趣的工件是它为每个 CPU 内核保存 LDT,它可以包含几乎可以从用户模式控制的任意数据,并且位于dblmap中的固定偏移位置。
LDT/GDT
LDT 和 GDT 包含许多段描述符。每个描述符要么是系统描述符,要么是标准代码/数据描述符。
代码/数据描述符是一个 8 字节的结构,描述了具有某些访问权限(即它是否可读/可写/可执行,以及可以访问它的特权级别)的内存区域(由基地址和限制指定)。这主要是一个 32 位保护模式的概念;如果执行 64 位代码,则忽略基地址和限制。描述符可以描述 64 位代码段,但不使用基本/限制。
系统描述符是特殊的,大多数扩展为 16 字节以适应指定 64 位地址。它们可以是以下类型之一:
LDT :指定 LDT 的地址和大小;
任务状态段 (TSS) :指定 TSS 的地址,该结构包含与权限级别切换相关的信息结构;
调用、中断或陷阱门:指定远程调用、中断或陷阱的入口点;
LDT中唯一允许的系统描述符是调用门,我们稍后会探讨。
段描述符使用 16 位段选择器引用:
例如,cs寄存器(代码段)是一个选择器,它引用当前正在执行的任何代码的段。通过在GDT/LDT中为64位和32位代码设置不同的描述符,操作系统可以通过使用适当的选择器在受保护模式(32位代码)和长模式(64位代码)之间进行用户空间跳转。特别是在macOS上,硬编码的选择器是:
0x2b:第5个 GDT 条目,ring3——64 位用户代码;
0x1b:第3个 GDT 条目,ring3——32 位用户代码;
0x08:第1个 GDT 条目,ring0——64 位内核代码;
0x50:第10个 GDT 条目,ring0——32 位内核代码;
LDT在macOS上的应用
进程可以通过调用i386_set_ldt()在其用户模式的LDT中设置描述符。它调用的对应内核函数是i386_set_ldt_impl()。
此函数对 LDT 中允许的描述符类型施加了一些限制。描述符必须为空,或者指定一个用户空间的32位代码/数据段:
就二进制格式而言,这转换为对每个8字节描述符的以下限制:
字节 5 (dp->access):必须是0,1,或者有高nibble 0xf(值匹配0xf)。
字节 6 (dp->granularity): 位5不能设置(掩码0x20)。
使用LDT在已知的内核地址创建伪内核对象,这些限制不会太严格。唯一真正的问题是能否在这样的对象中构造有效的内核指针。
为了解决这个问题,我们可以将假对象错位 6 个字节。这将使 dp->access 与内核指针的高字节(将为 0xff)重叠。唯一的限制是指针的低字节(与 dp->granularity 重叠)不能设置位5 。
成功设置LDT描述符将创建一个堆分配的LDT结构,该结构的指针存储在当前任务结构的task->i386_ldt字段中。
每当发生上下文切换或手动更新LDT时,内核通过user_ldt_set()将来自该结构的描述符复制到实际的LDT(在dblmap中)。
进程可以使用i386_get_ldt()查询当前的LDT条目,调用内核函数i386_get_ldt_impl()。这将直接从实际LDT(在dblmap中)复制描述符,而不是从LDT结构体复制描述符。
已知内核地址上的已知代码
在编写没有内核代码地址泄漏的 macOS 内核漏洞利用时,__HIB 文本部分中的函数子集可能会用作有用的调用目标。
dblmap 中一些值得注意的函数(在固定偏移处)是:
memcpy(), bcopy();
memset(), memset_word(), bzero();
hibernate_page_bitset()——设置或清除位图结构中的位;
hibernate_page_bitmap_pin()——可以将 32 位 int 从第一个结构参数复制到第二个指针参数;
hibernate_sum_page() ——从它的第一个参数返回一个 32 位 int 作为一个数组,使用第二个作为索引;
hibernate_scratch_read(), hibernate_scratch_write()——类似于memcpy,但第一个参数是一个结构;
当控制流被劫持时,这些函数对内核利用的效用将严重依赖于特定的上下文。上面列出的大多数内核调用目标需要3个参数。如果一个给定的漏洞允许按顺序进行多个受控调用,你还可以使用早期调用来设置寄存器/状态,以便在以后的调用中使用。
无论情况如何,dblmap通常可以用来将一个简单的内核控制流劫持原语转换为同时产生真正内核文本(代码地址)泄漏的内容。
其他dblmap技巧
已知内核地址上真正任意的64位值
我们已经介绍了dblmap如何允许我们轻松地在LDT中放置几乎任意的数据。
但是,假设你需要一个真正任意的64位值(可能你需要的函数指针位为5,并且不符合 LDT 限制)。当进行系统调用时,调度存根会将 rsp(用户空间堆栈指针)保存在 dblmap 的临时位置(cpshadows[cpu].cpu_uber.cu_tmp)。用户 rsp 值也将放置在 dblmap 中的中断堆栈上(对于 cpu 0 的 master_sstk 或 scdtables[cpu])。
虽然不是其预期用途,但 rsp 可以被视为通用寄存器,并分配任意 64 位值。除非需要在系统调用之前和之后设置/恢复 rsp 给开发带来不便,否则这会在已知地址处提供任意 64 位值。
将内核指针泄漏到LDT
LDT的另一个有用的方面是,它可以用作真正的内核地址泄漏的可写位置,可以使用i386_get_ldt()从用户模式查询和读取。假设你能够使用劫持函数调用将任意地址复制到LDT中。将dblmap中的一个函数指针复制到LDT中,然后查询LDT的适当范围,将获得文本泄漏。
或者作为另一个例子,也许你劫持的函数调用的第一个参数是LDT中的假对象,第二个参数是某个有效的c++对象,第三个参数是任何足够小的整数。调用 memcpy() 会将 vtable和一些对象的字段复制到 LDT。查询 LDT 然后将这些泄漏读取到用户空间。
记住,每个CPU核有一个LDT,在macOS上没有方法将进程固定到任何特定的核上。如果一个线程执行被劫持的函数调用,将泄漏复制到它的LDT中,然后在调用i386_get_ldt()之前被抢占,那么泄漏实际上将丢失。
参考及来源:https://blog.ret2.io/2022/08/17/macos-dblmap-kernel-exploitation/