对于Linux内核攻击,通过控制流劫持基本元素/基元(CFHP,control flow hijacking primitive)来获取root权限非常重要。随着安全研究人员提出各种内核保护措施,这种攻击变得越来越难以实现。本文中提出RetSpill,可以利用已存在于内核堆栈上的用户空间数据进行提权。
本文进行了系统性研究,确定了四种将用户空间数据溢出到内核堆栈的常见方法。尽管这些方法在内核安全规范内完全合理,但当与CFH漏洞结合使用时,就引入了一条新的利用路径,使RetSpill能够可靠地将这些漏洞直接转化为提权。最后,讨论了如何改进防御方法。相关代码位于: https://github.com/sefcom/RetSpill
0x01 引言
随着Linux在云服务、物联网设备和手机等领域的日益普及,Linux内核漏洞已经成为计算机安全的主要威胁。Linux内核漏洞可以允许攻击者提升权限并在被攻击系统中引发严重问题。其中,能执行控制流劫持(CFH)的漏洞是一种普遍且严重的缺陷。攻击者利用这些漏洞来实现控制流劫持基元(例如,通过覆盖存储在内核堆上的函数指针),绕过现代防御措施(例如SMAP),最终将攻击者的权限级别从普通用户提升到root。尽管存在防御措施,但CFH仍然广泛用于Linux内核的攻击。然而,从CFHP到完整的内核提权利用存在重大差距。在大多数现代情况下,CFHP源于堆上的缺陷(例如,由于使用后释放而产生的函数指针覆盖),而不是堆栈上的缺陷。这对于攻击利用来说是有问题的,因为在堆上构建和触发ROP载荷是困难的。
当前解决这个问题的方法尝试将堆栈指针转移到堆上的虚假堆栈或flat physical mapping中。然而这些方法是不稳定的,因为它们依赖于特定的内核内存布局、精确的特定gadget的存在,或者不总是与CFHP一起授予的额外基元(例如寄存器控制)。对于攻击者来说,直接的堆栈溢出漏洞几乎已经不复存在(以及有效的缓解措施,如堆栈Canaries)。
本研究开发了一种通用技术来在堆栈上准备ROP载荷,这利用了内核预期设计:Linux的内核堆栈设计用于保存各种形式的数据,包括来自用户空间的用户控制数据。攻击者可以利用这个预期的存储来在内核空间中存储精心制作的恶意数据,然后在通过栈迁移触发漏洞后访问这些数据。基于这一发现,本文提出了新的利用技术和一种新的防御方法。RetSpill能够绕过大多数现有的内核安全防护措施(包括函数粒度内核地址空间布局随机化),而无需其他条件(如寄存器控制或特定的内存配置要求)。重要的是,RetSpill可以进行半自动化。
0x02 背景
(1)Linux内核利用:通常,Linux内核利用的目标是利用内核漏洞,从权限较低的用户(例如,nobody)提升到root。与用户空间级别的利用相比,例如,一旦获得shell就被视为成功攻击,Linux内核利用需要考虑安全返回到用户空间,而不触发内核错误或杀死已提权的用户空间进程。
(2)控制流劫持基元:一个利用基元(Exploitation Primitive)可以表示一种机器状态转换,其中违反了安全策略。通过CFHP,攻击者可以控制内核的程序计数器(PC,program counter)。
(3)控制流量劫持(CFH)攻击:它始于CFHP,将CFHP扩大到更强的基元,如返回导向编程(ROP,Return Oriented Programming)和shellcode执行,最终实现提权。现代内核中由于加固技术的存在,获得CFHP并不意味着能够获得root权限,也不意味着能够执行ROP或任意代码执行,因此获得CFHP变得更加困难。
(4)现有保护措施:旨在防止攻击者未经授权地访问用户空间数据。监督者模式执行防制(SMEP,Supervisor Mode Execution Prevention)和内核页表隔离(KPTI,Kernel Page Table Isolation)禁止从内核空间执行用户空间代码。SMAP禁止内核直接读取/写入用户空间数据。NX-physmap将physmap区域标记为不可执行。这些保护措施限制了攻击者将CFHP转化为任意代码执行的能力。
(5)其他保护措施:旨在限制攻击者的能力,受到有限的代码执行限制:CR Pinning防止未经授权修改CR4寄存器(其中包含启用某些缓解措施的位),从而防止攻击者禁用缓解措施。STATIC_USERMODE_HELPER禁止滥用“用户模式助手”进行提权。运行时内核补丁(RKP,Runtime Kernel Patching)禁止直接修改进程凭证。pt-rand使直接修改内核页表的技术无效。值得注意的是,RANDKSTACK是一种旨在随机化内核堆栈布局的保护措施,以防止攻击者利用未初始化变量漏洞。其对内核堆栈布局的更改如下图所示。与它们在用户空间应用程序中的用法类似,STACK CANARY可以减轻堆栈溢出漏洞,从而消除了覆盖返回地址(和更高内存地址)以实现ROP的可能性。
(6)保护措施绕过:由于强大的堆栈保护措施,大多数现代CFHP都是内核堆的误用导致的。通常情况下,攻击者通过堆溢出等方式覆盖了堆中攻击对象的函数指针。然后,攻击者调用触发系统调用,该系统调用调用被覆盖的函数指针并获得CFHP。由于不再可行(NX-physmap和SMEP),并且堆栈canaries的使用和堆栈溢出漏洞的减少,典型的CFH攻击需要将CFHP与其他基元(例如堆地址泄露)结合使用,以获得堆栈控制并启动ROP链,如下表所示。
(7)不完全绕过-转向堆:大多数现代内核利用堆相关的漏洞,并必须在堆上准备和触发攻击载荷。例如,所有15个公开的kCTF CFHP攻击都会将堆中的伪堆栈用于实现ROP。这有缺点:它不支持直接重写载荷,因为载荷位于内核堆中,用户无法直接访问该区域。换句话说,要执行不同的载荷,攻击者需要交换包含载荷的堆对象,甚至再次触发漏洞,这会显著降低需要触发不同载荷以实现提权的攻击的可靠性。此外,这些攻击要么依赖于特定的内核内存布局,要么依赖于精确的特制gadget,要么依赖于额外的基元(如寄存器控制)。如上表所示,转向堆栈需要比RetSpill更多的基元,这显著增加了开发攻击的难度。
(8)内核ROP:与用户空间ROP类似,内核ROP赋予攻击者通过链接ROP gadget实现任意执行的能力。与用户空间ROP相比,内核ROP有两个额外的要求。首先,ROP链中的泄漏信息需要传递回用户空间。这可以通过使用CPU特权转换期间不清除寄存器的事实来完成。攻击者可以将泄漏的信息加载到寄存器中,执行特权转换回用户空间,并获取泄漏的值。其次,内核空间中的ROP链需要以正常的方式结束,以避免出现恶性事件。目前有三种方法可以实现这一点:
a. 返回到用户空间,例如使用KPTI trampoline ;
b. 无限睡眠,冻结当前任务;
c. 通过调用do_task_dead函数杀死当前任务。
(9)内核堆栈和系统调用:在Linux内核中,每个任务都有自己的内核空间堆栈,以便执行从用户空间调用的系统调用。内核堆栈在创建每个内核任务时被分配为固定大小的缓冲区。当用户空间调用系统调用时,程序计数器将切换到内核空间函数,堆栈指针将设置为内核堆栈。然后,内核将将用户空间上下文(包括所有用户空间寄存器)推送到堆栈底部,并调用相应的系统调用处理程序。保留用户空间上下文称为pt_regs。当返回到用户空间时,内核将恢复用户空间上下文,并恢复用户空间程序的执行。 由于内核堆栈的固定大小特性,开发人员通常会将大块数据存储在动态分配的内存区域(例如堆)中,以避免堆栈耗尽。但是出于性能考虑,一些性能关键的系统调用仍然在内核堆栈上存储大量数据。
0x03 威胁模型
在威胁模型中,目标Linux内核具有与Kepler模型相同的所有保护措施,此外还包括前沿内核加固技术功能粒度内核地址空间布局随机化 (FG-KASLR, Function-Granular Kernel Address Space Layout Randomization) 。具体来说,允许目标Linux内核启用SMEP、SMAP、KPTI、NX-physmap、CR Pinning、STATIC_USERMODE_HELPER、RKP、pt-rand、RANDKSTACK、STACK CANARY以及FG-KASLR等保护措施。
此外,模型需要两种利用基元:控制流劫持和内核镜像基地址泄漏。这两种基元都是合理的,并且可以通过合理数量的Linux漏洞来实现。
0x04 漏洞利用设计
现代CFH攻击包括两个步骤:
1)将用户控制的数据注入内核内存,
2)使用此受控数据进行ROP。
保护措施会阻止攻击者直接访问用户空间数据,只留下了physmap和内核堆两种已知的选择来存储用户控制的ROP链。访问这两个区域的难度需要在现代CFH攻击中引入额外的利用基元(例如寄存器控制或附加地址泄漏)。
RetSpill:在系统调用期间,潜在的攻击者控制的用户数据会按设计加载到内核堆栈上。当获取PC控制权时,这发生在攻击者设置CFHP的条件后,攻击者可以立即在内核堆栈上使用这些受控用户数据来发动代码重用攻击(例如ROP)。然而,这还不足以完成完整的攻击,由此产生的各个ROP攻击是有限的,一些触发系统调用仅具有七个可控的ROP载荷。因为在编译期间函数的堆栈布局是固定的,攻击者可以反复调用触发系统调用无限次,以触发不同的ROP载荷。有了这个方法,RetSpill可以在内核空间获得无限的任意读/写/执行权限,因此即使存在现代内核保护措施,也完全突破了用户空间与内核空间之间的安全边界。
A. 数据泄漏
攻击者控制的用户空间数据可以直接或间接泄漏到内核堆栈上。 直接数据泄漏是指在一个系统调用中将用户数据直接加载到内核堆栈上的情况。如下图中所示,poll系统调用在使用copy_from_user将用户空间数据直接复制到内核堆栈上时执行直接数据泄漏。间接数据泄漏发生在数据通过多个系统调用加载到内核堆栈的情况下。例如,通过精心构造的参数,对open系统调用的调用将来自用户空间的数据存储到内核堆中,随后对readlink的调用将此受控数据加载到内核堆栈中。
本文主要分析直接数据泄漏,首先调查了用户空间数据在系统调用期间如何流入内核空间。共识别了两种方法:用户空间寄存器和用户空间内存。更具体地说,当用户空间程序调用系统调用时,其通用寄存器包含可能流入内核空间的数据。在系统调用的生命周期内,内核可能需要来自用户空间内存的附加信息才能完成系统调用,这导致用户空间数据流入内核空间。
为了全面发现直接用户数据泄漏的原因,对用户空间数据(包括用户空间寄存器和内存)进行了污点分析,并观察了在触发系统调用中内核堆栈上的轨迹,以确定数据泄漏的原因。通过手动分析污点分析结果,确定了直接用户数据泄漏的三种原因。根据用户数据临时存储的位置,间接数据泄漏方法可能有许多变体。本文确定了一种常见的间接数据泄漏,其中用户数据由于未初始化的内存而临时存储在内核堆栈上并在系统调用之间共享。
B. 泄漏源
(1)有效数据:出于性能原因,Linux内核有时会直接将用户空间数据复制到内核堆栈上。以上图中的简化的poll系统调用处理程序为例。它将相当数量的(在构建中为0x1e0字节)用户空间数据复制到位于内核堆栈上的stack_pps中。如果攻击者控制用户空间中的文件对象,当通过file->f_op->poll调用获得CFHP时,将控制内核堆栈上的0x1e0字节。然后,攻击者可以使用add rsp, X; retgadget来迁移到受控区域并发动代码重用攻击。在poll系统调用的生命周期内,堆栈上的数据是有效的(即其存在于内核堆栈上不会导致安全违规),但它被用于恶意目的。
(2)保留寄存器:每个用户空间线程都有自己的内核堆栈。当用户空间线程调用系统调用时,内核将通过设置rsp寄存器来切换到使用关联的内核堆栈。在堆栈指针更改后,内核立即将用户空间上下文推送到内核堆栈上以保存上下文。“用户空间