APT CTF Write Up
Write Up of the Reverse Engineering challenges I made for the APT CTF at UM6P - 1337
CrackMe!
In this challenge, I gave the participants a binary file. Upon execution, it presents a simple GUI with a text input box and a “Check” button — essentially a flag checker. The goal is to reverse-engineer the binary and recover the correct input that will produce the flag.
Step 1: Initial Binary Analysis
We begin by using the file command to inspect the binary:
1
2
$ file CrackMe!
CrackMe!: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=fc89f558d158bb1cc6e5e463d6fe7c536da15abb, stripped
So it’s a 64-bit ELF binary, dynamically linked, and stripped (no symbols).
Next, we want to determine if it’s packed and what language it was originally written in.
Step 2: Identifying the Language and Packer
We use Detect It Easy (DIE) to analyze the binary:
DIE tells us this is a Python application bundled with PyInstaller. PyInstaller packages Python applications into standalone executables, embedding the Python interpreter and required libraries.
Step 3: Extracting the PyInstaller Archive
To decompile the executable, we use pyinstxtractor.py
— a Python script that extracts PyInstaller-packed binaries.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ python3 pyinstxtractor.py CrackMe!
[+] Processing CrackMe!
[+] Pyinstaller version: 2.1+
[+] Python version: 3.12
[+] Length of package: 12277682 bytes
[+] Found 365 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth__tkinter.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: check.pyc
[+] Found 104 files in PYZ archive
[+] Successfully extracted pyinstaller archive: CrackMe!
Among the extracted files, we find our entry point: check.pyc
Step 4: Decompiling the Obfuscated Python Code
We decompile check.pyc using pylingual
and discover it’s obfuscated:
The obfuscation was done using Py-Fuscate — a tool that compresses and marshals Python bytecode.
To reverse it, you can manually:
Decompress with zlib
Unmarshal the code with marshal
Repeat the process if the next layer is also obfuscated or uses a different compression method
This can become a time-consuming and repetitive task because multiple layers of obfuscation are used.
Or save time and use my online deobfuscator tool pydeobf.xyz or use the Python Deobfuscator for Py-Fuscate on my GitHub, which deobfuscates Py-Fuscate code and outputs the raw bytecode. Why bytecode? because the marshal module works at the bytecode level, not the source level.
Deobfuscated bytecode using my online tool:
Step 5: Deobfuscation to Source Code
Once we extract the bytecode, we can either:
Manually reverse it (takes a lot of time), or
Let an LLM (like ChatGPT) regenerate the readable Python code from bytecode.
Here’s the recovered source code:
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
49
50
import tkinter as tk
def encrypt_text_to_coords(text, key):
encrypted = [ord(b) ^ ord(key[i % len(key)]) for i, b in enumerate(text.encode())]
coords = []
for byte in encrypted:
rank = byte // 8 + 1
file = 'abcdefgh'[byte % 8]
coords.append(f"{file}{rank}")
return coords
def check_encryption(input_text, coords, key):
encrypted_coords = encrypt_text_to_coords(input_text, key)
return encrypted_coords == coords
coords = [
'e4', 'a3', 'c3', 'f2', 'd1', 'b1', 'b6', 'h1',
'f6', 'a4', 'b1', 'b2', 'd2', 'd7', 'a4', 'b6',
'h1', 'g4', 'f2', 'g7', 'c3', 'f2', 'd1', 'b1'
]
key = b"lifelover"
def validate_input():
user_input = entry.get()
if check_encryption(user_input, coords, key):
result_label.config(
text=f"Correct! here is your flag: APT}",
fg="green"
)
else:
result_label.config(text="Incorrect! ghiyerha", fg="red")
# GUI setup
root = tk.Tk()
root.title("Crack me!")
label = tk.Label(root, text="Enter the secret:", font=("Courier", 14))
label.pack(pady=10)
entry = tk.Entry(root, font=("Courier", 14), width=30)
entry.pack(pady=10)
validate_button = tk.Button(
root,
text="Check",
font=("Courier", 14),
command=validate_input
)
validate_button.pack(pady=10)
result_label = tk.Label(root, text="", font=("Courier", 14))
result_label.pack(pady=10)
root.mainloop()
Step 6: Reverse Engineering the Algorithm
The encryption function XORs the input string with a repeating key lifelover
, then maps each result byte to a chessboard-style coordinate using:
1
2
Rank = (byte // 8) + 1
File = 'abcdefgh'[byte % 8]
To reverse it:
Convert each coordinate back into its byte representation.
XOR with the key.
Decode into a UTF-8 string.
Example :
1
e4 → file e → index 4, rank 4 → (4-1)*8 + 4 = 28
Step 7: Solver Script
Here’s a solver python code that will give us the correct secret :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def coords_to_encrypted_bytes(coords):
files = 'abcdefgh'
encrypted = []
for coord in coords:
file_char = coord[0]
rank = int(coord[1])
file_index = files.index(file_char)
byte = (rank - 1) * 8 + file_index
encrypted.append(byte)
return encrypted
def decrypt_coords(encrypted_bytes, key):
return bytes([b ^ key[i % len(key)] for i, b in enumerate(encrypted_bytes)])
coords = [
'e4', 'a3', 'c3', 'f2', 'd1', 'b1', 'b6', 'h1',
'f6', 'a4', 'b1', 'b2', 'd2', 'd7', 'a4', 'b6',
'h1', 'g4', 'f2', 'g7', 'c3', 'f2', 'd1', 'b1'
]
key = b"lifelover"
encrypted = coords_to_encrypted_bytes(coords)
secret = decrypt_coords(encrypted, key).decode()
print(secret)
Output : python_b_thon_w_bla_thon
FLAG : APT{python_b_thon_w_bla_thon}
Second Challenge: ransomware
In this challenge, I gave participants a Windows executable ransomware and an encrypted flag file. Their objective was to reverse engineer the binary and its encryption method to recover the original file.
This is my approach to solving this challenge.
Static Analysis
First, I performed static analysis using DIE (Detect It Easy):
The analysis revealed a 64-bit console executable compiled in C++ using Visual Studio 2022. It also uses OpenSSL, which likely handles the file encryption functionality.
Decompilation
I used IDA to decompile the binary. Examining the main function revealed:
The execution flow is:
1 - The binary prints “[+] WELCOME TO WINDOWS ACTIVATIOR”
2 - It calls sub_140006300
with an argument (time calculations suggest this is a sleep function, confirmed by sandbox execution showing a delay between messages)
3 - It prints “ “[-] Activating Windows…”
4 - It calls two important functions: sub_140001600
and sub_1400017F0
Looking deeper at these two functions:
Both functions:
Load binary resources named “IV_RESOURCE” and “KEY_RESOURCE”
XOR-decrypt them with the byte 0x45 (decimal 69)
Append the result to a std::string
This indicates the ransomware uses hardcoded XOR-obfuscated encryption keys stored in the .rsrc section.
Resource Analysis
To extract these resources, I used Resource Hacker to examine the .rsrc section:
After obtaining the XOR-encrypted KEY and IV values, I performed XOR decryption:
KEY : CHABANI_CHABANA!
IV : !CHABANI_LABANI!
Decryption
With the key and initialization vector identified, I determined that the ransomware likely uses AES encryption. I attempted decryption with these values:
FLAG: APT{w_ch4k_ch4b4n1_ch4b4n4}