HTB: Maze Challenge

Maze - HackTheBox Challenge Writeup

FieldValue
NameMaze
CategoryReversing
DifficultyMedium
Authord3vn0mi

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:

  1. PyInstaller extraction from a Windows PE executable
  2. Bytecode deobfuscation involving marshal and compression (LZMA/zlib)
  3. Encrypted ZIP extraction with AES encryption
  4. Binary decryption using a seeded PRNG key derived from image metadata
  5. 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:

Terminal window
file maze.exe
strings 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:

Terminal window
python3 pyinstxtractor.py maze.exe
cd maze.exe_extracted
ls PYZ-00.pyz_extracted/

Key files extracted:

  • maze.pyc — Entry point bytecode
  • obf_path.pyc — Obfuscated helper module

Step 3: Decompile maze.pyc

Using decompyle3 with proper Python version handling:

import sys
from decompyle3.decompile import decompile_file
# Decompile maze.pyc
decompile_file('maze.pyc', open('maze_decompiled.py', 'w'))

Decompiled maze.pyc:

import sys, obf_path
ZIPFILE = "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, io
from marshal import loads
import lzma
import zlib
# Load obf_path.pyc and extract bytecode
with 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-compressed
compressed_data = b'\xfd7zXZ\x00\x00...' # From bytecode
decompressed = lzma.decompress(compressed_data)
# Further decompressed with zlib
final_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 from maze.png file metadata at specific byte offsets: 4817, 2624, 2640, 2720)
  • Key generation: Use Python’s random.seed(493) followed by random.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 hint
png_data = open('maze.png', 'rb').read()
# Hint bytes at offsets 4817, 2624, 2640, 2720 encode seed=493
seed = 493
key = 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:

Terminal window
chmod +x dec_maze
objdump -d -M intel dec_maze --disassemble=main | head -150
readelf -S dec_maze | grep -A1 rodata
readelf -x .rodata dec_maze

Key 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 prefix
prefix = "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:

Terminal window
echo 'HTB{REDACTED}' | ./dec_maze
# Expected: success/confirmation message

Tools Used

ToolPurpose
pyinstxtractor-ngExtract PyInstaller bundles
decompyle3 / pydisasmDecompile Python bytecode
xdisLow-level bytecode analysis
pyzipperDecrypt AES-encrypted ZIP files
objdumpDisassemble ELF binaries
readelfExtract ELF sections and metadata
straceTrace binary execution (debugging)

Key Learnings

  1. PyInstaller Defense Bypass: Large “compiled” binaries often hide Python source. Extraction tools like pyinstxtractor-ng can recover the original bytecode.

  2. Bytecode Obfuscation Layers: Attackers chain compression (LZMA, zlib) with marshal serialization. Tools like xdis and decompyle3 help unwrap multi-stage obfuscation.

  3. Steganography in Metadata: The seed 493 hidden in maze.png byte offsets demonstrates information hiding in image metadata—useful for key derivation.

  4. PRNG-Based Keys: Seeded random number generators produce deterministic but non-obvious keys. Recovering the seed is critical.

  5. 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.

  6. 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.