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.
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:
-
Backup GPT Header at LBA
0x1fffff(0x3ffffe00) and Backup GPT Partition Array at LBA0x1fffdf(0x3fffbe00):- Partition 0:
Microsoft reserved partition, LBA0x22–0x7fff(0x00004400–0x00fffe00). - Partition 1:
Basic data partition, LBA0x8000–0x1fefff(0x01000000–0x3fdffe00).
- Partition 0:
-
Backup NTFS Volume Boot Record (VBR) at the last sector of Partition 1 (
0x3fdffe00):- Bytes per sector:
512 - Sectors per cluster:
8(4096bytes per cluster) $MFTstarting cluster (LCN):0x14f55(85845), which maps to byte offset0x01000000 + 85845 * 4096 = 0x15f55000.$MFTMirrstarting cluster (LCN):0x2(0x01002000).
- Bytes per sector:
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) +.textsectionSizeOfRawData(0x200) equals0x400(1024bytes). - Beyond offset
0x400lies a 42-byte PE overlay (0x400–0x42a):e6a49e50f4a8c10de9c6b823fbf5df3ab0f3df17ecb1953ab7b48156e4b1db56b5b2d808f0da8756f9da - Also,
kbdusx.dllexhibits timestomping ($STANDARD_INFORMATIONvs$FILE_NAMEmismatch) and a conspicuous PE COFF headerTimeDateStampat offset0x88: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:0x01da498aed83c000tx_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
$Bitmapallocation spacings. - The ordinal tags (
f!r5t--,53c0nd--, etc.) serve double duty: they confirm each extraction is correct and they specify concatenation order.
The flagReveal
hiCTF{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.