written by treeindustry
The maze trials were just the start. As a master of mazes, you find yourself still confined in a maze. At least you’ve got some superpowers this time…
nc challs.nusgreyhats.org 31112
The only algo challenge in this ctf out of the ≈3 programming challenges and I thought it would be quick and easy because another team already solved it, but it remained at 1 solve during the 4 hours I spent on it.
Overview
The context is that we are given a maze, and we have to find the length of the shortest path from the bottom right corner to the top left corner. But we have superpowers, meaning we can phase through a wall a few times.
The first question looks like this:
1 | -------------------------------------------------- |
The size of the maze increases with each question, and the timeout is like 5 seconds, so the time limit is real.
Idea
My first idea was to use recursion to solve the maze, because recursion is easy to code. The length of the path to a certain square is the smallest length of the path to any adjacent square, +1.
We can represent the maze in 3D, with each layer being the number of wall-phases we have left. When we go through a wall, we jump down a layer. If we are at the bottom layer, we solve the maze as normal.
Implementation details
First we need a simple representation of the maze.
1 | from pwn import * |
Because the walls are made of a lot of different characters, we check for empty spaces instead. The 0s in the array will represent walls, and the 1s will represent spaces.
Next we make the recursion algorithm:
1 | def mazerun(maze, x, y, phases, wherefrom): |
We track the direction we come from so we do not backtrack and create an infinite loop.
For each direction, we check if there is a wall in the way, if not it continues as per normal, if there is a wall it will subtract 1 wall-phase.
Because the number of wall-phases is limited, I wasn’t too concerned about the extra time needed to consider the phases.
What actually happened
Running out of time
It turns out recursion is slow (who would have guessed?) so we pull out the recursion + memory combo.
1 | memo = np.full((maze_length,maze_height,phases+1), 981) |
1 | def mazerun(maze, x, y, phases, wherefrom): |
If the value already exists in memory, we do not need to search anymore. This actually saves enough time to not timeout the connection.
Memory problems
But actually, sometimes the memory was confidently incorrect. And then we would end up getting a path length higher than it should be.
1 | I think you can run faster! You could've followed this map instead and got 60 steps! |
The challenge mocking us, how funny.
Because the algorithm prevents backtracking, we would sometimes block the shortest path and end up saving into memory the wrong value for a tile. Because we never check again, we would never know.
There was no easy fix for this, so I ended up forcing the program to update all the adjacent tiles whenever a new value for a tile is saved into a memory (and do so recursively, to save the entire zone that got corrupted initially).
1 | def updatenode(maze, x, y, phases, value): |
It will terminate if the values for the adjacent tiles are all less than or equal to the current tile, because that means there is no new correct minimum.
Final code
1 | from pwn import * |
1 | def mazerun(maze, x, y, phases, wherefrom): |
The result:
1 | \n--------------------------------------------------\nCongratulations! You've made it out of the maze! Your determination, courage, and problem-solving skills have led you to freedom. Now, embrace the next chapter of your journey with the same resilience and bravery. The maze may be behind you, but the adventure continues!\n\ngrey{g1ad3rs_pha5erS_y0u_hAvE_jo1n3d_tH3_m4ze_eSc4p3rs!}\n |