← Back to Writeups
CUSTOMMediumReversing

Pac-Man

XESXOR9/10/20264 min read
#reversing#custom#medium#6502_disassembly#code_padding_boundary_analysis#ines_rom_parsing#injected_code_detection#score_trigger_analysis#xor_cipher_decryption

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

  • strings found 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
LDA $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
LDX #$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
#!/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. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍