CAT CTF 25 RE Challenges Official Writeup
Write up for my reverse engineering challenges in CAT CTF 25.
Introduction
But first of all, I’m really happy that I had the chance to lead and host such a great event together with my team. Huge thanks to all the authors who worked alongside me — their effort and dedication through the whole process made this possible. This year’s CTF was special because it was entirely organized by members of the CAT Reloaded Cybersecurity Circle. We had around 600 players and more than 200 teams participating over a 36-hour event, which made it an incredible experience for everyone involved.
Pickle
Introduction
This challenge focuses on a type of file called pickle, which is used to serialize and deserialize Python objects.
Players are provided with 3 files, 2 python helpers to run the pkl
file, and 1 chall.pkl
which holds the actual code
Idea
chall.pkl
when unpickled, reconstructs a Python function from compressed bytecode and then runs it.rehyd.py
a “rehydration” helper that turns a compressed, base85-encoded blob back into a live Python function.run.py
a tiny runner that loadschall.pkl
and calls the reconstructed function (the flag checker).
rehyd.py
1
2
3
4
5
6
7
8
import marshal, gzip, base64
def _rehydrate(b85, entry):
code = marshal.loads(gzip.decompress(base64.b85decode(b85)))
ns = {}
exec(code, ns)
return ns[entry]
This function is how the pickled payload “rehydrates” the checker function at load time, without shipping readable source.
What it does is typically Decode → Decompress → Demarshal
: base64.b85decode → gzip.decompress → marshal.loads
This yields a code object (what Python compiles a function/module into), then executes it in a new namespace, returning the function object. (you can read about what are marshal and pickle for more detailed explanation)
run.py
1
2
3
4
5
6
7
import rehyd_solve
import pickle
with open("chall.pkl", "rb") as fh:
check_flag = pickle.load(fh)
check_flag()
This simply loads the unpickled object from chall.pkl
and calls it.
Solve
What can you do here is try to catch the function object before it is called, dump it, and you will have a .pyc
file.
With that you can decompile and get a full view of source code
1
2
3
4
5
6
7
8
9
10
11
12
import rehyd, marshal, gzip, base64, importlib.util
def dumping_rehydrate(b85, entry):
code = marshal.loads(gzip.decompress(base64.b85decode(b85)))
with open("dumped_flag_checker.pyc", "wb") as fh:
fh.write(importlib.util.MAGIC_NUMBER + b"\0"*12 + marshal.dumps(code))
print("executed")
ns = {}
exec(code, ns)
return ns[entry]
rehyd._rehydrate = dumping_rehydrate # overwrite in-place
dumped_flag_checker.pyc
After decompiling the pyc file (I used pylingual for decomplication), it’s a very large source code with a lot of functions looks very similar
We can assume that only one of them will be executed, that hold the real flag checking routine
One way to know the exact function, is to take a look at the chall.pkl
you find a string pointing to the actual entry point at the end of the file
The code of desired functions:
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
37
38
39
40
41
42
43
44
45
46
47
48
def tralalero_tralala(key, plain):
S = list(range(256))
j = 0
key_bytes = key.encode()
for i in range(256):
j = (j + S[i] + key_bytes[i % len(key_bytes)]) % 256
S[i], S[j] = (S[j], S[i])
i = j = 0
result = []
for char in plain.encode():
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = (S[j], S[i])
K = S[(S[i] + S[j]) % 256]
result.append(char ^ K)
return ''.join((f'{b:02x}' for b in result))
def Chimpanzini_Bananini():
password = input('Enter the Flag: ')
part1 = xor(password[0:5].encode(), password[5:10].encode())
if part1.hex() != '302e0b1933':
print('wrong')
return
part2 = zlib.crc32(password[10:14].encode())
if part2 != 3979310991:
print('wrong')
return
part3 = zlib.crc32(password[14:18].encode())
if part3 != 448183154:
print('wrong')
return
part4 = xor(password[0:18].encode(), password[18:36].encode())
if part4.hex() != '70373134241b5c6b2d2c6b42076f2c442a2b':
print('wrong')
return
part5 = hashlib.md5(password[36:38].encode()).hexdigest()
if part5 != '346b81a32e7007eccadf60252bb599f0':
print('wrong')
return
part6 = hashlib.md5(password[38:40].encode()).hexdigest()
if part6 != '2c3ba657da75eab82c88c429fbbf2207':
print('wrong')
return
part7 = tralalero_tralala('flag{real_is_rare__fake_is_everywhere}', password[40:58])
if part7 != '3856abb119718a174973a5fbbf46727f419c':
print('wrong')
return
print('Flag is correct!')
The rest is the easy part
What the code do in summary:
Prompts the user for input (flag).
Part 1: XORs characters 0–4 with 5–9, checks against hex
302e0b1933
.Part 2: Computes
zlib.crc32
of characters 10–13, must equal3979310991
.Part 3: Computes
zlib.crc32
of characters 14–17, must equal448183154
.Part 4: XORs characters 0–17 with 18–35, checks against hex
70373134241b5c6b2d2c6b42076f2c442a2b
.Part 5: MD5 of characters 36–37 must be
346b81a32e7007eccadf60252bb599f0
.Part 6: MD5 of characters 38–39 must be
2c3ba657da75eab82c88c429fbbf2207
.Part 7: Encrypts characters 40–57 using a custom RC4-like function (tralalero_tralala) with key
flag{real_is_rare__fake_is_everywhere}
, must equal3856abb119718a174973a5fbbf46727f419c
.
If all checks pass, prints Flag is correct!; otherwise prints wrong at the failing step.
Solution
We know that the flag format is CATF{
and that’s enough to break the whole system:
Part 1 (bytes 0–9): XOR check
- You know
password[0:5] = b"CATF{"
. Useb[5:10] = xor(b[0:5], bytes.fromhex("302e0b1933"))
. - Result: bytes 5–9 =
b"so__H"
- You know
Part 2 (bytes 10–13): CRC32 == 3979310991
- Find 4 bytes whose
zlib.crc32(x) == 0xED2F778F
(small brute-force over printable bytes or targeted search). - Result: bytes 10–13 =
b"4ve_"
- Find 4 bytes whose
Part 3 (bytes 14–17): CRC32 == 448183154
- Same method as Part 2 for the next 4-byte chunk.
- Result: bytes 14–17 =
b"Y0u_"
Part 4 (bytes 18–35): block XOR
- Given
xor(password[0:18], password[18:36]) == bytes.fromhex("70373134241b5c6b2d2c6b42076f2c442a2b")
, computepassword[18:36] = xor(password[0:18], given_hex_bytes)
using the 18 bytes you now know. - Result: bytes 18–35 =
b"3ver_h34rd_4b0ut_t"
- Given
Part 5 (bytes 36–37): MD5 == 346b81a32e7007eccadf60252bb599f0
- Brute-force 2 bytes (65,536 cases) until
md5(pair).hexdigest()
matches. - Result: bytes 36–37 =
b"h1"
- Brute-force 2 bytes (65,536 cases) until
Part 6 (bytes 38–39): MD5 == 2c3ba657da75eab82c88c429fbbf2207
- Same 2-byte brute-force as Part 5.
- Result: bytes 38–39 =
b"s_"
Part 7 (bytes 40–57): RC4-like (
tralalero_tralala
) with known key- You can just use cyberchecf with key
flag{real_is_rare__fake_is_everywhere}
on3856abb119718a174973a5fbbf46727f419c
- Result: bytes 40–57 =
b"cucumb3red_th1ng?}"
- You can just use cyberchecf with key
Construction all that together, You get the flag CATF{so__H4ve_Y0u_3ver_h34rd_4b0ut_th1s_cucumb3red_th1ng?}
Knowledge Gained
This challenge introduces you to the concept of Python’s pickle serialization, and how it can be exploited to execute arbitrary code.
And teach you how to deal with strange file types you might face in real life scenarios, and how can you search, learn and extract useful information from them.
Aimlab.exe
If you know me, you will know that I can’t be an author in a CTF without writing a game hacking challenge😂.
This time I made another unity challenge, but a little bit different from the previous one.
A tiny change in the building process of unity games can change things dramatically.
I built this game with Il2Cpp instead of mono, so there is no open source code anymore.
Introduction
The first look on the challenge, it’s a unity game, a very simple shooter game
We are i a room with a text “1”, when we kill all targets, we move to room 2
And nothing happens after that.
The hint says that there might be some additional levels in the game, so let’s try to reach them
Know your tools
For hacking unity games, there no tool better than Unity Explorer with Melon loader
But lately, unity explorer doesn’t get updated, so it doesn’t work on newer versions of unity games
But thanks to the power of open-source, there are forks online with hotfixes for that, one of them is this repo to download Unity explorer
Once you have melon loader downloaded, simply add your game and install it
Now go to where you downloaded unity explorer, and copy the folders inside to the game’s folder, and open the game
You will find yourself facing a weird interface that feels like you are real hacker XD
Playing around
I won’t go into details about unity explorer, it’s a task for you to search about.
But from the home interface you can identify few things, like object explorer, some objects with names related to the game objects
One of the really basic features you can apply to the objects in the game is to just disable them, you will also notice a feature named freecam, which can be helpful to explore the game environment faster
After a little bit of exploring, you find a hint to tell you look from above.
And here you go
CATF{W4LL_H4CK_4CTIV4T3D}
Knowledge Gained
The goal of this challenge is to introduce you to the incredible tool Unity Explorer and all its incredible features
And also introduce you to unity IL2Cpp games, there are still many internals and techniques to exploit unity, so keep searching about it, and stay tuned, I might make another one in the future😉.
pout
Introduction
This is a “simple” flag checker program that just asks for flag and validate (0 solve btw)
And by simple I really mean simple
The real art
- First step: Open IDA and take a look at this piece of art
As a first step to solve a flag checker program, is to see when will it compare your input with the hardcoded flag.
Simple checkers just do string compares.
But in order to find the exact function of comparison, it’s kinda hard in this binary XD.
Because there are no functions other than main
anyway.
As a first try, let’s give the program dummy input, try to break before the end of the program, and see what changed in memory.
You can identify the ending blocks by zooming in a little bit and notice a change in the long horizontal line.
- Second step: break on all block, give the program input, and when the program breaks, take a look on strings.
- Third step: and here is your flag :).
CATF{easier_th4n_y0u_th1nk_;)}
Intended sol?
If you are insane enough to actually reverse this, there is a lot of possible approaches you can take, I’ll list some of them.
you can set a hardware break point on memory reads where your input is, and see where and how the program actually deals with each character. This would be useful in case of other operations being done on the string before comparing (like XOR the input and compare with encrypted flag) .
another way is to set breakpoint read
system calls.
and another way is to stop the program when he takes your input, give it dummy input, and keep stepping from this point trying to identify what actually happens to your input (this will take long time so you can combine it with setting a breakpoint on your input in memory to fast things up) and try to identify the pattern where the program actually execute useful code and not just random code for art.
Knowledge Gained
in this challenge i wanted to proof that even a simple 2 strings compare can literally be insane to figure out if some hard anti-reverse engineering techniques were involved.
This challenge is compiled using Artfuscator, which is built on REpsych, which itself is built on movfuscator, which is a single instruction C compiler that uses only mov
for execution.
Final Words
At the end of the day, the theme behind all these challenges is simplicity. None of them require long, complicated steps or advanced cryptography tricks. Instead, they’re straightforward but different — introducing new methods, tools, and techniques to help you get comfortable with these kinds of problems. The goal is to broaden your perspective beyond the usual Linux or Windows binary challenges, where you might spend hours buried in IDA reversing a stripped library. 😅
Now, that doesn’t mean spending hours reversing is wrong — far from it! That is real reverse engineering. But being a reverse engineer also means having the mindset to explore new approaches, try fresh ideas, and step outside the traditional routines that often feel more like a test of patience than a test of skill.
I hope you enjoyed these challenges as much as I did creating them.
Thank you for reading, and I hope you found it helpful.
If you have any questions or comments, feel free to contact me on LinkedIn — Discord — GitHub
Also, you can check my other blog where I post some cool DFIR CTF write-ups too from time to time