写在最前面
最近在组会上进行了一次论文分享,考虑到以后博客上也不会再更新什么 CTF 相关的内容了,要开始慢慢的进行内容转型了,所以打算把当时组会分享的内容更新到博客上:)
这篇博客的语言风格将以我当时组会汇报时的风格进行编写,可能和之前的博客语言风格有较大的区别
System Register Hijacking
组会汇报时用的PPT:https://github.com/Qanux/paper-presentations/blob/main/20251106.pdf
今天我要分享的这篇论文来自 usenix 2025,讲的是 linux kernel 上的系统寄存器劫持,论文名为 System Register Hijacking: Compromising Kernel Integrity By Turning System Registers Against the System,可以在如下链接来该找到论文的原文:System Register Hijacking
这篇论文是我几天前偶然刷到的,我觉得挺好玩所以就将原本打算分享的那篇论文临时更改为这篇。考虑到这篇论文可能有人读过以及尽可能的在较短时间内讲透这篇论文,我这里只会对这篇论文的重点部分进行分享。
Kernel hijacking
Hijacking control flow:rop、cop、jop、call/jmp shellcode
Hijacking data streams:dirty pipe、dirty cred
黑客攻击 linux kernel 最常见的两种方式是劫持程序控制流和劫持程序数据流,劫持控制流比较常见,比如 rop、cop 以及 call 恶意代码等等。而劫持数据流则多用于向只读文件中写入用户自定义的恶意数据。
如图中 blackhat 的 ppt 就是通过 cve 修改 kernel 中的 file 结构体进而让普通用户可以越权向 passwd 文件中写入自己的数据。
而这篇论文中提出的系统寄存器劫持可以让劫持程序流更加简单。
传统内核攻击依赖通用寄存器,而内核存在系统寄存器直接控制 CPU 安全特性和关键数据如中断描述符表 IDT 和页表等等,下面列出了 cr0 和 cr4 寄存器所控制的一些安全特性。由此可见如果攻击者能过够劫持系统寄存器,就能够造成巨大的安全威胁。
CR0 相关特性
CR0.WP (写保护,bit 16):当置位时,内核态对标记为只读的页也会被禁止写入。常用于保护页表、只读代码/数据;
CR0.PG (分页启用,bit 31):启用分页机制,是所有基于页的隔离/权限(U/S、 NX 等)的基础;
CR0.PE (保护模式启用,bit 0):决定是否处于保护模式,现代系统始终开启;与安全隔离的基本前提相关。
CR0.AM (对齐掩码,bit 18,配合 EFLAGS.AC ):启用对齐检查(主要对用户态有效),降低某些未对齐访问的未定义行为风险。
CR4 相关特性
CR4.SMEP (Supervisor Mode Execution Prevention,bit 20):禁止内核态执行用户态页面中的代码(U/S=1),防止“内核跳到用户代码执行”的攻击路径。
CR4.SMAP (Supervisor Mode Access Prevention,bit 21):禁止内核态直接读写用户态数据(U/S=1),除非暂时通过 stac / clac 设置 EFLAGS.AC 允许访问;防止利用不安全的 copy 路径。
CR4.UMIP (User-Mode Instruction Prevention,bit 11):限制用户态执行 SGDT/SIDT/SLDT/SMSW/STR 等指令获取系统信息,缓解信息泄露与 KASLR 绕过。
CR4.FSGSBASE (bit 16):允许用户态执行 wrgsbase/rdgsbase/rdfsbase/wrfsbase 。
CR4.PAE (bit 5):启用物理地址扩展;在 32 位模式下要与 IA32_EFER.NXE 一起才能使用 NX (不可执行)位,实现数据执行保护(DEP)。
CR4.PKE (Protection Keys for Userspace,bit 22):启用用户态内存保护键(PKU),配合 PKRU 在用户态细粒度地禁/允读写执行,强化隔离策略。
Table 1: Security-Sensitive System Register-modifying instructions identified on x86-64 and aarch64, which mitigations or structures they control, and the preconditions necessary (SC is Stack Control, RC is Register Control). Representative gadgets for each instruction where valid gadgets that are short enough to display were present are listed beneath the associated instruction. * Their iret, popf, stac, and clac instructions are able to modify the AC bit in the EFLAGS register, which controls the status of SMAP.
论文的作者在 linux kernel 中找到非常多可以用于修改系统寄存器的 gadget,如上图表格所示,其中这些 gadget 包括 x86-64 和 aarch64 两种架构,这次论文分享则专注于 x86-64 这个结构。
| Architecture | Technique | Target Register | Prerequisites | Defense Bypassed |
|---|---|---|---|---|
| x86-64 | swapgs Stack Pivoting | GSBase | No register/stack control | FineIBT/kCFI |
| x86-64 | popf Extension + RetSpill | EFLAGS.AC | Stack control | SMAP |
| x86-64 | cr4 Hijacking | cr4 | Register control | SMEP/SMAP |
| x86-64 | cr0 Hijacking | cr0 | Register control | Write Protection (WP) |
| x86-64 | IDT Hijacking | IDTR | Stack control | Exception Handling Mechanism |
| AArch64 | PAN Hijacking | PAN MSR | Register control | PAN (Privileged Access Never) |
| AArch64 | SPSR_EL1 Hijacking | SPSR_EL1 | Stack control + Register control | PAN + Control Flow |
作者通过他找到的 gadget 提出了7种 linux kernel 基于劫持系统寄存器的攻击技术,主要分为两类劫持目标,一种是劫持特性控制寄存器去禁用一些安全机制进而让攻击者更好的进行漏洞的利用,另外一种则是劫持结构地址寄存器去控制一些非常重要的内核结构体进而去构造更加强大的利用原语。
1 | virtual_mapped: |
作者内核中找到了一些非常好用的 gadget 可以直接控制 cr0 和 cr4 这两个寄存器,如上面所示,攻击者可以利用这些 gadget 去修改 cr0 和 cr4 寄存器关闭一些保护机制进而降低 rop 链的编写难度,毕竟高版本的 kernel 比较好用的 gadget 非常少,通常为了控制一个通用寄存器需要多个 gadget 进行组合。
同时作者也提出可以用 popf;ret 这个 gadget 来让 rop 变得更加容易。当用户态切换到内核态时,os 会将用户态的寄存器保存在 kernel 的栈上,这个结构被叫做 pt_regs,这个结构在早期的 kernel 版本中在栈中的偏移是固定的
攻击者在可以在用户态提前在这些通用寄存器上布置好 rop 链,然后在内核态劫持程序流时直接栈迁移到这个栈上的结构进行 rop。但是通用寄存器的数量是有限的,也就是说我们 rop 的长度是有限的,而当我们的目的不再是提权而是进行容器逃逸比如 docker 或者 nsjail 逃逸时,rop 的长度将会变得非常的长
1 | void rop_chain(uint64_t* data){ |
上面的这个 rop 就是用来 docker 和 nsjail 逃逸的 rop,非常的长,利用那几个通用寄存器完全不足以用来布置这么长的 rop
作者提出这个时候就可以使用这个 popf;ret 妙妙小 gadget 了。攻击者可以现在用户态布置好较长的 rop 链,然后在 kernel 执行 rop 时使⽤ popf; ret 这个 gadget 暂时禁⽤ SMAP 检查来然后栈迁移至⽤⼾内存中的更⻓ rop 链,这将会大大的减短内核态中 rop 的长度。
1 | u64 chain[4]; |
如上面所示,攻击者只需通过 0x20 字节的内核 rop 链即可将内核的栈栈迁移至用户态进而执行提前在用户态布置好的更长的 rop。
早期对 linux kernel 中断向量的攻击是直接去修改中断向量的指针,而现在的 linux kernel 已经将这块区域设置为只读。作者发现 kernel 中存在一个叫 lidt 的 gadget 可以直接修改 IDTR.base 到一块攻击者可用的内存上,进而让每次中断/异常的“门描述符”都由攻击者提供,变相的让攻击者的获得任意地址 call 的利用原语。
然后这是这篇论文最精彩的部分,就是通过劫持 gsbase 来进行栈迁移。linux 中存在两个 gsbase 分别指向两个 gs 结构,一个在用户态,一个在内核态。在新版本的 linux kernel 中支持 wrgsbase 指令让用户可以设置用户态的 gsbase。gs 结构里面存放着 rsp 等重要信息。当用户态通过系统调用 syscall 进入到内核时,会先调用 swapgs 来进行用户态和内核态的 gsbase 的交换,然后从内核 gsbase 中取出内核栈的地址赋值给 rsp 寄存器。作者提出如果我们能够合理利用这个特性,就能够实现栈迁移。
当我们获得 kernel 任意地址 call 时,利用过程大致如下:
1 | // Store our malicious gsbase address in userspace's gsbase |
直接看可能有点抽象,所以这里我就简单的画了一个流程图:
这里我只进行简单的介绍,如果看不懂的话可以自己去调试一遍,我相信这将会极大的加深你对这种利用手法的理解
首先用户在一块可控的内核空间上伪造一个恶意的 gs 结构,并且将里面存放的栈地址指向恶意的即后面 rop 链的地址,然后使用 wrgsbase 指令将用户态的 gsbase 指向我们在 kernel 中伪造的 gs 结构
假设我们已经获取 kernel 任意地址 call 的原语,我们在内核态去调用 entry_SYSCALL_compat 这个函数,在继续讲之前先看看这个函数他做了什么:
1 | .text:FFFFFFFF82201A20 000 0F 01 F8 swapgs |
可以看见他先使用了 swapgs 来交换用户态和内核态的 gsbase,然后将对应的 gs 结构体里面的栈地址取出并赋值给 rsp,由于当前程序流处于内核态且 gsbase 是 kernel 的 gsbase,所以执行 swapgs 后当前的 gsbase 就指向用户态的 gs 地址,然后将其 gs 结构的栈地址取出赋值给 rsp。我们前面已经将用户态的 gsbase 指向了用户伪造的恶意 gs 结构,且栈地址被我们修改为我们用于布置 rop 的地址,也就是说执行完 entry_SYSCALL_compat 可以直接将栈迁移到我们的提前布置好的 rop 上。由于我们的程序流还是在内核态而 gsbase 指向用户态的 gs 结构,很容易会出现一些奇奇怪怪的现象,所以 rop 在一开始就要先调用 swapgs; ret 来将 gsbase 切换为内核态的,然后后面就是正常的 rop 了:)
作者直接拿原有的几个 cve 的 exp 进行修改,将最后劫持程序流利用那部分修改成他提出的 swapgs Stack Pivoting 方法进行对比成功率,发现大部分的情况成功率都不变,少部分情况成功率出现了小幅度的降低,可见这种利用方法是可行的,至于实用性,他好像没有说🤣
然后作者提出他的这个 swapgs Stack Pivoting 利用手法是唯一一个可以从前向边缘来绕过 FineIBT 保护机制的(我很怀疑🥲,但是汇报时没有提出来)
说到 FineIBT,那肯定要先介绍 IBT,在说 IBT 前那肯定要先介绍 CET
CET 机制是 Intel 提出的⽤于缓解 ROP/JOP/COP 的新技术。基于硬件⽀持的解决⽅案,旨在预防前向( call/jmp )和后向( ret )控制流指令劫持。
用于预防后向控制流指令劫持的方法是 shadow stack,这并不在今天的讨论范围内,如果感兴趣可以自行上网去查找资料。而用于预防向前控制流指令劫持的方法则是使用 IBT。IBT 为程序每个有可能被 call 或者 jmp 的地方加了个 endbr 指令,当程序执行了 call 或者 jmp 指令的时候下一条指令必须是 endbr,否则就会抛出 #cp 异常。这极大的限制了攻击者去 call 或者 jmp 一些比较利于利用的 gadget,但由于程序中拥有大量的 endbr 指令,所以攻击者依然可以通过更加曲折的路径来进行利用链的构造,依然能够进行向前控制流指令劫持,所以这个时候就有人提出了 IBT 的加强版本即 FineIBT
关于 FineIBT 的论文可以在如下链接进行查看:FineIBT 这里只会进行简单的描述
每个函数都会有一个自己的 SID 值,当程序在 call 或者 jmp 到一个函数前会先设置一个 SID 值来表示他要 call 或者 jmp 的是这一类 SID 的函数,每个函数的开头会先进行 SID 的校验,如果 SID 不一致程序将会停止运行,而 SID 一致的话则会继续执行该函数。
如上图所示,main 函数在 call 之前将 SID 设置为 0xc00010ff,即将 0xc00010ff 赋值给 eax 寄存器。当 call 的函数为 func0 时,通过检验发现 eax 寄存器的指为 0xc00010ff,然后跳转到 func0_entry 来继续执行 func0 函数。而如果 main 函数 call 的是 func1 函数时,由于 SID 校验不通过(func1 SID 的值为 0xbaddcafe),则会向下执行 hlt 指令来让程序停止运行。
而在 linux 中 FineIBT 长下图这个样:
每个(其实也不是)函数都会有一个对应的 _cfi 前缀的函数,这个函数第一条指令为 endbr,然后后面接着的是 SID 校验,当校验成功时才会跳转到真正的函数入口,而真正的函数是没有 endbr 指令的,所以我们无法通过任意地址 call/jmp 原语来直接调用他。
介绍完 FineIBT 后,来看看作者是如何绕过这个保护的。原理很简单
如上图所示,作者发现有许多与 syscall 入口相关的函数都有 endbr 指令但是不需要进行 SID 校验,而刚好他提出的 swapgs Stack Pivoting 用的就是这些函数,所以原理就是这么简单🤣🤣🤣
作者在后面也提出了他前面所提出的各种利用方法对应的缓解措施,对于这个 FineIBT 绕过的 Mitigation 就是给这些函数都加上 _cfi 🤣🤣🤣
这里有个比较有趣的现象是当我们直接去 IDA 中去看这个 _cfi 函数时会发现里面全是 nop,而真正的函数入口处有 endbr,根本起不到防御的作用,而当我们在启动的时候开启了 FineIBT 选项后 kernel 在加载进内存后他会动态修改他的代码段,在 _cfi 上加上 endbr 以及一些 SID 的校验操作,然后将真正的函数入口处的 endbr 改成 nop,如下图所示:
作者在论文的最后提出了应对各种利用手法的 Mitigation,具体内容如下:
cr0/cr4:
扩展 CR-Pinning: 把关键控制寄存器的安全位(如 CR0.WP 、 CR4.SMEP/SMAP )固定为期望值,并对任何修改这些位的尝试进行拦截、校验或拒绝
lidt:
这些 lidt gadget可以通过强制其值始终设置为它们所在的位置的常量虚拟地址来缓解。在现代 PML4 Linux 上,这始终是 0xfffffe0000000000。在任何 lidt 指令后对值进⾏后检查并重置其值应 该⾜以防⽌其被滥⽤。
popf:
该类指令数量太多,暂无较好的方法
swapgs:
对来⾃⽤⼾空间的 GSBase 值 进⾏检测
FineIBT bypass:
给漏网之鱼函数都加上_cfi_
仔细一看这些 Mitigation 还真的是“简单粗暴”啊🤔
说到底,这篇论文提出的各种系统寄存器的劫持手段在非常多年前就给用所用过了。可能让人比较陌生的是对 gsbase 的劫持,可是这玩意也在 2014 年的一个 cve 上被人利用过,以及在许多高质量的 CTF 比赛中都被拿来出过题,我在下图中简单的举了几个例子,感兴趣的话可以去看看:
所以这篇论文的创新点在哪里呢,就是找到了几个比较好用的 gadget 和提出了一些非常“简单粗暴”的 Mitigation?🤔
那我就很纳闷了,为什么这也能够发顶会,于是我和我的小伙伴们展开了讨论:
最终结论是作者的文笔比较好,所以能发顶会🤣
写道最后
如果以后我组会分享的内容我感觉有意思的话应该也会更新到博客上来:)
reference
CET
https://linuxkernel.org.cn/doc/html/latest/arch/x86/shstk.html
CFI
https://www.cnblogs.com/Iflyinsky/p/18328698
https://tjtech.me/kernel-cfi-failure-analysis.html
https://clang.llvm.org/docs/ControlFlowIntegrity.html
BTI
https://zhuanlan.zhihu.com/p/370947458#:~:text=BTI%E7%9A%84%E5%85%A8%E7%A7%B0%E6%98%AFbranch%20target%20indentificating%EF%BC%8C%E6%98%AF,Armv8.5%20%E5%A2%9E%E5%8A%A0%E7%9A%84%E9%99%90%E5%88%B6%E6%94%BB%E5%87%BB%E7%9A%84%E5%AE%89%E5%85%A8%E7%89%B9%E6%80%A7%EF%BC%8C%E4%B8%BB%E8%A6%81%E6%98%AF%E6%9D%A5%E8%A7%A3%E5%86%B3JOP%EF%BC%88jump-oriented%20programming%EF%BC%89%E8%BF%99%E7%A7%8D%E6%94%BB%E5%87%BB%E6%96%B9%E5%BC%8F%E3%80%82
FS/GS
https://zhuanlan.zhihu.com/p/435518616
https://zhuanlan.zhihu.com/p/434821566
percpu
https://zhuanlan.zhihu.com/p/363969903
IBT
https://cn-sec.com/archives/828785.html
THP
https://www.modb.pro/db/402621?utm_source=index_ai
FineIBT
https://dl.acm.org/doi/fullHtml/10.1145/3607199.3607219
https://blog.csdn.net/Linux_Everything/article/details/146357554
System Register Hijacking
https://hongkai.org/slides/sec25_System_Register_Hijacking_slides.pdf
Swapgs
https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458305132&idx=1&sn=be19372c359a3de431828ec1448dc93a&chksm=b181f2e686f67bf0cd07fe04cb761349334caca84469f600163d1d32f7003ee62f306647bc20&scene=27
Retspill
https://zhuanlan.zhihu.com/p/699064533
CR-Pinning
https://patchew.org/linux/20251007065119.148605-1-sohil.mehta@intel.com/20251007065119.148605-6-sohil.mehta@intel.com/