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!


  1. Nice work,
    how to know sys_addr = read_addr - 0x9ef70
    should I download all the version of libc files?It seems hard to find..

    1. I found the copyright string in their libc version, which told me that they have "Ubuntu EGLIBC 2.15-0ubuntu10.3". I downloaded that and measured the offset between the read and system functions.

  2. This comment has been removed by the author.

  3. nice! what did you decompile the binary with?

    and how did you see the password was "letmein"?

  4. strncpy(mmap_buf, (const char *)&buf_2, 0x80u);
    -> Will it overwrite from mmap_buf + 33 --> nmap_buff + 39 ??? (Your mess...)

    1. Sorry, where I used "mmap_buf + 33", 33 was in a DWORD context. The strncpy function will write 0x80 bytes from the beginning of mmap_buf, and the terminating zero will be written to mmap_buf+0x80, which is the first byte of ((DWORD*) mmap_buf + 33). The overflow is only 1 byte here, but it's enough.

  5. what do you mean "original address" ? i still dont understand. can you explaint it clearly ?
    thank you in advance.

    1. Again, sorry for using DWORD and byte addresses all over the place. By original address I meant the address located at mmap_buf+132 (in bytes). We can overwrite the first byte of this address, and since it's little-endian, that will actually mean the last byte of the address. This address points to the format string that will be used with sprintf and then puts. If we control this format string, we have found a vulnerability.

  6. I got it ! Thank you
    But i still stand questions. I cant' set break point in gdb to debug this file. It said "No symbol table is loaded. Use the "file" command." That's why i can't see the stack. How can I debug this file in gdb ? I'm using Ubuntu 12.04
    0xc10 - What is this number mean ? How could you get it ?

    Thank you very much !

    1. I just used 'disas main', then put breakpoints to different places using 'break *main+XX' where XX corresponds to a line index in the disassembly.

  7. if we overflow 0x00 to the last byte of the address, it will point to the beginning of our input
    --> For exampe, if mmap_buf is at 0x08000010, then the value at (DWORD) 0x08000010 + 32 will be 0x08000094 (mmap_buf + 132). So, if we overflow 0x00 to the last byte of it, its value will be 0x08000000 (94 will be replaced), different from 0x08000010 - the beginning of our input?!
    What's wrong here ?

    1. That's almost true, except the value will be 0x08000000 instead of the original 0x08000094, where the format string is.

      Here is an image that shows what happens whit a buffer at 0x08000000: Green represents the address of the original format string. The value at the yellow cell points to the original format string, but notice that if we write exactly 128 chars, like in the picture, it will be replaced with 00 (not shown yet in the picture) and thus point to the orange cell instead.

    2. Actually it's at 0x11111000 :)

    3. Yeah, it's allocated by '(char *)mmap((void *)0x11111000, ...)'

  8. How could you know read_addr = base_address - 0xf5c60 ?

    1. I found the same version of libc and measured the distance.

    2. base_address you mean ebp ? it's stack address, i dont understand what does it relative to read_addr ?

    3. No, the base address means the address where libc is loaded.

  9. You said "We will have to subtract 0xc10 from this value to get the base address"...but on my machine i am getting 0x19a63 difference . Am i missing something ?

    1. Hi! I think it only depends on your libc version