Challenge 2: play_me
===
The play_me file is a CTF challenge based on a Z80-style CPU. We've hidden a flag
with the format of "flag{ xxxxx }" somewhere in the application. Your task is to
find this flag and provide a write-up on your approach.
Deliverables:
1. A write up describing how you approached the challenge and found the flag (if found).
2. The flag (if found).
An unfamiliar architecture makes for an interesting target — I had no prior experience with Z80 or Game Boy ROM internals going into this one.
Starting with basic file identification:
The target is a Game Boy ROM. Loading it in OpenEmu gives us our first look at the challenge:
The game displays a prompt instructing us to enter the correct key combination to obtain the flag.
The first attempt at static analysis used a standard Ghidra instance. It was unable to produce meaningful disassembly — the built-in Z80 language definition generated nonsensical output, and automatic function identification failed entirely:
Research into Z80/Game Boy reverse engineering tooling led to GhidraBoy, a Ghidra extension specifically designed for Game Boy ROM analysis. This YouTube guide on Game Boy ROM hacking provided additional context for getting started.
With GhidraBoy installed, the language selection correctly identifies the Game Boy architecture:
The result is coherent disassembly and decompiled output:
The goal was to locate the key-combo input validation logic. A natural starting point was finding the prompt string in the binary and tracing references back to the code that reads input.
Searching for the message string in Ghidra locates it at address 0x6644:
Cross-referencing that address reveals a reference at 0x0414:
The function at 0x0384 appears to be the main game loop. It prints the prompt, handles button input, and contains a reference to a "Flag generated" message, suggesting this is where flag generation is triggered:
The if statements here iterate over bits of the byte at 0xff00, which is the Game Boy joypad register. For context, the joypad register layout is as follows:
Bit 7 - Not used
Bit 6 - Not used
Bit 5 - P15 Select Action buttons (0=Select)
Bit 4 - P14 Select Direction buttons (0=Select)
Bit 3 - P13 Input: Down or Start (0=Pressed) (Read Only)
Bit 2 - P12 Input: Up or Select (0=Pressed) (Read Only)
Bit 1 - P11 Input: Left or B (0=Pressed) (Read Only)
Bit 0 - P10 Input: Right or A (0=Pressed) (Read Only)
Much of the binary required manual disassembly (pressing D in Ghidra) since automatic analysis left large regions un-decoded. This was time-consuming but necessary to build a clearer picture of the code structure.
A suspicious code block stood out during analysis:
The sequence 1 2 1 2 8 8 4 4 is oddly specific and potentially encodes the expected key combination. If interpreted as directional inputs (bits 0–4 of the joypad register), this decodes to Right Left Right Left Down Down Up Up — the Konami Code in reverse. Several input sequences were tested, none successfully.
Switching to SameBoy, a Game Boy emulator with a built-in debugger, the code was examined dynamically. After loading the ROM and pausing execution, the program counter was observed at address 0x0860:
Manually patching the program counter to jump to the flag generation call site (eval pc=$0625) was attempted, but without setting up the stack correctly first, this approach failed:
After roughly 24 hours of analysis without a breakthrough on the key combination, the decision was made to shift focus to the flag generation logic itself.
Picking back up on Tuesday morning — Wednesday at 11:30am, having completed challenges 3 and 4 in the interim. Still driven to find this flag.
The two function calls following the do...while loop in 0x0384 had been noted from the beginning — they appear directly after the "Flag generated" message reference:
Focusing on the function at 0x0996:
This function references a global byte sequence (0x1c 0x1c 0x24 0x24 0x20 0x20 0x38 0x38 0x20 0x20 0x20 0x20) set only at runtime, making static analysis of its full behavior difficult. The debugger was the better tool here.
The critical detail from the earlier failed jump attempt: when jumping to 0x0625 (the function call site), the stack had not been initialized. The correct entry point is 0x0619, which sets up the stack frame before the call:
Jumping to 0x0619 in SameBoy and stepping through the subsequent instructions, the flag appeared immediately in the VRAM viewer:
flag{congrats!}
The correct key combination was never determined. Whether one is strictly required or whether the ROM simply uses it as one of several paths to flag generation remains an open question. The objective — locating the flag and documenting the approach — was achieved roughly 48 hours after receiving the challenges.
In any case, this was a rewarding challenge. Going in with no knowledge of Game Boy internals or Z80 architecture and emerging with the flag required research, persistence, and a willingness to pivot approach when progress stalled.
Challenge file: play_me.gb