基本信息
- 原文标题:RetSpill: Igniting User-Controlled Data to Burn Away Linux Kernel Protections
- 原文作者:Kyle Zeng, Zhenpeng Lin, Kangjie Lu, Xinyu Xing, Ruoyu Wang, Adam Doupé, Yan Shoshitaishvili, Tiffany Bao
- 作者单位:Arizona State University
- 关键词:Linux内核, 漏洞可利用性, 自动化评估, RetSpill
- 原文链接:https://dl.acm.org/doi/10.1145/3576915.3623220
- 开源代码:https://github.com/sefcom/RetSpill
1. 论文要点
论文简介:作者提出RetSpill,通过syscall将用户数据(ROP链)布置到内核栈上,然后结合控制流劫持(CFH)漏洞进行提权,能够绕过当前Linux内核上开启的所有防护机制(例如FG-KASLR)。作者还提出了新的防护机制。
主要内容:作者发现,通过syscall在内核栈平均可以布置11个ROP,足以构造任意读写和执行。
实验:通过对22个CFH漏洞的CVE进行测试,成功自动生成20个提权EXP。
2. 背景与介绍
防护机制:SMEP[47] / SMAP[12] / KPTI[64] / NX-physmap[31] / CR Pinning[63] / STATIC_USERMODE_HELPER[35] / RKP[56](不允许直接修改进程凭证)/ pt-rand[14](不允许直接修改内核页表来进行数据流攻击)/ RANDSTACK[53](随机化栈布局,防止利用未初始化使用漏洞) / STACK CANARY。
漏洞利用:目前最常见的是利用堆漏洞,先通过覆写堆对象上的函数指针来构造CFHP,然后组合其他利用原语来控制栈、执行ROP链,各种利用方法和所需的原语参见 Table 1。
栈迁移的局限:目前常用的方法是在堆上布置ROP,然后将栈指针指向堆上伪造的栈。本方法存在两个局限,一是不能直接重写payload,需要重新喷射包含payload的堆对象或再次触发漏洞,降低了漏洞利用的稳定性;二是依赖特定的内存布局、特定的ROP、额外的利用原语(例如寄存器控制)。本文提出的RetSpill方法需要的原语最少。
内核ROP:和用户空间ROP相比,内核ROP有两个要求。一是信息泄露,需要将信息传到用户空间,可先将信息存到某个寄存器,该寄存器在进行上下文切换时不会被清零;二是退出时要避免panic(可利用KPTI trampoline[38]返回用户空间、无限休眠[74]、调用do_task_dead()
杀死当前任务)。
内核栈与Syscall:调用syscall时,会切换到内核栈,然后将用户上下文(寄存器)压到栈底部,将用户上下文称为 pt_regs
,返回用户程序时会恢复用户上下文。
威胁模型:开启的保护机制类似Kepler[70],增加了FG-KASLR[34]。也即 SMEP, SMAP, KPTI, NX-physmap, CR Pinning, STATIC_USERMODE_HELPER, RKP, pt-rand, RANDKSTACK, STACK CANARY, FG-KASLR。假设已经有了CFH原语,且能够泄露内核基址。
3. RetFill利用
3-1. 数据注入
数据注入分类:本文主要关注直接数据注入。
- 直接数据注入:通过syscall直接将数据(用户寄存器或用户内存)传入内核栈。例如,见Listing 1,poll调用
copy_from_user()
将用户内存数据拷贝到内核栈上。 - 间接数据注入:通过多个syscall来传递数据。例如,先调用
open
将数据存入内核堆,再调用readlink
将数据载入内核栈。
数据注入方法:
- (1)有效数据:
poll
将0x1e0字节数据拷贝到stack_pps
。如果攻击者控制了用户空间的file
对象,通过file->f_op->poll
劫持控制流时,就能控制内核栈上0x1e0字节的数据,再利用add rsp, X; ret
gadget跳转到可控区域,执行ROP链。 - (2)上下文寄存器:调用syscall时,内核栈上的
pt_regs
结构会保存用户上下文。 - (3)调用约定:内核调用约定中,被调用者和调用者都需要保存寄存器(被称为
callee-saved
/caller-saved
寄存器),一般在函数开头压栈,结尾出栈。两种方式布置数据:- 某些syscall会直接调用handler函数,将用户寄存器保存到
pt_regs
后,寄存器上还存有用户数据,handler函数内会把用户寄存器压栈。 - syscall中的handler可能会调用其他helper函数,也会将用户寄存器压栈保存。
- 某些syscall会直接调用handler函数,将用户寄存器保存到
- (4)未初始化内存:同一线程的上一syscall的栈数据仍存在栈上,如果在当前syscall函数中部劫持函数指针,就能避免栈初始化。可参考导向型栈喷射技术[45]来布置栈数据。例如,见Listing 2,原本会在
recvmsg
hadnler(第6行)初始化address
对象,如果利用漏洞来覆写sock
结构,控制sock->ops->recvmsg
劫持控制流,这样就不会初始化address
对象。
3-2. 执行ROP
跳转到ROP:可通过add rsp, X; ret
gadget跳转到用户控制的ROP区域。
ROP链碎片化原因:一是不同的syscall数据注入能力有限,方法二最稳定;二是某些CFHP原语对参数有要求,例如CVE-2010-2959要求rdi为fd参数;三是用户可控的栈内存不连续,需跳转。
独立的ROP链:由于是在堆上构造的CFHP,所以EXP中各个线程都能触发,可创建子线程来触发ROP链,避免影响主线程。这样就能在不同的线程、不同的上下文下执行不同的ROP链,不需要多次触发漏洞。优势一,可在ROP链末尾调用do_task_dead
优雅退出;优势二,可多次执行ROP链,绕过随机化保护,不影响稳定性。
任意读/写/执行:用户数据放栈上有利于无限次调用,每次调用时重新布置ROP即可。
3-3. 绕过保护机制
- SMEP / SMAP / KPTI:不依赖用户数据,可绕过;
- RANDKSTACK:在栈帧和
pt_regs
之间插入了随机偏移,只影响方法2(上下文寄存器)和方法4(未初始化内存)。方法2可能可用,由于随机偏移只有5bit是随机的,我们可以硬编码一个偏移,然后在ROP前置一些ret-sled
(ret
gadget),增大执行ROP的机率。如果系统禁用了panic_on_oops
(发行版中只有CentOS是开启的),就能绕过RANDSTACK,可以在多个子进程中尝试执行payload,执行失败也不会触发崩溃。 - STACKLEAK / STRUCTLEAK / INITSTACK:强制初始化栈,影响方法4(未初始化内存)。
- FG-KASLR:boot阶段对内核函数地址随机化,但不会对asm内联代码随机化。RetSpill可以利用地址固定的gadget来构造任意读原语,动态泄露函数地址[38],绕过FG-KASLR。
- KCFI / IBT:CFI保护机制,针对
forward-edge
CFH,部分阻止了CFHP攻击。例如,KCFI编译的内核中,__efi_call()
没有验证其调用目标。还可以利用backward-edge
来劫持控制流(示例参见[30])。 - Shadow Stack:针对
backward-edge
CFH,本机制还没有在x86_64的Linux内核上实现,只能理论上分析如何绕过。可以利用forward-edge
CFH,但不能执行ROP,可以利用JOP[3]/PCOP[55]。 - CFI + Shadow Stack:无法绕过。
3-4. 半自动化
IGNI框架:半自动化生成RetSpill利用。生成的EXP仅执行commit_creds(init_cred)
便返回到用户态。工作流见Figure 2。输入内核Image和CFHP的漏洞,输出 1) 负责栈移动的gadget; 2) ignite函数,负责数据注入,调用触发syscall,生成提权EXP。
技术实现:采用污点分析识别能够注入数据的syscall,采用符号执行angr生成ROP链。注意,目前没有用到未初始化内存来布置数据,因为无法确定性的控制内核栈内存。
难点:
- 从大量syscall调用中识别出能触发CFH的syscall;
- 识别用户可控的数据,且不干扰CFH原语的获得;
- 如何在离散的区域布置ROP链。方法类似BOPC[28]
4. 实验
数据选取:22个CVE,13个exp来自KHeaps[75],9个来自公开exp。
保护机制:SMEP, SMAP, KPTI, NX-physmap, CR pinning, STACK CANARY。未开启STATIC_USERMODE_HELPER, FG-KASLR, RKP, PT-Rand。
实验结果:见Table 4,在获得CFHP时在栈上平均可布置16.5个gadget;IGNI能自动生成20个EXP;采用自编写漏洞模块来测试保护机制绕过能力,能够成功绕过 RANDSTACK、KCFI / IBT、FG-KASLR。成功案例是CVE-2022-1786。
5. 防护机制
思路:当前CFI+Shadow Stack还没有准备好,且依赖特殊硬件,本文方法是消除往内核栈布置用户数据的路径。
(1)上下文寄存器:可将上下文寄存器保存在task_struct
上。
(2)未初始化内存:STACKLEAK / STRUCTLEAK / INITSTACK / RANDKSTACK 都能防护。
(3)有效数据 / 调用约定:在每个栈帧的底部都插入一个随机偏移,这样能防止攻击者猜中偏移使用到用户数据。如图。