ReAL File System

Forensics HARD · 0 solves bi0sCTF 2024
bi0sctf{ReAL_1_w0nd3r_wHa7_t1m3_is_17_14dbc653fdb414c1d}

A ReFS 3.4 disk‑forensics challenge, solved end‑to‑end with the open‑source forefst.py + refsanalysis.py (v1.6.0). The image was hand‑corrupted at the bootstrap level and littered with clock‑tampered timestamps — and forefst reads it, diagnoses the tampering, and replays the whole attack.

Links

The six questions (timezone UTC+05:30, format YYYY-MM-DD HH:MM:SS.XX):
  1. List all directories that have been renamed — original name, new name, rename time.
  2. Name all the deleted directories with deletion timestamps.
  3. List all directories with their creation times (including originals that were renamed/deleted).
  4. Recover the deleted files and give the md5sum of each.
  5. Identify all deleted files (Simple + Permanent) with deletion timestamps and type.
  6. Restore & decrypt the encrypted files and give the md5sum of each.
TL;DR The image is a bare ReFS partition (no GPT → --partition-start 0) whose bootstrap chain was hand‑zeroed. forefst summary flags the tampering and bootstraps from the redundant checkpoint anyway. mlog --parse replays every rename / move / delete with original names (from 128‑bit object IDs) and timestamps; object‑ID gaps prove permanent directory deletion; recyclebin and export recyclebin recover the deleted files. All answers are proven against the handout’s own SHA‑256 checker.

Contents

  1. The tool sees the corrupted bootstrap (Q—the “repair” puzzle)
  2. One command replays the attack (Q1 · Q2 · Q3)
  3. Object‑ID gaps: durable deletion proof (Q2)
  4. Recovering files & recycle metadata (Q4 · Q5)
  5. Proving it against the checker
  6. Q6 — the clock‑tampering crypto (in brief)
  7. Takeaways

1. The tool sees the corrupted bootstrap

The intended challenge is to notice a broken superblock/checkpoint and repair it by recomputing Logical Cluster Numbers in a hex editor. v1.6 turns that into a one‑line diagnosis: summary validates the whole bootstrap chain, reports the tampering, and still boots from the good checkpoint.

$ forefst.py ReAL_File_System.001 summary --partition-start 0
Bootstrap structures
  VBR primary    LBA 0        — checksum OK
  SUPB primary   LCN 0x1e     — signature OK  !! CORRUPT self-reference: stored 0x0, expected own LCN 0x1e
  CHKP primary   LCN 0x27d0   — signature OK, vclock=68  !! CORRUPT self-reference: stored 0x0, expected 0x27d0
  SUPB backup    LCN 0xf7ffd/0xf7ffe  — signature OK  !! CORRUPT self-reference: stored 0x0
  CHKP secondary LCN 0x1dc48  — signature OK, vclock=67  !! CORRUPT self-reference: stored 0x0
  ** BOOTSTRAP TAMPERING: 5 structure(s) carry a CORRUPT self-reference
     a zeroed self-LCN does not occur normally — treat it as deliberate tampering / hand-corrupted bootstrap.

  ReFS version 3.4   Directories: 7   Files: 17   (bootstrapped from checkpoint vclock=68)
What was corrupted & why it doesn’t stop us Every bootstrap structure’s self‑reference LCN (the block’s record of its own location) was zeroed. That field isn’t needed to bootstrap from a checkpoint, so the volume is still fully readable — but a zeroed self‑LCN never happens naturally, so forefst surfaces it as deliberate tampering instead of silently ignoring it. The intended “hex‑edit the LCNs” step is replaced by a single diagnostic line.

2. One command replays the attack (Q1 · Q2 · Q3)

ReFS keeps a durable redo log. mlog --parse decodes it into concrete actions and resolves each object’s birth name vs current name from its 128‑bit object ID — so a renamed directory still shows the name it was created with.

$ forefst.py ReAL_File_System.001 mlog --parse --partition-start 0
[CREATE] OID 0x704 → de34f60ab                 2024-02-18T07:36:59.227Z
[CREATE] /88077a4a1370 → e88e52cac            2024-02-18T07:37:31.132Z
[CREATE] /cad090f9724d → fb828d071            2024-02-18T07:38:13.882Z
[CREATE] OID 0x707 → f8f1c218f9               2024-02-18T07:38:40.586Z
[CREATE] /$RECYCLE.BIN/…/$RPNGMPK → c062fb828  2024-02-18T07:39:14.554Z
[CREATE] /bb6de6190                        2024-02-18T07:39:34.008Z
[RENAME] / → e88e52cac (same parent 0x600)     2024-02-18T07:40:20.495Z
[MOVE  ] / → c062fb828 (parent 0x600 → 0x703)  2024-02-18T07:39:14.554Z
[DELETE] / → f8f1c218f9 (object table destroyed) 2024-02-18T07:41:28.575Z
[RENAME] / → fb828d071 (same parent 0x600)     2024-02-18T07:42:12.481Z
[DELETE] / → de34f60ab  (object table destroyed) 2024-02-18T07:43:19.604Z
[DELETE] /  (object table destroyed)           2024-02-18T07:48:22.479Z   # essay.txt
[DELETE] /  (object table destroyed)           2024-02-18T07:49:26.698Z   # ead47cb

That output answers three questions at once:

Attack timeline (UTC+05:30, straight from mlog --parse) 13:06:59 create de34f60ab 13:11:28 DELETE f8f1c218f9 (perm) 13:07:31 create e88e52cac 13:12:12 rename fb828d071 → cad090f9724d 13:08:13 create fb828d071 13:13:19 DELETE de34f60ab (perm) 13:08:40 create f8f1c218f9 13:14:31 recycle 19ff211f 13:09:14 create c062fb828 13:15:00 recycle simple-pass.txt 13:09:34 create bb6de6190 13:15:52 recycle fe0c329 13:10:20 rename e88e52cac → 88077a4a1370 13:18:22 DELETE essay.txt (perm) 13:10:48 recycle c062fb828 (dir) 13:19:26 DELETE ead47cb (perm)

3. Object‑ID gaps: durable deletion proof (Q2)

$ refsanalysis.py ReAL_File_System.001 objects --partition-start 0
0x701  0x702  0x703  [0x704 —]  0x705  0x706  [0x707 —]  0x708  0x709
Why gaps are evidence ReFS object IDs are monotonic and never reused, so the missing 0x704/0x707 are permanent proof that de34f60ab and f8f1c218f9 once existed and were destroyed — it survives even a complete overwrite. (Files carry no object ID, so this signal is directory/system‑object only.)

4. Recovering files & recycle metadata (Q4 · Q5)

v1.6’s recyclebin decodes the non‑resident $I records directly — original paths, sizes, deletion times — and export recyclebin writes each payload back under its original name:

$ forefst.py ReAL_File_System.001 recyclebin --partition-start 0
  $IPNGMPK  Original path: I:\c062fb828        Deleted: 2024-02-18 07:40:48.62   (dir, simple delete)
  $I0IY71M  Original path: I:\19ff211f         Deleted: 2024-02-18 07:44:31.43   Size: 20,000
  $I9238F6  Original path: I:\simple-pass.txt  Deleted: 2024-02-18 07:45:00.51   Size: 346
  $I8ZS4M3  Original path: I:\fe0c329          Deleted: 2024-02-18 07:45:52.…    Size: 16,823

$ forefst.py ReAL_File_System.001 export recyclebin out/ --partition-start 0 && md5sum out/*
  4c009b045056af8f9bb401c69408d2cf  19ff211f
  c50c5bcb9e98537e3d63df1bc68a81d0  fe0c329
  f91488b7e00c31793bd7aa98c51896d0  simple-pass.txt

Those three MD5s are Q4. Q5 = the three simple deletions above (type Simple) plus the two permanent file deletions from the MLog: essay.txt @ 13:18:22.47 and ead47cb @ 13:19:26.69 (type Permanent).

Timestamp nuance (Q5) The grader’s recycle deletion time is the $I file’s own $SI creation time (the recycle‑write transaction), e.g. fe0c329 → 13:15:52.49 — not the FILETIME embedded inside the $I payload (.47, ~11 ms earlier).

5. Proving it against the checker

The handout ships its grading hashes, so every answer list is fed through the exact grader pipeline and compared — no guessing:

Q1: VERIFIED   Q2: VERIFIED   Q3: VERIFIED
Q4: VERIFIED   Q5: VERIFIED   Q6: VERIFIED

6. Q6 — the clock‑tampering crypto (in brief)

time_update.exe (MinGW C++) writes each *.tort as "TORT" + a uint32 plaintext length + an XOR payload:

out[i] = pt[i] ^ key1[i % |key1|] ^ key2[i % |key2|]
key2   = md5(key1).hexdigest()                    # 32 ASCII hex chars
key1   = base30( rotl(FILETIME) … TEA-mix … ) + 4×nonce_chars   # CryptGenRandom nonce

The FILETIME is the tampered system time set right before the file’s rename — recoverable from the same MLog rename records shown above (milliseconds = 0). Brute the 4‑char nonce (30⁴) against each file’s magic, strip the 8‑byte TORT header to the stored length, and hash. Full crypto detail is in the bi0s writeup; 7/8 files reproduce directly (PDF, GIF, WAV, JPEG, PNG, ZIP/OOXML, MP4) and the eight decrypted MD5s satisfy the checker.

Bonus: an LCG you can run backwards The malware reseeds srand(time(0)) every operation, and its Sleep(1000) chains the tampered times as T_write = msvcrtLCG(unix(T_rename) + 1). A key time overwritten in the log ring is recovered by inverting that LCG from the surviving data‑write time.

Takeaways

======== forefst v1.6 session ======== forefst summary → BOOTSTRAP TAMPERING flagged forefst mlog --parse → CREATE/RENAME/MOVE/DELETE + times refsanalysis objects → OID gaps 0x704 / 0x707 forefst recyclebin/export → original names + payloads checker Q1..Q6 all VERIFIED bi0sctf{ReAL_1_w0nd3r_wHa7_t1m3_is_17_14dbc653fdb414c1d} ======================================

bi0sCTF 2024 · Forensics · ReFS 3.4 · solved with forefst.py + refsanalysis.py v1.6.