HTB: Maze Challenge
Maze - HackTheBox Challenge Writeup
| Field | Value |
|---|---|
| Name | Maze |
| Category | Reversing |
| Difficulty | Medium |
| Author | d3vn0mi |
Challenge Description
I am stuck in a maze. Can you lend me a hand to find the way out?
A multi-layered reversing challenge involving PyInstaller extraction, Python bytecode obfuscation, encrypted archives, and binary analysis. The challenge requires peeling back several layers of protection to recover a hidden flag.
Solution Overview
This challenge combines several reversing techniques:
- PyInstaller extraction from a Windows PE executable
- Bytecode deobfuscation involving
marshaland compression (LZMA/zlib) - Encrypted ZIP extraction with AES encryption
- Binary decryption using a seeded PRNG key derived from image metadata
- ELF binary analysis with flag validation logic
The solution involves recovering the correct XOR key from hidden sources and decrypting multiple layers to reach the final binary.
Key Steps
Step 1: Identify PyInstaller Bundle
The challenge file maze.exe is a PyInstaller-packed executable. Identify it using file analysis and strings:
file maze.exestrings maze.exe | grep -E '_MEIPASS|pyi_rth_|CArchive'Output: PE32+ executable with PyInstaller artifacts (Python 3.8).
Step 2: Extract PyInstaller Archive
Use pyinstxtractor-ng to extract the bundled Python files:
python3 pyinstxtractor.py maze.execd maze.exe_extractedls PYZ-00.pyz_extracted/Key files extracted:
maze.pyc— Entry point bytecodeobf_path.pyc— Obfuscated helper module
Step 3: Decompile maze.pyc
Using decompyle3 with proper Python version handling:
import sysfrom decompyle3.decompile import decompile_file
# Decompile maze.pycdecompile_file('maze.pyc', open('maze_decompiled.py', 'w'))Decompiled maze.pyc:
import sys, obf_pathZIPFILE = "enc_maze.zip"print("Look who comes to me :)")print()inp = input("Now There are two paths from here. Which path will u choose? => ")if inp == "Y0u_St1ll_1N_4_M4z3": obf_path.obfuscate_route()else: print("Unfortunately, this path leads to a dead end.") sys.exit(0)Finding: The password Y0u_St1ll_1N_4_M4z3 triggers obf_path.obfuscate_route().
Step 4: Unwrap obf_path.pyc Obfuscation
The obf_path.pyc contains marshalled, LZMA-compressed, and zlib-compressed code. Unwrap it in layers:
import sys, iofrom marshal import loadsimport lzmaimport zlib
# Load obf_path.pyc and extract bytecodewith open('PYZ-00.pyz_extracted/obf_path.pyc', 'rb') as f: pyc_header = f.read(16) # Skip PYC header (magic + timestamp) code_obj = loads(f.read())
# Decompile to extract the marshal.loads() call# The marshalled data is LZMA-compressedcompressed_data = b'\xfd7zXZ\x00\x00...' # From bytecodedecompressed = lzma.decompress(compressed_data)
# Further decompressed with zlibfinal_data = zlib.decompress(decompressed)Result: Executes code that processes enc_maze.zip with the password Y0u_Ar3_W4lkiNG_t0_Y0uR_D34TH.
Step 5: Extract and Decrypt enc_maze.zip
The ZIP file is AES-encrypted with LZMA compression. Extract using pyzipper:
import pyzipper
def decrypt_maze_zip(file_path, password): with pyzipper.AESZipFile(file_path, "r", compression=pyzipper.ZIP_LZMA, encryption=pyzipper.WZ_AES) as extracted_zip: extracted_zip.extractall(pwd=password.encode())
decrypt_maze_zip("enc_maze.zip", "Y0u_Ar3_W4lkiNG_t0_Y0uR_D34TH")Extracted: maze — an encrypted binary file.
Step 6: Recover the Real XOR Key
The decompiled obf_path.pyc hint reveals:
- Seed value:
493(extracted frommaze.pngfile metadata at specific byte offsets: 4817, 2624, 2640, 2720) - Key generation: Use Python’s
random.seed(493)followed byrandom.randint(32, 125)× 300 times
The initial decryption code in obf_path uses an all-zero decoy key. The real key is generated from the seed:
import random
def generate_real_key(seed, length): random.seed(seed) key = [] for _ in range(length): key.append(random.randint(32, 125)) return key
# Read the PNG to extract seed hintpng_data = open('maze.png', 'rb').read()# Hint bytes at offsets 4817, 2624, 2640, 2720 encode seed=493
seed = 493key = generate_real_key(seed, 300)print(f"Generated key of length {len(key)}: {key[:10]}...")Step 7: Decrypt the Maze Binary
Apply XOR decryption with the recovered key:
def decrypt_maze(encrypted_file, key): with open(encrypted_file, 'rb') as f: data = bytearray(f.read())
# Decryption applied with step pattern for i in range(0, len(data), 10): if i % len(key) < len(key): data[i] = (data[i] ^ key[i % len(key)]) % 256
return bytes(data)
decrypted = decrypt_maze("maze", key)with open("dec_maze", "wb") as f: f.write(decrypted)Step 8: Analyze the Decrypted ELF Binary
The decrypted binary is a valid ELF executable. Analyze with objdump:
chmod +x dec_mazeobjdump -d -M intel dec_maze --disassemble=main | head -150readelf -S dec_maze | grep -A1 rodatareadelf -x .rodata dec_mazeKey finding: The .rodata section contains a table of expected sums. The flag validation checks if the sum of each 3 consecutive characters matches entries in this table.
Step 9: Solve the Recurrence Relation
Extract the validation table from .rodata and use the known prefix `HTB{REDACTED} to recover subsequent characters:
# Known prefixprefix = "HTB{REDACTED}validation_table = [0xde, 0x111, 0x134, 0x122, ...] # From .rodata
# Solve recurrence: sum(char[i], char[i+1], char[i+2]) == table[i]def solve_flag(prefix, table): flag = list(prefix) for i in range(len(prefix) - 2, len(table)): # char[i] + char[i+1] + char[i+2] == table[i] # We know char[i] and char[i+1], solve for char[i+2] known_sum = ord(flag[i]) + ord(flag[i+1]) target = table[i] char_i_2 = target - known_sum if 32 <= char_i_2 <= 126: flag.append(chr(char_i_2)) else: break return ''.join(flag)
flag = solve_flag(prefix, validation_table)Step 10: Verify Against the Binary
Test the recovered flag against the compiled binary:
echo 'HTB{REDACTED}' | ./dec_maze# Expected: success/confirmation messageTools Used
| Tool | Purpose |
|---|---|
pyinstxtractor-ng | Extract PyInstaller bundles |
decompyle3 / pydisasm | Decompile Python bytecode |
xdis | Low-level bytecode analysis |
pyzipper | Decrypt AES-encrypted ZIP files |
objdump | Disassemble ELF binaries |
readelf | Extract ELF sections and metadata |
strace | Trace binary execution (debugging) |
Key Learnings
-
PyInstaller Defense Bypass: Large “compiled” binaries often hide Python source. Extraction tools like
pyinstxtractor-ngcan recover the original bytecode. -
Bytecode Obfuscation Layers: Attackers chain compression (LZMA, zlib) with
marshalserialization. Tools likexdisanddecompyle3help unwrap multi-stage obfuscation. -
Steganography in Metadata: The seed
493hidden inmaze.pngbyte offsets demonstrates information hiding in image metadata—useful for key derivation. -
PRNG-Based Keys: Seeded random number generators produce deterministic but non-obvious keys. Recovering the seed is critical.
-
Constraint-Based Reverse Engineering: Flag validation constraints (e.g., sum-of-3 checks) create a system of equations solvable when partial input (prefix) is known.
-
Layered Encryption: Multiple passwords (
Y0u_St1ll_1N_4_M4z3,Y0u_Ar3_W4lkiNG_t0_Y0uR_D34TH) and decoys (all-zero key) make linear progression harder—require understanding the full chain.
Flag
HTB{REDACTED}Validation: Confirmed by execution against the decrypted binary.