Gamer - Level 1

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"
}

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

  1. 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.
  2. Translate to Bits: Convert directions (Up=0, Down=1, Left=2, Right=3) into bit pairs.
  3. Reverse XOR: Pack 4 directions into a byte and XOR with the xor_values to 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!}