Sunday, February 9, 2014

Olympic CTF - pwn300 (echof) writeup

The vulnerability

This task was a pwnable for 300 points, and it was fun to exploit. The name contains a big hint: there is a format string vulnerability in the program. Here are the relevant parts of the decompiled code:
for ( i = 0; i <= 15; ++i ) {
    bzero(&buf_2, 0x100u);
    read(0, &buf_2, 0x80u);
    buf_2_len = strlen((const char *)&buf_2);
    if ( strchr((const char *)&buf_2, 'n') ) {
        result = puts("i hate this symbol!");
    mmap_buf = (char *)mmap((void *)0x11111000, 0x1000u, 3, 50, -1, 0);
    *((_DWORD *)mmap_buf + 33) = 'ruoY';
    *((_DWORD *)mmap_buf + 34) = 'sem ';
    *((_DWORD *)mmap_buf + 35) = 'egas';
    *((_DWORD *)mmap_buf + 36) = 'd%( ';
    *((_DWORD *)mmap_buf + 37) = 'tyb ';
    *((_DWORD *)mmap_buf + 38) = ':)se';
    *((_DWORD *)mmap_buf + 39) = '\ns% ';
    mmap_buf[160] = 0;
    *((_DWORD *)mmap_buf + 32) = mmap_buf + 132;
    strncpy(mmap_buf, (const char *)&buf_2, 0x80u);
    mmap_buf[buf_2_len] = 0;
    sprintf((char *)&buf_2, *((const char **)mmap_buf + 32), buf_2_len, mmap_buf);
    puts((const char *)&buf_2);
    result = munmap(mmap_buf, 0x1000u);
Some key points:
  • the read function will read 0x80 bytes and it doesn't use a terminating null byte.
  • we can't use %n in the format string, so no arbitrary write yet
  • (_DWORD *)mmap_buf + 32 will represent the beginning of the format string, and this is what we pass to the sprintf function
  • the original address is mmap_buf + 132, which leaves 128 bytes for the user input and 4 to store this address
  • there is a one byte overflow in strncpy, since unlike read, it does use a trailing zero
  • if we overflow 0x00 to the last byte of the address, it will point to the beginning of our input, and it will be interpreted as the format string
  • we can do this by passing exactly 128 bytes of data

The exploit

The vulnerability is clear now, but actual exploitation is many steps away. Since we can't use %n, we have to transform this format string vulnerability into a stack-based buffer overflow. Here are the steps that we have to take to do that.

Leak the base address

Since ASLR is on, we have to leak a base address to know where we are in the memory. After looking at the stack in gdb, we can come up with the following payload:
"%79$08x" + "_" * 120 + "\n"
We will have to subtract 0xc10 from this value to get the base address. We also have to leak a stack address in the same fashion.

Find the return address

If we look at the stack in gdb, we can see that the return address is ~272 bytes after the user-supplied, formatted string on the stack. With this payload, we can overwrite the return address along with the first argument that we will get when we jump to the new return address:
"%0162x" + "_" * 110 + addr * 2 + arg

Defeat the stack protector

There is a stack cookie that we have to bypass. We can just leak it and overwrite it carefully when we are overflowing to the return address, but there is a catch: the last byte is always 0x00. Since we can send a message 16 times, it is easy to solve this though. When the return address is written, we overwrite the stack cookie with the leaked value, but put 0x41 (or anything really) to the last byte. On a second try, we overflow just enough bytes that the terminating zero will be placed to the last byte of the cookie.

To leak the stack protector, we use "%78$08x" + "_" * (...)
To place a terminating null char at the end of the cookie: "%0134x" + "_" * 121 + "\n"

Finding libc

This took me by far the most time, I guess I suck at finding libc versions. I ended up doing the same thing as before (see the writeup here): I started leaking libc addresses as strings and found the copyright string.

First, to get an address from libc we have to look at the got. The first address there will be for the read function. To read at address 0x41414141, we can use this input:
AAAA + "_"*4 + "%14$08x" + "_" * 111 + "\n"
After a bunch of tries, I found the copyright string. The libc version was "Ubuntu EGLIBC 2.15-0ubuntu10.3". To confirm, I calculated the offset between fflush and read and checked it on my local copy. It was a match. Then I checked the address of the system function and leaked the beginning of its bytecode just to check that it wasn't removed. Luckily it was intact, so we came really close to the final exploitation.

Final exploit

The steps for the final exploit:

  • Leak base address, stack protector and a stack address
  • Overwrite stack protector (with the mask), return address, argument to system and the payload itself
  • Put the zero byte in the stack protector
  • Finish the program so that the main function returns to system

Here is the payload for the first and second steps:
"%79$08x %78$08x %10$08x " + "_" * 103 + "\n"
"%0176x" + "_" * 80 + (stack_protector | 0x41) + "_" * 12 + ret_addr * 2 + (stack_addr + 288)  + "_" * 4 + "cat flag;#\n"
Actual code for the exploit can be found here:

Thanks to More Smoked Leet Chicken for the CTF. I only got to solve this task, but it was great!