ctf-agent 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/AGENTS.md +131 -0
  2. package/Dockerfile +192 -0
  3. package/README.md +522 -0
  4. package/agents/ctf-analyzer.md +58 -0
  5. package/agents/ctf-controller.md +151 -0
  6. package/agents/ctf-speedrun.md +74 -0
  7. package/bin/ctf-agent.js +119 -0
  8. package/docker-compose.yml +13 -0
  9. package/mcp_config.json +19 -0
  10. package/package.json +44 -0
  11. package/references/ctf-triage-ladder.md +68 -0
  12. package/references/exploit-databases.md +575 -0
  13. package/references/llm-safety-and-policy-compliance.md +173 -0
  14. package/references/multi-agent-orchestration-and-policy-routing.md +278 -0
  15. package/references/security-events-and-intelligence.md +566 -0
  16. package/references/version-matrix.md +59 -0
  17. package/rules/ctf-execution-rules.md +44 -0
  18. package/rules/ctf-safety-framing-rules.md +140 -0
  19. package/rules/flag-validation-rules.md +25 -0
  20. package/scripts/__init__.py +1 -0
  21. package/scripts/ctf-tools.lock +205 -0
  22. package/scripts/ctf_init.py +1026 -0
  23. package/scripts/ctf_update.py +583 -0
  24. package/scripts/ctfd_client.py +81 -0
  25. package/scripts/cve_lookup.py +453 -0
  26. package/scripts/extract_flags.py +155 -0
  27. package/scripts/install_as_agent.py +278 -0
  28. package/scripts/install_ctf_tools.sh +603 -0
  29. package/scripts/parallel_triage.py +395 -0
  30. package/scripts/prompt_policy_sanitizer.py +283 -0
  31. package/scripts/scope_guard.py +444 -0
  32. package/scripts/skill_validator.py +504 -0
  33. package/scripts/workspace_cleaner.py +141 -0
  34. package/skills/ctf-ai-ml/SKILL.md +120 -0
  35. package/skills/ctf-ai-ml/adversarial-ml.md +623 -0
  36. package/skills/ctf-ai-ml/llm-attacks.md +487 -0
  37. package/skills/ctf-ai-ml/model-attacks.md +422 -0
  38. package/skills/ctf-crypto/SKILL.md +320 -0
  39. package/skills/ctf-crypto/advanced-math.md +798 -0
  40. package/skills/ctf-crypto/classic-ciphers.md +651 -0
  41. package/skills/ctf-crypto/ecc-attacks.md +347 -0
  42. package/skills/ctf-crypto/exotic-crypto-2.md +380 -0
  43. package/skills/ctf-crypto/exotic-crypto.md +528 -0
  44. package/skills/ctf-crypto/historical.md +113 -0
  45. package/skills/ctf-crypto/lattice-and-lwe.md +524 -0
  46. package/skills/ctf-crypto/modern-ciphers-2.md +563 -0
  47. package/skills/ctf-crypto/modern-ciphers-3.md +453 -0
  48. package/skills/ctf-crypto/modern-ciphers.md +649 -0
  49. package/skills/ctf-crypto/prng-attacks.md +257 -0
  50. package/skills/ctf-crypto/prng.md +664 -0
  51. package/skills/ctf-crypto/rsa-attacks-2.md +792 -0
  52. package/skills/ctf-crypto/rsa-attacks.md +487 -0
  53. package/skills/ctf-crypto/stream-ciphers.md +390 -0
  54. package/skills/ctf-crypto/zkp-and-advanced.md +456 -0
  55. package/skills/ctf-forensics/3d-printing.md +121 -0
  56. package/skills/ctf-forensics/SKILL.md +379 -0
  57. package/skills/ctf-forensics/disk-advanced.md +497 -0
  58. package/skills/ctf-forensics/disk-and-memory.md +491 -0
  59. package/skills/ctf-forensics/disk-recovery.md +699 -0
  60. package/skills/ctf-forensics/linux-forensics.md +511 -0
  61. package/skills/ctf-forensics/network-advanced.md +583 -0
  62. package/skills/ctf-forensics/network.md +645 -0
  63. package/skills/ctf-forensics/peripheral-capture.md +287 -0
  64. package/skills/ctf-forensics/signals-and-hardware.md +713 -0
  65. package/skills/ctf-forensics/steganography.md +694 -0
  66. package/skills/ctf-forensics/stego-advanced-2.md +475 -0
  67. package/skills/ctf-forensics/stego-advanced.md +481 -0
  68. package/skills/ctf-forensics/stego-image.md +691 -0
  69. package/skills/ctf-forensics/windows.md +625 -0
  70. package/skills/ctf-malware/SKILL.md +181 -0
  71. package/skills/ctf-malware/c2-and-protocols.md +274 -0
  72. package/skills/ctf-malware/pe-and-dotnet.md +108 -0
  73. package/skills/ctf-malware/scripts-and-obfuscation.md +449 -0
  74. package/skills/ctf-misc/SKILL.md +498 -0
  75. package/skills/ctf-misc/bashjails.md +323 -0
  76. package/skills/ctf-misc/ctfd-navigation.md +465 -0
  77. package/skills/ctf-misc/dns.md +255 -0
  78. package/skills/ctf-misc/encodings-advanced.md +504 -0
  79. package/skills/ctf-misc/encodings.md +431 -0
  80. package/skills/ctf-misc/games-and-vms-2.md +254 -0
  81. package/skills/ctf-misc/games-and-vms-3.md +690 -0
  82. package/skills/ctf-misc/games-and-vms-4.md +229 -0
  83. package/skills/ctf-misc/games-and-vms.md +529 -0
  84. package/skills/ctf-misc/linux-privesc.md +333 -0
  85. package/skills/ctf-misc/pyjails.md +671 -0
  86. package/skills/ctf-misc/rf-sdr.md +91 -0
  87. package/skills/ctf-osint/SKILL.md +198 -0
  88. package/skills/ctf-osint/geolocation-and-media.md +464 -0
  89. package/skills/ctf-osint/social-media.md +312 -0
  90. package/skills/ctf-osint/web-and-dns.md +341 -0
  91. package/skills/ctf-pwn/SKILL.md +214 -0
  92. package/skills/ctf-pwn/advanced-exploits-2.md +579 -0
  93. package/skills/ctf-pwn/advanced-exploits-3.md +598 -0
  94. package/skills/ctf-pwn/advanced-exploits-4.md +590 -0
  95. package/skills/ctf-pwn/advanced-exploits-5.md +119 -0
  96. package/skills/ctf-pwn/advanced-exploits.md +773 -0
  97. package/skills/ctf-pwn/advanced.md +326 -0
  98. package/skills/ctf-pwn/field-notes.md +245 -0
  99. package/skills/ctf-pwn/format-string.md +694 -0
  100. package/skills/ctf-pwn/heap-fsop.md +285 -0
  101. package/skills/ctf-pwn/heap-techniques-2.md +333 -0
  102. package/skills/ctf-pwn/heap-techniques.md +513 -0
  103. package/skills/ctf-pwn/kernel-bypass.md +421 -0
  104. package/skills/ctf-pwn/kernel-techniques.md +366 -0
  105. package/skills/ctf-pwn/kernel.md +636 -0
  106. package/skills/ctf-pwn/overflow-basics.md +611 -0
  107. package/skills/ctf-pwn/rop-advanced.md +725 -0
  108. package/skills/ctf-pwn/rop-and-shellcode.md +659 -0
  109. package/skills/ctf-pwn/sandbox-escape.md +313 -0
  110. package/skills/ctf-reverse/SKILL.md +163 -0
  111. package/skills/ctf-reverse/anti-analysis-ctf.md +204 -0
  112. package/skills/ctf-reverse/anti-analysis.md +693 -0
  113. package/skills/ctf-reverse/field-notes.md +376 -0
  114. package/skills/ctf-reverse/languages-compiled.md +666 -0
  115. package/skills/ctf-reverse/languages-platforms.md +592 -0
  116. package/skills/ctf-reverse/languages.md +553 -0
  117. package/skills/ctf-reverse/patterns-ctf-2.md +397 -0
  118. package/skills/ctf-reverse/patterns-ctf-3.md +797 -0
  119. package/skills/ctf-reverse/patterns-ctf.md +670 -0
  120. package/skills/ctf-reverse/patterns-runtime.md +274 -0
  121. package/skills/ctf-reverse/patterns.md +572 -0
  122. package/skills/ctf-reverse/platforms-hardware.md +387 -0
  123. package/skills/ctf-reverse/platforms.md +664 -0
  124. package/skills/ctf-reverse/tools-advanced-2.md +421 -0
  125. package/skills/ctf-reverse/tools-advanced.md +407 -0
  126. package/skills/ctf-reverse/tools-dynamic.md +679 -0
  127. package/skills/ctf-reverse/tools-emulation.md +319 -0
  128. package/skills/ctf-reverse/tools.md +573 -0
  129. package/skills/ctf-web/SKILL.md +153 -0
  130. package/skills/ctf-web/auth-and-access-2.md +82 -0
  131. package/skills/ctf-web/auth-and-access.md +783 -0
  132. package/skills/ctf-web/auth-infra.md +321 -0
  133. package/skills/ctf-web/auth-jwt.md +186 -0
  134. package/skills/ctf-web/client-side-advanced.md +739 -0
  135. package/skills/ctf-web/client-side.md +529 -0
  136. package/skills/ctf-web/cves.md +373 -0
  137. package/skills/ctf-web/field-notes.md +482 -0
  138. package/skills/ctf-web/node-and-prototype.md +200 -0
  139. package/skills/ctf-web/server-side-2.md +337 -0
  140. package/skills/ctf-web/server-side-advanced-2.md +559 -0
  141. package/skills/ctf-web/server-side-advanced-3.md +125 -0
  142. package/skills/ctf-web/server-side-advanced-4.md +480 -0
  143. package/skills/ctf-web/server-side-advanced.md +378 -0
  144. package/skills/ctf-web/server-side-deser.md +443 -0
  145. package/skills/ctf-web/server-side-exec-2.md +799 -0
  146. package/skills/ctf-web/server-side-exec.md +457 -0
  147. package/skills/ctf-web/server-side.md +629 -0
  148. package/skills/ctf-web/sql-injection.md +790 -0
  149. package/skills/ctf-web/web3.md +374 -0
  150. package/skills/ctf-writeup/SKILL.md +90 -0
  151. package/skills/solve-challenge/SKILL.md +269 -0
  152. package/skills.json +16 -0
@@ -0,0 +1,475 @@
1
+ # CTF Forensics - Advanced Steganography (Part 2)
2
+
3
+ See also: [stego-advanced.md](stego-advanced.md) for audio steganography (FFT frequency domain, DTMF, SSTV, LSB audio, musical notes, metadata encoding, waveform binary, spectrogram QR) and whitespace/archive encoding.
4
+
5
+ ## Table of Contents
6
+ - [Video Frame Accumulation for Hidden Image (ASIS CTF Finals 2013)](#video-frame-accumulation-for-hidden-image-asis-ctf-finals-2013)
7
+ - [Reversed Audio Hidden Message (ASIS CTF Finals 2013)](#reversed-audio-hidden-message-asis-ctf-finals-2013)
8
+ - [Video Frame Averaging for Hidden Content (SECCON 2015)](#video-frame-averaging-for-hidden-content-seccon-2015)
9
+ - [JPEG XL TOC Permutation Steganography (BSidesSF 2026)](#jpeg-xl-toc-permutation-steganography-bsidessf-2026)
10
+ - [Arnold's Cat Map Image Descrambling (Nuit du Hack 2017)](#arnolds-cat-map-image-descrambling-nuit-du-hack-2017)
11
+ - [High-Resolution SSTV Custom FM Demodulation (PlaidCTF 2017)](#high-resolution-sstv-custom-fm-demodulation-plaidctf-2017)
12
+ - [MJPEG Extra Bytes After FFD9 Steganography (PoliCTF 2017)](#mjpeg-extra-bytes-after-ffd9-steganography-polictf-2017)
13
+ - [EXIF Zlib Data with Non-Default LSB Pixel Pattern (ASIS CTF Finals 2017)](#exif-zlib-data-with-non-default-lsb-pixel-pattern-asis-ctf-finals-2017)
14
+ - [PDF Cross-Reference Table Covert Channel (SEC-T CTF 2017)](#pdf-cross-reference-table-covert-channel-sec-t-ctf-2017)
15
+ - [ANSI Escape Code Steganography in Network Capture (Square CTF 2017)](#ansi-escape-code-steganography-in-network-capture-square-ctf-2017)
16
+ - [Pixel-Wise ECB Deduplication for Image Recovery (BackdoorCTF 2017)](#pixel-wise-ecb-deduplication-for-image-recovery-backdoorctf-2017)
17
+ - [Multi-Color QR Code Binary Mapping Brute Force (STEM CTF 2019)](#multi-color-qr-code-binary-mapping-brute-force-stem-ctf-2019)
18
+
19
+ ---
20
+
21
+ ## Video Frame Accumulation for Hidden Image (ASIS CTF Finals 2013)
22
+
23
+ **Pattern:** Video shows small images (icons, shapes) flashing briefly at different screen positions. Individual frames appear random, but the positions trace out a hidden pattern (QR code, text, image) when all frames are composited together.
24
+
25
+ **Extraction workflow:**
26
+
27
+ 1. Extract individual frames from the video:
28
+ ```bash
29
+ ffmpeg -i challenge.mp4 -vsync 0 frames/frame_%04d.png
30
+ ```
31
+
32
+ 2. Composite all frames by taking the maximum (or union) of all pixel values:
33
+ ```python
34
+ from PIL import Image
35
+ import os
36
+
37
+ frames_dir = 'frames'
38
+ frame_files = sorted(os.listdir(frames_dir))
39
+
40
+ # Load first frame as base
41
+ base = Image.open(os.path.join(frames_dir, frame_files[0])).convert('L')
42
+
43
+ # Accumulate: take maximum pixel value across all frames
44
+ import numpy as np
45
+ accumulated = np.array(base, dtype=np.float64)
46
+ for f in frame_files[1:]:
47
+ frame = np.array(Image.open(os.path.join(frames_dir, f)).convert('L'), dtype=np.float64)
48
+ accumulated = np.maximum(accumulated, frame)
49
+
50
+ result = Image.fromarray(accumulated.astype(np.uint8))
51
+ result.save('accumulated.png')
52
+ ```
53
+
54
+ 3. Alternative: convert to GIF and delete the black background frame in GIMP to see all positions overlaid.
55
+
56
+ 4. Clean up the revealed pattern (e.g., QR code) — select foreground, grow/shrink selection, flood fill, scale to expected dimensions (e.g., 21x21 for Version 1 QR):
57
+ ```bash
58
+ # Scan for QR code
59
+ zbarimg accumulated.png
60
+ ```
61
+
62
+ **Key insight:** When a video shows objects flashing at seemingly random positions, composite all frames together. The positions themselves encode the hidden data — each frame contributes one pixel/cell to a larger image. Convert to GIF for frame-by-frame inspection in GIMP, or use PIL/NumPy to take per-pixel maximum across all frames.
63
+
64
+ ---
65
+
66
+ ## Reversed Audio Hidden Message (ASIS CTF Finals 2013)
67
+
68
+ **Pattern:** Audio track (standalone or extracted from video) sounds garbled or unintelligible. Playing it in reverse reveals speech, numbers, or other meaningful content.
69
+
70
+ **Extraction and reversal:**
71
+ ```bash
72
+ # Extract audio from video
73
+ ffmpeg -i challenge.mp4 -vn -acodec pcm_s16le audio.wav
74
+
75
+ # Reverse audio
76
+ sox audio.wav reversed.wav reverse
77
+ # Or: ffmpeg -i audio.wav -af areverse reversed.wav
78
+
79
+ # Play to hear hidden message
80
+ play reversed.wav
81
+ ```
82
+
83
+ **Alternative:** Open in Audacity -> Effect -> Reverse. Listen for speech, numbers, or encoded data.
84
+
85
+ **Key insight:** Reversed audio is one of the simplest audio steganography techniques. If audio sounds like garbled speech with recognizable cadence, try reversing it first. The hidden content is often a numeric string (e.g., an MD5 hash) or instructions for the next step of the challenge. Check both the audio and video tracks of multimedia files independently.
86
+
87
+ ---
88
+
89
+ ## Video Frame Averaging for Hidden Content (SECCON 2015)
90
+
91
+ Extract content hidden across multiple video frames by temporal averaging:
92
+
93
+ ```python
94
+ import numpy as np
95
+ from PIL import Image
96
+ import glob
97
+
98
+ frames = sorted(glob.glob('frames/*.png'))
99
+ N = len(frames)
100
+
101
+ # Accumulate frames as floating-point to preserve precision
102
+ acc = np.zeros(np.array(Image.open(frames[0])).shape, dtype=np.float64)
103
+ for f in frames:
104
+ acc += np.array(Image.open(f), dtype=np.float64) / N
105
+
106
+ # Convert back to uint8
107
+ result = Image.fromarray(np.round(acc).astype(np.uint8))
108
+ result.save('averaged.png')
109
+ ```
110
+
111
+ Use histogram equalization to enhance contrast if the averaged image is faint:
112
+
113
+ ```python
114
+ from PIL import ImageOps
115
+ enhanced = ImageOps.equalize(result.convert('L'))
116
+ enhanced.save('enhanced.png')
117
+ ```
118
+
119
+ **Key insight:** Content obscured by motion, noise, or rapid changes across frames becomes visible when averaged. Extract frames with `ffmpeg -i video.mp4 frames/%04d.png` first. Works for hidden QR codes, text, and watermarks.
120
+
121
+ ---
122
+
123
+ ## JPEG XL TOC Permutation Steganography (BSidesSF 2026)
124
+
125
+ **Pattern (image-progress):** JPEG XL's Table of Contents (TOC) supports a permutation field that reorders how AC groups (progressive scan tiles) are stored in the file. The convergence order during progressive decoding — which 256x256 tiles appear first as you truncate the file at increasing offsets — encodes the flag.
126
+
127
+ **Decoding approach:**
128
+ 1. **Progressive truncation:** Truncate the JXL file at increasing byte offsets (e.g., every 1KB)
129
+ 2. **Decode each truncation:** Use `djxl` to decode each truncated file
130
+ 3. **Measure tile convergence:** Compare each decoded truncation against the full decode to determine which 256x256 tiles have converged (match the final image)
131
+ 4. **Read convergence order:** The order in which tiles reach their final state spells the flag
132
+
133
+ ```python
134
+ import subprocess
135
+ import numpy as np
136
+ from PIL import Image
137
+
138
+ # Full decode as reference
139
+ subprocess.run(['djxl', 'flag.jxl', 'full.png'])
140
+ full = np.array(Image.open('full.png'))
141
+ h, w = full.shape[:2]
142
+ tile_size = 256
143
+ tiles_x = (w + tile_size - 1) // tile_size
144
+ tiles_y = (h + tile_size - 1) // tile_size
145
+
146
+ # Track when each tile converges
147
+ converged = {}
148
+ jxl_data = open('flag.jxl', 'rb').read()
149
+
150
+ for offset in range(1000, len(jxl_data), 1000):
151
+ # Write truncated file
152
+ with open('/tmp/trunc.jxl', 'wb') as f:
153
+ f.write(jxl_data[:offset])
154
+
155
+ # Try to decode (may fail for very short truncations)
156
+ result = subprocess.run(['djxl', '/tmp/trunc.jxl', '/tmp/trunc.png'],
157
+ capture_output=True)
158
+ if result.returncode != 0:
159
+ continue
160
+
161
+ partial = np.array(Image.open('/tmp/trunc.png'))
162
+
163
+ # Check which tiles match the full decode
164
+ for ty in range(tiles_y):
165
+ for tx in range(tiles_x):
166
+ tile_id = ty * tiles_x + tx
167
+ if tile_id in converged:
168
+ continue
169
+ y0, y1 = ty * tile_size, min((ty+1) * tile_size, h)
170
+ x0, x1 = tx * tile_size, min((tx+1) * tile_size, w)
171
+ if np.array_equal(partial[y0:y1, x0:x1], full[y0:y1, x0:x1]):
172
+ converged[tile_id] = offset
173
+
174
+ # Sort tiles by convergence order
175
+ order = sorted(converged.items(), key=lambda x: x[1])
176
+ flag_chars = [chr(tile_id) for tile_id, _ in order]
177
+ print('Flag:', ''.join(flag_chars))
178
+ ```
179
+
180
+ **Alternative — direct TOC extraction:**
181
+ ```bash
182
+ # Modified djxl with debug prints can extract TOC permutation directly
183
+ # Look for the permutation array in the JXL frame header
184
+ # The TOC permutation maps: stored_order[i] -> logical_group[i]
185
+ # Inverse gives: logical_group -> stored_order (convergence priority)
186
+ ```
187
+
188
+ **JPEG XL progressive structure:**
189
+ - **DC groups:** Low-frequency data (converges first, gives blurry preview)
190
+ - **AC groups:** High-frequency detail, stored per 256x256 tile
191
+ - **TOC permutation:** Reorders the storage of AC groups — controls which tiles get detail first during progressive loading
192
+ - **Lehmer code:** JXL encodes the permutation as a Lehmer code sequence in the TOC header
193
+
194
+ **Key insight:** JPEG XL's TOC permutation is a legitimate feature for progressive rendering optimization (prioritize important image regions). As a steganographic channel, it's invisible — the fully decoded image looks identical regardless of permutation. The hidden data is only revealed by observing the progressive convergence order, which requires truncating the file at multiple points.
195
+
196
+ **Detection:** JXL file where progressive rendering shows tiles appearing in an unusual order (e.g., spelling text). Challenge mentions "progressive", "convergence", or "order matters".
197
+
198
+ **References:** BSidesSF 2026 "image-progress"
199
+
200
+ ---
201
+
202
+ ## Arnold's Cat Map Image Descrambling (Nuit du Hack 2017)
203
+
204
+ Arnold's Cat Map is a chaotic area-preserving transformation that is periodic — iterating it enough times restores the original image. When an image appears scrambled with a noise-like pattern but retains the correct dimensions and color histogram, suspect a Cat Map scramble.
205
+
206
+ ```python
207
+ from PIL import Image
208
+ import numpy as np
209
+
210
+ img = np.array(Image.open('scrambled.png'))
211
+ N = img.shape[0] # Must be square
212
+
213
+ def arnold_cat_map(image, n):
214
+ """Apply Arnold's Cat Map transformation"""
215
+ result = np.zeros_like(image)
216
+ for x in range(n):
217
+ for y in range(n):
218
+ nx = (2*x + y) % n
219
+ ny = (x + y) % n
220
+ result[nx, ny] = image[x, y]
221
+ return result
222
+
223
+ # Iterate until original image reappears (period depends on N)
224
+ current = img.copy()
225
+ for i in range(1, N * N):
226
+ current = arnold_cat_map(current, N)
227
+ Image.fromarray(current).save(f'frame_{i:04d}.png')
228
+ # Check if we've returned to original (or visually inspect)
229
+ ```
230
+
231
+ **Key insight:** Arnold's Cat Map is periodic with period dividing `3*N` for most image sizes. Iterating the forward transform eventually restores the original. For large images, compute the period analytically via `lcm` of matrix eigenvalue orders in `Z/NZ` rather than brute-forcing all iterations.
232
+
233
+ **Detection:** Square image that looks like uniformly scrambled noise but has a plausible color distribution. Challenge mentions "cat", "Arnold", "chaotic", or "permutation".
234
+
235
+ ---
236
+
237
+ ## High-Resolution SSTV Custom FM Demodulation (PlaidCTF 2017)
238
+
239
+ When a WAV file contains an SSTV signal at higher-than-standard sample rate (e.g., 96kHz vs standard 2.3kHz bandwidth), standard SSTV decoders fail on the high-frequency content. Use custom FM demodulation.
240
+
241
+ ```python
242
+ # Method 1: GNU Radio
243
+ # Hilbert Transform -> Quadrature Demod -> low-pass filter
244
+
245
+ # Method 2: Manual arccos + derivative (handles clipping)
246
+ import numpy as np
247
+ from scipy.io import wavfile
248
+
249
+ rate, data = wavfile.read('signal.wav')
250
+ # Normalize to [-1, 1]
251
+ data = data / np.max(np.abs(data))
252
+ # Clamp to valid arccos range
253
+ data = np.clip(data, -0.999, 0.999)
254
+ # Instantaneous frequency via arccos derivative
255
+ phase = np.arccos(data)
256
+ freq = np.diff(phase) * rate / (2 * np.pi)
257
+ # Map frequency to pixel intensity (1500-2300Hz typical SSTV range)
258
+ pixels = np.clip((freq - 1500) / 800 * 255, 0, 255).astype(np.uint8)
259
+ ```
260
+
261
+ **Key insight:** Standard SSTV decoders (QSSTV, MMSSTV) assume standard bandwidth (~2.3kHz). High-sample-rate recordings may contain wider-bandwidth signals that these decoders truncate. Manual FM demodulation via `arccos` + differentiation (avoiding Hilbert transform artifacts on clipped signals) recovers the full frequency range.
262
+
263
+ **Detection:** WAV file at unusually high sample rate (48kHz, 96kHz) where standard SSTV decoders produce garbled or partial output. Spectrogram shows frequency-modulated signal structure.
264
+
265
+ ---
266
+
267
+ ## MJPEG Extra Bytes After FFD9 Steganography (PoliCTF 2017)
268
+
269
+ MJPEG video frames that contain extra bytes after the JPEG end-of-image marker (FFD9) hide data in the padding.
270
+
271
+ ```python
272
+ # Split MJPEG into individual frames
273
+ frames = open('video.mjpeg', 'rb').read().split(b'\xff\xd8')
274
+
275
+ hidden = b""
276
+ for frame in frames:
277
+ if not frame: continue
278
+ frame = b'\xff\xd8' + frame
279
+ # Find JPEG EOI marker
280
+ eoi = frame.find(b'\xff\xd9')
281
+ if eoi != -1:
282
+ extra = frame[eoi + 2:] # bytes after FFD9
283
+ if extra:
284
+ hidden += extra
285
+
286
+ print(hidden.decode(errors='ignore'))
287
+ ```
288
+
289
+ **Key insight:** JPEG decoders stop at the FFD9 (End of Image) marker and ignore trailing bytes. In MJPEG streams, each frame is a complete JPEG — appending 1+ extra bytes after each frame's FFD9 creates a covert channel invisible to video players.
290
+
291
+ **Detection:** MJPEG file where individual frames are slightly larger than expected. `binwalk` on raw MJPEG may show repeated JPEG headers. Hex dump shows non-zero data between FFD9 and the next FFD8.
292
+
293
+ ---
294
+
295
+ ## EXIF Zlib Data with Non-Default LSB Pixel Pattern (ASIS CTF Finals 2017)
296
+
297
+ A JPG's EXIF `ImageDescription` field contains zlib-compressed then base64-encoded data. Detect via the `\x78\x9C` zlib magic bytes after base64 decoding. After decompression, the hint references the Stegano Python library with a `triangular_numbers` generator for non-sequential pixel selection (positions 1, 3, 6, 10, ...).
298
+
299
+ ```bash
300
+ # Step 1: Extract EXIF ImageDescription
301
+ exiftool -ImageDescription image.jpg
302
+ # Or:
303
+ python3 -c "
304
+ from PIL import Image
305
+ img = Image.open('image.jpg')
306
+ desc = img._getexif()[270] # Tag 270 = ImageDescription
307
+ print(repr(desc))
308
+ "
309
+
310
+ # Step 2: Base64-decode, then zlib-decompress
311
+ python3 -c "
312
+ import base64, zlib
313
+ desc = '<exif_description_value>'
314
+ decoded = base64.b64decode(desc)
315
+ print(zlib.decompress(decoded).decode())
316
+ "
317
+
318
+ # Step 3: Extract hidden data using Stegano with triangular_numbers generator
319
+ python3 -c "
320
+ from stegano import lsb
321
+ from stegano.lsb import generators
322
+ print(lsb.reveal('image.png', generators.triangular_numbers()))
323
+ "
324
+ ```
325
+
326
+ **Key insight:** Standard LSB tools (zsteg, stegsolve) fail with non-sequential pixel patterns. The Stegano library supports custom generators; always check EXIF metadata for hints about which generator to use. The `\x78\x9C` bytes are the deflate magic — a reliable indicator of zlib-compressed content.
327
+
328
+ ---
329
+
330
+ ## PDF Cross-Reference Table Covert Channel (SEC-T CTF 2017)
331
+
332
+ PDF xref table entries normally use generation number 0 (live objects) or 65535 (free/deleted). Non-standard generation numbers encode data: read each non-zero, non-65535 generation number in order, interpret as hex -> ASCII characters (may need to reverse the string).
333
+
334
+ ```bash
335
+ # Inspect raw xref entries with pdf-parser.py
336
+ python pdf-parser.py --stats suspicious.pdf
337
+ python pdf-parser.py --type /XRef suspicious.pdf
338
+
339
+ # Or read the raw xref table directly
340
+ python3 -c "
341
+ with open('suspicious.pdf', 'rb') as f:
342
+ data = f.read().decode('latin-1')
343
+
344
+ # Find xref section
345
+ xref_idx = data.rfind('xref')
346
+ xref_section = data[xref_idx:xref_idx+2000]
347
+ gen_numbers = []
348
+ for line in xref_section.splitlines():
349
+ parts = line.split()
350
+ if len(parts) == 3 and parts[2] in ('n', 'f'):
351
+ gen = int(parts[1])
352
+ if gen not in (0, 65535):
353
+ gen_numbers.append(gen)
354
+
355
+ # Convert hex values to ASCII
356
+ flag = bytes.fromhex(''.join(f'{g:02x}' for g in gen_numbers)).decode()
357
+ print(flag)
358
+ # Also try reversed: print(flag[::-1])
359
+ "
360
+ ```
361
+
362
+ **Key insight:** PDF xref generation numbers are rarely validated by viewers, making them a low-noise steganographic channel. Any value other than 0 (live) or 65535 (deleted) is suspicious. Use `pdf-parser.py --raw` to inspect raw xref entries without parser normalization.
363
+
364
+ ---
365
+
366
+ ## ANSI Escape Code Steganography in Network Capture (Square CTF 2017)
367
+
368
+ Network packet data contains ANSI escape sequences (color codes, cursor movement). Raw hex and strings tools show garbled output. Pipe raw bytes through a terminal pager (`more`, `less -r`) to render the escape codes — the flag becomes visible as colored or positioned text.
369
+
370
+ ```bash
371
+ # Extract raw TCP stream payload
372
+ tshark -r capture.pcap -q -z "follow,tcp,raw,0" | \
373
+ tail -n +7 | tr -d '\n' | xxd -r -p > stream.bin
374
+
375
+ # Render ANSI escape codes (simplest approach)
376
+ more stream.bin
377
+ # or
378
+ cat stream.bin | less -r
379
+
380
+ # Alternative: extract data field directly
381
+ tshark -r capture.pcap -T fields -e data | xxd -r -p | more
382
+ ```
383
+
384
+ ANSI escape patterns to recognize:
385
+ - `\x1b[<n>m` — color/attribute codes
386
+ - `\x1b[<row>;<col>H` — cursor position
387
+ - `\x1b[<n>A/B/C/D` — cursor movement (up/down/right/left)
388
+
389
+ **Key insight:** ANSI escape sequences encode visual information only revealed by terminal rendering. Always try `more` or `less -r` if content looks like terminal output. Cursor-positioning sequences can spell out text that only appears correct on a terminal.
390
+
391
+ ---
392
+
393
+ ## Pixel-Wise ECB Deduplication for Image Recovery (BackdoorCTF 2017)
394
+
395
+ An image is encrypted by replacing each pixel's value with a hash (ECB-mode pixel encryption). Since the pixel value space is small (256 for grayscale, or limited palette), precompute a hash-to-pixel lookup table and remap each hash value back to the original pixel.
396
+
397
+ ```python
398
+ from PIL import Image
399
+ import hashlib
400
+
401
+ img = Image.open('encrypted.png').convert('L') # Grayscale
402
+ pixels = list(img.getdata())
403
+
404
+ # Build lookup table: hash(pixel) -> pixel value
405
+ # The encryption maps each unique pixel value to a unique hash
406
+ # Since the space is small (256 values), enumerate all possible originals
407
+ lookup = {}
408
+ for original_val in range(256):
409
+ # Determine which hash function was used (MD5, SHA1, etc.)
410
+ h = hashlib.md5(bytes([original_val])).hexdigest()
411
+ lookup[h] = original_val
412
+
413
+ # Reconstruct: each "pixel" in encrypted image is actually a hash index
414
+ # For palette-based images, map color index -> original pixel
415
+ unique_colors = list(set(pixels))
416
+ color_map = {}
417
+ for i, color in enumerate(unique_colors):
418
+ # ECB: identical pixels -> identical cipher values
419
+ # Count unique values to confirm small space
420
+ pass
421
+
422
+ # Simpler: if encrypted values are small integers (0-255 remapped)
423
+ # The structure is preserved — just find the right permutation
424
+ reconstructed = Image.new('L', img.size)
425
+ # Map each encrypted value back using the lookup
426
+ ```
427
+
428
+ **Key insight:** ECB-mode pixel encryption leaks structure via identical ciphertexts for identical plaintext pixels. With only 256 possible grayscale values, the full lookup table is trivial to precompute. The encrypted image will show the same shapes/edges as the original — recognizable structure confirms ECB mode.
429
+
430
+ ---
431
+
432
+ ## Multi-Color QR Code Binary Mapping Brute Force (STEM CTF 2019)
433
+
434
+ **Pattern:** A QR-like image uses N colors instead of black/white. A valid QR code requires only two states (black=1, white=0), so each color must map to one of those. With N non-trivial colors, iterate all 2^N binary partitions and try to decode each candidate. Typical N=6 produces 64 candidates; 3 of the 64 often decode (redundancy baked into QR error correction).
435
+
436
+ ```python
437
+ from PIL import Image
438
+ from itertools import product
439
+ import subprocess, os
440
+
441
+ img = Image.open('QvR.png').convert('RGB')
442
+ px = img.load()
443
+ w, h = img.size
444
+
445
+ # Collect distinct non-pure colors (ignore black/white which are unambiguous)
446
+ palette = set()
447
+ for y in range(h):
448
+ for x in range(w):
449
+ c = px[x, y]
450
+ if c not in ((0, 0, 0), (255, 255, 255)):
451
+ palette.add(c)
452
+ palette = sorted(palette) # deterministic order
453
+ print(f'{len(palette)} variable colors -> {2**len(palette)} attempts')
454
+
455
+ for bits in product([0, 1], repeat=len(palette)):
456
+ mapping = dict(zip(palette, bits))
457
+ out = Image.new('1', (w, h), 1)
458
+ op = out.load()
459
+ for y in range(h):
460
+ for x in range(w):
461
+ c = px[x, y]
462
+ if c == (0, 0, 0): v = 0
463
+ elif c == (255, 255, 255): v = 1
464
+ else: v = mapping[c]
465
+ op[x, y] = v
466
+ fn = f'try_{"".join(map(str, bits))}.png'
467
+ out.save(fn)
468
+ r = subprocess.run(['zbarimg', '-q', fn], capture_output=True, text=True)
469
+ if r.stdout.strip():
470
+ print(fn, '->', r.stdout.strip())
471
+ ```
472
+
473
+ **Key insight:** QR codes are strictly binary — any multi-color image that "looks like" a QR is hiding a 2^N coloring. Because QR has heavy Reed-Solomon error correction, multiple partitions can decode (each carries a different message in the same physical grid). Always try all 2^N mappings; with N<=8 the brute force is negligible and `zbarimg` filters the valid ones automatically.
474
+
475
+ **References:** STEM CTF: Cyber Challenge 2019 — QvR Code, writeup 13375