Neven Marić
Hidden Investigations CTF 2026

ForensicsHidden Investigations CTF 2026

RESURRECTION (Operation Eclipse Void)

Recover a five-part flag from a 1 GiB raw disk image by reconstructing wiped GPT/NTFS geometry, then extracting data from PE overlays, NTFS runlist cluster lengths, USN journal timestamps, EVTX tokens, and $Bitmap orphan cluster spacings.

  • forensics
  • disk-image
  • ntfs
  • gpt
  • pe-overlay
  • usn-journal
  • evtx
  • bitmap
  • steganography
  • xor

What this one teaches

  • A wiped primary GPT/MBR doesn't kill the volume — the backup GPT header and backup NTFS VBR at the end of the disk are enough to reconstruct partition geometry and locate the $MFT.
  • Data can be encoded in structural metadata: NTFS runlist cluster lengths, USN journal timestamp nano-deltas, EVTX binary XML substitution slots, and $Bitmap allocation spacings are all steganographic channels.
  • Each phase has an ordinal tag (f!r5t--, 53c0nd--, etc.) that both confirms extraction correctness and determines concatenation order.

Task description

Tier-0 infrastructure collapsed under an absolute scorched-earth purge. The adversary severed their uplink leaving zero userland footprints, zero dropped scripts, and a pulverized volume geometry that defies standard triage parsing.

Classified signals intelligence indicates the transmission was shattered not into files, but into the structural dissonance of the medium itself. The beacon exists solely within the silent spaces:

Shattered-Geometry, Chrono-Echoes, and Unmapped-Voids.

Recover the transmission from the wreckage.

Note: Each recovered phase reveals an ordinal identifier tag. Concatenate all five payload segments in order.

  • Category: forensics (medium)

We are provided with a 1 GiB raw disk image (resurrection.raw, 1,073,741,824 bytes).

Volume triage and geometry reconstruction

The primary MBR and primary GPT header at the start of the disk (0x00000000) have been wiped to zeroes. However, inspecting the end of the disk reveals intact backup structures:

  1. Backup GPT Header at LBA 0x1fffff (0x3ffffe00) and Backup GPT Partition Array at LBA 0x1fffdf (0x3fffbe00):

    • Partition 0: Microsoft reserved partition, LBA 0x22 – 0x7fff (0x00004400 – 0x00fffe00).
    • Partition 1: Basic data partition, LBA 0x8000 – 0x1fefff (0x01000000 – 0x3fdffe00).
  2. Backup NTFS Volume Boot Record (VBR) at the last sector of Partition 1 (0x3fdffe00):

    • Bytes per sector: 512
    • Sectors per cluster: 8 (4096 bytes per cluster)
    • $MFT starting cluster (LCN): 0x14f55 (85845), which maps to byte offset 0x01000000 + 85845 * 4096 = 0x15f55000.
    • $MFTMirr starting cluster (LCN): 0x2 (0x01002000).

Extracting the five phases

Parsing the $MFT at 0x15f55000 reveals the file system hierarchy along with five distinct structural/metadata anomalies corresponding to the five parts of the flag. Each recovered string begins with a leetspeak ordinal tag (f!r5t--, 53c0nd--, th1jd--, f01_1rth--, 51f7h--).

Phase 1: PE Overlay and TimeDateStamp key (f!r5t--)

In MFT[177] (ConsoleHost_history.txt), we find a PowerShell command history containing a hint:

Get-Process | Where-Object { $_.Handles -gt 500 }
vssadmin list shadows
wevtutil qe Security "/q:*[System[(EventID=4624)]]" /c:5 /rd:true /f:text
Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Keyboard Layouts\*'
# WARNING: Detected anomalous unsigned input filter driver hooking layout table: kbdusx.dll
whoami /priv

Inspecting MFT[179] (kbdusx.dll, located at LCN 2281 / offset 0x018e9000, size 1068 bytes):

  • The PE header’s SizeOfHeaders (0x200) + .text section SizeOfRawData (0x200) equals 0x400 (1024 bytes).
  • Beyond offset 0x400 lies a 42-byte PE overlay (0x400 – 0x42a):
    e6a49e50f4a8c10de9c6b823fbf5df3ab0f3df17ecb1953ab7b48156e4b1db56b5b2d808f0da8756f9da
  • Also, kbdusx.dll exhibits timestomping ($STANDARD_INFORMATION vs $FILE_NAME mismatch) and a conspicuous PE COFF header TimeDateStamp at offset 0x88: 80 85 ec 65 (0x65ec8580).
  • XORing the 42-byte PE overlay with the 4-byte TimeDateStamp (80 85 ec 65) yields Phase 1:
    f!r5t--hiCTF{p3_0v3rl4y_71m3d473574mp_k3y_

Phase 2: Shattered-Geometry — NTFS runlist cluster lengths (53c0nd--)

Inspecting MFT[111] (Confidential_Q3), we observe that despite having directory flags (flags=3), it also possesses a non-resident $DATA (0x80) attribute with 34 fragmented data runs:

runs = [
    (6000, 53), (6061, 51), (6170, 99), (6228, 48), (6348, 110), (6458, 100),
    (6513, 45), (6568, 45), (6688, 110), (6753, 55), (6865, 102), (6928, 53),
    (7033, 95), (7157, 114), (7284, 117), (7404, 108), (7581, 49),
    (7644, 53), (7709, 55), (7814, 95), (7923, 99), (8041, 108), (8104, 53),
    (8209, 95), (8322, 103), (8383, 51), (8441, 48), (8560, 109), (8621, 51),
    (8686, 55), (8810, 114), (8941, 121), (9046, 95)
]

Taking the cluster lengths of each run in the runlist ([53, 51, 99, 48, ...]) as ASCII character codes decodes directly to:

53c0nd--n7f5_runl157_cl5_g30m37ry_

Phase 3: Chrono-Echoes — USN journal nano-FILETIME deltas (th1jd--)

Inspecting MFT[181] (tx_journal.log at LCN 2282 / offset 0x018ea000), we find 34 USN Journal (USN_RECORD_V2) entries for tx_00.db through tx_33.db. Examining their 64-bit FILETIME timestamps (100-nanosecond intervals):

  • tx_00.db: 0x01da498aed83c000
  • tx_01.db: 0x01da498aed83e774 (+10100)
  • tx_02.db: 0x01da498aed840edc (+10088)
  • …

Each consecutive timestamp delta is 10000 + x:

deltas = [10100, 10088, 10033, 10090, 10084, 10029, 10029, 10039, 10098,
          10036, 10094, 10099, 10036, 10083, 10039, 10033, 10032, 10094,
          10079, 10094, 10036, 10094, 10032, 10079, 10086, 10033, 10092,
          10035, 10039, 10033, 10093, 10035, 10079]

Subtracting 10000 and adding 16 (0x10) to each delta (chr(d - 10000 + 16)) yields:

th1jd--7r4ns4c710n_n4n0_f1l371m3_

Phase 4: EVTX binary XML tokens (f01_1rth--)

Inspecting MFT[193] (Security_Audit.evtx at LCN 2427 / offset 0x0197b000, size 69,632 bytes), the first chunk (ElfChnk at LCN 2428 / offset 0x0197c000) contains a single event record at offset 0x80. Inside the event payload is a BinXml template instance (0x0c) containing 33 substitution array slots of type 0x04 (unsigned byte / 32-bit aligned values). Extracting the 33 values at offsets 0x1cd + 4*i:

f01_1rth--3v7x_b1n4ry_xml_70k3n5_

Phase 5: Unmapped-Voids — $Bitmap orphan cluster spacings (51f7h--)

Inspecting $Bitmap (MFT[6], located at LCN 85836 / offset 0x15f4c000), we check which clusters are marked as allocated (bit = 1):

  • Normal system/file allocations end at LCN 85908.
  • Starting at LCN 180000, there is a series of 40 isolated single-cluster allocations (bit = 1) in an otherwise completely unmapped void:
    180000, 180053, 180102, 180204, 180259, 180363, 180408, 180453, ...

Calculating the differences between consecutive allocated LCNs:

diffs = [53, 49, 102, 55, 104, 45, 45, 98, 49, 55, 109, 52, 112, 95, 111,
         114, 112, 104, 52, 110, 95, 118, 48, 49, 100, 95, 114, 51, 53, 117,
         114, 114, 51, 99, 55, 49, 48, 110, 125]

Converting these deltas to ASCII characters gives:

51f7h--b17m4p_orph4n_v01d_r35urr3c710n}

Solver

import struct

IMAGE_PATH = "resurrection.raw"
PART_OFFSET = 0x01000000
CLUSTER_SIZE = 4096

with open(IMAGE_PATH, "rb") as f:
    # Phase 1: kbdusx.dll PE Overlay XOR TimeDateStamp (LCN 2281)
    f.seek(PART_OFFSET + 2281 * CLUSTER_SIZE)
    kbd = f.read(1068)
    pe_key = kbd[0x88:0x8C]
    overlay = kbd[0x400:0x42A]
    p1 = bytes(b ^ pe_key[i % 4] for i, b in enumerate(overlay)).decode()

    # Phase 2: MFT[111] Confidential_Q3 $DATA Runlist Cluster Lengths
    f.seek(0x15F55000 + 111 * 1024)
    rec111 = bytearray(f.read(1024))
    usa_ofs, usa_cnt = struct.unpack("<HH", rec111[4:8])
    for i in range(1, usa_cnt):
        rec111[i * 512 - 2 : i * 512] = rec111[usa_ofs + i * 2 : usa_ofs + i * 2 + 2]
    rpos = 0xEC + 0x40
    lengths = []
    while rec111[rpos] != 0:
        hdr = rec111[rpos]; rpos += 1
        llen, olen = hdr & 0xF, hdr >> 4
        length = int.from_bytes(rec111[rpos : rpos + llen], "little")
        rpos += llen + olen
        lengths.append(length)
    p2 = "".join(chr(c) for c in lengths)

    # Phase 3: tx_journal.log USN_RECORD_V2 Timestamp Deltas (LCN 2282)
    f.seek(PART_OFFSET + 2282 * CLUSTER_SIZE)
    txj = f.read(4096)
    ts = [struct.unpack("<Q", txj[i * 80 + 32 : i * 80 + 40])[0] for i in range(34)]
    p3 = "".join(chr((ts[i + 1] - ts[i]) - 10000 + 16) for i in range(33))

    # Phase 4: Security_Audit.evtx BinXml Tokens (LCN 2428)
    f.seek(PART_OFFSET + 2428 * CLUSTER_SIZE)
    evtx = f.read(600)
    p4 = "".join(chr(evtx[pos]) for pos in range(0x1CD, 0x251, 4))

    # Phase 5: $Bitmap Orphaned Cluster Allocation Deltas (LCN 85836)
    f.seek(PART_OFFSET + 85836 * CLUSTER_SIZE)
    bmap = f.read(32768)
    ones = [
        byte_i * 8 + bit_i
        for byte_i in range(180000 // 8, 184000 // 8)
        for bit_i in range(8)
        if (bmap[byte_i] >> bit_i) & 1
    ]
    p5 = "".join(chr(ones[i + 1] - ones[i]) for i in range(len(ones) - 1))

flag = "".join(p.split("--", 1)[1] for p in [p1, p2, p3, p4, p5])
print("Flag:", flag)

Lessons

  • A wiped primary GPT/MBR doesn’t kill the volume — backup structures at the end of the disk are enough to reconstruct everything.
  • NTFS has many structural metadata channels that can encode data: runlist cluster lengths, USN journal timestamp deltas, EVTX binary XML substitution slots, and $Bitmap allocation spacings.
  • The ordinal tags (f!r5t--, 53c0nd--, etc.) serve double duty: they confirm each extraction is correct and they specify concatenation order.
The flagRevealhiCTF{p3_0v3rl4y_71m3d473574mp_k3y_n7f5_runl157_cl5_g30m37ry_7r4ns4c710n_n4n0_f1l371m3_3v7x_b1n4ry_xml_70k3n5_b17m4p_orph4n_v01d_r35urr3c710n}

This CTF was possibly solved, and the writeup most likely written by an LLM. Apologies for inconsistencies.