DEF CON CTF Qualifier 2017 Writeups

1 May 2017, Giovanni Cherubin

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:

$ ./4a2181aaf70b04ec984c233fbe50a1fe600f90062a58d6b69ea15b85531b9652
enter code:

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import string
import subprocess

CMD = './4a2181aaf70b04ec984c233fbe50a1fe600f90062a58d6b69ea15b85531b9652'

def test(s):
	"""Execute the command CMD with "s" as input,
	return the exit code.
    """
    child = subprocess.Popen(CMD, stdout=subprocess.PIPE,
                             stdin=subprocess.PIPE)
    child.communicate(s)

    return child.returncode

if __name__ == '__main__':
    prev_r = test(s)        # Previous return code

    found = False
    while not found:
		for c in string.printable:
            r = test(s + c)
            if r != prev_r:
                s += c
                prev_r = r
                if r == 0:
                    found = True
                break
        else:
            # In case we tried all characters but it didn't work
            break
    print("found: {}".format(s))

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from pwn import remote

def brute(fname):
    s = ''
    prev_r = test(s, fname)

    while True:
        for c in string.printable:
            r = test(s + c, fname)
            if r != prev_r:
                s += c
                prev_r = r
                break
        else:
            return s

def do(r):
    f = r.readline().strip()

    if "The flag is" in f:
        print(f)
        return True

    print("solving for {}".format(f))
    sol = brute(os.path.join('targets',  f))
    print(sol)
    # Send solution
    r.sendline(base64.encodestring(sol).strip())
    
    return False

if __name__ == '__main__':
    r = remote(host, port)
    found = False
    while not found:
        found = do(r)

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:

r = test(s + c + '\x00', fname)

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$:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def find_common_code(s, fname):
    # We just need to test for three characters.
    # It doesn't matter what these characters are (as long as
    # they are different).
    r1 = test(s + 'a', fname)
    r2 = test(s + 'b', fname)
    r3 = test(s + 'c', fname)
    if r1 == r2:
        return r1
    if r1 == r3:
        return r1
    if r2 == r3:
        return r2
    else:
        raise Exception("your assumptions are wrong")

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
def brute(fname):
    s = ''
    std_code = find_standard_code(s, fname)

    while True:
        for c in string.printable:
            r = test(s + c, fname)
            if r != std_code:
                s += c
                std_code = find_standard_code(s, fname)
                break
        else:
            return s

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:

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def brute(fname):
    s = ''
    std_code = find_standard_code(s, fname)

    while True:
        for c in string.printable:
            r = test(s + c + '\x00', fname)
            if r != std_code:
                s += c
                std_code = find_standard_code(s, fname)
                break
        else:
            return s

        # If the first 6 characters of the solution are the
        # same, we failed
        if len(s) >= 6 and len(set(list(s))) == 1:
            return None

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def brute_reverse(fname):
    s = ''
    std_code = find_standard_code(s, fname)

    while True:
        for c in string.printable:
            r = test('\x00' + c + s, fname)
            if r != std_code:
                s = c + s
                std_code = find_standard_code(s, fname)
                break
        else:
            return s

        # If the first 6 characters of the solution are the
        # same, we failed
        if len(s) > 6 and len(set(list(s))) == 1:
            return None

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:

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.