这是一道关于 SYSRET
漏洞利用的一道题目,感觉非常有意思,在此仅做记录。
参考:
zolutal: corCTF 2023: sysruption writeup
Will's Root: corCTF 2023 sysruption - Exploiting Sysret on Linux in 2023
SYSRET — Return From Fast System Call
Vitaly Nikolenko: CVE-2014-4699: Linux Kernel ptrace/sysret vulnerability analysis
entry_SYSCALL_64 source code
THE INTEL SYSRET PRIVILEGE ESCALATION
这里默认读者对系统调用、中断异常故障有基本的了解,知道段选择子是什么、其特权级代表什么含义。如果不是很了解的话建议做一做 hxp CTF 2022: one_byte
这道题目,可以帮助你快速了解。但还是建议看下保护模式相关的书籍,其介绍的更加详细。
启动脚本如下:
看到 -cpu host
就想到 EntryBleed
,这个漏洞我记得在之前的 SCTF
似乎考过。所以这里的 kaslr
可以很简单地利用侧信道绕过。
FizzBuzz101
大师在题目中重新引入了 sysret
漏洞,其 patch
如下:
可以看到,这里删除了 sysret
执行前对返回地址 %rcx
的 canonical
检查。原来的意思是如果 %rcx
是一个 non canonical
地址,则跳转的 slow exit path [swapgs_restore_regs_and_return_to_usermode]
,否则执行 fast exit path [sysret]
。
那么什么叫做 canonical
地址呢?我们知道在 64-bit
时代,虚拟地址空间寻址只用了 48 bit
,因为 48 bit
的地址空间是足够的,并且对于 48 bit
的虚拟地址空间,只需要 4 级页表即可;而对于 64 bit
的虚拟地址空间,则需要 6 级页表,而页表查询是需要时间的。所以综合考虑,最终只使用了 48 bit
来寻址。那么这里就有 16 bit
没有被使用,而为了便于后续扩展,这里采用的方式是:
高 16 bit [48 - 63 bit]
必须和第 17 bit
相同,也就是说高 17 bit
必须相同,那么这些地址就叫做 canonical address
(其实就是有效地址)
所以最后的虚拟地址空间为:0~0x7fffffffffff
和 0xffff800000000000~0xffffffffffffffff
而一般而言:0~0x7fffffffffff
为用户态虚拟地址空间;0xffff800000000000~0xffffffffffffffff
为内核态虚拟地址空间
而可以看到 entry_SYSCALL_64
源码中对上述 canonical address check
的描述:
可以知道,当 SYSRET
返回到一个 non canonical
地址时,会在内核态触发 #GP
,而这本质上就是让用户接管内核,因为用户可以在用户空间控制 RSP
。当然这里不理解没关系,继续往下看就 ok
啦。
这里还是先把 entry_SYSCALL_64
函数过一遍,当然这个函数比较简单,并且注释很清楚,所以只会翻译重点注释:
sysret
指令的作用总的来说就是:
加载 rcx
到 rip
中
切换代码段选择子
来看下 Intel
和 AMD
手册对 sysret
的伪代码规范性描述:
可以看到在 Intel
规范中,如果 RCX
即返回地址不是一个 canonical address
的话,就会触发 #GP
,然而可以看到其 CS
选择子的设置却在 #GP
后面,也就是说在 #GP
抛出时 CS
特权级为 0, 即 #GP
是在内核态抛出的。
但是在 AMD
规范中,其是先设置了 CS
的选择子,所以其并没有对地址进行显式的 canonical
检查,因为就算后面进行指令预取时发现其为 non canonical address
也没有关系,因为此时的 CS
选择子的特权级为 3,最后 #GP
是在用户态抛出的。
这会造成什么后果呢?在上面 entry_SYSCALL_64
函数的分析中,我们说了在 sysret
执行前恢复了 rsp
并且没有对 rsp
的检查。而我们知道当特权级从低往高转移时,会利用 tss
中的相关 ss/rsp
进行堆栈的切换(当然具体实现时,似乎都没有使用 tss
,据说是因为其效率太低了),而由于 #GP
是在特权级为 0 抛出的,所以这里没有发生特权级的低到高切换,所以堆栈不会发生变化,即使用的还是之前的 rsp
。哪问题不就来了吗?之前的 rsp
是用户态可控的啊,所以最好的效果如下:
#GP
在 0 特权级执行
#GP
使用用户空间提供的堆栈指针
由于水平有限,最后漏洞利用完全参考 corCTF 2023: sysruption writeup
和 Vitaly Nikolenko: CVE-2014-4699: Linux Kernel ptrace/sysret vulnerability analysis
,而第一篇文章也是参考的第二篇文章,所以读者可以选择细读一下第二篇文章。
在文章中,其提到的用 ptrace
去触发漏洞,但是这里存在一定的限制,但其给出了解决方案,即:
这里给出文章中的 poc
:
这里可以简单测试一下:
结果如下:
可以看到这里的 RIP = entry_SYSRETQ_unsafe_stack+0x3/0x6
,说明确实是在 sysret
中触发的,并且这里的 RSP = 0xdeadbeef
,并且 CPU
特权级为 0,这些都是符合预期的。但是这里却发生了 double fault
,这是致命的。
难道是 0xdeadbeef
不是一个合法的地址,于是进行如下测试:
还是 double fault
:
所以这里似乎跟 rsp
的值没啥关系。
这里产生 double fault
的原因是 GP handler
非预期的使用了用户空间的 gsbase
,gsbase
寄存器是用来访问 percpu
变量的,比如在系统调用时,entry_SYSCALL_64
的第一条指令就是 swapgs
即切换到内核 gsbase
,然后返回时又调用 swapgs
切换到用户 gsbase
。
接下来看下 GP handler - asm_exc_general_protection
:
可以看到这里首先会调用 error_entry
:
如果你做了 one_byte
这题,这里的 calc
应该比较熟悉
首先可以看到这里会先 push regs
到栈中,寄存器的值是可控的,rsp
可控的,所以这里相当于任意内核地址写了(只是相当于)。
然后可以看到如果这里的 cs.cpl
是 3 特权级的话,就会执行一次 swapgs
,而我们知道漏洞触发后这里的 cs.cpl = 0
,所以这里就不会执行 swapgs
。而在之前的 entry_SYSCALL_64
分析中,我们知道在执行 sysret
之前已经执行过了一次 swapgs
:
所以这里 GP handler
使用的是用户态的 gs[gsbase]
,而 asm_exc_general_protection
后面会调用 exc_general_protection
:
而在 exc_general_protection
中用户态 gs
被首次使用从而导致 double fault
在 Vitaly Nikolenko
的文章中,其是通过覆写 IDT
表从而劫持 PF handler
到用户态代码,其文章是 14 年的,内核版本为 3.x
,但现在都 2024 年了,IDT
早已不可写了,而且 SMEP
也将直接限制内核直接执行用户态代码。
在 zolutal
的文章中,其提到既然是由用户态 gsbase
导致的 PF
,那么我们是否可以直接控制用户态的 gsbase
,让其指向一个内核地址从而防止 PF
。
而作者发现在 x86
中存在一个 fsgsbase
扩展通常是开启的【参考 intel
官方文档】,其可以让我们在用户态通过 wrgsbase
汇编指令去设置 gsbase
。
这里最稳定的做法就是将 user gsbase
设置为 kernel gsbase
,所以这里的泄漏 kernel gsbase
。而 kernel gsbase
在 physmap
中,所以这里也是利用侧信道泄漏,这里还是见 EntryBleed
,但是其似乎不是很稳定,所以 FizzBuzz101
调整了一下使其更加稳定了,主要就是调整了一下步距,具体见其文章。
测试可以发现,这里的 rsp
不能为用户态地址(好像说 pti
有 SMAP
的作用,所以这里会出现一些问题),然后简单设置 rsp
为内核可读写地址(其实就是需要栈的属性),然后发现并没有产生 double fault
:
当然这里产生的 #PF
可以暂时不管,这是由于 poc
中的一些参数没有设置好,这节的重点在于解决 double fault
问题。
在上述的分析中,我们得到了一个内核地址写原语。这里题目给了 kconfig
,查看可以知道其没有开启 CONFIG_STATIC_USERMODEHELPER
,所以这里可以尝试写 modprobe_path
提权或者拿 flag
。
第一版 exp
:
写 modprobe_path
前:
写 modprobe_path
后:
get_flag
:
可以看到最后在 get_flag
时,在 inc_rlimit_ucounts
中发生了 #PF
,既然是缺页故障,拿必然就是某个读取值存在问题了。
哪这里多半就是 rax
的值存在问题了,调试跟踪:
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2024-3-1 14:16
被XiaozaYa编辑
,原因: