PICOCTF 2026 • FORENSICS • NESTED SUBTREE & BLOB RESCUE

Forensics Git 2: Recovering Deleted Directory Trees & Git Artifacts

By Abdo•8 min read•PicoCTF 2026
TARGET IMAGEdisk.img (1 GB)
SUSPECT COMMITe80b38... (Commit 4)
RESCUED BLOBInode 65730 (3.txt)
FLAGpicoCTF{g17_r35cu3...}
Git Nested Tree and Blob Rescue Forensics
FIGURE 1: Minimalist Dark Glass UI & Git Directed Acyclic Graph (DAG) TraversalCommit → Root Tree → Subtree → Blob
Forensic Scenario

Investigating a raw 1 GB ext4 disk image (disk.img) containing a developer repository (killer-chat-app). In the repository's log archive, files 1.txt, 2.txt, and 4.txt are present, but 3.txt is missing. Through Git reflog analysis, we discover that Commit 4 ("Add secret hideout chat log") added the file before Commit 5 deleted it. By walking Git's hierarchical nested tree objects (Commit → Root Tree → Subdirectory Tree → Blob), we carve the deleted blob from the ext4 filesystem and decompress the hidden secret.

01. Spotting the Sequence Anomaly & Reflog Triage

After locating the primary ext4 filesystem at Sector offset 1140736 via mmls, we inspect user space using fls:

POWERSHELL • LIST DIRECTORY CONTENTS WITH FLS
fls.exe -o 1140736 -r .\disk.img 64770
// Filesystem listing inside killer-chat-app:
+ d/d 65664: killer-chat-app
+++ d/d 65704: logs
++++ r/r 65711: 1.txt
++++ r/r 65705: 2.txt
! MISSING: 3.txt is completely absent from the directory
++++ r/r 65736: 4.txt

To determine what happened to 3.txt, we dump Git's reflog history from logs/HEAD (Inode 65707):

POWERSHELL • DUMP REFLOG
icat.exe -o 1140736 .\disk.img 65707
// Reflog Commit Log Sequence:
... commit: Add video game chat log
... commit: Add TV show chat log
582763... e80b38b3322a5ba32ac07076ef5eeb4a59449875 commit: Add secret hideout chat log
e80b38... 2151ef0ccc15aed1ab88e1afdc7484aaeff211c4 commit: Remove secret hideout log
2151ef... 01533f718556a0e59f1467dae4fa462eed82c2a1 commit: Add random chat log

02. Multi-Tiered Git DAG Traversal (Commit → Root → Subtree → Blob)

Because 3.txt was placed inside a subdirectory (logs/), Git maintains a hierarchical tree structure. Each directory level is its own separate tree object (mode 40000).

1. Decompressing Commit 4 (Hash e80b38..., Inode 65735):
python -c "import subprocess, zlib; print(zlib.decompress(subprocess.check_output(['icat.exe', '-o', '1140736', '.\disk.img', '65735'])))"
b'commit 246tree ead27e2bd5a0fc22868ffb629a768f82dfcda11c... Add secret hideout chat log'
2. Decompressing Root Tree (Hash ead27e..., Inode 65734):
python -c "import subprocess, zlib; print(zlib.decompress(subprocess.check_output(['icat.exe', '-o', '1140736', '.\disk.img', '65734'])))"
b'tree 99100755 client...40000 logs"÷Ðɽ...100755 server...'
// First byte " (0x22) reveals Subtree Hash starting with 22f7d0c9... (Inode 65732)
3. Decompressing Logs Subtree (Hash 22f7d0..., Inode 65732):
python -c "import subprocess, zlib; print(zlib.decompress(subprocess.check_output(['icat.exe', '-o', '1140736', '.\disk.img', '65732'])))"
b'tree 99100644 1.txt...100644 2.txt...100644 3.txtqxdD3ç... (Hash: 7178644433e7cb...)'
// Character 'q' (0x71) locates Blob in folder .git/objects/71/ (Inode 65730)

03. Inflating the Hidden Chat Log & Flag Capture

Having located the raw zlib-compressed object for 3.txt at Inode 65730, we execute an automated extraction script:

PYTHON • SOLVE.PY
import subprocess
import zlib

inode = "65730"  # Inode for 3.txt compressed blob (hash 717864...)
cmd = ["icat.exe", "-o", "1140736", ".\\disk.img", inode]
raw_compressed = subprocess.check_output(cmd)
decompressed = zlib.decompress(raw_compressed)
print(decompressed.decode('utf-8', errors='ignore'))
// Recovered Chat Log Transcript:
blob 188
Rex: Meet at the old arcade basement for the secret hideout.
Jay: Ask Rusty at the door and use password picoCTF{g17_r35cu3_16ac6bf3}.
Rex: Bring the decoder map so we can plan the route.
OFFICIAL CAPTURED FLAGpicoCTF{g17_r35cu3_16ac6bf3}