← Back to Writeups
CUSTOMMediumForensics

Старый боец (Old Fighter)

XESXOR9/2/20264 min read
#forensics#custom#medium#wad_directory_parsing#duplicate_lump_name_detection#lump_size_anomaly_detection#doom_patch_format_validation#png_magic_byte_carving#image_upscaling

Старый боец (Old Fighter)

Platform: hackerlab | Category: Forensics | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2025-05-03 | Status: Solved Techniques: wad_directory_parsing, duplicate_lump_name_detection, lump_size_anomaly_detection, doom_patch_format_validation, png_magic_byte_carving, image_upscaling

Summary

Task: a DOOM PWAD file (my.wad, 238 lumps) hides a flag. Solution: parse the WAD directory, spot two duplicate PANBOOK3 lumps with an anomalous size, validate they are not real DOOM patches, carve them by PNG magic bytes, and read the flag rendered as text on a bookshelf image.

Старый боец (Old Fighter) — hackerlab

Description

Куда теперь занесло этого Палача? Хотя в любом случае он справится

English: "Where has the Executioner (Палач) ended up now? He'll manage anyway."

Download: a single old_fighter.zip containing one file, my.wad.

Semantic clues

  • "Старый боец" = Old Fighter.
  • "Палач" (the Executioner) is the classic Russian nickname for DOOM's protagonist (Doomguy).

Both hints point directly at DOOM. The artifact is a DOOM data file (WAD), so the intended path is WAD-level forensics, not classic image/audio stego. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Analysis

Extracting the zip yields my.wad:

$ file my.wad
my.wad: doom patch PWAD data containing 238 lumps

A PWAD (Patch WAD) is a container with a fixed structure:

  • Header (12 bytes): 4-byte magic PWAD, int32 numlumps, int32 dirofs.
  • Directory: numlumps entries of 16 bytes each — int32 filepos, int32 size, 8-byte name (NUL-padded).
  • Each lump is raw data referenced by filepos/size.

Parsing the directory listed 238 lumps: a complete MAP01 (THINGS, LINEDEFS, SIDEDEFS, VERTEXES, SEGS, SSECTORS, NODES, SECTORS, REJECT, BLOCKMAP) plus many texture / flat / patch graphic lumps. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Red herrings (what did NOT work)

  • Rendering MAP01 geometry (VERTEXES + LINEDEFS) with matplotlib produced a normal, complete, playable level — no flag drawn into the walls.
  • THINGS positions — normal monster/item placement, not a message.
  • strings / grep for CODEBY, flag, hackerlab — no plaintext anywhere.

Key insight — anomalous duplicate lumps

Scanning the directory for anomalies revealed two duplicate lumps both named PANBOOK3 at directory indices 222 and 223, each with size 12747 bytes. The legitimate texture PANBOOK3 lives at index 218 with size 17544 bytes. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Duplicate lump names with a different size = strong tampering signal.

Attempting to parse idx 222/223 as a DOOM patch (picture) lump gave absurd header values — width 20617, height 18254 — proving these are not valid DOOM patches. (A real patch header is int16 width, int16 height, int16 leftoffset, int16 topoffset, followed by width int32 column offsets and column "post" data.)

Dumping their raw bytes showed the real format:

$ xxd dup_222.bin | head -1
00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Magic 89 50 4E 47 0D 0A 1A 0A → these lumps are PNG files (128×128, 8-bit RGBA), smuggled into the WAD disguised as texture lumps at file offsets 3419459 and 3432206 (size 12747 each).

Solution

  1. Extract my.wad from the zip.
  2. Parse the PWAD header and 16-byte directory entries.
  3. Flag the anomaly: two PANBOOK3 lumps (idx 222, 223) with size 12747, distinct from the genuine texture at idx 218 (size 17544).
  4. Confirm they are not DOOM patches (impossible width/height in the patch header).
  5. Carve them by PNG magic bytes and save as .png.
  6. Open the PNG — a bookshelf graphic with overlaid text; upscale with NEAREST to read it. The flag is rendered across three lines: CODEBY{D00m / REDACTED / }. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍
python
#!/usr/bin/env python3
import struct

with open("my.wad", "rb") as f:
    data = f.read()

magic, numlumps, dirofs = struct.unpack_from("<4sii", data, 0)
assert magic == b"PWAD", magic
print(f"{magic.decode()} lumps={numlumps} dirofs={dirofs}")

# Parse directory
lumps = []
for i in range(numlumps):
    off = dirofs + i * 16
    filepos, size = struct.unpack_from("<ii", data, off)
    name = data[off + 8:off + 16].split(b"\x00")[0].decode("latin1")
    lumps.append((i, name, filepos, size))

# Detect duplicate names with conflicting sizes
from collections import defaultdict
by_name = defaultdict(list)
for i, name, pos, size in lumps:
    by_name[name].append((i, pos, size))
for name, entries in by_name.items():
    if len(entries) > 1:
        sizes = {s for _, _, s in entries}
        if len(sizes) > 1:
            print(f"[!] duplicate '{name}' with differing sizes: {entries}")
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

# Carve any lump that is actually a PNG
PNG_MAGIC = b"\x89PNG\r\n\x1a\n"
for i, name, pos, size in lumps:
    blob = data[pos:pos + size]
    if blob.startswith(PNG_MAGIC):
        out = f"carved_{i}_{name}.png"
        with open(out, "wb") as o:
            o.write(blob)
        print(f"[+] PNG lump idx={i} name={name} offset={pos} size={size} -> {out}")

Carved PNGs carved_222_PANBOOK3.png / carved_223_PANBOOK3.png contain the flag text on a bookshelf image. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍