Neven Marić
Hidden Investigations CTF 2026

MiscHidden Investigations CTF 2026

Warden’s Maze

maze is a stripped static-PIE x86-64 Rust binary that serves a Pac-Man-style maze over a single TCP connection (plain locally on port 7700; the hosted service is TLS/SNI-terminated). You are @, dodging a warden G and collecting pellets.

  • misc
  • reverse-engineering
  • rust
  • binary-exploitation
  • integer-overflow
  • signed-truncation
  • game-hacking
  • bfs

What this one teaches

  • Hand-rolled array index arithmetic with signed-byte truncation can cause writes to land 112 cells away from the intended target — a single i8 overflow opens a wall.
  • Visibility/rendering passes that write back to the game map are a surprising attack surface — the movement code can be bulletproof while the drawing code corrupts state.
  • Map mutations from exploits can persist across game resets (warden catches only reset position, not the grid), so the solver only needs to trigger the bug once.

Task description

A tiny terminal arcade game, served over a single TCP connection. You are a dot in a Pac-Man-style maze: collect pellets, dodge the patrolling warden, and explore. Dead in the centre is a vault — walled off with its own distinct tile and sealed on every side. The prize sits inside it.

There is no key, no switch, no hidden door. By the ordinary rules of movement the vault simply cannot be entered, and the movement code holds up to any amount of poking. The way in is not a game mechanic — it is a genuine mistake in the engine itself, and finding it means reverse engineering the provided game binary: understanding how the maze is stored, how the renderer decides what you can see, and the one place where that visibility code reaches into the map with a hand-rolled index. Get a single step of that arithmetic to land where the author never expected, and a wall becomes a floor.

The game binary is in the download; the live service runs that exact binary. The validation token is minted per instance and only appears on a genuine win.

  • Category: misc / reverse
  • Challenge ID: 1a18e0db-e534-4694-b19c-c2307a094a7e
  • Flag format: hiCTF{...}
  • Flag (live instance): hiCTF{maze-warden-vault-seed-2026_37c07a9003} — submitted, API returned goodFlag.

Reverse engineering

Map storage

The maze is a 24×21 grid parsed (parser @ 0x6117) from a newline-separated string in .rodata (0x5e664, length 0x20c = 21 rows × 25 − 1). Characters map to internal type codes:

char type meaning rendered as
# 1 wall #
. 2 pellet .
= 3 vault wall %
- 4 vault interior (space)
* 5 prize $
/P/G 0 floor /@/G

Grid is row-major, stride = width = 24. Player P starts at (row 1, col 12), warden G at (row 19, col 11), prize * at (row 11, col 11). The vault is a =-walled box: rows 9 & 14 and cols 9 & 14 (cols/rows 9..14), interior -.

Movement / collision (0x68c2)

Input letters index a direction table at 0x5e604: w=(0,-1) up, d=(1,0) right, s=(0,1) down, a=(-1,0) left (row grows downward). A move is allowed only if the destination cell’s type & 0x3f is in the set {0,2,4,5} (bitmask 0x35). Types 1 (#) and 3 (=) block. Stepping onto type 5 is the win. Because the vault is fully ringed by type-3 walls, it is unreachable by movement alone. Pellets (type 2) add score and are cleared to 0x40 (floor + “explored” bit 6).

The bug — visibility pass (0x9680)

After every move the server redraws the board and runs a visibility routine that marks cells “explored” (sets bit 0x40). For every cell within Euclidean radius 6 (dx² + dy² ≤ 36) that has clear line of sight (LOS blocked by #/=), it performs the “mark” write. The destination index is hand-rolled and truncated to a signed byte:

write_idx = player_idx + (int8)(dx + 24*dy)
grid[write_idx] = (target_cell & 0x3f) | 0x40      // value comes from the TARGET

Normally dx + 24*dy equals the real relative offset of the target, so the routine marks the target itself. But movsx rcx, cl keeps only the low 8 bits. Within the radius-6 disk, the only offset that overflows a signed byte is dy = ±6, dx = 0:

  • dy = +6: 24*6 = 144 → (int8)144 = −112
  • dy = −6: −144 → (int8)(−144) = +112

So the type of the cell 6 rows straight below/above the player is written 112 cells backward/forward in the grid — onto a completely different cell. If that cell is a vault wall = (type 3) and the real target is floor (type 0), the wall is overwritten with 0 | 0x40 → type 0 = walkable floor. A wall becomes a floor, exactly as the author never intended. Opened cells persist, even across warden catches (which only reset your position, not the map).

The exploit geometry

Stand at (row 14, col 4):

  • dy = +6 target = (row 20, col 4) = floor, LOS clear (straight down column 4).
  • write_idx = 14*24 + 4 − 112 = 228 = (row 9, col 12) = the vault’s top wall.

That = becomes floor. The vault is now enterable from above: (8,12) → (9,12 opened) → (10,12) → (11,12) → (11,11)=$.

Solver

solve.py is an adaptive controller (robust to the warden). Each turn it:

  1. Parses the rendered board (true @/G/$ positions and the live map, including any opened walls).
  2. BFS-plans the next move: first navigate to (14,4) to open the top wall; once $ is reachable, BFS straight to it. The warden’s cell + neighbours are avoided (with fallbacks).
  3. If caught (reset to entrance), it simply re-paths — the opened wall stays open.

It uses only the Python stdlib. TLS mode wraps the socket with server_hostname=HOST (cert verification disabled, since the platform routes by SNI).

Verification

  • Local (plain TCP against the real ELF in Docker): 10/10 wins with the warden active, printing The vault is open. You take the prize. and the local placeholder token hiCTF{warden_vault_cleared_845f9b65} (seed-derived; the live service mints its own suffix).
  • Live (TLS): one shot → hiCTF{maze-warden-vault-seed-2026_37c07a9003}.

Run command

python solve.py maze-breach-<id>.chall.hiddeninvestigations.net 1337 tls

(For a static reference, a non-warden-robust 45-move win is saaaasssaassdsssdssssaaaadwdddwwwawwdddddsssa; prefer the adaptive solver.)

The flagRevealhiCTF{maze-warden-vault-seed-2026_37c07a9003}

This CTF was possibly solved, and the writeup most likely written by an LLM. Apologies for inconsistencies.