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

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 1
    Explore the dungeon and identify blocked areas
    Observation
    I noticed the binary is a roguelike dungeon crawler where flag characters are visible on the map but located in areas that cannot be entered, which suggested the first step was to map out what is visible versus what is reachable so I could identify exactly which tiles to target after bypassing the collision check.
    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 2
    Find the collision check in Ghidra
    Observation
    I noticed the game blocked movement onto specific tiles rather than the entire map, which suggested a single conditional branch in the movement function was comparing the next tile against the wall character # (ASCII 0x23) and returning early, making Ghidra the right tool to locate and patch that exact instruction.
    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 3
    Run the patched binary and navigate through walls
    Observation
    I noticed the Ghidra patch removed the tile comparison that guarded the return-without-moving path, which suggested running the patched binary and walking directly through the previously blocked # tiles would now be possible and would reveal the flag characters scattered across the formerly inaccessible room.
    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 has the correct meaning at the moment the 'jne' or 'je' instruction at the collision check is about to execute. If you flip it one instruction earlier or later, you are toggling a bit that controls a completely different branch, so the game either crashes, skips an unrelated update, or still blocks you. Set the breakpoint precisely on the conditional jump address from Ghidra before running 'set $eflags ^= 0x40', then issue 'continue' so the next instruction is that exact jump.

    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' dialog defaults to an original binary format with only the patch bytes changed, but some configurations rewrite the ELF program headers or drop the PT_INTERP segment. Running 'file wizardlike-patched' will show 'ELF 64-bit LSB executable' without a dynamic linker path if that happened. Instead, use Ghidra's 'Save As' with format 'ELF' and verify with 'readelf -l wizardlike-patched' that the INTERP segment is intact and points to a valid loader before executing.

    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.

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 check in Ghidra to NOP, run the patched binary, and navigate to the hidden flag room.

Key takeaway

Binary patching replaces a single instruction (typically a conditional jump) in an existing executable to alter program logic without access to source code. Changing a 'jne' to a 'nop' or 'jmp' bypasses an entire enforcement branch, which is the same technique used in software license bypasses, game cheats, and malware that disables antivirus hooks. GDB provides the same capability at runtime without touching the file on disk, by flipping CPU flag register bits at a breakpoint. Both approaches are staples of reverse engineering because most client-side enforcement relies on a single trusted conditional that the attacker, with full control of the process, can trivially subvert.

Related reading

Want more picoCTF 2022 writeups?

Useful tools for Reverse Engineering

What to try next