Tasteless CTF 2019

I competed in Tasteless CTF this weekend with redpwn. We solved one challenge, House of Bad Taste, which was an interesting glibc heap pwn.

House of Bad Taste

Flag

tctf{p01nt3r_c00k1e_b3st_c00ki3!}

Analysis

As usual, the first step is to run checksec against the binary.

$ checksec chall
[*] '/pwn/tasteless19/house-of-bad-taste/chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

$ strings libc.so.6 | grep GNU
GNU C Library (Ubuntu GLIBC 2.29-0ubuntu2) stable release version 2.29.
Compiled by GNU CC version 8.3.0.

With all protections enabled, it’ll probably be another heap exploit. GLIBC 2.29 means tcache protections are enabled.

The next step is to decompile the binary with Ghidra.

Vulnerability

Note that free doesn’t actually zero the array pointers, it merely zeroes the bit in the bitmask.

    free(ptrs[uVar1 & 0xffffffff]->buffer);
    free(ptrs[uVar1 & 0xffffffff]);
    bitmask = bitmask & ~(byte)(1 << ((byte)uVar1 & 0x1f));

While most of the other functions check the bitmask, edit does not, only checking for null pointers.

    pnVar1 = ptrs[i & 0xffffffff];
    if (pnVar1 == NULL) {

Thus, our vulnerability is that we get to call realloc on a freed tcache chunk.

      sVar2 = get_size();
      pnVar1->size = sVar2;
      pcVar3 = (char *)realloc(pnVar1->buffer,pnVar1->size);

Exploit

What does pnVar1->buffer equal on a freed tcache chunk though? It turns out, the additional protections place a pointer to the tcache bin on the freed chunks themselves. This pointer (at offset 8) aligns perfectly with ->buffer! (Note that tcache bins are stored in a chunk on the heap).

Thus, we can completely control the tcache bin if we realloc, and then read into it. Controlling the tcache bins are quite powerful, as we can modify both the tcache pointers and the bin size counts.

We get a libc leak by setting a tcache bin size count to a large number, and then freeing the corresponding sized chunk (assuming the tcache bin is large enough to go into the unsorted bin). Note that the show functions don’t stop at a null byte (I used the hex option for the leak in my solve script).

write(1,ptrs[(ulong)uParm1]->buffer,ptrs[(ulong)uParm1]->size);

After getting a libc leak, we position a tcache bin under __free_hook, and overwrite it with system. Finally, we free a chunk with “/bin/sh” to get system("/bin/sh").

My full solution script can be found here.