Series
Tenable Zero Day Assessment — View all write-ups

Challenge

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.

Initial Reconnaissance

Starting with basic file identification:

file command output — Game Boy ROM identified

The target is a Game Boy ROM. Loading it in OpenEmu gives us our first look at the challenge:

ROM running in OpenEmu — prompt to enter key combo

The game displays a prompt instructing us to enter the correct key combination to obtain the flag.

Tooling Research: Ghidra + GhidraBoy

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:

Vanilla Ghidra — failed disassembly of Game Boy ROM

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:

GhidraBoy language selection in Ghidra

The result is coherent disassembly and decompiled output:

Successful disassembly with GhidraBoy

Static Analysis: Locating the Input Handling Code

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:

Ghidra string search — message string at 0x6644

Cross-referencing that address reveals a reference at 0x0414:

Cross-references to the message string Reference to message string 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:

Function at 0x0384 — potential main game loop

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.

Hypothesis: Identifying the Key Combination

A suspicious code block stood out during analysis:

Code block with sequence 1 2 1 2 8 8 4 4

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:

SameBoy debugger — paused at 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:

SameBoy — naive jump to 0x0625

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.

Alternative Approach: Targeting Flag Generation Directly

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:

Function calls at flag generation site

Focusing on the function at 0x0996:

Function 0x0996 decompiled

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:

Setup code at 0x0619 before function call at 0x0625

Jumping to 0x0619 in SameBoy and stepping through the subsequent instructions, the flag appeared immediately in the VRAM viewer:

SameBoy VRAM viewer — flag rendered

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