Disko 4: Recovering Deleted Files from FAT32 Disk Images

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:
fsstat.exe .disko-4.dd02. 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:
fls.exe -r -d .disko-4.dd03. Carving Inode 532021 & The PowerShell Trap
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 -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)
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()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:
#!/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()============================================================
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}
##################################################