Skip to main content

Wizardlike picoCTF 2022 Solution

Reverse engineer a dungeon-crawler binary and bypass its movement restrictions to reach the hidden flag.

Published: July 20, 2023Updated: August 25, 2026

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. Get the files from the challenge page on CyLab Security Academy (formerly play.picoctf.org).

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

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

bash
chmod +x wizardlike
bash
./wizardlike

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
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
    Observation
    The binary is a roguelike where flag characters are visible on the map but sit in rooms you cannot enter. Map what is visible against what is reachable first, so you know which tiles to head for once the collision check is bypassed.
    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
    Observation
    The game blocks movement onto specific tiles rather than everywhere, which means one conditional in the movement function compares the next tile against the wall character and returns early. Ghidra will locate that exact instruction to patch.
    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   al, 0x23                      ; 0x23 is ASCII '#' -> 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. Which patch you want depends on which way that jump points. In the shape above the jump is the good path (jne update_position skips the bail-out), so replace it with an unconditional jmp update_position; NOP-ing it there would fall straight into the ret and block every move, not just the ones into walls. When the compiler emits the mirror image instead (je bail_out, then fall through to the move), the NOP (no-operation, 0x90) is the right patch. Read the branch target before choosing.

    In Ghidra: right-click the conditional jump instruction, select "Patch Instruction", and type the replacement (JMP at the branch target for the shape above, or NOP if the jump is the one heading for the bail-out). Then export the patched binary (File > Export Program > ELF).

  3. Step 3Run the patched binary and navigate through walls
    Observation
    The patch removes the tile comparison guarding the return-without-moving path. Run the patched binary and you can walk straight through the previously blocked tiles into the room holding the scattered flag characters.
    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
    What didn't work first

    Tried: Trying to use GDB's 'set $eflags ^= 0x40' trick but applying it before reaching the collision branch, so the flag flip lands on an unrelated instruction and the wall still blocks movement.

    The zero flag only carries the meaning you want at the instant the collision check's conditional jump is about to execute. Flip it an instruction earlier or later and you are toggling a bit governing some other branch, so the game crashes, skips an unrelated update, or still blocks you. Break exactly on the jump address from Ghidra, flip the flag, then continue so that jump is the very next instruction.

    Tried: Exporting the patched binary from Ghidra and running it, but getting 'Exec format error' or a segfault immediately on launch because Ghidra's default export options changed the ELF header or stripped the interpreter path.

    Ghidra's Export Program defaults to the original format with only the patched bytes changed, but some configurations rewrite the ELF program headers or drop the interpreter segment. If that happens, the file command reports an executable with no dynamic linker path. Use Save As with the ELF format instead, and check with readelf that the interpreter segment survives and points at a valid loader before running it.

    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). The map spells out the whole flag, wrapper included, so record the visible glyphs in order from p through the closing brace and submit that. 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.

Interactive tools
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.
  • Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.

Flag

Reveal flag

picoCTF{ur_4_w1z4rd_...}

This challenge was not solved during the competition. Patch the wall-collision branch in Ghidra so the move always goes through, run the patched binary, and navigate to the hidden flag room.

Key takeaway

Binary patching replaces one instruction, usually a conditional jump, to change program logic without any source. Turn a conditional jump into a nop or an unconditional one and an entire enforcement branch disappears, which is the same technique behind license bypasses, game cheats, and malware disabling antivirus hooks. GDB does the same at runtime without touching the file, by flipping CPU flag bits at a breakpoint. Both are staples of reverse engineering, because most client-side enforcement hangs on a single trusted conditional that an attacker owning the process can simply subvert.

Related reading

Useful tools for Reverse Engineering

Where to go next