
Ctf Forensics
- 6.2k installs
- 2.9k repo stars
- Updated July 31, 2026
- ljagiello/ctf-skills
ctf-forensics is an agent skill for Provides digital forensics and signal analysis techniques for CTF challenges. Use when analyzing disk images, memory dumps, event logs, network captures, cryptocurrency transaction
About
Provides digital forensics and signal analysis techniques for CTF challenges. Use when analyzing disk images, memory dumps, event logs, network captures, cryptocurrency transactions, steganography, PDF analysis, Windows registry, Volatility, PCAP, Docker images, coredumps, side-channel power traces, DTMF audio spectrograms, packet timing analysis, CD audio disc images, or recovering deleted files --- name: ctf-forensics description: Provides digital forensics and signal analysis techniques for CTF challenges. Use when analyzing disk images, memory dumps, event logs, network captures, cryptocurrency transactions, steganography, PDF analysis, Windows registry, Volatility, PCAP, Docker images, coredumps, side-channel power traces, DTMF audio spectrograms, packet timing analysis, CD audio disc images, or recovering deleted files and credentials. license: MIT compatibility: Requires filesystem-based agent (Claude Code or similar) with bash, Python 3, and internet access for tool installation. allowed-tools: Bash Read Write Edit Glob Grep Task WebFetch WebSearch metadata: user-invocable: "false" --- # CTF Forensics & Blockchain Quick reference for forensics CTF challenges.
- CTF Forensics & Blockchain
- [3d-printing.md](3d-printing.md) - 3D printing forensics (PrusaSlicer binary G-code, QOIF, heatshrink)
- If you recover an encrypted blob and the hard part becomes RSA, AES, or lattice work, switch to `/ctf-crypto`.
- If the evidence really points to malware staging, beacon config extraction, or packed samples, switch to `/ctf-malware`.
- If the artifact is a web app backup or API dump and the remaining problem is application logic, switch to `/ctf-web`.
Ctf Forensics by the numbers
- 6,173 all-time installs (skills.sh)
- +165 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #110 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
ctf-forensics capabilities & compatibility
- Capabilities
- ctf forensics & blockchain · [3d printing.md](3d printing.md) 3d printing f · if you recover an encrypted blob and the hard pa · if the evidence really points to malware staging · if the artifact is a web app backup or api dump
- Use cases
- documentation
What ctf-forensics says it does
--- name: ctf-forensics description: Provides digital forensics and signal analysis techniques for CTF challenges.
license: MIT compatibility: Requires filesystem-based agent (Claude Code or similar) with bash, Python 3, and internet access for tool installation.
allowed-tools: Bash Read Write Edit Glob Grep Task WebFetch WebSearch metadata: user-invocable: "false" --- # CTF Forensics & Blockchain Quick reference for forensics CTF challenges.
npx skills add https://github.com/ljagiello/ctf-skills --skill ctf-forensicsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6.2k |
|---|---|
| repo stars | ★ 2.9k |
| Security audit | 1 / 3 scanners passed |
| Last updated | July 31, 2026 |
| Repository | ljagiello/ctf-skills ↗ |
When should developers use ctf-forensics and what problem does it solve?
Provides digital forensics and signal analysis techniques for CTF challenges. Use when analyzing disk images, memory dumps, event logs, network captures, cryptocurrency transactions, steganography, PD
Who is it for?
Developers working with ctf-forensics patterns described in the skill documentation.
Skip if: Skip when cached docs are empty or the task is outside the skill's documented scope.
When should I use this skill?
Provides digital forensics and signal analysis techniques for CTF challenges. Use when analyzing disk images, memory dumps, event logs, network captures, cryptocurrency transactions, steganography, PD
What you get
Grounded guidance and workflows from SKILL.md for ctf-forensics.
- Recovered artifacts
- Forensic analysis findings
- Extracted credentials or flags
Files
CTF Forensics & Blockchain
Quick reference for forensics CTF challenges. Each technique has a one-liner here; see supporting files for full details.
Prerequisites
Python packages (all platforms):
pip install volatility3 Pillow numpy matplotlibLinux (apt):
apt install binwalk foremost libimage-exiftool-perl tshark sleuthkit \
ffmpeg steghide testdisk john pcapfixmacOS (Homebrew):
brew install binwalk exiftool wireshark sleuthkit ffmpeg \
testdisk john-jumboRuby gems (all platforms):
gem install zstegAdditional Resources
- 3d-printing.md - 3D printing forensics (PrusaSlicer binary G-code, QOIF, heatshrink)
- windows.md - Windows forensics (registry, SAM, event logs, recycle bin, NTFS alternate data streams, USN journal, PowerShell history, Defender MPLog, WMI persistence, Amcache)
- network.md - Network forensics basics (tcpdump, TLS/SSL keylog decryption, TLS master key extraction from coredump, Wireshark, PCAP, port scanning, SMB3 decryption, 5G/NR protocols, WordPress recon, credentials, USB HID steno, BCD encoding, HTTP file upload exfiltration, split archive reassembly via timestamp ordering)
- network-advanced.md - Advanced network forensics (packet interval timing encoding, NTLMv2 hash cracking, TCP flag covert channel, DNS last-byte steganography, DNS trailing byte binary encoding, multi-layer PCAP with XOR + ZIP and mDNS key, Brotli decompression bomb seam analysis, SMB RID recycling via LSARPC, Timeroasting MS-SNTP hash extraction, dnscat2 reassembly, RADIUS shared secret cracking, RC4 stream identification, ICMP payload byte rotation, ICMP ping time-delay covert channel)
- peripheral-capture.md - USB/HID/Bluetooth peripheral traffic reconstruction (USB HID mouse/pen drawing recovery, USB HID keyboard capture decoding, USB keyboard LED Morse code exfiltration, USB HID keyboard arrow key navigation tracking, Bluetooth RFCOMM packet reassembly)
- disk-and-memory.md - Core disk/memory forensics (Volatility, disk mounting/carving, VM/OVA/VMDK, VMware snapshots, GIMP raw memory dump visual inspection, coredumps, Windows KAPE triage, PowerShell ransomware, Android forensics, Docker container forensics, cloud storage forensics, BSON reconstruction, TrueCrypt/VeraCrypt mounting)
- disk-advanced.md - Advanced disk and memory techniques (deleted partitions, ZFS forensics, GPT GUID encoding, VMDK sparse parsing, memory dump string carving, ransomware key recovery, WordPerfect macro XOR, minidump ISO 9660 recovery, APFS snapshot recovery, RAID 5 XOR recovery, HFS+ resource fork recovery, Kyoto Cabinet hash DB forensics, SQLite edit history reconstruction)
- disk-recovery.md - Disk recovery and extraction patterns (LUKS master key recovery, PRNG timestamp seed brute-force, VBA macro binary recovery, FemtoZip decompression, XFS filesystem reconstruction, tar duplicate entry extraction, nested matryoshka filesystem extraction, anti-carving via null byte interleaving, BTRFS subvolume/snapshot recovery, FAT16 free space data recovery, FAT16 deleted file recovery via Sleuth Kit fls/icat, ext2 orphaned inode recovery via fsck, corrupted ZIP header repair)
- steganography.md - General steganography (binary border stego, PDF multi-layer stego, SVG keyframes, PNG reorder, file overlays, GIF frame diff Morse code, GZSteg + spammimic, spreadsheet frequency recovery, Kitty terminal graphics protocol decoding, ANSI escape sequence steganography, autostereogram solving, two-layer byte+line interleaving, multi-stream video container stego, progressive PNG layered XOR decryption, QR code reconstruction from curved reflection)
- stego-image.md - Image-specific steganography (JPEG unused DQT table LSB, BMP bitplane QR extraction, image puzzle reassembly, F5 JPEG DCT ratio detection, PNG unused palette entry stego, QR code tile reconstruction, seed-based pixel permutation + multi-bitplane QR, JPEG thumbnail pixel-to-text mapping, conditional LSB with pixel filtering, JPEG slack space, nearest-neighbor interpolation stego, RGB parity steganography)
- stego-advanced.md - Advanced steganography part 1: audio and signal techniques (FFT frequency domain, DTMF audio, SSTV+LSB, DotCode barcode, custom frequency dual-tone keypad, multi-track audio differential subtraction, cross-channel multi-bit LSB, audio FFT musical notes, audio metadata octal encoding, nested tar whitespace encoding, DeepSound audio stego with password cracking, audio waveform binary encoding, audio spectrogram hidden QR)
- stego-advanced-2.md - Advanced steganography part 2: video, image transform, and format-specific techniques (video frame accumulation, reversed audio, video frame averaging, JPEG XL TOC permutation steganography, Arnold's Cat Map descrambling, high-resolution SSTV custom FM demodulation, MJPEG FFD9 trailing byte stego, EXIF zlib + Stegano pixel patterns, PDF xref covert channel, ANSI escape code stego, pixel-wise ECB deduplication)
- linux-forensics.md - Linux/app forensics (log analysis, Docker image forensics, attack chains, browser credentials, Firefox history, TFTP, TLS weak RSA, USB audio, Git directory recovery, KeePass v4 cracking, Git reflog/fsck squash recovery, browser artifact analysis (Chrome/Chromium/Firefox history, cookies, downloads, local storage, session restore), corrupted git blob repair via byte brute-force, VBA macro Excel cell data to ELF binary extraction, Python in-memory source recovery via pyrasite)
- signals-and-hardware.md - Hardware signal decoding with decode code (VGA frame parsing, HDMI TMDS symbol decode, DisplayPort 8b/10b + LFSR descrambler), Voyager Golden Record audio, Saleae Logic 2 UART decode, Flipper Zero .sub files, side-channel power analysis (DPA), keyboard acoustic side-channel, CD audio disc image steganography (CIRC de-interleaving + spiral rendering), caps-lock LED Morse code from video, Linux input_event keylogger dump parsing, serial UART from WAV audio, USB MIDI Launchpad grid reconstruction
---
When to Pivot
- If you recover an encrypted blob and the hard part becomes RSA, AES, or lattice work, switch to
/ctf-crypto. - If the evidence really points to malware staging, beacon config extraction, or packed samples, switch to
/ctf-malware. - If the artifact is a web app backup or API dump and the remaining problem is application logic, switch to
/ctf-web. - If the forensic evidence is really an encoding puzzle, steganography trick, or esoteric format rather than true forensics, switch to
/ctf-misc. - If you need to trace infrastructure, attribute actors, or investigate public records from forensic findings, switch to
/ctf-osint. - If the recovered artifact is a compiled binary or firmware that needs disassembly and analysis, switch to
/ctf-reverse.
Quick Start Commands
# File analysis
file suspicious_file
exiftool suspicious_file # Metadata
binwalk suspicious_file # Embedded files
strings -n 8 suspicious_file
hexdump -C suspicious_file | head # Check magic bytes
# Disk forensics
sudo mount -o loop,ro image.dd /mnt/evidence
fls -r image.dd # List files
photorec image.dd # Carve deleted files
# Memory forensics (Volatility 3)
vol3 -f memory.dmp windows.info
vol3 -f memory.dmp windows.pslist
vol3 -f memory.dmp windows.filescanSee disk-and-memory.md for full Volatility plugin reference, VM forensics, and coredump analysis.
Log Analysis
grep -iE "(flag|part|piece|fragment)" server.log # Flag fragments
grep "FLAGPART" server.log | sed 's/.*FLAGPART: //' | uniq | tr -d '\n' # Reconstruct
sort logfile.log | uniq -c | sort -rn | head # Find anomaliesSee linux-forensics.md for Linux attack chain analysis and Docker image forensics.
Windows Event Logs (.evtx)
Key Event IDs:
- 1001 - Bugcheck/reboot
- 1102 - Audit log cleared
- 4720 - User account created
- 4781 - Account renamed
RDP Session IDs (TerminalServices-LocalSessionManager):
- 21 - Session logon succeeded
- 24 - Session disconnected
- 1149 - RDP auth succeeded (RemoteConnectionManager, has source IP)
import Evtx.Evtx as evtx
with evtx.Evtx("Security.evtx") as log:
for record in log.records():
print(record.xml())See windows.md for full event ID tables, registry analysis, SAM parsing, USN journal, and anti-forensics detection.
- NTFS Alternate Data Streams (ADS): Hidden data attached to files via named NTFS streams. Invisible to
dir/Explorer. Detect withfls -r image.dd | grep ":", extract withicat. See windows.md.
When Logs Are Cleared
If attacker cleared event logs, use these alternative sources: 1. USN Journal ($J) - File operations timeline (MFT ref, timestamps, reasons) 2. SAM registry - Account creation from key last_modified timestamps 3. PowerShell history - ConsoleHost_history.txt (USN DATA_EXTEND = command timing) 4. Defender MPLog - Separate log with threat detections and ASR events 5. Prefetch - Program execution evidence 6. User profile creation - First login time (profile dir in USN journal)
See windows.md for detailed parsing code and anti-forensics detection checklist.
Steganography
steghide extract -sf image.jpg
zsteg image.png # PNG/BMP analysis
stegsolve # Visual analysis- Binary border stego: Black/white pixels in 1px image border encode bits clockwise
- FFT frequency domain: Image data hidden in 2D FFT magnitude spectrum; try
np.fft.fft2visualization - DTMF audio: Phone tones encoding data; decode with
multimon-ng -a DTMF - Multi-layer PDF: Check hidden comments, post-EOF data, XOR with keywords, ROT18 final layer
- SSTV + LSB: SSTV signal may be red herring; check 2-bit LSB of audio samples with
stegolsb - SVG keyframes: Animation
keyTimes/valuesattributes encode binary/Morse via fill color alternation - PNG chunk reorder: Fix chunk order: IHDR → ancillary → IDAT (in order) → IEND
- File overlays: Check after IEND for appended archives with overwritten magic bytes
- APNG frame extraction: Animated PNG has multiple frames; extract with
apngdisor parsefdAT/fcTLchunks. See steganography.md. - PNG height/CRC manipulation: Modify IHDR height field, brute-force until CRC matches to reveal hidden rows. See steganography.md.
- Pixel coordinate chain stego: Linked-list traversal where R=data byte, G/B=next pixel coordinates. See stego-image.md.
- AVI frame differential: XOR consecutive video frames to reveal hidden data in pixel differences. See stego-image.md.
- Custom freq DTMF: Non-standard dual-tone frequencies; generate spectrogram first (
ffmpeg -i audio -lavfi showspectrumpic), map custom grid to keypad digits, decode variable-length ASCII - JPEG DQT LSB: Unused quantization tables (ID 2, 3) carry LSB-encoded data; access via
Image.open().quantizationand extract bit 0 from each of 64 values - Multi-track audio subtraction: Two nearly-identical audio tracks in MKV/video;
sox -m a0.wav "|sox a1.wav -p vol -1" diff.wavcancels shared content, flag appears in spectrogram of difference signal (5-12 kHz band) - Packet interval timing: Identical packets with two distinct interval values (e.g., 10ms/100ms) encode binary; filter by interface, compute inter-packet deltas, threshold to bits
See steganography.md, stego-advanced.md, and stego-advanced-2.md for full code examples and decoding workflows.
PDF Analysis
exiftool document.pdf # Metadata (often hides flags!)
pdftotext document.pdf - # Extract text
strings document.pdf | grep -i flag
binwalk document.pdf # Embedded filesAdvanced PDF stego (Nullcon 2026 rdctd): Six techniques -- invisible text separators, URI annotations with escaped braces, Wiener deconvolution on blurred images, vector rectangle QR codes, compressed object streams (mutool clean -d), document metadata fields.
See steganography.md for full PDF steganography techniques and code.
Disk / VM / Memory Forensics
# Disk images
sudo mount -o loop,ro image.dd /mnt/evidence
fls -r image.dd && photorec image.dd
# VM images (OVA/VMDK)
tar -xvf machine.ova
7z x disk.vmdk -oextracted "Windows/System32/config/SAM" -r
# Memory (Volatility 3)
vol3 -f memory.dmp windows.pslist
vol3 -f memory.dmp windows.cmdline
vol3 -f memory.dmp windows.netscan
vol3 -f memory.dmp windows.dumpfiles --physaddr <addr>
# String carving
strings -a -n 6 memdump.bin | grep -E "FLAG|SSH_CLIENT|SESSION_KEY"
# Coredump
gdb -c core.dump # info registers, x/100x $rsp, find "flag"See disk-and-memory.md for full Volatility plugin reference, VM forensics, and VMware snapshots. See disk-advanced.md for deleted partition recovery, ZFS forensics, and ransomware analysis.
Windows Password Hashes
# Extract with impacket, crack with hashcat -m 1000
python -c "from impacket.examples.secretsdump import *; SAMHashes('SAM', LocalOperations('SYSTEM').getBootKey()).dump()"See windows.md for SAM details and network-advanced.md for NTLMv2 cracking from PCAP.
Bitcoin Tracing
- Use mempool.space API:
https://mempool.space/api/tx/<TXID> - Peel chain: ALWAYS follow LARGER output; round amounts indicate peels
Uncommon File Magic Bytes
| Magic | Format | Extension | Notes |
|---|---|---|---|
OggS | Ogg container | .ogg | Audio/video |
RIFF | RIFF container | .wav,.avi | Check subformat |
%PDF | .pdf | Check metadata & embedded objects | |
GCDE | PrusaSlicer binary G-code | .g, .bgcode | See 3d-printing.md |
Common Flag Locations
- PDF metadata fields (Author, Title, Keywords)
- Image EXIF data
- Deleted files (Recycle Bin
$Rfiles) - Registry values
- Browser history
- Log file fragments
- Memory strings
WMI Persistence Analysis
Pattern (Backchimney): Malware uses WMI event subscriptions for persistence (MITRE T1546.003).
python PyWMIPersistenceFinder.py OBJECTS.DATA- Look for FilterToConsumerBindings with CommandLineEventConsumer
- Base64-encoded PowerShell in consumer commands
- Event filters triggered on system events (logon, timer)
See windows.md for WMI repository analysis details.
Network Forensics Quick Reference
- TFTP netascii: Binary transfers corrupted; fix with
data.replace(b'\r\n', b'\n').replace(b'\r\x00', b'\r') - TLS keylog decryption: Import SSLKEYLOGFILE or RSA private key into Wireshark (Edit → Preferences → Protocols → TLS)
- TLS weak RSA: Extract cert, factor modulus, generate private key with
rsatool, add to Wireshark - USB audio: Extract isochronous data with
tshark -e usb.iso.data, import as raw PCM in Audacity - NTLMv2 from PCAP: Extract server challenge + NTProofStr + blob from NTLMSSP_AUTH, brute-force
- WPA/WEP WiFi decryption:
aircrack-ng -w wordlist capture.pcapcracks WPA handshake; WEP cracked with enough IVs. See network.md. - PCAP repair:
pcapfix -d corrupted.pcaprepairs broken PCAP headers/checksums for Wireshark loading. See network.md. - USB HID keyboard decoding: Extract 8-byte HID reports from USB captures; byte 2 = keycode, byte 0 = modifiers (Shift). See peripheral-capture.md.
- dnscat2 reassembly: Decode hex/base32 subdomain labels, strip 9-byte dnscat2 header, deduplicate retransmissions, reassemble payload. See network-advanced.md.
- USB keyboard LED exfiltration: Host-to-device HID SET_REPORT packets toggle Caps Lock LED. Timing encodes Morse code. See peripheral-capture.md.
See network.md for SMB3 decryption, credential extraction, and linux-forensics.md for full TLS/TFTP/USB workflows.
Browser Forensics
- Chrome/Edge: Decrypt
Login DataSQLite with AES-GCM using DPAPI master key - Firefox: Query
places.sqlite--SELECT url FROM moz_places WHERE url LIKE '%flag%'
See linux-forensics.md for full browser credential decryption code.
Additional Technique Quick References
- Docker image forensics: Config JSON preserves ALL
RUNcommands even after cleanup.tar xf app.tarthen inspect config blob. See linux-forensics.md. - Linux attack chains: Check
auth.log,.bash_history, recent binaries, PCAP. See linux-forensics.md. - RAID 5 XOR recovery: Two disks of a 3-disk RAID 5 → XOR byte-by-byte to recover the third:
bytes(a ^ b for a, b in zip(disk1, disk3)). See disk-advanced.md. - GIMP raw memory dump visual inspection: When Volatility fails, open
.dmpin GIMP as raw RGB data at monitor width (~1920); scroll to find framebuffer screenshots of user's desktop. See disk-and-memory.md. - Kyoto Cabinet hash DB forensics: Recover key ordering from KC hash database with zeroed keys by inserting sequential probe keys and binary-diffing to find which hash slot each overwrites. See disk-advanced.md.
- PowerShell ransomware: Extract scripts from minidump, find AES key, decrypt SMTP attachment. See disk-and-memory.md.
- Linux ransomware + memory dump: If Volatility is unreliable, recover AES key via raw-memory candidate scanning and magic-byte validation; re-extract zip cleanly to avoid missing files/false negatives. See disk-advanced.md.
- Deleted partitions:
testdiskorkpartx -av. See disk-advanced.md. - ZFS forensics: Reconstruct labels, Fletcher4 checksums, PBKDF2 cracking. See disk-advanced.md.
- BSON reconstruction: Reassemble BSON (Binary JSON) documents from raw bytes; parse with
bsonPython library. See disk-and-memory.md. - TrueCrypt mounting: Mount TrueCrypt/VeraCrypt volumes with known password using
veracrypt --mountorcryptsetup open --type tcrypt. See disk-and-memory.md. - Hardware signals: VGA/HDMI TMDS/DisplayPort, Voyager audio, Saleae UART decode, Flipper Zero. See signals-and-hardware.md.
- Caps-lock LED Morse from video: Track caps-lock LED pixel across security camera frames with OpenCV; on/off durations encode Morse code (short=dot, long=dash). See signals-and-hardware.md.
- I2C protocol decoding: Decode I2C bus captures (SDA/SCL lines) to extract data from EEPROM or sensor communications. See signals-and-hardware.md.
- Punched card OCR: Decode IBM-29 punch card images by mapping hole positions to characters using standard encoding grid. See signals-and-hardware.md.
- USB HID mouse drawing: Render relative HID movements per draw mode as bitmap; separate modes, skip pen lifts, scale 5-8x. See peripheral-capture.md.
- Side-channel power analysis: Multi-dimensional power traces (positions × guesses × traces × samples). Average across traces, find sample with max variance, select guess with max power at leak point. See signals-and-hardware.md.
- Packet interval timing: Binary data encoded as inter-packet delays in PCAP. Two interval values = two bit values. See network-advanced.md.
- BMP bitplane QR: Extract bitplanes 0-2 per RGB channel with NumPy; hidden QR often in bit 1 (not bit 0). See stego-image.md.
- Image puzzle reassembly: Edge-match pixel differences between piece borders, greedy placement in grid. See stego-image.md.
- DeepSound audio stego with password cracking: Extract hash with
deepsound2john.py, crack with John, retrieve hidden files from WAV; always check both spectrogram and DeepSound. See stego-advanced.md. - QR code reconstruction from curved reflection: Manually reconstruct QR from glass sphere reflection in video; flip, de-warp, use known plaintext prefix to fix early bytes, high ECC corrects the rest. See steganography.md.
- Audio FFT notes: Dominant frequencies → musical note names (A-G) spell words. See stego-advanced.md.
- Audio metadata octal: Exiftool comment with underscore-separated octal numbers → decode to ASCII/base64. See stego-advanced.md.
- G-code visualization: Side projections (XZ/YZ) reveal text. See 3d-printing.md.
- Git directory recovery:
gitdumper.shfor exposed.gitdirs. See linux-forensics.md. - KeePass v4 cracking: Standard
keepass2johnlacks v4/Argon2 support; useivanmrsulja/keepass2johnfork orkeepass4brute. Generate wordlists withcewl. See linux-forensics.md. - Cross-channel multi-bit LSB: Different bit positions per RGB channel (R[0], G[1], B[2]) encode hidden data. See stego-advanced.md.
- F5 JPEG DCT detection: Ratio of ±1 to ±2 AC coefficients drops from ~3:1 to ~1:1 with F5; sparse images need secondary ±2/±3 metric. See stego-image.md.
- PNG unused palette stego: Unused PLTE entries (not referenced by pixels) carry hidden data in red channel values. See stego-image.md.
- Keyboard acoustic side-channel: MFCC features from keystroke audio + KNN classification against labeled reference. 10ms window captures impact transient. See signals-and-hardware.md.
- TCP flag covert channel: 6 TCP flag bits (FIN/SYN/RST/PSH/ACK/URG) = values 0-63, encoding base64 characters. Nonsensical flag combos on a consistent dest port = covert data. See network-advanced.md.
- Brotli decompression bomb seam: Compressed bomb has repeating blocks; flag breaks the pattern at a seam. Compare adjacent blocks to find discontinuity, decompress only that region. See network-advanced.md.
- Git reflog/fsck squash recovery:
git rebase --squashleaves orphaned objects recoverable viagit fsck --unreachable --no-reflogs. See linux-forensics.md. - DNS trailing byte binary: Extra bytes (
0x30/0x31) appended after DNS question structure encode binary bits; 8-bit MSB-first chunks → ASCII. See network-advanced.md. - Fake TLS + mDNS key + printability merge: TCP stream disguised as TLS hides ZIP; XOR key from mDNS TXT record; merge two decrypted arrays by selecting printable characters. See network-advanced.md.
- Seed-based pixel permutation stego: Deterministic pixel shuffle (Fisher-Yates with known seed) + multi-bitplane interleaved LSB extraction from Y channel → hidden QR code. See stego-image.md.
- BTRFS snapshot recovery: Deleted files persist in BTRFS snapshots/alternate subvolumes.
mount -o subvol=@backupaccesses historical copies. See disk-recovery.md. - JPEG XL TOC permutation: JXL's progressive TOC permutation controls tile convergence order during partial decode. Truncate at increasing offsets, measure which tiles converge first → convergence order encodes flag. See stego-advanced-2.md.
- Kitty terminal graphics:
ESC_Gprotocol embeds zlib-compressed RGB image data in base64 chunks. Strip escape sequences, concatenate, decompress, reconstruct. See steganography.md. - ANSI escape sequence stego: Flag text interleaved between ANSI color codes and braille characters. Invisible when rendered; extract by stripping escape sequences and non-ASCII. See steganography.md.
- Autostereogram solving: Duplicate layer, difference blend, shift horizontally ~100px to reveal hidden 3D text. See steganography.md.
- Two-layer byte+line interleaving: Two files byte-interleaved, then scanlines interleaved. Deinterleave even/odd bytes first (valid images), then even/odd lines. See steganography.md.
- SMB RID recycling: Guest auth + LSARPC
LsaLookupSidswith incrementing RIDs enumerates AD accounts from PCAP. See network-advanced.md. - Timeroasting (MS-SNTP): NTP requests with machine RIDs extract HMAC-MD5 hashes from DC; crack with hashcat -m 31300. See network-advanced.md.
- Android forensics: Extract APK with
adb pull, analyze withapktool, checkshared_prefs/and SQLite databases in/data/data/<package>/. See disk-and-memory.md. - Docker container forensics:
docker saveexports layered tars; deleted files persist in earlier layers.docker history --no-truncreveals build secrets. See disk-and-memory.md. - Cloud storage forensics: S3/GCP/Azure versioning preserves deleted objects.
list-object-versionsrecovers deleted flags. See disk-and-memory.md. - APFS snapshot recovery: Copy-on-write filesystem preserves historical file states in snapshots; use
icatwith different XID block offsets to read inodes across transaction IDs. See disk-advanced.md. - Windows KAPE triage: Pre-collected artifact ZIPs; start with PowerShell history → Amcache → MFT → registry hives. See disk-and-memory.md.
- WordPerfect macro XOR:
.wcmfiles contain macros with embedded encrypted data; XOR formula(a+b)-2*(a&b)= bitwise XOR. See disk-advanced.md. - TLS master key from coredump: Search coredump for session ID (from Wireshark handshake); read 48 bytes before it as master key. Create Wireshark pre-master-secret log file. See network.md.
- Corrupted git blob repair: Single-byte corruption changes SHA-1; brute-force each byte position (256 × file_size) verifying with
git hash-object. See linux-forensics.md. - Split archive reassembly from PCAP: Same-sized HTTP-transferred files with MD5-hash names are archive fragments; order by Apache directory listing timestamps, concatenate, extract password from TCP chat stream. See network.md.
- Video frame accumulation: Video with flashing images at various positions; composite all frames (per-pixel maximum) reveals hidden QR code or image. See stego-advanced-2.md.
- Reversed audio: Garbled audio that sounds like speech played backwards;
sox audio.wav reversed.wav reverseor Audacity Effect → Reverse reveals hidden message. See stego-advanced-2.md. - Multi-stream video container stego: MP4/MKV with multiple video streams; default stream is a red herring, flag in secondary stream.
ffprobe -hide_banner file.mp4to enumerate,ffmpeg -i file.mp4 -map 0:1 -frames:v 1 flag.jpgto extract. See steganography.md. - FAT16 free space recovery: Flag hidden in unallocated clusters of FAT16 filesystem. Parse FAT table, enumerate free clusters (entry = 0x0000), read data region. See disk-recovery.md.
- FAT16 deleted file recovery (fls/icat): FAT deletion replaces first byte of directory entry with
0xE5but data remains.fls -r -d image.imglists deleted entries,icat image.img <inode>recovers by inode. See disk-recovery.md. - Ext2 orphaned inode recovery: Deleted file leaves orphaned inode;
e2fsck -y disk.imgreconnects to/lost+found. Also usedebugfslsdeloricat. See disk-recovery.md. - Linux input_event keylogger parsing: 24-byte
struct input_eventbinary dump; filtertype==1(EV_KEY),value==1(press), map keycodes viainput-event-codes.h. See signals-and-hardware.md. - VBA macro cell data to binary: Excel cells with numeric values; VBA
CByte((val-78)/3)transforms to ELF bytes. Reimplement in Python, never run the macro. See linux-forensics.md. - RGB parity steganography: Sum R+G+B per pixel; even=white, odd=black renders hidden binary bitmap. See stego-image.md.
- Hidden PDF objects: Unreferenced content stream objects not in
/Kidsarray. Add to/Kids, increment/Count, re-render. See network-advanced.md. - Arnold's Cat Map descrambling: Periodic chaotic transform on square images; iterate forward map until original reappears. Period divides
3*N. See stego-advanced-2.md. - Python in-memory source recovery: Attach
pyrasite-shellto running Python process, decompilefunc_codeobjects withuncompyle6(Python <=3.8) orpycdc(Python 3.9+), dumpglobals()for secrets. See linux-forensics.md. - HFS+ resource fork recovery: Hidden data in HFS+ Resource Forks invisible to
binwalk/foremost; use HFSExplorer + 010 Editor HFS template to extract extent records. See disk-advanced.md. - Serial UART from WAV audio: Square wave in audio encodes UART serial data; determine baud rate, parse start/stop bits, decode LSB-first byte frames. See signals-and-hardware.md.
- High-resolution SSTV demodulation: Standard SSTV decoders fail on high-sample-rate recordings; use manual FM demodulation via
arccos+ differentiation. See stego-advanced-2.md. - Corrupted ZIP header repair: Fix filename length fields in both Local File Header (offset 26) and Central Directory (offset 28); fallback: brute-force raw deflate at candidate offsets. See disk-recovery.md.
- SQLite edit history reconstruction: Replay insert/remove diffs from SQLite diff table to reconstruct document at every intermediate state; flag may have been typed then deleted. See disk-advanced.md.
- MJPEG FFD9 trailing byte stego: Extra bytes after JPEG EOI marker (FFD9) in MJPEG frames create invisible covert channel; split on FFD8, extract post-FFD9 data. See stego-advanced-2.md.
- USB MIDI Launchpad grid reconstruction: MIDI Note On/Off in USB PCAP maps to 8x8 Launchpad grid (
key = row*16 + col); reconstruct visual patterns from button press sequences. See signals-and-hardware.md.
SMB RID Recycling via LSARPC (Midnight 2026)
Enumerate AD accounts from PCAP by analyzing LSARPC LsaLookupSids calls with sequential RIDs after Guest auth. Filter: dcerpc.cn_bind_to_str contains lsarpc.
See network-advanced.md for full RPC call sequence and Wireshark filters.
Timeroasting / MS-SNTP Hash Extraction (Midnight 2026)
Extract crackable HMAC-MD5 hashes from MS-SNTP responses by sending NTP requests with machine account RIDs. Crack with hashcat -m 31300.
# Extract NTP payloads, convert to hashcat format, crack
tshark -r capture.pcapng -Y "ntp && ip.src == <DC_IP>" -T fields -e udp.payload
hashcat -m 31300 -a 0 -O hashes.txt rockyou.txt --usernameSee network-advanced.md for payload parsing script and full attack chain.
HTTP Exfiltration in PCAP
Quick path: tshark --export-objects http,/tmp/objects extracts uploaded files instantly. Check for multipart POST uploads, unusual User-Agent strings, and exfiltrated files (images with flag text). See network.md.
Common Encodings
echo "base64string" | base64 -d
echo "hexstring" | xxd -r -p
# ROT13: tr 'A-Za-z' 'N-ZA-Mn-za-m'ROT18: ROT13 on letters + ROT5 on digits. Common final layer in multi-stage forensics. See linux-forensics.md for implementation.
CTF Forensics - 3D Printing / CAD File Forensics
Table of Contents
- PrusaSlicer Binary G-code (.g / .bgcode)
- QOIF (Quite OK Image Format)
- G-code Analysis Tips
- G-code Side View Visualization (0xFun 2026)
- Uncommon File Magic Bytes
---
PrusaSlicer Binary G-code (.g / .bgcode)
File magic: GCDE (4 bytes)
The .g extension is PrusaSlicer's binary G-code format (bgcode). It stores G-code in a block-based structure with compression.
File structure:
Header: "GCDE"(4) + version(4) + checksum_type(2)
Blocks: [type(2) + compression(2) + uncompressed_size(4)
+ compressed_size(4) if compressed
+ type-specific fields
+ data + CRC32(4)]Block types:
- 0 = FileMetadata (has encoding field, 2 bytes)
- 1 = GCode (has encoding field, 2 bytes)
- 2 = SlicerMetadata (has encoding field, 2 bytes)
- 3 = PrinterMetadata (has encoding field, 2 bytes)
- 4 = PrintMetadata (has encoding field, 2 bytes)
- 5 = Thumbnail (has format(2) + width(2) + height(2))
Compression types: 0=None, 1=Deflate, 2=Heatshrink(11,4), 3=Heatshrink(12,4)
Thumbnail formats: 0=PNG, 1=JPEG, 2=QOI (Quite OK Image)
Parsing and extracting G-code:
import struct, zlib
import heatshrink2 # pip install heatshrink2
with open('file.g', 'rb') as f:
data = f.read()
pos = 10 # After header
while pos < len(data) - 8:
block_type = struct.unpack('<H', data[pos:pos+2])[0]
compression = struct.unpack('<H', data[pos+2:pos+4])[0]
uncompressed_size = struct.unpack('<I', data[pos+4:pos+8])[0]
pos += 8
if compression != 0:
compressed_size = struct.unpack('<I', data[pos:pos+4])[0]
pos += 4
else:
compressed_size = uncompressed_size
# Type-specific extra header fields
if block_type in [0,1,2,3,4]:
pos += 2 # encoding field
elif block_type == 5:
pos += 6 # format + width + height
block_data = data[pos:pos+compressed_size]
pos += compressed_size + 4 # data + CRC32
if block_type == 1: # GCode block
if compression == 3: # Heatshrink 12/4
gcode = heatshrink2.decompress(block_data, window_sz2=12, lookahead_sz2=4)
elif compression == 1: # Deflate (zlib)
gcode = zlib.decompress(block_data)
# Search gcode for hidden comments/flagsCommon hiding spots:
- G-code comments (
;=== FLAG_CHAR ... ===) at specific layer heights - Custom G-code sections (
;TYPE:Custom) - Metadata fields (object names, filament info)
- Thumbnail images (extract and view QOIF/PNG)
QOIF (Quite OK Image Format)
Magic: qoif (4 bytes) + width(4 BE) + height(4 BE) + channels(1) + colorspace(1)
Lightweight image format used in PrusaSlicer thumbnails. Decode with Python struct or use the qoi library.
G-code Analysis Tips
# Search for flag patterns in decompressed gcode
grep -i "flag\|meta\|ctf\|secret" output.gcode
# Look for custom comments at layer changes
grep ";.*FLAG\|;.*LAYER_CHANGE" output.gcode
# Extract XY coordinates for visual patterns
grep "^G1" output.gcode | awk '{print $2, $3}' > coords.txtG-code Side View Visualization (0xFun 2026)
Pattern (PrintedParts): Plot X vs Z (side view) with Y filtering. Extrusion segments at specific Y ranges form readable text.
# Extract XY coordinates from G-code
grep "^G1" output.gcode | awk '{print $2, $3}' > coords.txt
# Plot with matplotlib for visual patternsLesson: G-code is just coordinate lists. Side projections (XZ or YZ) reveal embossed/engraved text.
---
Uncommon File Magic Bytes
| Magic | Format | Extension | Notes |
|---|---|---|---|
GCDE | PrusaSlicer binary G-code | .g, .bgcode | 3D printing, heatshrink compressed |
qoif | Quite OK Image Format | .qoi | Lightweight image format, often embedded |
OggS | Ogg container | .ogg | Audio/video |
RIFF | RIFF container | .wav,.avi | Check subformat |
%PDF | .pdf | Check metadata & embedded objects |
CTF Forensics - Advanced Disk and Memory Techniques
Table of Contents
- Deleted Partition Recovery
- ZFS Forensics (Nullcon 2026)
- GPT Partition GUID Data Encoding (VuwCTF 2025)
- Windows Minidump String Carving (0xFun 2026)
- VMDK Sparse Parsing (0xFun 2026)
- Memory Dump String Carving (Pragyan 2026)
- Memory Dump Malware Extraction + XOR (VuwCTF 2025)
- Linux Ransomware Memory-Key Recovery (MetaCTF 2026)
- WordPerfect Macro XOR Extraction (srdnlenCTF 2026)
- Minidump ISO 9660 Recovery + XOR Key (srdnlenCTF 2026)
- APFS Snapshot Historical File Recovery (srdnlenCTF 2026)
- RAID 5 Disk Recovery via XOR (Crypto-Cat)
- HFS+ Resource Fork Hidden Binary Recovery (CONFidence CTF 2017)
- Kyoto Cabinet Hash Database Forensics via Incremental Key Insertion (ASIS CTF 2018)
- SQLite Edit History Reconstruction from Diff Table (Google CTF 2017)
- See Also
---
Deleted Partition Recovery
Pattern (Till Delete Do Us Part): USB image with deleted partition table.
Recovery workflow:
# Check for partitions
fdisk -l image.img # Shows no partitions
# Recover partition table
testdisk image.img # Interactive recovery
# Or use kpartx to map partitions
kpartx -av image.img # Maps as /dev/mapper/loop0p1
# Mount recovered partition
mount /dev/mapper/loop0p1 /mnt/evidence
# Check for hidden directories
ls -la /mnt/evidence # Look for .dotfolders
find /mnt/evidence -name ".*" # Find hidden filesFlag hiding: Path components as flag chars (e.g., /.Meta/CTF/{f/l/a/g})
---
ZFS Forensics (Nullcon 2026)
Pattern: Corrupted ZFS pool image with encrypted dataset.
Recovery workflow: 1. Label reconstruction: All 4 ZFS labels may be zeroed. Find packed nvlist data elsewhere in the image using strings + offset searching. 2. MOS object repair: Copy known-good nvlist bytes to block locations, recompute Fletcher4 checksums:
def fletcher4(data):
a = b = c = d = 0
for i in range(0, len(data), 4):
a = (a + int.from_bytes(data[i:i+4], 'little')) & 0xffffffff
b = (b + a) & 0xffffffff
c = (c + b) & 0xffffffff
d = (d + c) & 0xffffffff
return (d << 96) | (c << 64) | (b << 32) | a3. Encryption cracking: Extract PBKDF2 parameters (iterations, salt) from ZAP objects. GPU-accelerate with PyOpenCL for PBKDF2-HMAC-SHA1, verify AES-256-GCM unwrap on CPU. 4. Passphrase list: rockyou.txt or similar. GPU rate: ~24k passwords/sec.
---
GPT Partition GUID Data Encoding (VuwCTF 2025)
Pattern (Undercut): "LLMs only" + "undercut" → not AI GPT, but GUID Partition Table.
Key insight: GPT partition GUIDs are 16 arbitrary bytes — can encode anything. Look for file magic headers in GUIDs.
# Parse GPT partition table
gdisk -l image.img
# Or with Python:
python3 -c "
import struct
data = open('image.img','rb').read()
# GPT header at LBA 1 (offset 512)
# Partition entries start at LBA 2 (offset 1024)
# Each entry is 128 bytes, GUID at offset 16 (16 bytes)
for i in range(128):
entry = data[1024 + i*128 : 1024 + (i+1)*128]
guid = entry[16:32]
if guid != b'\x00'*16:
print(f'Partition {i}: {guid.hex()}')
"First GUID starts with `BZh11AY&SY` (bzip2 magic) → concatenate GUIDs, decompress as bzip2, then decode ASCII85.
---
Windows Minidump String Carving (0xFun 2026)
Pattern (kd): Go binary crash dump. Flag as plaintext string constant in .data section survives in minidump memory.
strings -a minidump.dmp | grep -i "flag\|ctf\|0xFUN"Lesson: Minidumps contain full memory regions. String constants, keys, and secrets persist. strings -a + grep is the fast path.
---
VMDK Sparse Parsing (0xFun 2026)
Pattern (VMware): Split sparse VMDK requires grain directory + grain table traversal.
Key steps: 1. Parse VMDK sparse header (grain size, GD offset, GT coverage) 2. Follow grain directory → grain table → data grains 3. Calculate absolute disk offsets across split files 4. Mount extracted filesystem (ext4, NTFS)
Lesson: Don't assume VM images can be mounted directly. Parse the VMDK sparse format manually.
---
Memory Dump String Carving (Pragyan 2026)
Pattern (c47chm31fy0uc4n): Linux memory dump with flag in environment variables or process data.
strings -a -n 6 memdump.bin | grep -E "SYNC|FLAG|SSH_CLIENT|SESSION_KEY"
# SSH artifacts reveal source IP and ephemeral port
# Environment variables may contain keys/tokens---
Memory Dump Malware Extraction + XOR (VuwCTF 2025)
Pattern (Jellycat): Extract fake executable from Windows memory dump. Cipher: subtract 0x32, then XOR with cycling key (large multi-line string, e.g., ASCII art).
Key lesson: Always extract and reverse the actual binary from memory rather than trusting strings output (string tables may be red herrings). XOR keys can be hundreds of bytes (ASCII art, lorem ipsum).
# Extract binary, find XOR key in data section
key = b"..." # Large ASCII art string
cipher = open('extracted.bin', 'rb').read()
plaintext = bytes((b - 0x32) ^ key[i % len(key)] for i, b in enumerate(cipher))---
Linux Ransomware Memory-Key Recovery (MetaCTF 2026)
Pattern: Linux memory dump + encrypted .veg files + enc_key.bin; ransomware uses hybrid crypto (AES for files, RSA-wrapped key). Volatility may fail process enumeration due symbol/KASLR (Kernel Address Space Layout Randomization) mismatch.
Fast workflow: 1. Confirm archive integrity before analysis.
unzip -l encrypted_files.zip
# Compare listed files/sizes vs extracted tree; re-extract cleanly if mismatch
unzip -o encrypted_files.zip -d encrypted_full2. Reverse ransomware binary quickly to identify mode/layout.
strings -a ransomware.elf | grep -E "enc_key|EVP_aes|PUBLIC KEY|.veg"
objdump -d ransomware.elf | less- Typical finding:
AES-256-OFB, IV prepended to each.veg, global 32-byte AES key, RSA public key hardcoded.
3. Try Volatility normally, then pivot immediately if empty/unstable.
vol -f memdump.raw linux.pslist
vol -f memdump.raw linux.proc.Maps
vol -f memdump.raw linux.vmayarascan- If Linux plugins return empty/invalid output despite correct banner/symbols, do raw-memory candidate scanning.
4. Recover AES key via anchored candidate scan + magic validation.
- Use recurring anchor strings in memory (e.g.,
/home/.../enc_key.bin, HOME path). - Derive candidate offsets near anchors (page-aligned windows).
- Test each 32-byte candidate by decrypting first blocks of multiple
.vegfiles and checking magic bytes (%PDF-,PK\x03\x04,\x89PNG\r\n\x1a\n). - Keep candidates that satisfy multiple independent signatures.
5. Decrypt full dataset and verify output completeness.
# OFB: iv = first 16 bytes, ciphertext starts at +16
# Decrypt all *.veg recursively from a clean extraction directory- Validate recovered file count against zip listing.
- Watch for duplicated mirror trees (e.g.,
snap/*/Downloads/...) and deduplicate logically.
6. Defend against false flags.
- Treat metadata-only flags as suspicious until corroborated by challenge context.
- Prefer tokens from primary project artifacts and perform uniqueness checks:
rg -n -a '[A-Za-z]+CTF\\{[^}]+\\}' recovered_full
pdftotext recovered_full/**/*.pdf - 2>/dev/null | rg '[A-Za-z]+CTF\\{'Key lessons:
- Don't trust a partial/stale extraction tree; re-extract zip cleanly.
- In OFB ransomware, magic-byte validation is a fast key oracle.
- A plausible
CTF{...}in metadata can be a decoy; confirm with corpus-wide consistency.
---
WordPerfect Macro XOR Extraction (srdnlenCTF 2026)
Pattern (Trilogy of Death Vol I: Corel): Corel Linux disk image containing WordPerfect macro file (fc.wcm) with XOR-encrypted byte arrays.
Key insight: WordPerfect macro files (.wcm) can contain executable macros with embedded encrypted data. The XOR formula (bb + kb) - 2*(bb & kb) is mathematically equivalent to bitwise XOR.
Brute-force 4-byte XOR key under charset constraints:
import string
docbody = [206, 56, 8, 128, 209, 47, 2, 149, ...] # encrypted bytes from macro
allowed = set(map(ord, string.ascii_lowercase + string.digits + "_{}"))
# Find valid key bytes independently for each position mod 4
cands = []
for j in range(4):
good = []
for k in range(256):
if all((docbody[i] ^ k) in allowed for i in range(j, len(docbody), 4)):
good.append(k)
cands.append(good)
# Try all combinations (usually very few candidates per position)
for k0 in cands[0]:
for k1 in cands[1]:
for k2 in cands[2]:
for k3 in cands[3]:
key = [k0, k1, k2, k3]
pt = ''.join(chr(c ^ key[i % 4]) for i, c in enumerate(docbody))
if pt.startswith("srd") and pt.endswith("}"):
print(pt)Lesson: Legacy document formats (WordPerfect, Lotus 1-2-3) can embed executable macros with obfuscated data. When you know the flag charset, brute-forcing a short XOR key is trivial by filtering each key byte independently.
---
Minidump ISO 9660 Recovery + XOR Key (srdnlenCTF 2026)
Pattern (Trilogy of Death Vol II: The Legendary Armory): Two relics in volatile memory (minidump) must be XORed; ISO 9660 directory entries in memory fragments point to hidden data.
Technique: 1. Search minidump for ISO 9660 directory entry signatures 2. Parse directory entries to locate target file offset and size 3. Decrypt file using recovered XOR key (e.g., 8-byte repeating key) 4. Parse resulting data as ZIP without central directory (local headers only)
ZIP local header parsing without central directory:
import struct, zlib
pos = 0
files = {}
while True:
off = dec.find(b"PK\x03\x04", pos)
if off < 0:
break
(ver, flag, method, _, _, crc, csize, usize, nlen, xlen) = struct.unpack_from(
"<HHHHHIIIHH", dec, off + 4)
name = dec[off + 30:off + 30 + nlen].decode()
data_off = off + 30 + nlen + xlen
comp = dec[data_off:data_off + csize]
if method == 8: # Deflate
raw = zlib.decompress(comp, -15)
else:
raw = comp
files[name] = raw
pos = data_off + csizeKey insight: When ZIP central directory is missing/corrupt, iterate local file headers (PK\x03\x04) directly. Each local header contains enough metadata (compression method, sizes, filename) to extract files independently.
---
APFS Snapshot Historical File Recovery (srdnlenCTF 2026)
Pattern (Trilogy of Death Vol III: The Poisoned Apple): APFS volume maintains historical snapshots; recovering earlier state of a key file reveals authentic value before poisoning.
Technique: 1. Extract APFS partition from DMG (locate by sector offset) 2. Search for APFS volume superblocks (magic APSB) across all snapshots, noting transaction IDs (XIDs) 3. Use icat (Sleuth Kit with APFS support) to read specific inodes across different snapshot XIDs 4. Compare file content across XID boundaries to identify when poisoning occurred 5. Use pre-poisoning value for decryption
Finding APFS volume superblocks across snapshots:
import struct
with open("apfs_partition.img", "rb") as f:
mm = f.read()
snaps = []
pos = 0
while True:
idx = mm.find(b"APSB", pos)
if idx < 0:
break
# XID is at offset -16 from magic (in block header)
hdr_start = idx - 32
xid = struct.unpack_from("<Q", mm, hdr_start + 16)[0]
blk = hdr_start // 4096
snaps.append((xid, blk))
pos = idx + 1
# Read target inode across snapshots
import subprocess
for xid, blk in sorted(set(snaps)):
try:
out = subprocess.check_output(
["icat", "-f", "apfs", "-P", "apfs", "-B", str(blk),
"apfs_partition.img", "449414"]) # target inode number
print(f"XID {xid}: {out[:64]}...")
except:
passDecryption with recovered authentic key:
import hashlib
from Cryptodome.Cipher import AES
# Pre-poisoning key value (found in earlier snapshot)
authentic_key_hex = "39f520679fd68654500f9cd44e8caed2bc897a3227dc297c4520336de2a59dd7"
key = hashlib.pbkdf2_hmac('sha256', bytes.fromhex(authentic_key_hex), salt, iterations)
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = cipher.decrypt(encrypted_flag)Key insight: APFS (and other copy-on-write filesystems like ZFS/Btrfs) preserve historical file states in snapshots. When a challenge involves "poisoned" or "tampered" data, always check for older snapshots containing the original values. Use icat with different block offsets to read the same inode across different transaction IDs.
---
RAID 5 Disk Recovery via XOR (Crypto-Cat)
Pattern: RAID 5 array with one damaged/missing disk. Two working disks are provided and the third must be reconstructed using XOR parity.
How RAID 5 parity works: Data is striped across N disks with distributed parity. For any stripe, Disk1 XOR Disk2 XOR ... XOR DiskN = 0. If one disk is missing, XOR the remaining disks to recover it.
Recovery script:
# Recover missing disk2 from disk1 and disk3
with open('disk1.img', 'rb') as f:
disk1 = f.read()
with open('disk3.img', 'rb') as f:
disk3 = f.read()
# XOR byte-by-byte to recover the missing disk
disk2 = bytes(a ^ b for a, b in zip(disk1, disk3))
with open('disk2.img', 'wb') as f:
f.write(disk2)After recovery:
# Reassemble the RAID array
mdadm --create /dev/md0 --level=5 --raid-devices=3 \
disk1.img disk2.img disk3.img
# Or mount individual recovered disk if it contains a filesystem
mount -o loop,ro disk2.img /mnt/recoveredKey insight: RAID 5 uses XOR parity across all disks in each stripe. XOR is self-inverse: if A XOR B XOR C = 0, then B = A XOR C. For N-disk RAID 5, XOR all N-1 working disks together to recover the missing one.
Detection: Challenge provides multiple disk images of identical size, mentions "array", "redundancy", or "parity". file command may identify them as filesystem images or raw data.
---
HFS+ Resource Fork Hidden Binary Recovery (CONFidence CTF 2017)
HFS+ files can have a Resource Fork containing hidden data invisible to most tools. Use HFSExplorer to inspect the catalog and 010 Editor with HFS template to extract.
# 1. Mount or open the HFS+ image
# Standard tools miss Resource Forks:
binwalk image.dmg # Won't find resource fork contents
strings image.dmg # May show fragments
# 2. Use HFSExplorer to browse the catalog
# Look for files with non-zero Resource Fork size
# Suspicious: nodeID 1337 or similar CTF-typical IDs
# 3. Check .fseventsd logs for historical file operations
pip install FSEventsParser
python FSEventsParser.py -s image.dmg -o events.csv
# Reveals creation/deletion of files across the volume
# 4. Extract Resource Fork data with 010 Editor:
# - Load disk image with HFS+ template
# - Navigate to catalog -> target file -> resource fork extents
# - Note start block and length from extent records
# - If split across multiple extents, extract and concatenate:
dd if=image.dmg bs=4096 skip=$BLOCK1 count=$LEN1 of=part1.bin
dd if=image.dmg bs=4096 skip=$BLOCK2 count=$LEN2 of=part2.bin
cat part1.bin part2.bin > recovered_binaryKey insight: HFS+ Resource Forks are a second data stream attached to files, invisible to most forensic tools that only examine the Data Fork. binwalk, foremost, and strings miss them. HFSExplorer shows both forks in the catalog; 010 Editor with the HFS template reveals extent records for manual extraction. .fseventsd logs can reveal that hidden files were created/deleted.
Detection: DMG or HFS+ disk image where standard carving finds nothing. file identifies as "Apple HFS+" or "Apple Partition Map". Challenge mentions "Mac", "Apple", or "hidden data".
---
Kyoto Cabinet Hash Database Forensics via Incremental Key Insertion (ASIS CTF 2018)
Pattern: Unknown binary file identified as Kyoto Cabinet (KC) hash database. Flag characters stored as values with zeroed-out keys. Since the database uses a fixed-size hash table, recover ordering by inserting sequential keys one at a time and observing which hash slot reference gets overwritten via binary diff.
# Identify format
file unknown.db # may not recognize KC format
strings unknown.db | head # look for "KCPH" magic
# Enumerate values
kchashmgr list tokyo.kch
# Recover key ordering via incremental insertion + binary diff
for i in $(seq -w 000 088); do
cp tokyo.kch test.kch
kchashmgr set test.kch "$i" "probe"
diff <(xxd tokyo.kch) <(xxd test.kch) | head -5
# Changed offset reveals which original entry maps to key $i
doneFull recovery script (Python):
import subprocess, shutil
original = 'tokyo.kch'
# Get all values from the database
values = subprocess.check_output(['kchashmgr', 'list', original]).decode().splitlines()
mapping = {}
for i in range(len(values)):
key = f'{i:03d}'
shutil.copy(original, 'test.kch')
subprocess.run(['kchashmgr', 'set', 'test.kch', key, 'probe'], check=True)
# Binary diff to find which slot changed
orig_hex = subprocess.check_output(['xxd', original]).decode()
test_hex = subprocess.check_output(['xxd', 'test.kch']).decode()
for orig_line, test_line in zip(orig_hex.splitlines(), test_hex.splitlines()):
if orig_line != test_line:
mapping[i] = orig_line # Record which entry was overwritten
break
# Reconstruct flag from ordered values
flag = ''.join(values[i] for i in sorted(mapping.keys()))
print(flag)Key insight: Hash databases store entries at positions determined by key hash values. When keys are zeroed/corrupted, the stored ordering is hash-based, not insertion-order. Insert probe keys one at a time and binary-diff the database to find which slot each probe overwrites, revealing the original key-to-value mapping.
---
SQLite Edit History Reconstruction from Diff Table (Google CTF 2017)
SQLite databases storing note/document edit history as diff entries (operation, position, text, diffset) can be replayed to reconstruct content at any point in time.
import sqlite3
db = sqlite3.connect('notes.db')
# Table structure: diffs(id, type, position, text, diffset)
# type: 'insert' or 'remove'
diffs = db.execute("SELECT type, position, text FROM diffs ORDER BY id").fetchall()
document = ""
for op_type, position, text in diffs:
if op_type == 'insert':
document = document[:position] + text + document[position:]
elif op_type == 'remove':
document = document[:position] + document[position + len(text):]
# Check for flag at each step (may have been typed then deleted)
if 'CTF{' in document or 'flag{' in document:
print(f"Flag found: {document}")Key insight: Collaborative editing tools store incremental diffs. Replaying all operations sequentially reveals content that existed at any point in the edit history, including secrets that were later deleted. Check for flags at every intermediate state, not just the final document.
Detection: SQLite database with tables containing type/operation, position, text columns. Challenge mentions "notes", "editor", "collaboration", or "history". Schema inspection via .schema or sqlite3 db.sqlite ".tables" reveals diff-style tables.
---
See Also
- disk-and-memory.md - Core disk and memory forensics (Volatility 3, disk image analysis, VM/OVA/VMDK forensics, VMware snapshots, GIMP raw memory dump visual inspection, coredump analysis, Windows KAPE triage, PowerShell ransomware, Android forensics, Docker container forensics, cloud storage forensics, BSON reconstruction, TrueCrypt/VeraCrypt mounting)
- disk-recovery.md - Disk recovery and extraction patterns (LUKS master key recovery, PRNG timestamp seed brute-force, VBA macro binary recovery, FemtoZip decompression, XFS reconstruction, tar duplicate entry extraction, nested matryoshka filesystem extraction, anti-carving via null byte interleaving)
CTF Forensics - Disk and Memory Analysis
Table of Contents
- Memory Forensics (Volatility 3)
- Disk Image Analysis
- VM Forensics (OVA/VMDK)
- VMware Snapshot Forensics
- GIMP Raw Memory Dump Visual Inspection (INShAck 2018)
- Coredump Analysis
- Windows KAPE Triage Analysis (UTCTF 2026)
- PowerShell Ransomware Analysis
- Android Forensics
- Container Forensics (Docker)
- Cloud Storage Forensics (AWS S3 / GCP / Azure)
- BSON (Binary JSON) Format Reconstruction (IceCTF 2016)
- TrueCrypt / VeraCrypt Volume Mounting (GreHack CTF 2016)
- Volatility mftparser Offset-Based Deleted File Recovery (BSides Delhi 2018)
- Brotli Blob Detection via ASCII-Art Signature (ASIS Finals 2018)
- corkami/pocs MD5 PDF Collision Generation (35C3 2018)
- See Also
---
Memory Forensics (Volatility 3)
vol3 -f memory.dmp windows.info
vol3 -f memory.dmp windows.pslist
vol3 -f memory.dmp windows.cmdline
vol3 -f memory.dmp windows.netscan
vol3 -f memory.dmp windows.filescan
vol3 -f memory.dmp windows.dumpfiles --physaddr <addr>
vol3 -f memory.dmp windows.mftscan | grep flagCommon plugins:
windows.pslist/windows.pstree- Process listingwindows.cmdline- Command line argumentswindows.netscan- Network connectionswindows.filescan- File objects in memorywindows.dumpfiles- Extract files by physical addresswindows.mftscan- MFT FILE objects in memory (timestamps, filenames). Note:mftparserwas Volatility 2 only; Vol3 usesmftscan
---
Disk Image Analysis
# Mount read-only
sudo mount -o loop,ro image.dd /mnt/evidence
# Autopsy / Sleuth Kit
fls -r image.dd # List files recursively
icat image.dd <inode> # Extract file by inode
# Carving deleted files
photorec image.dd
foremost -i image.dd---
VM Forensics (OVA/VMDK)
# OVA = TAR archive containing VMDK + OVF
tar -xvf machine.ova
# 7z reads VMDK directly (no mount needed)
7z l disk.vmdk | head -100
7z x disk.vmdk -oextracted "Windows/System32/config/SAM" -rKey files to extract from VM images:
Windows/System32/config/SAM- Password hashesWindows/System32/config/SYSTEM- Boot keyWindows/System32/config/SOFTWARE- Installed softwareUsers/*/NTUSER.DAT- User registryUsers/*/AppData/- Browser data, credentials
---
VMware Snapshot Forensics
Converting VMware snapshots to memory dumps:
# .vmss (suspended state) + .vmem (memory) → memory.dmp
vmss2core -W path/to/snapshot.vmss path/to/snapshot.vmem
# Output: memory.dmp (analyzable with Volatility/MemprocFS)Malware hunting in snapshots (Armorless): 1. Check Amcache for executed binaries near encryption timestamp 2. Look for deceptive names (Unicode lookalikes: ṙ instead of r) 3. Dump suspicious executables from memory 4. If PyInstaller-packed: pyinstxtractor → decompile .pyc 5. If PyArmor-protected: use PyArmor-Unpacker
Ransomware key recovery via MFT:
- Even if original files deleted, MFT preserves modification timestamps
- Seed-based encryption: recover mtime → derive key
vol3 -f memory.dmp windows.mftscan | grep flag
# mtime as Unix epoch → seed for PRNG → derive encryption key---
GIMP Raw Memory Dump Visual Inspection (INShAck 2018)
Pattern: When Volatility fails or profiles don't match, open raw memory dumps directly in GIMP as raw image data. Scroll through memory while adjusting image width to find previously-displayed images rendered as pixel data.
Steps: 1. Open .dmp file in GIMP: File > Open, set image type to "Raw image data" 2. Set pixel format to RGB, width to ~1920 (monitor resolution) 3. Scroll through the file offset while adjusting width with arrow keys 4. Previously-displayed images (desktop, browser content) become visible when width matches the original stride
# Alternative: use Python + PIL to scan memory as pixel data
python3 -c "
from PIL import Image
import numpy as np
with open('memory.dmp', 'rb') as f:
data = f.read()
# Try common display widths: 1920, 1366, 1280, 1024
for width in [1920, 1366, 1280, 1024]:
stride = width * 3 # RGB = 3 bytes per pixel
# Sample at various offsets through the dump
for offset in range(0, len(data) - stride * 100, stride * 500):
chunk = data[offset:offset + stride * 100]
if len(chunk) == stride * 100:
img = Image.frombytes('RGB', (width, 100), chunk)
# Check if image has meaningful content (not all zeros/noise)
arr = np.array(img)
if 10 < arr.mean() < 245 and arr.std() > 20:
img.save(f'frame_{width}_{offset}.png')
print(f'Potential image at offset {offset}, width {width}')
"Key insight: Raw memory dumps contain framebuffer data that was displayed on screen. GIMP can render arbitrary binary data as pixels. When the image width matches the original display stride, screenshots of the user's desktop become visible without any forensics tools, profiles, or decryption.
---
Coredump Analysis
gdb -c core.dump
(gdb) info registers
(gdb) x/100x $rsp
(gdb) find 0x0, 0xffffffff, "flag"---
Windows KAPE Triage Analysis (UTCTF 2026)
Pattern (Landfall, Sherlockk, Cold Workspace): KAPE (Kroll Artifact Parser and Extractor) triage collection ZIP containing Windows forensic artifacts. Multiple challenges reference the same triage dataset.
KAPE triage structure:
Modified_KAPE_Triage_Files/
├── C/
│ ├── Users/<username>/
│ │ ├── AppData/Local/Microsoft/Windows/PowerShell/PSReadLine/
│ │ │ └── ConsoleHost_history.txt # PowerShell command history
│ │ ├── NTUSER.DAT # User registry hive
│ │ └── AppData/Roaming/Microsoft/Windows/Recent/ # Recent files
│ ├── Windows/
│ │ ├── System32/config/
│ │ │ ├── SAM # Password hashes
│ │ │ ├── SYSTEM # System config + boot key
│ │ │ └── SOFTWARE # Installed software
│ │ └── appcompat/Programs/
│ │ └── Amcache.hve # Execution history with SHA-1 hashes
│ └── $MFT # Master File Table
└── ...High-value artifacts:
1. PowerShell history — reveals attacker commands:
cat "C/Users/*/AppData/Local/Microsoft/Windows/PowerShell/PSReadLine/ConsoleHost_history.txt"
# Look for: credential access, lateral movement, data staging2. Amcache — executed programs with timestamps and hashes:
# Parse with Eric Zimmerman's AmcacheParser or regipy
python3 -c "
from regipy.registry import RegistryHive
reg = RegistryHive('C/Windows/appcompat/Programs/Amcache.hve')
for entry in reg.recurse_subkeys(as_json=True):
print(entry)
" | grep -i "flag\|suspicious\|malware"3. MFT resident data — small files stored directly in MFT records:
# Parse MFT for resident file data (files < ~700 bytes stored inline)
# Use analyzeMFT or python-ntfs
import struct
with open('$MFT', 'rb') as f:
mft_data = f.read()
# Search for flag patterns in raw MFT data
import re
flags = re.findall(rb'utflag\{[^}]+\}', mft_data)
for flag in flags:
print(f"Found: {flag.decode()}")4. Environment variables from memory dumps (Cold Workspace pattern):
# Small .dmp files may be minidumps with environment variable blocks
strings -a cold-workspace.dmp | grep -i "flag\|password\|key\|secret"
# Environment variables survive in process memory snapshotsChallenge patterns from UTCTF 2026:
- Landfall: Flag hidden in PowerShell history or Amcache execution records
- Sherlockk: Correlate Amcache entries with MFT timestamps to identify malicious activity
- Cold Workspace: Flag in environment variables extracted from memory dump
- Checkpoint A/B: Multi-part investigation using combined artifacts
Key insight: KAPE triage ZIPs contain pre-collected forensic artifacts — no need for full disk imaging. Start with PowerShell history (fastest wins) → Amcache (execution timeline) → MFT (resident data for small files) → registry hives (persistence, credentials).
---
PowerShell Ransomware Analysis
Pattern (Email From Krampus): PowerShell memory dump + network capture.
Analysis workflow: 1. Extract script blocks from minidump:
python power_dump.py powershell.DMP
# Or: strings powershell.DMP | grep -A5 "function\|Invoke-"2. Identify encryption (typically AES-CBC with SHA-256 key derivation)
3. Extract encrypted attachment from PCAP:
# Filter SMTP traffic in Wireshark
# Export attachment, base64 decode4. Find encryption key in memory dump:
# Key often generated with Get-Random, regex search:
strings powershell.DMP | grep -E '^[A-Za-z0-9]{24}$' | sort | head5. Find archive password similarly, decrypt layers
---
Android Forensics
# Extract APK from device
adb pull /data/app/com.target.app/base.apk
# Analyze APK contents
apktool d base.apk -o decompiled/
# Check: AndroidManifest.xml, res/values/strings.xml, shared_prefs/
# Extract data from Android backup
adb backup -apk -shared -all -f backup.ab
java -jar abe.jar unpack backup.ab backup.tar
tar xf backup.tar
# SQLite databases (contacts, messages, browser history)
sqlite3 /data/data/com.android.providers.contacts/databases/contacts2.db ".tables"
sqlite3 /data/data/com.android.providers.telephony/databases/mmssms.db "SELECT * FROM sms"
# Parse Android filesystem image
mkdir android_mount && mount -o ro android_image.img android_mount/
# Key locations:
# /data/data/<app>/databases/ — app SQLite databases
# /data/data/<app>/shared_prefs/ — app preferences (XML)
# /data/system/packages.xml — installed packages
# /data/misc/wifi/wpa_supplicant.conf — saved WiFi passwordsKey insight: Android stores app data in /data/data/<package>/ with SQLite databases and XML shared preferences. adb backup captures the full app state. For CTFs, check shared_prefs/ for hardcoded secrets and databases/ for flags.
---
Container Forensics (Docker)
# Export Docker image layers
docker save IMAGE:TAG -o image.tar
tar xf image.tar
# Each layer is a directory with layer.tar containing filesystem changes
# Check: layer.tar files for added/modified files, deleted files (.wh.* whiteout)
# Inspect image history for build commands (may contain secrets)
docker history IMAGE:TAG --no-trunc
# Shows every Dockerfile instruction including ARGs and ENV values
# Extract filesystem without running the container
docker create --name extract IMAGE:TAG
docker export extract -o container_fs.tar
docker rm extract
# Analyze with dive (layer-by-layer diff viewer)
dive IMAGE:TAG
# Common forensic targets in container images:
# /app/.env, /app/config/* — application secrets
# /root/.bash_history — build-time commands
# /etc/shadow — leaked credentials
# Deleted files visible in earlier layers even if removed in later onesKey insight: Docker images are layered — a file deleted in a later layer still exists in the earlier layer's tar. Use docker history --no-trunc to see full Dockerfile commands including secrets passed via ARG or ENV. The dive tool visualizes layer diffs interactively.
---
Cloud Storage Forensics (AWS S3 / GCP / Azure)
# Enumerate public S3 buckets
aws s3 ls s3://target-bucket/ --no-sign-request
aws s3 cp s3://target-bucket/flag.txt . --no-sign-request
# Check bucket versioning (previous versions may contain deleted flags)
aws s3api list-object-versions --bucket target-bucket --no-sign-request
aws s3api get-object --bucket target-bucket --key secret.txt --version-id VERSION_ID out.txt
# GCP Cloud Storage
gsutil ls gs://target-bucket/
gsutil cp gs://target-bucket/flag.txt .
# Azure Blob Storage
az storage blob list --container-name target --account-name storageaccount
az storage blob download --container-name target --name flag.txt --account-name storageaccountKey insight: Cloud storage versioning preserves deleted objects. Even if a flag file is deleted from the bucket, previous versions may still be accessible via list-object-versions. Always check for versioning-enabled buckets.
---
BSON (Binary JSON) Format Reconstruction (IceCTF 2016)
BSON is MongoDB's binary serialization format. Corrupted BSON files need header repair before parsing, and may contain base64-encoded file fragments.
import bson
# BSON header: first 4 bytes = little-endian document size
# Fix corrupted header by setting correct size
with open('data.bson', 'rb') as f:
data = bytearray(f.read())
# Fix size header if corrupted (e.g., missing first 3 bytes)
import struct
correct_size = len(data) + 3 # account for missing bytes
data = struct.pack('<I', correct_size)[1:] + data # prepend missing bytes
# Parse BSON documents
docs = bson.decode_all(bytes(data))
for doc in docs:
print(doc)
# Reconstruct file from BSON chunks (common pattern):
# Each document has: {index: N, data: "base64_chunk"}
import base64
chunks = sorted(docs, key=lambda d: d.get('index', d.get('i', 0)))
reconstructed = b''
for chunk in chunks:
b64_data = chunk.get('data', chunk.get('d', ''))
reconstructed += base64.b64decode(b64_data)
with open('reconstructed.png', 'wb') as f:
f.write(reconstructed)Key insight: BSON starts with a 4-byte little-endian size field. If the file appears corrupted, check if the first bytes are missing or incorrect. Parse with bson.decode_all() (from pymongo), sort chunks by index, and concatenate base64-decoded data to reconstruct embedded files.
---
TrueCrypt / VeraCrypt Volume Mounting (GreHack CTF 2016)
Encrypted volumes in CTF challenges may use TrueCrypt or VeraCrypt. Identify by logo/branding clues, then mount with a recovered keyfile or password.
# Identify TrueCrypt volumes:
# - No file signature/magic bytes (by design)
# - Exact size is multiple of 512 bytes
# - High entropy throughout the file
# - Context clues: TrueCrypt logo in related images
# Mount with password:
truecrypt -t -p "password123" volume.tc /mnt/tc
veracrypt -t -p "password123" volume.tc /mnt/vc
# Mount with keyfile (no password):
truecrypt -t -p "" -k keyfile.png volume.tc /mnt/tc
veracrypt -t -p "" -k keyfile.png volume.tc /mnt/vc
# Mount hidden volume (different password):
truecrypt -t -p "hidden_password" volume.tc /mnt/tc
# Common keyfile locations in CTFs:
# - Images extracted from other challenge steps
# - GPG-encrypted files with keys found in git repos
# - Files embedded in other forensic artifacts
# If TrueCrypt is not available (discontinued):
# Use VeraCrypt (backwards-compatible with TrueCrypt volumes)
# Add --truecrypt flag for old TC volumes:
veracrypt -t --truecrypt -p "password" volume.tc /mnt/vcKey insight: TrueCrypt volumes have no magic bytes or identifiable header -- they look like random data. Identify them from context clues (related images showing TrueCrypt logo, file sizes that are exact multiples of 512, or challenge descriptions mentioning encryption). VeraCrypt with --truecrypt flag handles legacy TC volumes.
---
Volatility mftparser Offset-Based Deleted File Recovery (BSides Delhi 2018)
Pattern: Standard dumpfiles or filescan + dumpfiles --physaddr fails on a deleted file because its directory entry has been marked free. The MFT record that still holds the file's $DATA attribute survives until the record is reused. Volatility 2's mftparser can dump the resident $DATA directly when given the exact --offset of the MFT record found via filescan.
# 1. Locate the MFT record offset (Volatility 2 example; Vol3 uses windows.mftscan)
vol.py -f Challenge.raw --profile=Win7SP1x86 mftparser \
| grep -A2 "target_filename"
# 2. Dump every attribute of the matching MFT entry, including $DATA
vol.py -f Challenge.raw --profile=Win7SP1x86 mftparser \
--offset=0x7ca3c00 --dump-dir=./out/
ls ./out/
# file.data.$DATA contains the recovered contentKey insight: NTFS marks files "deleted" by flipping one bit in the MFT record header (0x16 byte: 0x01 == in use). Until the record is reallocated, the entire $DATA attribute is still intact and only lazily freed. Use mftparser --offset=<record> for resident files (under ~700 bytes — stored inline in the MFT) and dd/icat with the cluster runs for larger files. Always also grep for the filename in windows.mftscan output before giving up: memory-resident MFT fragments are still findable after on-disk deletion.
References: BSides Delhi CTF 2018 — Never Too Late Mister, writeups 11963, 11970
---
Brotli Blob Detection via ASCII-Art Signature (ASIS Finals 2018)
Pattern: Binwalk and file miss Brotli-compressed data because the format has no fixed magic. Decompress candidate blobs with brotli.decompress(); the Brotli reference implementation embeds its own ASCII-art logo Brrroootttllliii as a sanity-check output. If the decompressed bytes contain that or other Brotli-specific telemetry strings, the original blob was Brotli.
import brotli
try:
out = brotli.decompress(blob)
if b'rrrooottl' in out or b'Brotli' in out:
print('Brotli-compressed')
except Exception: passKey insight: Any compressor without a magic byte is identifiable by trial decompression. For Brotli, zstd, snappy, lzma-alone, spin through each library in order until one succeeds without raising.
References: ASIS CTF Finals 2018 — Green Cabbage, writeup 12419
---
corkami/pocs MD5 PDF Collision Generation (35C3 2018)
Pattern: Challenge demands two valid PDFs with the same MD5 but different content. Use corkami/pocs pdf.py combined with enscript | ps2pdf to produce a PDF header with collision-friendly padding, then drive the collision via fastcoll (or hashclash for chosen-prefix). Works because the PDF format tolerates garbage in the %PDF trailer region that the MD5 collision block can overwrite.
enscript -p out.ps content.txt
ps2pdf out.ps base.pdf
python pdf.py base.pdf target1.pdf target2.pdf
fastcoll -p base.pdf -o target1.pdf target2.pdf
md5sum target1.pdf target2.pdf # identicalKey insight: PDF collision is a one-command pipeline with the right toolchain. The harder variant is chosen-prefix MD5 (different visible content) which requires hashclash and 10-20 CPU-hours. Check pocs/collisions/ for every file format with prebuilt scaffolds.
References: 35C3 CTF 2018 — collider, writeup 12836
---
See Also
- disk-advanced.md - Advanced disk and memory techniques (deleted partition recovery, ZFS forensics, GPT GUID encoding, VMDK sparse parsing, memory dump string carving, ransomware key recovery, WordPerfect macro XOR, minidump ISO 9660 recovery, APFS snapshot recovery, RAID 5 XOR recovery, Kyoto Cabinet hash DB forensics)
- disk-recovery.md - Disk recovery and extraction patterns (LUKS master key recovery, PRNG timestamp seed brute-force, VBA macro binary recovery, FemtoZip decompression, XFS reconstruction, tar duplicate entry extraction, nested matryoshka filesystem extraction, anti-carving via null byte interleaving)
CTF Forensics - Disk Recovery and Extraction Patterns
Table of Contents
- LUKS Master Key Recovery from Memory Dump (Hack.lu 2015)
- PRNG Timestamp Seed Brute-Force for Encryption Key Recovery (CSAW 2015)
- VBA Macro Encoded Binary Recovery (Sharif CTF 2016)
- FemtoZip Shared Dictionary Decompression (Sharif CTF 2016)
- XFS Filesystem Reconstruction from Corrupted Metadata (BSidesSF 2025)
- Tar Archive Duplicate Entry Extraction (BSidesSF 2025)
- Nested Matryoshka Filesystem Extraction (BSidesSF 2025)
- Anti-Carving via Null Byte Interleaving (BSidesSF 2024)
- BTRFS Subvolume/Snapshot Recovery (BSidesSF 2026)
- FAT16 Free Space Data Recovery (BSidesSF 2026)
- FAT16 Deleted File Recovery via Sleuth Kit (MetaCTF Flash 2026)
- Ext2 Orphaned Inode Recovery via fsck (BSidesSF 2026)
- Corrupted ZIP Repair via Header Field Manipulation (PlaidCTF 2017)
- Recovering Deleted .git Repository from FAT Image (Square CTF 2017)
- DNSSEC Key Recovery from Git Commit History (Hack.lu 2017)
- XZ Stream Header Repair via CRC32 Reconstruction (Hackover 2018)
- ZipCrypto Known-Plaintext Cracking via bkcrack (Codegate 2019)
- SQLite Serial-Type Byte Forensics (RITSEC 2018)
- Recursive Binwalk Chain PNG->PDF->DOCX->PNG->Base64 (TAMUctf 2019)
- Regex-Password Nested Zip Chain with exrex (UTCTF 2019)
- See Also
---
LUKS Master Key Recovery from Memory Dump (Hack.lu 2015)
Recover LUKS encryption keys from VM memory dumps using AES key schedule detection:
1. Extract memory: Obtain memory dump from VM snapshot (.elf, .vmem, .raw) 2. Find AES keys: Use aeskeyfind to detect AES key schedules in memory
aeskeyfind memory.elf
# Output: candidate AES-256 keys (64 hex chars each)3. Write key to file: Convert hex key to binary
echo "deadbeef..." | xxd -r -p > master.key4. Add new LUKS passphrase using master key:
cryptsetup luksAddKey --master-key-file master.key /dev/mapper/volume
# Enter new passphrase when prompted
cryptsetup luksOpen /dev/mapper/volume decrypted
mount /dev/mapper/decrypted /mntKey insight: AES key schedules have a distinctive mathematical structure that aeskeyfind detects regardless of where they appear in memory. Works for LUKS, dm-crypt, FileVault, and BitLocker volumes.
Companion tools: rsakeyfind (RSA keys), aesfix (corrupted key recovery).
---
PRNG Timestamp Seed Brute-Force for Encryption Key Recovery (CSAW 2015)
When encryption keys are generated from PRNG seeded with timestamps, brute-force the seed:
1. Identify seed source: Look for Time.now.to_i, time(NULL), System.currentTimeMillis() used as PRNG seed 2. Determine time window: Use file metadata (creation/modification timestamps) to bound the search 3. Brute-force seeds: Try each second in a +/-24 hour window around the file timestamp
import struct
from Crypto.Cipher import AES
# Ruby-compatible Random implementation (or use ctypes for C rand)
for seed in range(timestamp - 86400, timestamp + 86400):
rng = RandomWithSeed(seed)
key = bytes([rng.rand(256) for _ in range(32)]) # AES-256
iv = bytes([rng.rand(256) for _ in range(16)])
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = cipher.decrypt(ciphertext)
# Validate: check for known file signatures
if plaintext[:4] == b'\x89PNG' or plaintext[:2] == b'\xff\xd8':
print(f"Found key with seed: {seed}")
breakKey insight: Expand the time window beyond the obvious timestamp -- clock skew, timezone differences, and filesystem granularity can shift the effective seed by hours.
---
VBA Macro Encoded Binary Recovery (Sharif CTF 2016)
Excel/Word macros may encode binary data in cell values. Extract and decode:
1. Extract macro: Use olevba or open in LibreOffice to inspect VBA code 2. Identify encoding: Look for cell iteration patterns like Cells(i, j).Value 3. Reverse the encoding formula:
# If macro encodes as: cell_value = byte_value * 3 + 78
# Reverse: byte_value = (cell_value - 78) // 3
import openpyxl
wb = openpyxl.load_workbook('challenge.xlsx')
ws = wb.active
binary_data = bytearray()
for row in ws.iter_rows():
for cell in row:
if cell.value is not None:
binary_data.append((int(cell.value) - 78) // 3)
with open('recovered.elf', 'wb') as f:
f.write(binary_data)Key insight: Check the recovered file with file command -- common outputs are ELF binaries, PE executables, or images containing the flag.
---
FemtoZip Shared Dictionary Decompression (Sharif CTF 2016)
FemtoZip uses a shared dictionary model for compressing corpora of similar documents. When given a .model file and compressed data:
# Install femtozip
git clone https://github.com/gtoubassi/femtozip
cd femtozip && make
# Decompress using provided model
./fzip --model fashion.model --decompress compressed_dir/ --output decompressed_dir/After decompression, search through potentially thousands of files:
# Filter by metadata fields
grep -r "category.*forensic" decompressed_dir/ | grep "year.*2016"Key insight: FemtoZip is rare in CTFs. Identify it by the .model file and the presence of many small compressed files that share common structure (JSON, XML templates).
---
XFS Filesystem Reconstruction from Corrupted Metadata (BSidesSF 2025)
When XFS superblock or allocation group metadata is corrupted but inodes are intact:
1. Parse inode directly: XFS inodes contain extent lists with [startoff, startblock, blockcount] tuples 2. Calculate block offsets: Multiply startblock by filesystem block size (typically 4K) 3. Extract file data: Copy blocks directly from the raw disk image
# Extract file from known inode extent
# startblock=104333, blockcount=256, block_size=4096
dd if=disk.img bs=4096 skip=104333 count=256 of=recovered.jpg
# Parse XFS inode structure (at known offset)
python3 -c "
import struct
with open('disk.img', 'rb') as f:
f.seek(inode_offset)
magic = f.read(2) # 'IN' = 0x494e
# Parse di_core (96 bytes): mode, uid, gid, nlink, size, etc.
# Parse extent list: each extent = 16 bytes
# startoff (54 bits) | startblock (52 bits) | blockcount (21 bits)
"Key insight: XFS stores extent maps inline in the inode (up to ~4 extents). For files with more extents, follow the B+tree root in the inode. Use xfs_db if available: xfs_db -r disk.img → inode <num> → print.
---
Tar Archive Duplicate Entry Extraction (BSidesSF 2025)
Tar format allows multiple entries with the same filename. Standard extraction overwrites earlier entries, but specific occurrences can be targeted:
# List all entries (shows duplicates)
tar -tvf archive.tar.xz | grep -c '^\.'
# Extract specific occurrence (1-indexed)
tar -Jxvf archive.tar.xz '.' --occurrence=2 -O > second_entry.bin
# Extract all occurrences via file carving
binwalk -e archive.tar
# Or iterate programmatically
python3 -c "
import tarfile
with tarfile.open('archive.tar.xz') as tf:
for i, member in enumerate(tf.getmembers()):
if member.name == '.':
data = tf.extractfile(member).read()
with open(f'entry_{i}.bin', 'wb') as f:
f.write(data)
"Key insight: The --occurrence=N flag in GNU tar selects the Nth entry with a matching name. Without it, only the last entry survives extraction. Challenges may hide flags in middle entries that normal extraction skips.
---
Nested Matryoshka Filesystem Extraction (BSidesSF 2025)
Disk images containing nested compressed filesystem layers (potentially 10-20+ levels deep):
#!/bin/bash
# Automated layer extraction
IMG="disk.img"
for i in $(seq 1 20); do
echo "=== Layer $i ==="
file "$IMG"
# Detect and decompress
case "$(file -b "$IMG")" in
*XZ*) xz -d "$IMG"; IMG="${IMG%.xz}" ;;
*gzip*) gunzip "$IMG"; IMG="${IMG%.gz}" ;;
*ext4*)
mkdir -p "layer_$i"
sudo mount -o ro,loop "$IMG" "layer_$i"
IMG=$(find "layer_$i" -type f -name "*.img" -o -name "*.xz" | head -1)
;;
*ISO*|*HFS*|*XFS*|*AmigaDOS*)
mkdir -p "layer_$i"
sudo mount -o ro,loop "$IMG" "layer_$i" 2>/dev/null || \
sudo mount -t affs -o ro,loop "$IMG" "layer_$i" 2>/dev/null
IMG=$(find "layer_$i" -type f | head -1)
;;
esac
doneFilesystem types encountered: ext4, XFS, HFS/HFS+, AFFS (AmigaDOS), FAT. Use losetup with --offset for partitioned images. Final layer typically contains an image or text file with the flag.
Key insight: Install uncommon filesystem drivers (hfsplus, affs) beforehand. Some layers require manual sector offset calculation when partition tables are absent.
---
Anti-Carving via Null Byte Interleaving (BSidesSF 2024)
Files stored with null bytes inserted at every other position defeat magic-byte-based file carving tools (binwalk, foremost, scalpel):
1. Identify anti-carving: File carving finds nothing, but xfs_db or filesystem-level tools show the file exists with correct size 2. Extract raw blocks: Use filesystem extent information to locate file data
# XFS: find file extents
xfs_db -r disk.img -c 'inode <inum>' -c 'print'
# Extract extent data
dd if=disk.img bs=4096 skip=<startblock> count=<blockcount> of=raw.bin3. Remove interleaved null bytes: Keep only even-positioned (or odd-positioned) bytes
with open('raw.bin', 'rb') as f:
data = f.read()
# Remove null bytes at odd positions
cleaned = bytes(data[i] for i in range(0, len(data), 2))
with open('recovered.png', 'wb') as f:
f.write(cleaned)# Perl one-liner equivalent
perl -0777 -pe 's/(.)./\1/gs' raw.bin > recovered.pngKey insight: When file carving fails but the filesystem metadata is intact, extract via block-level access and look for byte-level obfuscation patterns. Null byte interleaving doubles the file size — compare actual size vs expected size as a detection heuristic.
---
---
BTRFS Subvolume/Snapshot Recovery (BSidesSF 2026)
Pattern (turn-back-the-clock): Deleted files on a BTRFS filesystem may persist in snapshots or alternate subvolumes. The default mount shows only the active subvolume, but backup snapshots contain historical file states.
Recovery workflow:
# 1. Set up loop device
sudo losetup /dev/loop0 challenge.img
# 2. List available subvolumes
sudo btrfs subvolume list /dev/loop0
# Output: ID 256 gen 7 top level 5 path @
# ID 257 gen 5 top level 5 path @backup
# 3. Mount the default subvolume (may show deleted files as missing)
sudo mount /dev/loop0 /mnt/default
ls /mnt/default/ # Flag file missing
# 4. Mount the backup subvolume
sudo mount -o subvol=@backup /dev/loop0 /mnt/backup
ls /mnt/backup/ # Flag file present!
cat /mnt/backup/flag.txt
# 5. Alternative: mount by subvolume ID
sudo mount -o subvolid=257 /dev/loop0 /mnt/backupKey BTRFS commands for forensics:
# Show filesystem info
btrfs filesystem show /dev/loop0
# List all subvolumes (including snapshots)
btrfs subvolume list -a /mnt
# Show snapshot details
btrfs subvolume show /mnt/@backup
# Find deleted subvolumes (orphaned)
btrfs-find-root /dev/loop0BTRFS snapshot types:
- Writable subvolumes:
@,@home— standard Ubuntu layout - Read-only snapshots: Created by
btrfs subvolume snapshot -r— immutable copies - Backup subvolumes:
@backup,@snap-YYYYMMDD— naming varies by tool (Timeshift, snapper)
Key insight: BTRFS is copy-on-write. Deleting a file from the active subvolume doesn't erase the data if a snapshot or alternate subvolume still references those blocks. Always enumerate all subvolumes with btrfs subvolume list. The -o subvol= mount option is the key to accessing non-default subvolumes.
Detection: file disk.img shows "BTRFS Filesystem". Challenge mentions "snapshots", "time travel", "turn back", or "recovery".
References: BSidesSF 2026 "turn-back-the-clock"
---
FAT16 Free Space Data Recovery (BSidesSF 2026)
Pattern (freeflag): Data is hidden in the free (unallocated) clusters of a FAT16 filesystem. The mounted filesystem shows no suspicious files, but free clusters contain recoverable data.
import struct
with open("disk.img", "rb") as f:
# Read FAT16 boot sector
f.seek(0)
boot = f.read(512)
bytes_per_sector = struct.unpack_from("<H", boot, 11)[0]
sectors_per_cluster = boot[13]
reserved_sectors = struct.unpack_from("<H", boot, 14)[0]
num_fats = boot[16]
sectors_per_fat = struct.unpack_from("<H", boot, 22)[0]
root_entries = struct.unpack_from("<H", boot, 17)[0]
cluster_size = bytes_per_sector * sectors_per_cluster
fat_start = reserved_sectors * bytes_per_sector
root_dir_start = fat_start + (num_fats * sectors_per_fat * bytes_per_sector)
data_start = root_dir_start + (root_entries * 32)
# Read FAT table
f.seek(fat_start)
fat = f.read(sectors_per_fat * bytes_per_sector)
# Find free clusters (FAT entry == 0x0000)
free_data = b""
for cluster in range(2, len(fat) // 2):
entry = struct.unpack_from("<H", fat, cluster * 2)[0]
if entry == 0x0000: # Free cluster
offset = data_start + (cluster - 2) * cluster_size
f.seek(offset)
free_data += f.read(cluster_size)
# Search for flag in free space
if b"CTF{" in free_data:
idx = free_data.index(b"CTF{")
print(free_data[idx:idx+100])Key insight: FAT16/FAT32 mark deleted file clusters as "free" (entry = 0x0000) but don't zero the data. Enumerating free clusters and reading their contents recovers deleted or hidden data. Tools like foremost, scalpel, or manual FAT parsing extract this data. Check the volume label for hints (e.g., "FREESPACE").
When to recognize: Challenge provides a filesystem image. Mounting shows nothing useful, but file identifies it as FAT16/FAT32. Volume label or challenge description hints at "free space", "deleted", or "hidden in plain sight".
References: BSidesSF 2026 "freeflag"
---
FAT16 Deleted File Recovery via Sleuth Kit (MetaCTF Flash 2026)
Pattern (rm -rf flag.png): A file has been deleted from a FAT16 filesystem image. The file's data and cluster chain remain intact, but the directory entry's first byte is replaced with 0xE5 (the FAT deletion marker). Sleuth Kit's fls and icat recover the file by inode.
# Step 1: Identify the filesystem
file flash.img
# flash.img: DOS/MBR boot sector, code offset 0x3e+2, ... FAT (16 bit) ...
# Step 2: List all files including deleted ones (-d = deleted only, -r = recursive)
fls -r -d flash.img
# r/r * 4: _lag.png (first char replaced by FAT deletion marker)
# Step 3: Recover the deleted file by its inode number
icat flash.img 4 > recovered_flag.png
# Step 4: Verify recovery
file recovered_flag.png
# recovered_flag.png: PNG image data, 800 x 600, 8-bit/color RGBAKey insight: FAT16/FAT32 deletion only marks the directory entry's first byte as 0xE5 and marks clusters as free in the FAT table, but the actual file data remains on disk until overwritten. The filename appears scrambled (e.g., flag.png becomes _lag.png), but fls -d lists deleted entries and icat extracts the full file by following the original cluster chain. This is more targeted than free space carving because it preserves the original file boundaries.
When to recognize: Challenge provides a FAT filesystem image with a deleted file. The challenge name or description hints at deletion (rm, deleted, removed). Mount shows the file is missing, but fls reveals the deleted directory entry.
Alternative approaches:
foremost/scalpelfor carving without filesystem awarenessfatcatfor low-level FAT manipulation- Manual hex editing: search for
0xE5entries in directory clusters
References: MetaCTF Flash CTF 2026 "rm -rf flag.png"
---
Ext2 Orphaned Inode Recovery via fsck (BSidesSF 2026)
Pattern (orphan): A file has been deleted from an ext2 filesystem, leaving an orphaned inode. The file doesn't appear in any directory listing, but fsck detects the unattached inode and can reconnect it to /lost+found.
# Mount the image — no flag visible
sudo mount -o loop disk.img /mnt
ls /mnt # Nothing useful
# Run fsck to detect orphaned inodes
sudo umount /mnt
e2fsck -y disk.img
# Output: "Unattached inode 13"
# Output: "Connect to /lost+found? yes"
# Re-mount and check lost+found
sudo mount -o loop disk.img /mnt
ls /mnt/lost+found/
# Found: #13
file /mnt/lost+found/\#13 # Identify file type (e.g., PNG)
cp /mnt/lost+found/\#13 recovered_flag.pngKey insight: Ext2/ext3/ext4 deletion removes directory entries but the inode and data blocks may persist until overwritten. e2fsck (with -y for auto-fix) detects these orphaned inodes and reconnects them to /lost+found with numeric names. For ext2 specifically (no journaling), recovery is more reliable because blocks aren't zeroed on deletion.
When to recognize: Challenge provides an ext2/ext3/ext4 filesystem image. Normal mounting shows nothing. Challenge hints at "deleted", "orphan", "lost", or "recovery". Always run fsck on forensics filesystem images.
Alternative tools:
debugfs— interactive ext2 exploration:debugfs disk.imgthenlsdelto list deleted inodesextundelete— automated ext3/ext4 recoveryicat(Sleuth Kit) — extract file by inode number:icat disk.img 13 > recovered
References: BSidesSF 2026 "orphan"
---
Corrupted ZIP Repair via Header Field Manipulation (PlaidCTF 2017)
ZIP archives with corrupted filename length fields can be repaired by hex-editing both the Local File Header and Central Directory Entry.
# ZIP Local File Header format (at offset 0x04 from PK\x03\x04):
# Offset 26: filename length (2 bytes, little-endian)
# ZIP Central Directory Entry (at PK\x01\x02):
# Offset 28: filename length (2 bytes, little-endian)
# Fix: set both filename lengths to actual filename size
import struct
with open('broken.zip', 'rb') as f:
data = bytearray(f.read())
# Find and fix Local File Header filename length
lfh = data.index(b'PK\x03\x04')
struct.pack_into('<H', data, lfh + 26, 8) # set to 8 bytes
# Find and fix Central Directory filename length
cde = data.index(b'PK\x01\x02')
struct.pack_into('<H', data, cde + 28, 8) # must match
# Write fixed bytes as filename
data[lfh+30:lfh+38] = b'flag.txt'
with open('fixed.zip', 'wb') as f:
f.write(data)
# Alternative: brute-force deflate at candidate offsets
import zlib
with open('broken.zip', 'rb') as f:
raw = f.read()
for offset in range(0x1E, 0x100):
try:
result = zlib.decompress(raw[offset:], -15)
print(f"Offset {offset:#x}: {result}")
break
except zlib.error:
continueKey insight: ZIP filename length fields appear in both the Local File Header (offset 26) and Central Directory (offset 28). Both must match and reflect the actual filename. When these are corrupted to absurd values (e.g., 9001), the archive appears empty. As a fallback, brute-force raw deflate decompression at candidate data offsets.
Detection: ZIP file that unzip -l reports as empty or produces errors about invalid filename lengths. hexdump shows valid PK\x03\x04 and PK\x01\x02 signatures but unreasonable values in length fields.
---
Recovering Deleted .git Repository from FAT Image (Square CTF 2017)
A FAT filesystem image with a deleted .git directory. Use TSK fls -r to list all files including deleted ones (marked with *). Extract deleted inodes with icat. Reconstruct the git object directory structure from the extracted files, then use git fsck and git log to recover commit history and flag.
# Step 1: List all files including deleted ones (* prefix = deleted)
fls -r disk.img | grep '\*'
# Example output:
# r/r * 5: .git/HEAD
# r/r * 6: .git/config
# r/r * 7: .git/objects/ab/cdef1234...
# Step 2: Extract deleted files by inode number
icat disk.img 5 > HEAD
icat disk.img 6 > config
# Repeat for all git object inodes
# Step 3: Rebuild .git directory structure
mkdir -p recovered/.git/objects/ab/
# Place each extracted object at its correct path
# Step 4: Recover commit history
cd recovered
git fsck --full # Check object integrity, find dangling commits
git log --all # Show all commits including unreferenced ones
git show <commit_hash> # Inspect specific commit for flagKey insight: FAT marks deleted files by changing the first byte of the directory entry to 0xE5 but keeps cluster data intact until reused. TSK's fls/icat extracts deleted files by inode, making deletion forensically reversible. Git objects are content-addressed — once extracted, git fsck finds all reachable commits even without a valid HEAD reference.
---
DNSSEC Key Recovery from Git Commit History (Hack.lu 2017)
DNSSEC private signing keys committed to a git repository and later deleted remain permanently in the commit history. Recover the keys to set up a local BIND instance and forge DNSSEC-signed DNS responses.
# Step 1: Find commits that deleted key files
git log --all --diff-filter=D -- '*.private' '*.key' 'Kexample.*.+*.+*.key'
# Step 2: Recover the deleted key files from the commit before deletion
git show <commit_hash>^:<path/to/Kzone.+005+12345.private> > recovered.private
git show <commit_hash>^:<path/to/Kzone.+005+12345.key> > recovered.key
# Alternative: search all commits for key material
git log --all -p -- '*.private' | grep -A 20 'Private-key-format'
# Step 3: Verify key contents
cat recovered.private
# Private-key-format: v1.3
# Algorithm: 5 (RSASHA1)
# ...
# Step 4: Use recovered keys to forge DNSSEC-signed responses
# Configure BIND with the recovered signing keys and sign the zone
dnssec-signzone -K /path/to/keys -o example.com zone.dbKey insight: Sensitive cryptographic key material in git history is permanently recoverable — git log --diff-filter=D finds all commits that deleted files, and git show <commit>^:<path> retrieves the file's state just before deletion. DNSSEC private keys enable forging any DNS record for the zone, allowing DNS cache poisoning or redirecting traffic to attacker-controlled servers.
---
XZ Stream Header Repair via CRC32 Reconstruction (Hackover 2018)
Pattern: The file has a valid XZ stream footer but the stream header has been overwritten (commonly with PK\x03\x04 to make it look like a ZIP). Rebuild the 12-byte XZ header from the format spec: magic FD 37 7A 58 5A 00, two bytes of stream flags, and a 4-byte little-endian CRC32 of those flags. Prepend the reconstructed header to the rest of the file and xz -d decompresses cleanly.
# 1. Confirm the footer — XZ stream footer magic is "YZ" at the end.
xxd broken.xz | tail -1
# 00002ff0: 00 00 01 59 5A ...YZ
# 2. Read stream_flags from the footer (byte at offset -6 from EOF)
STREAM_FLAGS=$(xxd -p -s -6 -l 2 broken.xz)
# e.g. 00 04 → CHECK_CRC64
# 3. Compute CRC32 of the 2 flag bytes (little-endian output)
CRC=$(python3 -c "import binascii; print(binascii.crc32(bytes.fromhex('$STREAM_FLAGS')).to_bytes(4,'little').hex())")
# 4. Rebuild the header and replace the first 12 bytes
printf '\xFD7zXZ\x00' > newhdr.bin
printf '%s' "$STREAM_FLAGS" | xxd -r -p >> newhdr.bin
printf '%s' "$CRC" | xxd -r -p >> newhdr.bin
dd if=newhdr.bin of=broken.xz bs=1 count=12 conv=notrunc
# 5. Decompress
xz -d broken.xzKey insight: XZ streams are defined by a fixed 12-byte header and a 12-byte footer that both include the same stream_flags byte — when the header is damaged you can copy the flags out of the still-intact footer and recompute the header CRC32 locally. The same header-reconstruction trick works for any format where the checksum input is small enough to brute-force or derive from the footer: GZIP (trailing isize/crc32), ZIP (central directory before the local file header), and zstd (frame header with skip-frames). When the challenge hands you a blob whose magic bytes belong to the wrong format, check the last few bytes for the real footer signature before trying to salvage the header.
References: Hackover CTF 2018 — UnbreakMyStart, writeup 11508
---
ZipCrypto Known-Plaintext Cracking via bkcrack (Codegate 2019)
Pattern: ZipCrypto (the legacy PKZIP stream cipher, not AES-256) falls to known-plaintext attacks when you have at least 12 bytes of known plaintext for an encrypted file. pkcrack is the classic tool but often fails on modern archives; bkcrack (https://github.com/kimci86/bkcrack) handles edge cases with partial headers.
# Extract any unencrypted neighbour and its encrypted version
unzip secret.zip unencrypted_known.txt
bkcrack -C secret.zip -c target.txt -p unencrypted_known.txt -P known.zip
# Decrypt the whole archive with the recovered internal state
bkcrack -C secret.zip -k <k0> <k1> <k2> -d target_decrypted.binKey insight: ZIP headers often include well-known constants (PNG/JPEG magic, empty README.txt, .gitignore). Any encrypted ZIP that also ships an unencrypted reference file — or where you can guess 12+ bytes of header — falls immediately to bkcrack. Swap to it when pkcrack throws.
References: Codegate CTF 2019 — Rich Project, writeup 12907
---
SQLite Serial-Type Byte Forensics (RITSEC 2018)
Pattern: Two near-identical SQLite files differ only in selected bytes. SQLite records encode each column with a "serial type" varint that both describes the type and carries the length (types ≥13 mean strings, length (type - 13) / 2). Walk the records, locate the changed serial-type bytes between versions, and read the adjacent text payload to recover hidden characters.
def extract_hidden(path):
with open(path, 'rb') as f: db = f.read()
offsets = [0x892, 0xBA5, 0xE13] # diff the two files first
return bytes(db[off] for off in offsets)Key insight: SQLite's varint serial-type scheme stores metadata inline with the payload, so an attacker who can flip one varint changes the interpretation of the next N bytes. Diff two versions byte-by-byte, cluster the diffs by record, and decode each varint to locate hidden text fields.
References: RITSEC CTF 2018 — Lite Forensics, writeup 12223
---
Recursive Binwalk Chain PNG->PDF->DOCX->PNG->Base64 (TAMUctf 2019)
Pattern: One carrier file hides a chain of embedded documents — PNG with a PDF appended, the PDF embeds a DOCX (which is a ZIP), the DOCX embeds another PNG, and that PNG has Base64 appended after the IEND/EOF. Each layer changes container format to evade naive string searches.
# Layer 1-2: carve everything out of the outer PNG (pulls PDF, ZIP streams, etc.)
binwalk --dd=".*" art.png
cd _art.png.extracted
file * # identify the Microsoft Word 2007+ blob
# Layer 3: DOCX is a ZIP archive
unzip 34591D -d docx/ # hex offset from binwalk becomes the filename
ls docx/word/media/ # image1.png is the next-layer carrier
# Layer 4: recurse binwalk into the inner PNG to pull an embedded PDF
binwalk --dd=".*" docx/word/media/image1.png
# Layer 5: check for data appended after %%EOF of the inner PDF
strings _image1.png.extracted/*.pdf | tail -n 10
# -> ZmxhZ3tQMGxZdEByX0QwX3kwdV9HM3RfSXRfTjB3P30K
echo 'ZmxhZ3tQMGxZdEByX0QwX3kwdV9HM3RfSXRfTjB3P30K' | base64 -dKey insight: When grep flag on the outermost file fails, assume each extracted file is itself a carrier. DOCX/XLSX/PPTX/APK/JAR are all ZIPs, so unzip works directly. PDFs commonly carry data after the final %%EOF, so always strings | tail or seek past the trailer. binwalk --dd=".*" writes every signature hit to disk so you can recurse with minimal typing.
References: TAMUctf 2019 — I Heard You Like Files, writeups 13412 and 13587
---
Regex-Password Nested Zip Chain with exrex (UTCTF 2019)
Pattern: Outer zip contains a hint.txt (regex) and archive.zip; the regex enumerates the password set for the inner zip. Each extracted zip produces the next regex hint. Chain is deep (1000+ layers) so it must be scripted. exrex.generate(regex) materialises every string matching a regex, which is perfect for constrained password spaces.
import exrex, zipfile, os
hint = r'^ 7 y RU[A-Z]KKx2 R4\d[a-z]B N$'
archive = 'RegularZips.zip'
for i in range(10000):
candidates = list(exrex.generate(hint))
out_dir = f'layer{i}'
os.makedirs(out_dir, exist_ok=True)
with zipfile.ZipFile(archive) as zf:
for pw in candidates:
try:
zf.extractall(out_dir, pwd=pw.encode())
print(f'[{i}] pw={pw}')
break
except Exception:
continue
else:
raise RuntimeError(f'no password matched regex at layer {i}')
with open(os.path.join(out_dir, 'hint.txt')) as f:
hint = f.read().strip()
archive = os.path.join(out_dir, 'archive.zip')
if not os.path.exists(archive):
print('FLAG IN', out_dir)
breakKey insight: When a zip's password is described by a regex, don't brute ASCII — use exrex to enumerate only matching strings (often just a handful of candidates per layer). Automate the extract-read-hint-repeat cycle; 1000 layers finish in seconds because the search space per layer is tiny.
References: UTCTF 2019 — Regular Zips, writeups 13951 and 13861
---
See Also
- disk-and-memory.md - Core disk/memory forensics (Volatility, disk image analysis, VM/OVA/VMDK, VMware snapshots, coredumps, KAPE triage, PowerShell ransomware, Android/Docker/cloud forensics, BSON reconstruction, TrueCrypt/VeraCrypt mounting)
- disk-advanced.md - Advanced disk and memory techniques (deleted partitions, ZFS forensics, GPT GUID encoding, VMDK sparse parsing, memory dump string carving, ransomware key recovery, WordPerfect macro XOR, minidump ISO 9660 recovery, APFS snapshots, RAID 5 XOR recovery)
CTF Forensics - Peripheral Capture Analysis
USB, HID, and Bluetooth peripheral traffic reconstruction from packet captures. For general network PCAP forensics (DNS/TCP/ICMP/SMB/RADIUS/RC4), see network-advanced.md. For basic network forensics, see network.md.
Table of Contents
- USB HID Mouse/Pen Drawing Recovery (EHAX 2026)
- USB HID Keyboard Capture Decoding (EKOPARTY CTF 2016)
- USB Keyboard LED Morse Code Exfiltration (BITSCTF 2017)
- USB HID Keyboard Arrow Key Navigation Tracking (HackIT 2017)
- Bluetooth RFCOMM Packet Reassembly (HITCON 2018)
- GBA USB URB_INTERRUPT Framebuffer Extraction (hxp 2018)
---
USB HID Mouse/Pen Drawing Recovery (EHAX 2026)
Pattern (Painter): PCAP contains USB HID interrupt transfers from a mouse/pen device. Drawing data encoded as relative movements with multiple draw modes.
Packet format (7-byte HID reports):
| Byte | Field | Notes |
|---|---|---|
| 0 | Button state | 0x01 = pressed (may be constant) |
| 1 | Mode/pad | 0=hover, 1=draw mode 1, 2=draw mode 2 |
| 2-3 | dx (int16 LE) | Relative X movement |
| 4-5 | dy (int16 LE) | Relative Y movement |
| 6 | Wheel | Usually 0 |
Extraction and rendering:
import struct
from PIL import Image, ImageDraw
# Extract HID data
# tshark -r capture.pcap -Y "usb.transfer_type==1" -T fields -e usb.capdata
packets = []
with open('hid_data.txt') as f:
for line in f:
raw = bytes.fromhex(line.strip().replace(':', ''))
if len(raw) >= 7:
btn = raw[0]
mode = raw[1]
dx = struct.unpack('<h', raw[2:4])[0]
dy = struct.unpack('<h', raw[4:6])[0]
packets.append((btn, mode, dx, dy))
# Accumulate positions per mode
SCALE = 5
positions = {0: [], 1: [], 2: []}
x, y = 0, 0
for btn, mode, dx, dy in packets:
x += dx
y += dy
positions[mode].append((x, y))
# Render each mode separately (different colors = different text layers)
for mode in [1, 2]:
pts = positions[mode]
if not pts:
continue
min_x = min(p[0] for p in pts) - 100
min_y = min(p[1] for p in pts) - 100
max_x = max(p[0] for p in pts) + 100
max_y = max(p[1] for p in pts) + 100
w = (max_x - min_x) * SCALE
h = (max_y - min_y) * SCALE
img = Image.new('RGB', (w, h), 'white')
draw = ImageDraw.Draw(img)
for i in range(1, len(pts)):
x0 = (pts[i-1][0] - min_x) * SCALE
y0 = (pts[i-1][1] - min_y) * SCALE
x1 = (pts[i][0] - min_x) * SCALE
y1 = (pts[i][1] - min_y) * SCALE
# Skip long jumps (pen lifts)
if abs(pts[i][0]-pts[i-1][0]) < 50 and abs(pts[i][1]-pts[i-1][1]) < 50:
draw.line([(x0,y0),(x1,y1)], fill='black', width=3)
img.save(f'mode_{mode}.png')Key techniques:
- Separate modes: Different button/mode values draw different text layers — render each independently
- Skip pen lifts: Large dx/dy jumps indicate pen was lifted, not drawn — filter by distance threshold
- High resolution: Scale 5-8x with margins for readable handwriting
- Time gradient: Color points by temporal order (rainbow gradient) to trace stroke direction
- Character segmentation: Group consecutive same-mode points by large X gaps to isolate characters
Alternative: AWK extraction + SVG rendering (faster pipeline):
# Extract capdata and convert to signed deltas in one pass
tshark -r pref.pcap -Y "usb.transfer_type==0x01 && usb.endpoint_address==0x81 && usb.capdata" \
-T fields -e usb.capdata > capdata.txt
awk '
function hexval(c){ return index("0123456789abcdef",tolower(c))-1 }
function hex2dec(h, n,i){ n=0; for(i=1;i<=length(h);i++) n=n*16+hexval(substr(h,i,1)); return n }
function s16(u){ return (u>=32768)?u-65536:u }
{ d=$1; if(length(d)!=14) next
btn=hex2dec(substr(d,3,2))
x=s16(hex2dec(substr(d,7,2) substr(d,5,2)))
y=s16(hex2dec(substr(d,11,2) substr(d,9,2)))
print btn, x, y }' capdata.txt > deltas.txtThen render with SVG (Python) — filter on pen-down state (button=2), accumulate deltas, flip Y axis, draw strokes between consecutive pen-down points.
Difference from keyboard HID: Mouse HID uses relative movements (accumulated), keyboard uses keycodes (direct). Mouse drawing requires rendering; keyboard requires keymap lookup.
---
USB HID Keyboard Capture Decoding (EKOPARTY CTF 2016)
USB keyboard captures contain HID scan codes that map to keystrokes. Decode the capture to reconstruct typed text.
# USB HID keyboard report format:
# Byte 0: Modifier keys (Shift, Ctrl, Alt)
# Byte 1: Reserved (0x00)
# Bytes 2-7: Up to 6 simultaneous key codes
# HID scan code to character mapping (partial)
HID_MAP = {
0x04: 'a', 0x05: 'b', 0x06: 'c', 0x07: 'd', 0x08: 'e',
0x09: 'f', 0x0a: 'g', 0x0b: 'h', 0x0c: 'i', 0x0d: 'j',
0x0e: 'k', 0x0f: 'l', 0x10: 'm', 0x11: 'n', 0x12: 'o',
0x13: 'p', 0x14: 'q', 0x15: 'r', 0x16: 's', 0x17: 't',
0x18: 'u', 0x19: 'v', 0x1a: 'w', 0x1b: 'x', 0x1c: 'y',
0x1d: 'z', 0x1e: '1', 0x1f: '2', 0x20: '3', 0x21: '4',
0x22: '5', 0x23: '6', 0x24: '7', 0x25: '8', 0x26: '9',
0x27: '0', 0x28: '\n', 0x2c: ' ', 0x2d: '-', 0x2e: '=',
0x2f: '[', 0x30: ']', 0x33: ';', 0x34: "'", 0x36: ',',
0x37: '.', 0x38: '/',
}
SHIFT_MAP = {
'a': 'A', 'b': 'B', '1': '!', '2': '@', '3': '#', '4': '$',
'5': '%', '6': '^', '7': '&', '8': '*', '9': '(', '0': ')',
'-': '_', '=': '+', '[': '{', ']': '}', ';': ':', "'": '"',
',': '<', '.': '>', '/': '?',
}
def decode_hid_keyboard(capture_data):
"""Decode USB HID keyboard capture to text"""
text = ""
for report in capture_data:
modifier = report[0]
keycode = report[2] # first key in report
if keycode == 0:
continue
char = HID_MAP.get(keycode, '')
if modifier & 0x22: # Left or Right Shift
char = SHIFT_MAP.get(char, char.upper())
text += char
return text
# Extract from Wireshark: tshark -r capture.pcapng -T fields -e usb.capdata
# Or from text dump: parse +XX/-XX format (+ = keydown, - = keyup)Key insight: USB HID keyboards send 8-byte reports where byte 0 is modifiers (Shift/Ctrl/Alt) and bytes 2-7 are active key scan codes. In Wireshark, filter with usb.transfer_type == 1 and extract usb.capdata. Ignore reports where byte 2 is 0x00 (key release).
---
USB Keyboard LED Morse Code Exfiltration (BITSCTF 2017)
Pattern (Ghost in the Machine): A pcap of USB keyboard traffic contains host-to-device packets with alternating 0x01/0x03 values controlling the Caps Lock LED state. Timing differences between LED state changes encode Morse code: durations >300ms represent dashes, shorter durations represent dots. Decode the Morse sequence to recover the flag.
from scapy.all import rdpcap
import struct
packets = rdpcap('usb_capture.pcap')
signals = []
for p in packets:
raw = bytes(p)
# USB HID SET_REPORT to keyboard (host -> device)
if len(raw) >= 35 and raw[30] in (0x01, 0x03):
timestamp = p.time
led_state = raw[30] # 0x01 = LED off, 0x03 = LED on
signals.append((timestamp, led_state))
# Convert timing to Morse
morse = ''
for i in range(0, len(signals) - 1, 2):
duration = signals[i+1][0] - signals[i][0]
if duration > 0.3:
morse += '-'
else:
morse += '.'
# Gap between signals indicates letter/word boundaryKey insight: Data exfiltration via keyboard LED state changes captured in USB pcap. The LED control packets use HID SET_REPORT class requests. Timing analysis of on/off transitions reveals Morse code patterns. Tools: Wireshark USB dissector, filter on usb.transfer_type == 0x02 (interrupt) and direction host→device.
---
USB HID Keyboard Arrow Key Navigation Tracking (HackIT 2017)
USB HID keyboard traffic from an Apple Keyboard requires tracking arrow key navigation. Decode HID keycodes using the USB HID usage table. Modifier byte 0x02 = Shift (uppercase). Track cursor position via up/down arrow presses to determine which line contains the flag.
tshark -r capture.pcap -T fields -e usb.capdata | \
python3 decode_hid.py # Must track arrow keys for line positionArrow key HID codes to track:
0x4F= Right Arrow0x50= Left Arrow0x51= Down Arrow (next line)0x52= Up Arrow (previous line)
# Skeleton: track line position during HID decode
line = 0
lines = {0: ""}
for report in hid_reports:
modifier = report[0]
keycode = report[2]
if keycode == 0x51: # Down arrow
line += 1; lines.setdefault(line, "")
elif keycode == 0x52: # Up arrow
line -= 1; lines.setdefault(line, "")
elif keycode in HID_MAP:
char = HID_MAP[keycode]
if modifier & 0x22:
char = char.upper()
lines[line] += char
# Flag is on a specific line determined by arrow navigationKey insight: USB keyboard captures must account for cursor movement keys (arrows, backspace). Track cursor line position to reconstruct text typed on each line separately — the flag may be on a non-zero line that arrow keys navigated to.
---
Bluetooth RFCOMM Packet Reassembly (HITCON 2018)
Pattern: A Lego EV3-over-Bluetooth capture contains RFCOMM frames whose payloads are EV3 direct commands. Packets are 32–34 bytes long, have an 8-byte RFCOMM header, and carry an order byte plus a group_number byte that together reorder into a coherent binary. Reassemble by (1) filtering btrfcomm in Wireshark, (2) sorting packets first by group_number then by order, and (3) concatenating the data fields after the header.
# Python with pyshark
import pyshark
cap = pyshark.FileCapture("capture.pcap", display_filter="btrfcomm")
frames = []
for pkt in cap:
raw = bytes.fromhex(pkt.btrfcomm.payload.replace(":", ""))
# RFCOMM header size varies: 4 (UIH) or 5 (with length extension)
hdr_len = 4 if raw[2] & 0x01 == 0 else 5
body = raw[hdr_len:]
order, group = body[0], body[1]
frames.append((group, order, body[2:]))
frames.sort()
binary = b"".join(chunk for _, _, chunk in frames)
open("payload.bin", "wb").write(binary)Key insight: RFCOMM is a TCP-like serial port emulation layered on L2CAP; it fragments application payloads when they exceed the MTU. CTF challenges love to split flags across many frames because most pcap walkthroughs stop at TCP/UDP and skip the Bluetooth link layer. Use Wireshark filters btrfcomm.channel, btl2cap, or btsnoop_hci to isolate the relevant flows, then sort by any available order/group bytes before concatenating. Similar logic applies to USB bulk transfers (usb.transfer_type == 0x03) and MIDI-over-BLE traffic.
References: HITCON CTF 2018 — EV3 Basic, writeup 11902
---
GBA USB URB_INTERRUPT Framebuffer Extraction (hxp 2018)
Pattern: pcap contains USB URB_INTERRUPT packets from a Game Boy Advance debug adapter. The GBA framebuffer is 240 × 160 at RGB565 (2 bytes/pixel = 76 800 bytes). Block type 6 carries memory dumps; split each packet's payload into the framebuffer grid and convert RGB565 to 8-bit RGB tuples.
from PIL import Image
from scapy.all import rdpcap
pkts = [p for p in rdpcap('cap.pcap') if p.haslayer('Raw')]
img = Image.new('RGB', (240, 160))
for p in pkts:
if p.Raw.load[3] == 0x06: # type 6 = memory dump
data = p.Raw.load[4:]
for i in range(76800 // 2):
rgb = int.from_bytes(data[2*i:2*i+2], 'little')
r = (rgb & 0xF800) >> 8
g = (rgb & 0x07E0) >> 3
b = (rgb & 0x001F) << 3
img.putpixel((i % 240, i // 240), (r, g, b))
img.save('screen.png')Key insight: Handheld console debug protocols usually wrap memory dumps in typed blocks. When you see GBA/NDS/PSP USB traffic, grep for block type 6 (framebuffer) or type 7 (audio) before parsing the rest.
References: hxp CTF 2018 — cheatquest of hxpschr 2, writeup 12591
Related skills
How it compares
Pick ctf-forensics over general security review skills when the task is artifact extraction from disk, memory, or network captures in a CTF context.
FAQ
What does ctf-forensics do?
Provides digital forensics and signal analysis techniques for CTF challenges. Use when analyzing disk images, memory dumps, event logs, network captures, cryptocurrency transactions, steganography, PDF analysis, Windows
When should I invoke ctf-forensics?
Provides digital forensics and signal analysis techniques for CTF challenges. Use when analyzing disk images, memory dumps, event logs, network captures, cryptocurrency transactions, steganography, PDF analysis, Windows
Where is the source documentation?
Ground claims in SKILL.md excerpts and linked reference files from the cached docs.
Is Ctf Forensics safe to install?
skills.sh reports 1 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.