Entropic Labyrinth - EHAX CTF 2026 Writeup
Category: Miscellaneous | Points: 500 | Solves: 17 | Author: Ravager
Note: We solved this challenge after the CTF ended as we were participating in two CTFs simultaneously and couldn't allocate time during the live event.
Challenge Description
I made my first game today but once i compiled it i lost track of all of it. Now it's just full of brainrot and I don't wish to play it anymore ;-;
Hint (Free): Cake or fake
Attachment: game.exe (PyInstaller-packed Windows executable)
Overview
The challenge provides a PyInstaller-packed Pygame application. The goal is to reverse engineer the binary, extract the Python source code, and recover a hidden flag encrypted within the game's code.
Solution Flow
game.exe ──> pyinstxtractor ──> game.pyc ──> bytecode analysis ──> XOR decrypt ──> FLAG
Step 1: Identifying the Binary
Running file on the binary reveals it's a PyInstaller-packed PE32+ executable:
$ file game.exe
game.exe: PE32+ executable (GUI) x86-64, for MS WindowsPyInstaller bundles Python applications into standalone executables. The binary contains a bootloader + a zlib-compressed archive of all Python modules.
PyInstaller packages Python scripts, dependencies, and a bootloader into a single executable.
Step 2: Extracting the Python Bytecode
We use pyinstxtractor to unpack the .exe and recover the embedded .pyc files:
$ python3 pyinstxtractor.py game.exe
[+] Processing game.exe
[+] Found PYZ archive
[+] Extracting PYZ archive
[+] Successfully extracted pyinstaller archive: game.exe_extractedThis gives us the main game bytecode:
game.exe_extracted/
game.pyc <-- Main game logic
PYZ.pyz_extracted/ <-- Standard library modules
pygame/ <-- Pygame assets
python3XX.dll <-- Python runtime
...
Step 3: Decompiling the Bytecode
Since the binary uses Python 3.11/3.12, standard decompilers may have limited support. We use Python's built-in dis module to disassemble the bytecode and manually reconstruct the source:
import marshal, dis
with open('game.pyc', 'rb') as f:
f.read(16) # Skip pyc header
code = marshal.loads(f.read())
dis.dis(code) # Full disassemblyReconstructed Source (Key Parts)
The decompiled code reveals a Pygame game with "brainrot" variable names (Gen-Z memes):
import pygame, sys, random
DOCTOR_WHO = '525f234f6c50235a24482644485545275c24596a'
Alpha_wolf = 'VGhlIEZsYWcgYWluJ3Qg...' # Base64: "The Flag ain't gonna be this easy to find vro"
Beta_wolf = "Don't look here...EH4X(F4K3_FL4G)...You scrolled too far"
class Ballin:
def __init__(self, x, y, wilted_rose, homies):
self.rect = pygame.Rect(x, y, wilted_rose, homies)
def Siks_Sayven(self):
return self.rect.width // (self.rect.height or 1)
def Ohio_Skibidi_brainrot_decryption(fine_shyte, sigma):
"""The REAL decryption function - simple XOR!"""
raw_bytes = bytes.fromhex(fine_shyte)
return ''.join(chr(b ^ sigma) for b in raw_bytes)
def cake_or_fake(fine_shyte, Ballins):
"""RED HERRING - produces non-deterministic fake output"""
random.shuffle(Ballins)
for boomer in Ballins:
fine_shyte = (fine_shyte - boomer.Siks_Sayven()) % 256
fine_shyte ^= boomer.Siks_Sayven()
return fine_shyte
def main():
# ... game initialization ...
skibidi_key = 23 # <-- XOR key hidden in constants!
# When player reaches goal:
fake_flag = ''.join(chr(cake_or_fake(b, Ballins.copy())) for b in raw)
# Displays: "FLAG: {fake_flag}" + "Reality drift detected."Step 4: Understanding the Cipher
The challenge contains two decryption paths - a red herring and the real one:
Red Herring: cake_or_fake()
This function uses random.shuffle() making the output non-deterministic. It's called in the game to produce a "fake flag" every time the player reaches the goal. The variable is literally named fake_flag and the game displays "Reality drift detected."
Real Decoder: Ohio_Skibidi_brainrot_decryption()
This function performs a simple single-byte XOR decryption:
Ciphertext (hex) ──> bytes.fromhex() ──> XOR each byte with key ──> Plaintext
XOR encryption is symmetric: the same key encrypts AND decrypts. plaintext XOR key = ciphertext and ciphertext XOR key = plaintext.
Each byte of the ciphertext is XOR'd with the same key byte (23) to recover the plaintext.
Step 5: Extracting the Flag
From the bytecode constants, we extract:
| Variable | Value |
|---|---|
DOCTOR_WHO | 525f234f6c50235a24482644485545275c24596a |
skibidi_key | 23 |
Decryption Script
#!/usr/bin/env python3
DOCTOR_WHO = '525f234f6c50235a24482644485545275c24596a'
skibidi_key = 23
raw = bytes.fromhex(DOCTOR_WHO)
flag = ''.join(chr(b ^ skibidi_key) for b in raw)
print(f'Flag: {flag}')XOR Breakdown
Hex: 52 5f 23 4f 6c 50 23 5a 24 48 26 44 48 55 45 27 5c 24 59 6a
Key: 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17
XOR: 45 48 34 58 7b 47 34 4d 33 5f 31 53 5f 42 52 30 4b 33 4e 7d
ASCII: E H 4 X { G 4 M 3 _ 1 S _ B R 0 K 3 N }
Flag
EH4X{G4M3_1S_BR0K3N}
The flag itself is self-referential: "G4M3 1S BR0K3N" (Game is broken) - a nod to the fact that the game's cake_or_fake function is intentionally broken and can never produce a valid flag!
Key Takeaways
-
Don't fall for red herrings: The
cake_or_fake()function withrandom.shuffle()is intentionally non-deterministic - it can NEVER produce a consistent flag. The hint "Cake or fake" is telling you the output is fake. -
Check all code paths: The real decryption function
Ohio_Skibidi_brainrot_decryption()was defined but NOT called in the game loop. The flag was hidden in the unused function. -
Brainrot naming is just obfuscation: All variable names (Ohio_Skibidi, Siks_Sayven, giggity, etc.) are Gen-Z meme terms used purely for confusion.
-
The game is an impossible escape room: The player starts INSIDE a closed rectangular box made of invisible walls. The walls randomly mutate, making escape nearly impossible - hence "I don't wish to play it anymore."
Tools Used
- pyinstxtractor - PyInstaller archive extractor
- Python
dismodule - Bytecode disassembler - Python
marshalmodule - Code object loader
Solved post-CTF. We were simultaneously competing in two CTFs and couldn't dedicate time to this challenge during the live event.