BSides Mumbai CTF 2026 — The Game That Lies
Challenge Description
The game logic seems to work fine, but the emulator screen is buggy and unreliable. See if you can cut through the lies and find the treasure hidden inside.
File: the_game_that_lies.gb (Game Boy ROM, 32KB)
The core mechanic of this challenge relies on Game Boy architecture (specifically VRAM tilemaps and WRAM buffers). The game attempts to trick static analysis tools, forcing us to combine static reversing with dynamic emulation using PyBoy (a Game Boy emulator written in Python).
Step 1 — Reconnaissance & Static Analysis
First, we analyzed the ROM file using standard tools like file and strings.
file the_game_that_lies.gb
# Output: Game Boy ROM image (Rev.01) [ROM ONLY], ROM: 256Kbit

Dumping the printable strings revealed standard game text, but also a suspicious sequence:
SECURITY SYSTEM
PRESS START
ENTER 8-STEP CODE
WRONG INPUT = RESET
DIAG MODE
DIAG CODE: 4471-B
RECOVERY:
SYSTEM READY

Right next to the string SYSTEM READY at offset 0x2A0 in the ROM, we found a raw byte sequence:
01 03 04 02 05 06 FF 00
By disassembling the joypad reading routine at 0x5F7 (which checks bits in the Game Boy’s 0xFF00 hardware register), we mapped these bytes to button presses:
01= UP (Bit 2)02= DOWN (Bit 3)03= LEFT (Bit 1)04= RIGHT (Bit 0)05= A (Bit 4)06= B (Bit 5)
This gave us a 6-step code: UP, LEFT, RIGHT, DOWN, A, B.
Step 2 — Falling for the Trap (Dynamic Emulation)
To test this code, we wrote a Python script using the PyBoy library. This allowed us to emulate the Game Boy headlessly, programmatically press the buttons, and read the Video RAM (VRAM) to see what the screen was trying to display without relying on buggy graphics rendering.
Why we did this: The challenge stated the screen is “buggy and unreliable”. In Game Boy architecture, text is typically rendered via a tilemap (indices mapping to font graphics). Even if the graphics are corrupted (XOR’d or blanked out), the tilemap indices usually still match standard ASCII.
PyBoy Script (The Lie)
from pyboy import PyBoy
pyboy = PyBoy('the_game_that_lies.gb', window='null')
pyboy.set_emulation_speed(0)
# Advance past boot screen and press START
for _ in range(300): pyboy.tick(render=False)
pyboy.button('start', delay=5)
for _ in range(120): pyboy.tick(render=False)
# Enter the static sequence found in ROM
for btn in ['up', 'left', 'right', 'down', 'a', 'b']:
pyboy.button(btn, delay=10)
for _ in range(60): pyboy.tick(render=False)
# Wait for output and dump VRAM (0x8000 - 0x9FFF)
for _ in range(300): pyboy.tick(render=False)
vram = bytes([pyboy.memory[0x8000 + i] for i in range(0x2000)])
# Read the tilemap area (0x9800)
tilemap = vram[0x1800:0x1C00]
print(bytes(tilemap).decode('latin-1', errors='ignore'))
pyboy.stop()The Output:
https://youtu.be/dQw4w9WgXcQ
We got Rickrolled. The string explicitly asked for an 8-STEP CODE, but our sequence was only 6 steps. The sequence at 0x2A0 was a deliberate lie placed to trick static analysts.
Step 3 — Bypassing the PRNG (The Truth)
To find the real 8-step code, we disassembled the ROM using mgbdis and traced the input buffering logic.
We discovered:
- User input is buffered into Work RAM (WRAM) starting at address
0xC0B3. - A function at
0x696runs heavily on boot before the user even presses a button. - This function acts as a PRNG (Pseudo-Random Number Generator), seeded by the value at
0xC0B2(which initializes to0xF0). It loops 8 times, generating 8 valid button presses, and pre-fills the input buffer at0xC0B3with the correct sequence.
How we solved it: Instead of manually reverse-engineering the PRNG math, we can just let the emulator boot up and dump the 0xC0B3 buffer before we press any buttons. This extracts the generated truth dynamically.
PyBoy Script (Extracting the Truth)
from pyboy import PyBoy
pyboy = PyBoy('the_game_that_lies.gb', window='null')
pyboy.set_emulation_speed(0)
# Let the boot PRNG run
for _ in range(300): pyboy.tick(render=False)
# Dump the generated 8-byte sequence from WRAM
wram = bytes([pyboy.memory[0xC000 + i] for i in range(0x2000)])
seq = wram[0xB3:0xBB]
print("Real Code Bytes:", seq.hex())
mapping = {1: "up", 2: "down", 3: "left", 4: "right", 5: "a", 6: "b"}
print("Real Code Sequence:", [mapping[v] for v in seq])
pyboy.stop()The Output:
Real Code Bytes: 0605020203020401
Real Code Sequence: ['b', 'a', 'down', 'down', 'left', 'down', 'right', 'up']
Step 4 — Extracting the Treasure
Armed with the true 8-step sequence, we modified our original PyBoy script to input this new code and parse the VRAM tilemap for the flag.
PyBoy Script (Final Exploit)
from pyboy import PyBoy
pyboy = PyBoy('the_game_that_lies.gb', window='null')
pyboy.set_emulation_speed(0)
# Advance past boot and press START
for _ in range(300): pyboy.tick(render=False)
pyboy.button('start', delay=5)
for _ in range(120): pyboy.tick(render=False)
# Enter the REAL 8-step sequence
sequence = ['b', 'a', 'down', 'down', 'left', 'down', 'right', 'up']
for btn in sequence:
pyboy.button(btn, delay=10)
for _ in range(60): pyboy.tick(render=False)
# Wait for decryption and dump VRAM
for _ in range(600): pyboy.tick(render=False)
vram = bytes([pyboy.memory[0x8000 + i] for i in range(0x2000)])
tilemap = vram[0x1800:0x1C00]
# Parse printable ASCII from the tilemap
flag_str = ""
for row in range(18):
line = tilemap[row*32:(row+1)*32]
out = ''
for t in line[:20]:
if 0x20 < t <= 0x7E: # Standard ASCII bounds
out += chr(t)
if out:
flag_str += out
print("Extracted String:", flag_str)
pyboy.stop()Final Output:
Extracted String: SYSTEMREADYBsides_Mumbai{st4t3_l1e5_wh3n_y0u_re4d_1t}

Flag
Bsides_Mumbai{st4t3_l1e5_wh3n_y0u_re4d_1t}
Summary
This challenge was a fantastic exercise in dynamic analysis. By combining static reverse engineering to find the input mechanisms and dynamic Python scripting with PyBoy to bypass the obfuscated screen and read raw RAM, we successfully extracted the flag and avoided the Rick Roll trap!