Blog of Malte Kraus


th3jackers CTF: Crypto 300 and Reversing 100 write-up

24 Jan 2015

So I don't really have much time for playing CTFs right now, but yesterday I took it for the "th3jackers" CTF. I solved the Reversing 100 and Crypto 300 challenges.

Crypto 300

For the Crypto 300 challenge, there were two files given, a wat.png that was no valid PNG file and a key.txt that said:

the key is : aniesse

Looking at wat.png in a hex editor, I saw the string "ssea" somewhere near the end so I suspected the file was just XORed with the (repeated) key. Undoing that gave in fact a valid PNG file:

A scrambled QR code

That looks like a scrambled QR code, but exactly how is it scrambled? I was scratching my head for a while until I threw strings at it to find the following text in it:

----- IMAGE ORDER ----------

These lines are encoded with Base64, so undoing that gives us:


So apparently the original QR code was split into 9x9 small images and then those images were scrambled around in the order given above. Note that every 3rd line has two 9s instead of a 6, so some information went missing.

So I wrote a script to do the unscrambling and the QR code had lost too much information to decode. I refined my script a bit - my first version split the image in a 3x9 pattern which left quite some artifacts due to rounding differences - but that didn't solve the problem. So I thought maybe instead of leaving whatever image data is at the position of those 6s, I should gray them out so that I don't give any wrong black/white information to the QR decoder. The result is an image like this, that still doesn't decode:

QR code with some small gray areas

If you look closely at the edges of the 2nd and 3rd gray box, you can see that the gray boxes don't align with the 15x15 pixel sized boxes that make up the QR code. For example, below the box in the middle we can see a small stripe of black. So we know there should be 3 black 15x15 boxes in the lower third of that gray box. Applying the same technique all around the 2nd and 3rd gray box gave me this image:

QR code with only about half as much gray

And feeding this into zbarimg successfully decodes to the string 8d54e284d4668bbed5dc391b27kc7fc05 :). I didn't really know what to do with this. It looks like a hex string, but there's a "k" in there so it can't really be. Even if it were, it can't be ASCII because there's a bunch of characters > 127. It's no Base64 either and Google doesn't know the string either. So that's where I finally went to bed, thinking I'd continue the next day. Turns out the CTF actually already ended after 12 hours, not 48 hours as it said on CTFTime.

Luckily, my teammate Tim had the glorious idea of just ignoring the "k" and to treat the string as a hex encoded md5 sum. So he went to MD5DB to paste the data and voila - we had the flag "beprepared".

For reference, this is the script I hacked together to descramble the QR code:

#!/usr/bin/env python2

from scipy import misc
import numpy as np

order = open('image_order', 'r').read().strip().split()
order = map(lambda xs: map(lambda x: int(x) - 1, xs), order)

wat = misc.imread('wat.out.png')
wat = wat * 0xff # saving will create a 8-bit png from the 1-bit source file

wy = wat.shape[0] / len(order[0])
wx = wat.shape[1] / len(order)

watout = np.tile(np.uint8(127), wat.shape)

for i, orderx in enumerate(order):
    line = [None]*len(order[0])
    for sidx, tidx in enumerate(orderx):
        line[tidx] = wat[i*wx:(i+1)*wx, sidx*wy:(sidx+1)*wy]

    for j, dij in enumerate(line):
        if dij is not None:
            watout[i*wx:(i+1)*wx, j*wy:(j+1)*wy] = dij

wati = misc.toimage(watout)'wat.out.out.png')

watout[135:150, 240:270].fill(255)
watout[135:150, 270:285].fill(0)
watout[180:195, 240:285].fill(0)

watout[285:300, 240:285].fill(255)
watout[330:345, 240:255].fill(255)
watout[330:345, 255:285].fill(0)

wati = misc.toimage(watout)'wat.out.out.fixed.png')

Reversing 100

This was a rather small task. It was a 32bit Linux binary. Static analysis showed that it first computed some kind of checksum over some memory area then use that (with some bit rotation) as the XOR key to a static memory buffer. Lazy as I am, I didn't want to write a program to compute the checksum, so I ran the program in GDB, set a breakpoint after the checksum was computed and printed it. But it wasn't as easy as that - the checksummed memory area was actually the main function and setting a breakpoint changes the first byte of a instruction to 0xcc. So that means you have to use a hardware breakpoint (hbreak) in GDB.

Besides that, everything was straight-forward and the following script gave me the correct flag flag{$O_mUcH_FuN_w1tH_r3v3R$iNg}:

#!/usr/bin/env python3
(gdb) info breakpoints 
Num     Type           Disp Enb Address    What
3       hw breakpoint  keep y   0x0804849d 
        breakpoint already hit 1 time
4       hw breakpoint  keep y   0x080484e6 
        breakpoint already hit 1 time

magic = 873788163
bar = b'ejm~HB\202\305Y=\263\350\036\304pDK^b\323\a\301NJ\205\325\236\274Y.\247\374'
res = bytearray(32)

# Rotate right: 0b1001 --> 0b1100
ror = lambda val, r_bits, max_bits: \
    ((val & (2**max_bits-1)) >> r_bits%max_bits) | \
    (val << (max_bits-(r_bits%max_bits)) & (2**max_bits-1))
ror32 = lambda val, bits: ror(val, bits, 32)

for i in range(31, -1, -1):
    magic = ror32(magic, 1)
    res[i] = (magic & 0xff) ^ bar[i]