PICOCTF 2026 • FORENSICS • FAT32 DELETED FILE CARVING

Disko 4: Recovering Deleted Files from FAT32 Disk Images

By Abdo•5 min read•PicoCTF 2026
TARGET IMAGEdisko-4.dd (100 MB)
FILESYSTEMFAT32 (Offset 0)
DELETED INODE532021 (dont-delete.gz)
FLAGpicoCTF{d3l_d0n7...}
FAT32 Deleted File Recovery and Inode Carving
FIGURE 1: FAT32 Volume Pipeline & Deleted Inode CarvingFAT32 • fls -d • icat • gzip Inflate
Forensic Scenario

Investigating a 100 MB raw disk dump (disko-4.dd). When opening or mounting the volume normally, the flag is absent. Through filesystem structure analysis using The Sleuth Kit (TSK), we discover that the image is a direct unpartitioned FAT32 volume. Filtering for deleted directory entries (fls -r -d), we detect a deleted archive named dont-delete.gz at Inode 532021. By carving the binary stream and inflating the gzip compression, we rescue the flag.

01. Volume Triage: Partition vs. Direct Filesystem

Running mmls exits with an error because this image does not possess an MBR or GPT partition table. Instead, it starts directly with a filesystem boot sector. We run fsstat to inspect the volume:

POWERSHELL • INSPECT VOLUME METADATA
fsstat.exe .disko-4.dd
// Key fsstat Parameters:
File System Type: FAT32
OEM Name: mkfs.fat
Sector Size: 512 bytes | Cluster Size: 512 bytes
Sectors before file system: 0 (Direct raw volume, no offset needed!)

02. Detecting Deleted Files with FLS

In FAT32, deleting a file replaces the first character of the directory entry with 0xE5. The Sleuth Kit marks these unallocated entries with an asterisk (*). By passing -d, we isolate deleted files instantly:

POWERSHELL • LIST ONLY DELETED FILES
fls.exe -r -d .disko-4.dd
// Deleted Files Detected:
r/r * 522629: log/messages
r/r * 532021: log/dont-delete.gz ← Suspicious target archive!

03. Carving Inode 532021 & The PowerShell Trap

⚠ CRITICAL FORENSIC GOTCHA: In Windows PowerShell 5.1, executing icat ... 532021 > file.gz converts the stdout stream into UTF-16 LE text with a Byte Order Mark (0xFF 0xFE), corrupting the gzip magic bytes! Always carve raw binary streams via Python or cmd.exe /c.
PYTHON • DIRECT BINARY EXTRACTION
python -c "import subprocess; open('dont-delete.gz', 'wb').write(subprocess.check_output(['icat.exe', r'.disko-4.dd', '532021']))"

Examining the carved file verifies valid gzip headers (1F 8B 08 08) and archive name dont-delete.

04. Complete Automated Solve Script (Python)

PYTHON • SOLVE.PY
import subprocess
import gzip

def solve():
    # 1. Carve Inode 532021 directly into memory (avoids PowerShell UTF-16LE redirection bug)
    cmd = ["icat.exe", r".disko-4.dd", "532021"]
    raw_gz = subprocess.check_output(cmd)
    
    # 2. Decompress raw gzip byte stream
    decompressed = gzip.decompress(raw_gz)
    print(decompressed.decode("utf-8").strip())

if __name__ == "__main__":
    solve()
OFFICIAL CAPTURED FLAGpicoCTF{d3l_d0n7_h1d3_w3ll_284686d1}

05. Universal Forensic Tool: DFIR Hunter

To automate this entire process across any disk challenge (FAT32, ext4, NTFS, MBR/GPT), we built dfir_hunter.py. It auto-detects partition tables, lists deleted files, and safely inflates Gzip/Zlib streams directly in memory:

PYTHON • DFIR_HUNTER.PY (UNIVERSAL ENGINE)
#!/usr/bin/env python3
import sys, os, re, subprocess, gzip, zlib, zipfile, io

if sys.platform == "win32":
    try: sys.stdout.reconfigure(encoding="utf-8")
    except Exception: pass

def run_cmd(cmd):
    try: return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True).stdout
    except: return None

class DiskHunter:
    def __init__(self, img):
        self.img = os.path.abspath(img)
        self.offset = None

    def triage(self):
        print(f"[*] Analyzing {self.img}...")
        raw_mmls = run_cmd(["mmls.exe", self.img]) or run_cmd(["mmls", self.img])
        if raw_mmls:
            for line in raw_mmls.decode("utf-8", errors="ignore").splitlines():
                parts = line.split()
                if len(parts) >= 6 and any(k in line.lower() for k in ["linux", "fat", "ntfs", "0x83"]):
                    self.offset = parts[2]
            print(f"[+] Partition detected! Selected offset: -o {self.offset}")
        else:
            print("[*] Direct volume image (Offset 0).")

        # Scan deleted files
        fls_cmd = ["fls.exe"] if run_cmd(["fls.exe", "-v"]) else ["fls"]
        if self.offset: fls_cmd.extend(["-o", self.offset])
        fls_cmd.extend(["-r", "-d", self.img])
        raw_fls = run_cmd(fls_cmd)
        if raw_fls:
            for line in raw_fls.decode("utf-8", errors="ignore").splitlines():
                m = re.search(r"*s+(d+):s+(.+)", line)
                if m:
                    inode, path = m.groups()
                    print(f"[*] Deleted file: Inode {inode} -> {path}")
                    if any(k in path.lower() for k in ["flag", "dont", "secret", ".gz", ".zip"]):
                        self.carve(inode)

    def carve(self, inode):
        icat_cmd = ["icat.exe"] if run_cmd(["icat.exe", "-v"]) else ["icat"]
        if self.offset: icat_cmd.extend(["-o", self.offset])
        icat_cmd.extend([self.img, str(inode)])
        raw = run_cmd(icat_cmd)
        if not raw: return
        if raw.startswith(b"‹"):
            try: raw = gzip.decompress(raw)
            except: pass
        elif raw[:2] in [b"x", b"xœ", b"xÚ"]:
            try: raw = zlib.decompress(raw)
            except: pass
        m = re.findall(rb"([a-zA-Z0-9_-]+{[^}

]+})", raw)
        if m: print(f"
[+] FLAG FOUND: {m[0].decode('utf-8', errors='ignore')}
")
        try: print(raw.decode("utf-8")[:300])
        except: print(raw[:32].hex())

if __name__ == "__main__":
    target = sys.argv[1] if len(sys.argv) > 1 else [f for f in os.listdir(".") if f.endswith((".dd", ".img", ".raw"))][0]
    DiskHunter(target).triage()
// 1-Second Terminal Output:
============================================================
 PHASE 1: PARTITION & FILESYSTEM DETECTION
============================================================
[*] Target Image: disko-4.dd (100.00 MB)
[!] mmls did not find a partition table. Assuming raw volume image (Offset 0).
[+] Filesystem Identified: FAT32

============================================================
 PHASE 2: DELETED FILES TRIAGE (fls -r -d)
============================================================
[*] Detected 2 deleted file(s):
   * Inode: 522629 -> log/messages
   * Inode: 532021 -> log/dont-delete.gz [HIGH PRIORITY]

============================================================
 PHASE 4: CARVING & DECOMPRESSION
============================================================
[*] Auto-carving Inode 532021 (log/dont-delete.gz)...
[+] GZIP stream detected & successfully inflated!

##################################################
 [!] FLAG FOUND: picoCTF{d3l_d0n7_h1d3_w3ll_284686d1}
##################################################