Maze Challenge Scenario
hackthebox
Task: a PyInstaller-packed Windows binary leads to a password-protected ZIP and a deliberately corrupted ELF checker. Solution: recover embedded Python secrets, derive the PNG-based hint, repair every-10th-byte corruption, and solve the final rolling-sum constraints.
$ ls tags/ techniques/
Maze Challenge Scenario — HackTheBox
Description
Provided files:
maze.png,enc_maze.zip, andmaze.exe.
English summary: The challenge starts from a PyInstaller-packed Windows executable and leads through several nested stages. The goal is to recover hidden secrets, unpack the protected archive, repair the corrupted Linux checker, and solve its final validation routine.
Challenge overview
This was a staged reverse engineering challenge:
- Reverse the PyInstaller-packed
maze.exe. - Recover the first required path string and the ZIP password from embedded Python bytecode.
- Extract the inner file
mazefromenc_maze.zip. - Understand why the ELF-like binary is corrupted and repair enough code/data to analyze it.
- Reconstruct the checker and solve the final constraints to obtain the flag.
Initial file triage and observations
The downloaded archive contained:
rev_maze/maze.pngrev_maze/enc_maze.ziprev_maze/maze.exe
Quick triage showed that maze.exe was a Windows PE produced with PyInstaller. That immediately suggested two useful directions:
- unpack the bundled Python application,
- inspect the embedded
.pycmodules instead of treating the PE as a normal native crackme.
The nested enc_maze.zip indicated a multi-stage design, and the presence of maze.png hinted that image data might later be reused as part of a key or seed derivation step.
PyInstaller unpacking and Python bytecode recovery
The executable was unpacked as a PyInstaller archive and the bundled Python bytecode was extracted from the embedded PYZ. Disassembly/decompilation of the recovered modules exposed the real control flow.
Important recovered modules included:
- the main maze logic,
obf_path,- additional obfuscated helper code inside the embedded archive.
This stage converted the challenge from “reverse a packed PE” into “read Python logic and recover staged constants.” That was the first big simplification.
Recovery of the hardcoded path string and ZIP password
Reading the recovered Python logic revealed two critical hardcoded values:
- first required input/path string:
Y0u_St1ll_1N_4_M4z3 - ZIP password for
enc_maze.zip:Y0u_Ar3_W4lkiNG_t0_Y0uR_D34TH
Those constants were enough to move to the next stage and extract the inner file named maze from the protected ZIP.
Analysis of obf_path and the PNG-derived seed hint
The obf_path helper contained another important clue. It derived a seed from selected bytes inside maze.png:
seed = maze_png[4817] + maze_png[2624] + maze_png[2640] + maze_png[2720]
For the supplied file this evaluates to:
493
The helper then printed the effective hint:
seed(493) for i in range(300): randint(32,125)
This was useful for understanding the challenge structure: the image was not just decoration, but part of the obfuscated guidance. It also confirmed that the author wanted the solver to follow the bytecode breadcrumbs instead of brute forcing later stages.
Extraction and repair strategy for the corrupted ELF
After unzipping enc_maze.zip, the inner file maze looked like an ELF, but was intentionally broken. The key observation was that corruption occurred at absolute file offsets divisible by 10.
That explained why the file looked “almost right” while still breaking headers, strings, code, and data.
Two additional observations mattered:
- the obvious Python “decrypt” stage mostly just altered every 10th byte,
- the advertised XOR stage was effectively a no-op because the XOR key was zero.
So the real challenge was not cryptography. It was selective binary repair.
The practical repair approach was:
- treat the inner file as an ELF-like binary with periodic corruption,
- repair enough header bytes to load it cleanly in tooling,
- fix damaged code/data bytes only where needed for control-flow recovery,
- use unaffected surrounding instructions and data layout to infer corrupted bytes.
Because every 10th byte could be wrong, both disassembly and constants had to be validated manually instead of trusted blindly.
Recovery of the flag-checking logic
Once the relevant parts of the checker were repaired, the final validation model became clear:
- the program expects a 38-byte input,
- it computes rolling 3-byte sums,
- each sum is compared against values stored in a corrupted dword table,
- both the code and the comparison table were affected by the same every-10th-byte corruption pattern.
In other words, for bytes x[i] the checker enforced constraints of the form:
x[i] + x[i+1] + x[i+2] == table[i]
After repairing the relevant table values and reconstructing the validation loop, the remaining task was straightforward constraint solving. Solving the repaired system produced the final flag:
HTB{w0W_Y0u_C0uld_E5c4p3_Th1s_M4Z33!!}
Solution
The full solve path was:
- Unpack
maze.exeas a PyInstaller binary. - Extract and disassemble the embedded Python bytecode.
- Recover
Y0u_St1ll_1N_4_M4z3andY0u_Ar3_W4lkiNG_t0_Y0uR_D34TH. - Use the ZIP password to extract the inner
mazefile. - Reverse
obf_pathto derive theseed(493)hint frommaze.pngbytes. - Notice that the ELF was periodically corrupted at offsets divisible by
10. - Repair the header, then the relevant checker code and dword table.
- Model the repaired checker as rolling 3-byte sum constraints over 38 characters.
- Solve the constraints and recover the flag.
#!/usr/bin/env python3 """ Reproduction helper for the recovered logic. This script documents the important constants and the final checker model. """ from pathlib import Path import random FLAG = "HTB{w0W_Y0u_C0uld_E5c4p3_Th1s_M4Z33!!}" FIRST_PATH = "Y0u_St1ll_1N_4_M4z3" ZIP_PASSWORD = "Y0u_Ar3_W4lkiNG_t0_Y0uR_D34TH" def derive_seed(png_path: str) -> int: data = Path(png_path).read_bytes() return data[4817] + data[2624] + data[2640] + data[2720] def rolling3(buf: bytes): return [buf[i] + buf[i + 1] + buf[i + 2] for i in range(len(buf) - 2)] def main(): png = "maze.png" if Path(png).exists(): seed = derive_seed(png) print(f"Recovered seed: {seed}") random.seed(seed) sample = ''.join(chr(random.randint(32, 125)) for _ in range(16)) print(f"Hint stream preview: {sample}") print(f"Recovered first path string: {FIRST_PATH}") print(f"Recovered ZIP password: {ZIP_PASSWORD}") print(f"Candidate length: {len(FLAG)}") print("Rolling 3-byte sums of final solution:") print(rolling3(FLAG.encode())) print(f"Flag: {FLAG}") if __name__ == "__main__": main()
Final flag
HTB{w0W_Y0u_C0uld_E5c4p3_Th1s_M4Z33!!}
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md