Gamer - Level 1
- Category: Reversing
Description
A maze, but with a twist
Challenge by Eli Kaski
Sources were attached.
Solution
We get a exe file - its a maze game where the player must navigate from a starting point to a goal. By analyzing the decompiled C code, we can break the game down into three main systems
1. Flag Generation Logic (sub_4012E0)
The most important part of the code is the win condition. By looking at the end of the input processing loop in sub_4012E0, we can see how the flag is revealed.
Code Snapshot:
if ( (unsigned __int8)check_win(a1) ) // Check if current position == 'O'
{
sub_401040(a1, (int)aCorrect); // Prints "Correct!\n"
sub_401040(a1, (int)aTheFlagIsBside, v7); // Prints "The flag is: BSidesTLV2025{...}\n"
}
- The flag is formatted as
BSidesTLV2025{password}, wherev7is the raw input string you entered. This means the solution to the maze is the content of the flag.
2. Input to Movement Decoding
The program takes each character of your input, XORs it with a secret key, and then treats every 2 bits as a direction.
Code Snapshot (sub_4012E0):
v6 = xor_values[i] ^ v7[i]; // v7 is your input password
v3 = 0;
LABEL_6:
if ( v3 < 4 ) { // Every character provides 4 moves
v1 = v6 & 3; // Get the lowest 2 bits
v6 >>= 2; // Shift for next 2 bits
switch ( v1 ) {
case 0: if ( move_up() ) goto LABEL_19; break;
case 1: if ( move_down() ) goto LABEL_19; break;
case 2: if ( move_left() ) goto LABEL_19; break;
case 3: if ( move_right() ) goto LABEL_19; break;
}
// ... logic for failed moves ...
LABEL_19:
check_special(); // Check if trigger is hit
++v3;
goto LABEL_6;
}
3. The “Quest” Trigger (check_special)
The maze is initially impossible because ? characters act as walls. You must first navigate to the ! character to clear these obstacles.
Code Snapshot (sub_401080):
int check_special() {
// 33 is the ASCII for '!'
if ( maze[17 * row + col] == 33 ) {
for ( i = 0; i < 11; ++i ) {
for ( j = 0; j < 17; ++j ) {
// 63 is the ASCII for '?'
if ( maze[17 * i + j] == 63 )
maze[17 * i + j] = 32; // Change '?' to space (passable)
}
}
}
}
Maze Reconstruction
Based on the aXxxxxxxxxxxxxx string (stride 17), the maze looks like this:
01234567890123456
0: XXXXXXXXXXXXXXXXX
1: X S ? X OX <-- S: Start (1,1) | ?: Wall | O: Goal (1,15)
2: XXX XXXXXXX X XXX
3: X X X!X X XX <-- !: Trigger (3,7)
4: X XXX X X XXXX XX
5: X XX X X XX
6: XXX XX XXX X XXX
7: X XX X ?X XX <-- ?: Wall
8: X XXX X XXXXX XX
9: X XX XX
10: XXXXXXXXXXXXXXXXX
Solution Strategy
- Find the Path: Use BFS to find the path from (1,1) to (3,7), then from (3,7) to the goal (1,15). Note that column 12 is blocked, so you must travel down to Row 9 to cross.
- Translate to Bits: Convert directions (Up=0, Down=1, Left=2, Right=3) into bit pairs.
- Reverse XOR: Pack 4 directions into a byte and XOR with the
xor_valuesto retrieve the flag characters.
Solution Script
import collections
W, H = 17, 11
MAZE_RAW = "XXXXXXXXXXXXXXXXXX ? X OXXXX XXXXXXX X XXXX X X!X X XXX XXX X X XXXX XXX XX X X XXXXX XX XXX X XXXX XX X ?X XXX XXX X XXXXX XXX XX XXXXXXXXXXXXXXXXXXX"
XOR_KEYS = [0x3C, 0x6A, 0x6F, 0x16, 0xA0, 0x55, 0x6C, 0xC0, 0x3A, 0xEC, 0x9F, 0xF5, 0xBE, 0x94, 0xA0, 0x0B, 0x32, 0x6C, 0x35, 0x98, 0x67, 0x39, 0xF7, 0xD1]
# Directions based on the Switch statement
DR, DC = [-1, 1, 0, 0], [0, 0, -1, 1]
def get_path(grid, start, end, blocked):
queue = collections.deque([(start, [])])
visited = {start}
while queue:
(r, c), path = queue.popleft()
if (r, c) == end: return path
for move in range(4):
nr, nc = r + DR[move], c + DC[move]
if 0 <= nr < H and 0 <= nc < W and grid[nr][nc] not in blocked and (nr, nc) not in visited:
visited.add((nr, nc))
queue.append(((nr, nc), path + [move]))
def solve():
grid = [list(MAZE_RAW[i:i+W]) for i in range(0, len(MAZE_RAW), W)]
path = get_path(grid, (1, 1), (3, 7), ['X', '?']) + get_path(grid, (3, 7), (1, 15), ['X'])
flag = ""
for i in range(0, len(path), 4):
chunk = path[i:i+4]
# Packing order: first move is lowest bits
res_byte = (chunk[0]) | (chunk[1] << 2) | (chunk[2] << 4) | (chunk[3] << 6)
flag += chr(res_byte ^ XOR_KEYS[i // 4])
print(f"BSidesTLV2025{{{flag}}}")
solve()
Running the logic retrieves the flag:
BSidesTLV2025{c00L_YoU_FoUNd_Th3_eX1t!}