首页
社区
课程
招聘
[原创] 以 corCTF 2023 sysruption 学习 sysret bug 的利用
发表于: 2024-2-1 17:06 12559

[原创] 以 corCTF 2023 sysruption 学习 sysret bug 的利用

2024-2-1 17:06
12559


这是一道关于 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 执行前对返回地址 %rcxcanonical 检查。原来的意思是如果 %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~0x7fffffffffff0xffff800000000000~0xffffffffffffffff

而一般而言:0~0x7fffffffffff 为用户态虚拟地址空间;0xffff800000000000~0xffffffffffffffff 为内核态虚拟地址空间

而可以看到 entry_SYSCALL_64 源码中对上述 canonical address check 的描述:

可以知道,当 SYSRET 返回到一个 non canonical 地址时,会在内核态触发 #GP,而这本质上就是让用户接管内核,因为用户可以在用户空间控制 RSP。当然这里不理解没关系,继续往下看就 ok 啦。

这里还是先把 entry_SYSCALL_64 函数过一遍,当然这个函数比较简单,并且注释很清楚,所以只会翻译重点注释:

sysret 指令的作用总的来说就是:

加载 rcxrip

切换代码段选择子

来看下 IntelAMD 手册对 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 writeupVitaly 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 非预期的使用了用户空间的 gsbasegsbase 寄存器是用来访问 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 gsbasephysmap 中,所以这里也是利用侧信道泄漏,这里还是见 EntryBleed,但是其似乎不是很稳定,所以 FizzBuzz101 调整了一下使其更加稳定了,主要就是调整了一下步距,具体见其文章。

测试可以发现,这里的 rsp 不能为用户态地址(好像说 ptiSMAP 的作用,所以这里会出现一些问题),然后简单设置 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编辑 ,原因:
收藏
免费 2
支持
分享
最新回复 (2)
雪    币: 6008
活跃值: (2585)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
2
咋删贴啊,咋投到 茶余饭后 板块去了
2024-2-1 18:05
0
雪    币: 3070
活跃值: (30876)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
感谢分享
2024-2-2 15:22
1
游客
登录 | 注册 方可回帖
返回
//