DEF CON CTF Qualifier 2017 Writeups
I thought I would share my solutions to some of the reversing challenges of last weekend’s DEF CON CTF Qualifier.
In particular, I solved a series of challenges which all shared the same common divisor: automatic reversing. These challenges also happened to have very similar (almost trivial) solutions (I could use the exact same script to solve some of them). I found this surprising, to the point where I started wondering whether this was the intended solution.
crackme
crackme was the first one of the series. We were given a binary to reverse, and we had to connect to a server to provide the solution we found.
Running the binary would give:
Entering some (wrong) input would just exit() with status 1.
A quick look at main with IDA Pro shows a pretty regular function, with no branches.
“enter code:” is printed, some string $s$ is read from stdin, “0xC6C” is called (note it is a position-independent binary, hence the oddly small addresses), and “the sum is {}” is printed. Since the last phrase was not printed when we first run the program, there must be some branching&exit happening within 0xC6C.
I immediately thought of solving this with angr. Unfortunately, this did not work, as it returned deadended paths. Why this happened is something I want to investigate in the future (see “Some thoughts” at the end of this article).
0xC6C is a long subroutine, with many calls other subroutines; a closer look reveals that each of them gets as input a character of $s$, in order, and checks it is the “correct” one.
Something helps even more: the exit code when no input byte is correct is 1, when the first one is correct is 2, and so on:
0x93B:
0x955:
I did not investigate this any further, and wrote a python script to bruteforce the solution byte by byte.
We run it, get “yes and his hands shook with ex” as a solution, and send it to the server base64-encoded. So this solves the first one.
magic
This one was an extension of crackme, where many binaries were given to reverse, and the server would interactively ask to reverse some. In fact, I reckon crackme was created just as a subcase of this challenge by the organisers (un-tared folder names were also the same).
To the previous code, we put the bruteforcing code in a function, and add the following to automate the query/response process:
Note that I put the binaries in targets/ before running the script.
sorcery
sorcery, instead of returning exit codes in sequence, would return $-6$ for one of the string.printable bytes. It took me a while to understand that I just needed to add one more byte to the string when bruteforcing the bytes.
That is, brute() would look exactly like before, but it would call something like:
alchemy
Exact same script used for sorcery. See above.
witchcraft
The primary difficulty with this challenge (although the difficulty was not part of the challenge itself) was finding a version of Swift that could run on my Debian (jessie). I solved this by installing swift-3.0-RELEASE-ubuntu14.04, rather than swift-2.0 that apt-get was offering, and rather than swift-3.1 which just wouldn’t work. Figuring this out took much longer than I wanted.
In this challenge, exit codes would change a bit to trick the reversing algorithm: wrong characters would return some $x$, the correct one would return $x’$, but this would change w.r.t. the position of the character in the string.
No problem. Simply, find the “common” exit code given some string prefix $s$:
This function returns what is the exit code given the prefix $s$ is wrong. find_common_code() should be called by brute() instead of setting prev_r=r, as follows:
And this solves the challenge.
enlightenment
Among those mentioned here, this was the challenge giving the highest scores. After playing around with it, I realised that:
- some binaries could be reversed as before (e.g., see witchcraft)
- some could not
Since the server queried solutions for random binaries, I tried running the previous code hoping for some luck. It did not work.
Looking closer at the exit codes, I realised that the behaviour was similar to the previous challenges: for one character the exit code would be different. However, appending it to a string would always return the same character ad libitum, so that solutions would look like “mmmmmmmmmmm…”. Nice, I like patterns.
The first step was to modify the bruteforcing function to halt when the solution was exhibiting this pattern:
Then I finally realised that the exit code would consider characters in the reverse order. Thus I wrote an alternative bruteforcing function, which would prepend (rather than append) to the solution.
The final code for this challenge does exactly as before, but it first tries brute(), and it tries brute_reverse() if the former failed.
Some thoughts
These challenges were definitely meant for automated reversing. However, if it hadn’t been for the weird patterns in the exit codes, the ideas presented here would have not worked. Whilst it may happen, “in the real world”, that exit codes help figuring out something about a program, I think it’s worth considering alternatives.
I could see two alternative methods for solving the challenges, which are independent of the weird exit codes’ behaviour:
- symbolic execution (e.g., angr)
- side channel (e.g., bruteforcing the solution byte by byte by counting the number of instructions executed (or the time to execute them) given each candidate character).
I did try both approaches with generic scripts I wrote some time ago, but they did not work. I’ll try to understand why.
Update (04/05/2017)
A great writeup shows how to solve magic with angr. Turns out that the binary was using musl as a replacement for libc, and in order for angr to do its magic (sorry for the easy pun) we needed to hook musl’s symbols to libc. I suspect that this was the solution the challenge author was hoping for.