This weekend, I spent some time participating in TUMCTF. This was organised by the “h4x0rpsch0rr” team. This was a relatively difficult CTF (as evidenced by the low number of solves on most challenges), but in the time I spent playing, I was able to solve three challenges, for which I will present the solutions for two of them (“the_joy_of_painting” was just viewing a flac file as spectrograph in Audacity) below.
Without further ado:
The “zwiebel” challenge is presented as a small Linux binary. Our investigation begins at the “main” function:
The combination of “mmap”, “memcpy” and a dynamic “call r14” immediately sticks out like a sore thumb: it’s clear that we are dealing with some manner of dynamic code. At this point, my first instinct was to load the binary in gdb, to verify my approach. However, on loading the executable, the binary doesn’t even ask for a key.
A little further investigation in IDA Pro determines the culprit:
An anti-debug check, disguised as “__printf”, is called before main, through _init_array. Patching this check out yields results, and shows that dynamic code (copied from “shc”) is indeed executing. The next step is to disassemble the code at “shc”. The code doesn’t immediately disassemble cleanly, but a little bit of fixup shows why:
From “gdb”, we can determine that “rbx” is seeded with the offset of the user entered key in memory, so this is quite clearly a key check function. The code we can see performs a single check (key & 0x40 must not equal zero), then applies a simple xor decrypt across the rest of the code, and then proceeds further (to unk_601375).
A quick “dump memory” in gdb shows that the next chunk is extremely similar in structure. From here, we can quickly automate the process of unpacking the binary (and solve the challenge), with a simple Python script:
- Load “shc.bin”
- Create an array of 50 zero bytes (the “output”)
- Start at offset 49 (first 8-byte block of xor size/key)
- Decrypt each block with the known XOR.
- After decrypting each block, look for a bit pattern of mov al, [rax+x] , and al, y, jz/jnz exit_wrapper
- If we have a “jz” condition:
- This means the boolean and condition must be fulfilled in the key the user enters
- Therefore, output[x] &= y
20 minutes and one Python script later, and we have the key:
The haggis challenge was presented as a Python file.
At a glance, the challenge chooses a random 10 bytes, and wants a message such that the following two conditions are fulfilled:
- The message starts with “I solemnly swear that I am up to no good.\0”
- The last 16-byte block of the encrypted message equals the randomly chosen bytes.
At this point, it is worth explaining how AES works in CBC mode: even for an absolute crypto newbie like me, it’s not overly complex (diagram shamelessly stolen from Wikipedia for ease of reference):
From here, we can see that the only “trick” to cipher block chaining is that each block of ciphertext is XOR’ed with the next plaintext block before encryption. That is, if we know the
Our first step is to manipulate the “message block structure”, so the “attacker-controlled” input starts on an aligned block. We run into some problems with the “pad” function, which appears to be intentionally broken, but we can get our message to look like this, with each rectangle aligned on a 16-byte boundary:
From here, we can deduce the following logical approach:
- Ignoring the first block (length of message – we fix this by tampering with the “haggis” function), we know that “haggis” of the first two blocks is always static (let this be “haggis_base“)
- We want to create a block of AES output such that encrypt(\x10\x10\x01… XOR chosen_output_1) equals the randomly chosen target.
- We can determine the value of “\x10\x10\x01… XOR chosen_output_1” by using AES decryption with a zero IV against the randomly chosen target.
- Therefore, we know “chosen_output_1“.
- We want to create a block of AES output such that encrypt(haggis_base ^ chosen_plaintext_1) equals chosen_output_1
- Again, we can use AES decryption with a zero IV against chosen_output_1 to determine chosen_plaintext_1 after XORing with haggis_base
This is simple enough to implement in Python, giving us the flag:
All in all, this was a cool CTF to occupy my time and keep my skills sharp while I moved forward on other projects. Thankyou to team h4x0rpsch0rr who put this together, and I’ll see you all in hackover.de’s CTF next weekend 😀