← All writeups

Reverse

miro — CDDC 2026

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

Files