Category: Reverse / Misc
Flag: CDDC2026{e23b9fa3ee00bdf9bf0331cfb18427fd}
Challenge
A miro.zip containing the build kit for a maze game:
miro/
├── Dockerfile # FROM g3un98/nsjail, copies flag + miro into /jail
├── start.sh # nsjail -Ml --port 31337 --chroot /jail -- miro
├── build.sh, run.sh
├── flag # placeholder: CDDC2026{THIS_IS_FAKE_FLAG}
└── miro # ELF64, statically linked Go binary, stripped
Connecting to the server shows a banner and 10 rounds of 8×8 mazes. You move with W/A/S/D (multi-char input allowed, e.g. DDSSDD). All 10 rounds share one global budget of 143 moves. Bumping a wall still consumes a move.
Round 1/10
Budget remaining: 143
#################
#P . . . . . . .#
# # # # # # # # #
...
#################
>
Win all 10 rounds and you get the flag. Run out of budget and you get Game Over!.
Recon
strings revealed a Go 1.24.13 binary with the source layout:
miro/main.go miro/game.go miro/maze.go
miro/render.go miro/seed.go miro/crypto.go
Function names from pclntab:
main.main main.runGame main.applyMove main.isValidMove
main.generateMaze main.newRng main.transformSeed main.packDir
main.renderMaze main.enc main.dec
main.(*Maze).setWallRight / setWallDown / wallRight / wallDown
main.(*rng).next / intn
Interesting messages (XOR-obfuscated in .rodata, decoded at runtime):
Round %d/10
Budget remaining: %d
Congratulations! You conquered the maze in %d total moves!
Game Over! Budget exceeded. You used %d moves (budget: %d).
Game Over! You couldn't reach the exit.
Error reading flag
First attempt — naive BFS
Wrote a Python solver that parses the rendered maze, BFS-shortest-paths it, and feeds moves over a socket. Result:
Round 1: len 14 Round 6: len 18
Round 2: len 14 Round 7: len 14
Round 3: len 22 Round 8: len 14
Round 4: len 14 Round 9: len 14
Round 5: len 16 Round 10: len 14
Sum: 154
154 > 143 → Game Over! Budget exceeded. Rounds 3, 5, 6 cost more than 14 (Manhattan minimum). With only 3 slack over the Manhattan total, naive BFS can't win.
Since path length parity is fixed (the grid graph is bipartite), every alternative path is also even-length, so the smallest sub-optimal detour adds exactly 2 moves. There's no way to shave individual rounds.
The trick — main.transformSeed
The presence of main.transformSeed in the symbol table hinted that the seed for the next maze isn't fixed. I tested it: solve round 1 with two different move strings that both end at the exit, then look at round 2.
| Round 1 input | Round 2 maze |
|---|---|
DDSSSDDDDDSSSS (14, BFS-shortest) |
maze A |
DADDSSSDDDDDSSSS (16, with a DA no-op loop) |
different maze B |
Confirmed: the seed for round N+1 is a function of the exact byte sequence submitted in round N. So by spending a few moves to wiggle in round 1, you can choose a completely different deck of mazes for rounds 2–10.
This converts the puzzle into a search: find a sequence of round-1 inputs (allowed to be slightly suboptimal) such that the downstream mazes all admit Manhattan-minimum paths.
Solving the search
I enumerated DDSSSDDDDDSSSS plus all 16-move variants formed by inserting one of AD / DA / WS / SW (reversible 2-move loops) at every position. For each variant: replay against a fresh process, then auto-BFS rounds 2–10 and record the lengths.
DDSSSDDDDDSSSS -> [14,14,22,14,16,18,14,14,14,14] sum=154
DADDSSSDDDDDSSSS -> [16,14,14,14,14,14,14,14,14,14] sum=142 ✓
DDADSSSDDDDDSSSS -> [16,16,14,16,14,18,14,14,18,14] sum=154
DWSDSSSDDDDDSSSS -> [16,20,16,16,14,14,16,16,16, 1] sum=145
DSWDSSSDDDDDSSSS -> [16,14,14,14,14,16,16,14,14,16] sum=148
DADDSSSDDDDDSSSS lands on a seed family where every subsequent maze has shortest path = 14. Total 142 ≤ 143. Done.
Winning script
paths = [
"DADDSSSDDDDDSSSS", # 16 — the seed-steering detour
"DDDDSDDSSDSSSS", # 14
"DDDDSSSDSSDDSS", # 14
"DDDDSSDDDSSSSS", # 14
"DDDDDDDSSSSSSS", # 14
"DDDDDDDSSSSSSS", # 14
"DDDDDDDSSSSSSS", # 14
"DSDDDDDDSSSSSS", # 14
"DDDSSSSDDDSDSS", # 14
"DDDDDDSDSSSSSS", # 14
] # sum = 142
(Rounds 2–10 are also re-BFS'd live in case of drift.)
Run:
$ python nc_solver.py cddc2026-challs-nlb-...elb.ap-southeast-1.amazonaws.com 47363
Round 1: sending DADDSSSDDDDDSSSS (len=16)
Round 2: sending DDDDSDDSSDSSSS (len=14)
...
Round 10 cleared! (14 moves used, total: 142/143)
Congratulations! You conquered the maze in 142 total moves!
CDDC2026{e23b9fa3ee00bdf9bf0331cfb18427fd}
Takeaways
- A 143-budget puzzle on top of 10 Manhattan-14 mazes is a giveaway that the maze sequence is not fixed — otherwise the author would have set the budget to exactly the sum of shortest paths under one canonical play.
- The function-name leak in stripped Go binaries (pclntab) is gold.
transformSeedandpackDirtold the whole story before any disassembly was needed. - Practically: when a "shortest path" strategy overshoots the budget by a small constant, look for state that the player can perturb — seeds, RNG, scoring multipliers — rather than chasing a tighter graph search.
Files
nc_solver.py— connects, parses, and submits paths (uses BFS for rounds 2–10).dfs_solver.py— generic BFS / shortest-path enumeration plus a DFS over alternate paths.probe2.py— the small experiment that foundDADDSSSDDDDDSSSS.