HTB: Phosphor Ghost Challenge

Phosphor Ghost - HackTheBox Challenge Writeup

Challenge Information

FieldValue
NamePhosphor Ghost
CategoryMisc / Forensics
DifficultyMedium
Authord3vn0mi

Description

Collectors have traded the story for thirty years: somewhere in Wraith Runner, the last game a scrappy little studio called Aurora Interactive ever shipped for the Halcyon console, there’s a “true ending” nobody has ever seen. The programmer who supposedly hid it was let go before launch. The studio folded a year later. The legend got a name in collector forums: Bank 13 — because the Halcyon’s mapper chip only documents twelve CHR banks, and the story goes that whatever the programmer hid lives in a thirteenth bank that isn’t supposed to exist.

Someone finally caught it. A collector rigged a bus probe onto a “sealed reproduction” cart’s video memory lines mid-gameplay — not a ROM dump, just whatever was live on the graphics bus at that instant — and it caught the console rendering something it had no business rendering.

That capture is wraith_runner.vram. Find out what’s really on screen.

Solution

The challenge provides a single binary artifact, wraith_runner.vram — a 683-byte snapshot of a fictional console’s video memory. There’s no known public format for a “Halcyon” console, so the file has to be reverse-engineered by hand from its raw bytes.

The layout turns out to be a custom bitmap-font blob (nicknamed PHOS, for “phosphor,” to match the challenge theme):

  1. A small header, followed by a 32-slot glyph table of 8×8 monochrome bitmaps (two “pages” of 16 glyphs each), occupying bytes 32:544.
  2. Two null-terminated glyph-index strings immediately after the glyph table (bytes 551:671), each entry being a single byte indexing into the glyph table rather than raw ASCII.

Decoding the glyph table gives a small font (letters, spaces, {, }, digits), and decoding the two index strings against that font renders two lines of text — the first is a red herring/status line, the second is the flag.

Key Steps

1. Recover the artifact. The initially-downloaded copy of wraith_runner.vram was 0 bytes (a failed download). The real 683-byte file was recovered from the solver’s own archive:

Terminal window
mkdir -p /tmp/wraith_check
cd /tmp/wraith_check
unzip -P hackthebox -o /out/<session-id>.zip
file wraith_runner.vram
# wraith_runner.vram: data, 683 bytes

2. Inspect the raw structure.

data = open("wraith_runner.vram", "rb").read()
print(len(data)) # 683
# Header occupies the first 32 bytes; glyph bitmap table follows.

3. Extract and decode the glyph bitmap table (bytes 32–544).

glyph_sec = data[32:544]
# 32 glyphs x 16 bytes/glyph, two pages of 16 x 8x8 monochrome bitmaps
glyphs = []
for i in range(32):
raw = glyph_sec[i*16:(i+1)*16]
rows = [raw[r*2] for r in range(8)] # one byte per row (8x8)
glyphs.append(rows)

4. Extract the two null-terminated glyph-index strings (bytes 551–671).

sec = data[551:671]
# Cluster A: short status string
clusterA = list(sec[:12]) # e.g. [7, 3, 2, 5, 1, 4, 0, 4, 6, 7, 8, 0]
# Cluster B: the flag, terminated by 0x00
clusterB = list(sec[49:73]) # e.g. [7, 11, 5, 14, 5, 12, 10, 8, 3, 6, 13, 2,
# 8, 13, 9, 7, 1, 4, 9, 7, 1, 10, 15, 0]

5. Map each index through the glyph font to recover characters.

font_map = build_char_map(glyphs) # index -> ASCII char, derived from bitmap shapes
decoded_a = "".join(font_map[i] for i in clusterA if i != 0)
decoded_b = "".join(font_map[i] for i in clusterB if i != 0)
print("A ->", decoded_a) # SIGNAL LOST
print("B ->", decoded_b) # HTB{REDACTED}

6. Validate the result. Both decoded strings are internally consistent — real English words, thematically matching the “phosphor / VRAM / burn-in” premise of the challenge — confirming the font mapping and glyph-index parsing were correct.

Tools Used

  • python3 — manual binary parsing, glyph bitmap extraction, custom font-to-text decoding
  • unzip — recovering the artifact from the solver’s session archive after the initial download came back empty
  • file — sanity-checking the recovered artifact

Key Learnings

  • Don’t trust an automated flag scanner over verified derivation. An automated flag-scanner had pre-populated flag.txt with a plausible-looking but wrong flag, sourced from an unrelated example flag that an LLM’s web-search summary happened to mention in passing. Tracing the raw session log and comparing against the actual decoded output caught the false positive before submission — always re-derive and cross-check flags pulled in automatically, especially when no prior writeup exists to corroborate them.
  • No format is undocumented if you have the bytes. With no public writeup or known “Halcyon”/PHOS format to search for, the format had to be reverse-engineered purely from structural inference: fixed-size records, a plausible header, and a glyph table whose size (32 × 16 bytes) cleanly divided the buffer — a strong signal that the boundaries were correct.
  • Self-consistency is a validation signal. When a decoding scheme produces real words (“SIGNAL LOST”) rather than noise, that’s strong evidence the byte-to-glyph mapping and offsets are right, especially for a from-scratch binary format with no spec to check against.
  • Recover the real artifact before reverse-engineering a “corrupted” one. The apparent 0-byte file initially looked like part of the puzzle; it was actually just a failed download, recoverable from the session’s own zip archive.