Wizardlike picoCTF 2022 Solution

Published: July 20, 2023

Description

A roguelike dungeon-crawler binary hides the flag in a room blocked by invisible walls. You can see the flag characters on the map but cannot reach them under normal collision rules.

Patch the wall-collision check in Ghidra, or use GDB to skip the check at runtime, allowing free movement through walls.

Download the binary and make it executable.

Run it to understand the game mechanics (WASD or arrow keys to move).

Use Ghidra to find and patch the wall-collision check.

bash
wget https://artifacts.picoctf.net/c/218/wizardlike && chmod +x wizardlike
bash
./wizardlike
Patching one conditional jump in the movement function turns this from a maze into a flat plane. Background reading: GDB for CTF covers the runtime set $eflags trick used here, and Ghidra Reverse Engineering covers the on-disk Patch Instruction workflow if you want a permanent fix.
  1. Step 1Explore the dungeon and identify blocked areas
    Run the binary and navigate the dungeon with WASD/arrow keys. You will see flag characters spelled out across rooms separated by # walls and locked corridors. Sketch the layout so you know where you need to walk after the patch.
    Learn more

    A typical screen looks like this (your character is @, walls are #, doors are +, flag characters are scattered glyphs visible through gaps):

    ####################
    #......##....p..i..#
    #..@...##..........#
    #......++....c..o..#
    ########++###..C..T#
    #......##....F..{..#
    #..h...##..........#
    #......##..!..}..._#
    ####################
                    ^ flag chars visible across the wall
    @ = player    # = wall    + = door    ! = key    h = monster

    The binary renders the map in the terminal (ncurses or direct escape codes). The flag is spelled out in a room or corridor that your character cannot enter under normal rules because the wall-collision function blocks attempts to walk onto # tiles. Before modifying anything, note which tiles you can see but cannot reach: this gives you the target coordinates.

  2. Step 2Find the collision check in Ghidra
    In Ghidra, find the movement function. It contains a check like 'if (tile[y][x] == WALL) return;'. Patch this check to always allow movement.
    Learn more

    Import the binary into Ghidra and run auto-analysis. Search for string references related to wall tiles (often '#' in ASCII dungeons) or look for the movement handling function in main().

    The collision check in decompiled C looks like:

    if (map[player_y + dy][player_x + dx] == '#') { return; }

    In the disassembly, the pattern looks like:

    mov   rax, QWORD PTR [rip+0xNNNN]   ; load map base pointer
    movzx eax, BYTE PTR [rax+rcx]       ; load tile at (player_y * width + dx)
    cmp   BYTE PTR [rip+0xMMMM], '#'    ; or: cmp al, 0x23  -> compare to wall char
    jne   <update_position>             ; skip the early return when not a wall
    ret                                 ; bail without moving
    update_position:
    mov   ...                            ; write new player_x / player_y

    Look for a cmp against 0x23 (the ASCII for #) followed by a conditional jump. Patching this conditional to a NOP (no-operation, 0x90) or flipping it to an unconditional jmp eliminates the wall check.

    In Ghidra: right-click the conditional jump instruction, select "Patch Instruction", and change it to NOP. Then export the patched binary (File > Export Program > ELF).

  3. Step 3Run the patched binary and navigate through walls
    Execute the patched binary. Your character can now move onto any tile. Navigate to the flag room and read the flag characters displayed on the map.
    bash
    chmod +x wizardlike-patched && ./wizardlike-patched
    bash
    # Navigate with WASD/arrow keys through the previously blocked walls
    Learn more

    An alternative to patching is using GDB at runtime: set a breakpoint on the conditional jump instruction address, and when it triggers run set $eflags ^= 0x40. The 0x40 bit is the zero flag; XORing flips it, which means a jne taken-because-not-a-wall becomes a jne not-taken (or vice versa). The single line turns "step into wall > bail" into "step into wall > proceed." You only have to do this once per movement attempt; continue until you reach the next blocked tile.

    Another approach is to directly modify the player's coordinates in memory while the program is paused in GDB: set *(int*)&player_x = TARGET_X. The cast tells GDB to write a 32-bit integer at the address of player_x, overwriting the current value with TARGET_X. Pair with set *(int*)&player_y = TARGET_Y and continue: the next render places you at the target tile, no walking required. (If player_x is not a symbol in a stripped build, replace &player_x with the absolute address you found in Ghidra.)

    Reading the flag off the rendered map. Once you can move freely, walk every accessible cell and watch the rendered output. The flag glyphs are written into the tile map at fixed coordinates, so you may need to step adjacent to each character (the renderer often only reveals a tile when the player is within line-of-sight). Record the visible glyphs in order, prefix with picoCTF{, and submit. If line-of-sight is blocking the read, set the player tile to each flag coordinate in turn with the GDB set *(int*) trick.

    This class of challenge - where the flag is hidden in the game world behind an artificial barrier - teaches binary patching, a fundamental technique in game modding, license bypass analysis, and malware modification.

Flag

picoCTF{ur_4_w1z4rd_...}

This challenge was not solved during the competition. Patch the wall-collision check in Ghidra to NOP, run the patched binary, and navigate to the hidden flag room.

Want more picoCTF 2022 writeups?

Useful tools for Reverse Engineering

Related reading

What to try next