Pac-Man
Pac-Man
Platform: volgactf | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-03-28 | Status: Solved Techniques: 6502_disassembly, code_padding_boundary_analysis, ines_rom_parsing, injected_code_detection, score_trigger_analysis, xor_cipher_decryption
Summary
Task: Modified NES Pac-Man ROM with hidden flag triggered by reaching a specific score. Solution: Disassembled 6502 code with radare2, found injected subroutine at PRG ROM padding boundary that checks for score 7331 and decrypts 25-byte flag using XOR cipher with index-dependent key.
Pac-Man — VolgaCTF 2026
Description
Once Pac-Man eats enough, the flag will awaken within him.
Given: PacMan_Japan_En.nes (24592 bytes) — a modified NES ROM image of the classic Pac-Man game. The goal is to find the hidden flag embedded in the ROM.
Analysis
File Identification
The file is an NES ROM in iNES/NES 2.0 format:
- 16-byte iNES header (magic:
NES\x1a) - 1×16KB PRG ROM — 6502 game code at file offset
0x10–0x4010 - 1×8KB CHR ROM — tile graphics at file offset
0x4010–0x6010 - Horizontal mirroring, NTSC
- Copyright: "COPY RIGHT 1984 1980 NAMCO LTD." — this is the original Pac-Man (Japan) ROM with CTF modifications
Initial Recon
stringsfound no plaintext flag — no "VolgaCTF" or "flag" anywhere- XOR brute-force with single-byte keys yielded nothing
- CHR ROM contains full A-Z alphabet (tiles 0x41–0x5A) and digits (tiles 0x30–0x39) mapping to ASCII
- Game text ("1 PLAYER", "2 PLAYERS", AKABEI, PINKY, GUZUTA, "PAUSE") found as tile index sequences in PRG ROM
Knowledge Base Insight
A search of CTF Writeup found a very similar challenge — "Танчики (Tanks)" from Duckerz CTF — a GBA ROM with a hidden encrypted flag triggered by reaching specific game coordinates. The approach (find injected code → identify trigger condition → extract/decrypt flag) was directly applicable here.
Injected Code Discovery
Using radare2 to disassemble the PRG ROM, discovered a suspicious subroutine at $B430 (PRG offset 0x3430). This address sits right at the boundary between legitimate game code and unused 0xFF padding — a classic sign of CTF code injection into a retro ROM.
The NES interrupt vectors confirm the ROM structure:
- NMI:
$C0FA, Reset:$C033, IRQ:$C167
Solution
Step 1: Disassemble the Injected Routine
The injected subroutine at $B430 has three parts:
Part 1 — Score Check ($B435–$B44F):
asm read-only • copy allowedLDA $0070 ; Load score digit 1 CMP #$07 ; Must be 7 BNE skip LDA $0071 ; Load score digit 2 CMP #$03 ; Must be 3 BNE skip LDA $0072 ; Load score digit 3 CMP #$03 ; Must be 3 BNE skip LDA $0073 ; Load score digit 4 CMP #$01 ; Must be 1 BNE skip
The score digits are stored as BCD at RAM $0070–$0073. The trigger score is 7331 — leet-speak, a classic CTF hint.
Part 2 — Decryption Loop ($B451–$B46C):
asm read-only • copy allowedLDX #$00 ; X = 0 (loop counter) loop: TXA ; A = X ASL A ; A = X * 2 STA $0401 ; temp = X * 2 TXA ; A = X CLC ADC $0401 ; A = X + X*2 = X*3 CLC ADC $B483,X ; A = X*3 + encoded[X] EOR #$5A ; A ^= 0x5A SEC SBC #$10 ; A -= 0x10 STA $0400,X ; Store decoded byte INX CPX #$19 ; 25 bytes total BNE loop
Part 3 — Cleanup: Clears RAM at $0400–$04FF and $0500–$05FF, then restores registers and returns.
Step 2: Extract the Encoded Data
At $B483 (PRG offset 0x3483), 25 encoded bytes:
3C 22 20 24 1F FA 2C F7 B9 1F EC 08 11 E0 E0 F7 05 05 E4 CE F9 F3 D8 F3 8F
Step 3: Decrypt the Flag
The decryption formula for each byte at index X:
decoded[X] = ((X * 3 + encoded[X]) ^ 0x5A) - 0x10
python read-only • copy allowed#!/usr/bin/env python3 """Pac-Man NES ROM flag decryptor — VolgaCTF 2026""" encoded = bytes.fromhex('3c2220241ffa2cf7b91fec0811e0e0f70505e4cef9f3d8f38f') flag = '' for x in range(25): val = ((x * 3 + encoded[x]) & 0xFF) ^ 0x5A val = (val - 0x10) & 0xFF flag += chr(val) print(flag) # VolgaCTF{REDACTED}
Output:
VolgaCTF{REDACTED}
The flag name itself is a hint: R0M_X0R — XOR-based encoding in a ROM.