Solving the Game
Wibbly wobbly timey wimey, or “wwtw” was a two point pwnable from Defcon quals this year. I worked on this challenge with my teammate, @MarvelousBreadchris. Running it right away shows us a little game screen:
You(^V<>) must find your way to the TARDIS(T) by avoiding the angels(A). Go through the exits(E) to get to the next room and continue your search. But, most importantly, don't blink! 012345678901234567890 00 01 02 03 ^ A 04 05 06 A 07 AA 08 E 09 10 11 12 13 14 15 16 17 18 A 19 A Your move (w,a,s,d,q): Too slow!
It’s actually a pretty easy game with five levels. Based on previous experiences with games in CTF challenges, I figured the real challenge was two-fold: first automate the game, and then exploit the immediately visible vulnerability. I was correct about the first part, but not entirely so about the second.
Getting the Password
Luckily for me, I have a @MarvelousBreadchris on my team, so I didn’t have to write the game solver. As soon as you solve the game and get to the “tardis” though, you get a password prompt. Getting it wrong causes the game to exit. Here’s where it gets a bit tricky: if you use a debugger and step through the password function one instruction at a time, you might find that you end up with the wrong password.
Basically, the function reads the raw bytes of itself, comparing your input to the printable bytes in the function. The problem only appears when you set a breakpoint on the function to extract the password, and try using that password without a debugger attached. The secret is in how debuggers work. On x86/64 architectures, debuggers place an
int3 instruction at the target breakpoint by writing a 0xCC byte. This changes the actual byte that ends up appearing in the function, basically working as a “hidden in plain sight” anti-debugging trick. To get the password, I wrote up an idapython one-liner (0xeb8 is the address of the function):
And we have the password:
Once in the “tardis,” you’re given a prompt, but trying to do anything useful doesn’t work:
Welcome to the TARDIS! Your options are: 1. Turn on the console 2. Leave the TARDIS Selection: 1 Access denied except between May 17 2015 23:59:40 GMT and May 18 2015 00:00:00 GMT
Clearly we couldn’t travel to a day before the CTF, so we needed to do something else. Doing some more reversing, I spotted this oddity:
Basically, it does something like this:
... inside the main loop after the game ... tardis_menu(); // print the menu // user_input is a 12 byte array of 4 bytes bzero(user_input, 8); // zero out eight bytes (user_input and user_input) read(stdin, user_input, 9); // read nine bytes
It zeroes out eight bytes of
user_input, but reads in nine. This means the ninth byte is never erased via bzero. Now we just need to figure out what the ninth byte of
user_input is used for. Continuing on with more reversing, I spotted a function,
time_vortex_thing, which is set via
alarm() to execute every two seconds. Here’s the interesting bit:
We can see it’s using
user_input as the file descriptor for
read. For the sake of “brevity”,
user_input actually holds the file descriptor for a loopback socket, which we don’t have access to as an outside attacker. This is unfortunate, because in the second block of the last screenshot, we see that the input on this socket is directly written into
current_timestamp - what the “tardis” uses to confirm the current time.
However, using the fact that we can send in nine bytes and overflow into
user_input, we actually control the file descriptor!! Using the overflow, we can redirect this time traveling bit to read from stdin, instead of the loopback socket! We’re time traveling!
After passing in a timestamp within the accepted range, a third option is unlocked:
After reading this function, it’s pretty clear what we need to do.
(You might need to click the picture to zoom in)
The teleport function reads a pair of coordinates and calls
atof() on it twice, once before the comma, and once after it. After it gets the floating point version of your strings, it does some comparisons (the
fucomip). If your coordinates match the “bad” coordinates, it prints some strange error message about time and space. The issue is with how it’s printed: it calls
printf() with your input (string form) as the first parameter!!!
We now have a controlled format string vulnerability that lets us basically read a limited set of memory, and write anywhere. The reading bit is really important here, because the binary is running both with ASLR and PIE - we don’t know where anything is located before hand. By passing in format specifiers, we can read addresses off the stack, disclosing things such as return addresses (which would tell us where the binary is located in memory), stack cookies (which would let us overwrite the return address on the stack without stack smashing protections stopping us), and more importantly, libc addresses on the stack (which gives us access to the entirety of libc).
My initial version of the exploit involved leaking out an address from the binary, using the controlled format string to write an to write a ROP chain to memory, and then using the format string again to replace a function in the GOT with a stack pivot that lands me in my ROP chain. It was a hueg pain in the butthole, so I decided to use the libc leak instead.
Because there is a
strchr called on your input at every iteration, I decided to overwrite its GOT entry with the libc address of
system. To do that, I used the controlled format string to leak a libc address from the stack. I wasn’t sure what it was the address of at first, but comparing against my local copy of libc, I saw it was
_IO_2_1_stdout_. Because I knew how far
_IO_2_1_stdout_ was from the base of libc, I was able to calculate the base, and then I was able to calculate where
system was in relation to that. The issue with this is that this only works for my local copy of
libc.so.6. Luckily, having solved another exploitable challenge, I just grabbed the
libc.so.6 from that system and used it to calculate the appropriate offsets.
Here’s the final exploit code (adjusted for my VM offsets):