HTB: Phosphor Ghost Challenge
Phosphor Ghost - HackTheBox Challenge Writeup
Challenge Information
| Field | Value |
|---|---|
| Name | Phosphor Ghost |
| Category | Misc / Forensics |
| Difficulty | Medium |
| Author | d3vn0mi |
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):
- 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. - 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:
mkdir -p /tmp/wraith_checkcd /tmp/wraith_checkunzip -P hackthebox -o /out/<session-id>.zipfile wraith_runner.vram# wraith_runner.vram: data, 683 bytes2. 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 bitmapsglyphs = []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 stringclusterA = list(sec[:12]) # e.g. [7, 3, 2, 5, 1, 4, 0, 4, 6, 7, 8, 0]
# Cluster B: the flag, terminated by 0x00clusterB = 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 LOSTprint("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 decodingunzip— recovering the artifact from the solver’s session archive after the initial download came back emptyfile— 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.txtwith 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”/
PHOSformat 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.