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,691 @@
1
+ # CTF Forensics - Image Steganography
2
+
3
+ Techniques specific to hiding data in image formats (JPEG, PNG, BMP, GIF). For non-image steganography (PDF, audio, terminal, text), see [steganography.md](steganography.md). For advanced techniques (FFT, SSTV, audio, video, JPEG XL), see [stego-advanced.md](stego-advanced.md) and [stego-advanced-2.md](stego-advanced-2.md).
4
+
5
+ ## Table of Contents
6
+ - [JPEG Unused Quantization Table LSB Steganography (EHAX 2026)](#jpeg-unused-quantization-table-lsb-steganography-ehax-2026)
7
+ - [BMP Bitplane QR Code Extraction + Steghide (BYPASS CTF 2025)](#bmp-bitplane-qr-code-extraction--steghide-bypass-ctf-2025)
8
+ - [Image Jigsaw Puzzle Reassembly via Edge Matching (BYPASS CTF 2025)](#image-jigsaw-puzzle-reassembly-via-edge-matching-bypass-ctf-2025)
9
+ - [F5 JPEG DCT Coefficient Ratio Detection (ApoorvCTF 2026)](#f5-jpeg-dct-coefficient-ratio-detection-apoorvctf-2026)
10
+ - [PNG Unused Palette Entry Steganography (ApoorvCTF 2026)](#png-unused-palette-entry-steganography-apoorvctf-2026)
11
+ - [QR Code Tile Reconstruction (UTCTF 2026)](#qr-code-tile-reconstruction-utctf-2026)
12
+ - [Seed-Based Pixel Permutation + Multi-Bitplane QR (L3m0nCTF 2025)](#seed-based-pixel-permutation--multi-bitplane-qr-l3m0nctf-2025)
13
+ - [JPEG Thumbnail Pixel-to-Text Mapping (RuCTF 2013)](#jpeg-thumbnail-pixel-to-text-mapping-ructf-2013)
14
+ - [Conditional LSB Extraction — Near-Black Pixel Filter (BaltCTF 2013)](#conditional-lsb-extraction--near-black-pixel-filter-baltctf-2013)
15
+ - [JPEG Slack Space Steganography (BSidesSF 2025)](#jpeg-slack-space-steganography-bsidessf-2025)
16
+ - [Nearest-Neighbor Interpolation Steganography (BSidesSF 2025)](#nearest-neighbor-interpolation-steganography-bsidessf-2025)
17
+ - [RGB Parity Steganography (Break In 2016)](#rgb-parity-steganography-break-in-2016)
18
+ - [Pixel Coordinate Chain Steganography (H4ckIT CTF 2016)](#pixel-coordinate-chain-steganography-h4ckit-ctf-2016)
19
+ - [AVI Frame Differential Pixel Steganography (H4ckIT CTF 2016)](#avi-frame-differential-pixel-steganography-h4ckit-ctf-2016)
20
+ - [JPEG Single-Bit-Flip Brute Force with OCR (SECCON 2017)](#jpeg-single-bit-flip-brute-force-with-ocr-seccon-2017)
21
+ - [GIF Frame PLTE Chunk Concatenation to ELF (IceCTF 2018)](#gif-frame-plte-chunk-concatenation-to-elf-icectf-2018)
22
+ - [Nested-Resize QR Overlay at Survivor Pixels (SECCON 2018)](#nested-resize-qr-overlay-at-survivor-pixels-seccon-2018)
23
+ - [ImageMagick +append Puzzle Stitching + gaps Solver (X-MAS CTF 2018)](#imagemagick-append-puzzle-stitching--gaps-solver-x-mas-ctf-2018)
24
+ - [Steghide Passphrase in JPEG Header Metadata (Saudi/Oman CTF 2019)](#steghide-passphrase-in-jpeg-header-metadata-saudioman-ctf-2019)
25
+ - [Corrupted PNG Magic and Lowercase Chunk Repair (Pragyan CTF 2019)](#corrupted-png-magic-and-lowercase-chunk-repair-pragyan-ctf-2019)
26
+
27
+ ---
28
+
29
+ ## JPEG Unused Quantization Table LSB Steganography (EHAX 2026)
30
+
31
+ **Pattern (Jpeg Soul):** "Insignificant" hint points to least significant bits in JPEG quantization tables (DQT). JPEG can embed DQT tables (ID 2, 3) that are never referenced by frame markers — invisible to renderers but carry hidden data.
32
+
33
+ **Detection:** JPEG has more DQT tables than components reference. Standard JPEG uses 2 tables (luminance + chrominance); extra tables with IDs 2, 3 are suspicious.
34
+
35
+ ```python
36
+ from PIL import Image
37
+
38
+ img = Image.open('challenge.jpg')
39
+
40
+ # Access quantization tables (PIL exposes them as dict)
41
+ # Standard: tables 0 (luminance) and 1 (chrominance)
42
+ # Hidden: tables 2, 3 (unreferenced by SOF marker)
43
+ qtables = img.quantization
44
+
45
+ bits = []
46
+ for table_id in sorted(qtables.keys()):
47
+ if table_id >= 2: # Unused tables
48
+ table = qtables[table_id]
49
+ for i in range(64): # 8x8 = 64 values per DQT
50
+ bits.append(table[i] & 1) # Extract LSB
51
+
52
+ # Convert bits to ASCII
53
+ flag = ''
54
+ for i in range(0, len(bits) - 7, 8):
55
+ byte = int(''.join(str(b) for b in bits[i:i+8]), 2)
56
+ if 32 <= byte <= 126:
57
+ flag += chr(byte)
58
+ print(flag)
59
+ ```
60
+
61
+ **Manual DQT extraction (when PIL doesn't expose all tables):**
62
+ ```python
63
+ # Parse JPEG manually to find all DQT markers (0xFFDB)
64
+ data = open('challenge.jpg', 'rb').read()
65
+ pos = 0
66
+ while pos < len(data) - 1:
67
+ if data[pos] == 0xFF and data[pos+1] == 0xDB:
68
+ length = int.from_bytes(data[pos+2:pos+4], 'big')
69
+ dqt_data = data[pos+4:pos+2+length]
70
+ table_id = dqt_data[0] & 0x0F
71
+ precision = (dqt_data[0] >> 4) & 0x0F # 0=8-bit, 1=16-bit
72
+ values = list(dqt_data[1:65]) if precision == 0 else []
73
+ print(f"DQT table {table_id}: {values[:8]}...")
74
+ pos += 2 + length
75
+ else:
76
+ pos += 1
77
+ ```
78
+
79
+ **Key insight:** JPEG quantization tables are metadata — they survive recompression and most image processing. Unused table IDs (2-15) can carry arbitrary data without affecting the image.
80
+
81
+ ---
82
+
83
+ ## BMP Bitplane QR Code Extraction + Steghide (BYPASS CTF 2025)
84
+
85
+ **Pattern (Gold Challenge):** BMP image with QR code hidden in a specific bitplane. Extract the QR code to obtain a steghide password.
86
+
87
+ **Technique:** Extract individual bitplanes (bits 0-2) for each RGB channel, render as images, scan for QR codes.
88
+
89
+ ```python
90
+ from PIL import Image
91
+ import numpy as np
92
+
93
+ img = Image.open('challenge.bmp')
94
+ pixels = np.array(img)
95
+
96
+ # Extract individual bitplanes
97
+ for ch_idx, ch_name in enumerate(['R', 'G', 'B']):
98
+ for bit in range(3): # Check bits 0, 1, 2
99
+ channel = pixels[:, :, ch_idx]
100
+ bit_plane = ((channel >> bit) & 1) * 255
101
+ Image.fromarray(bit_plane.astype(np.uint8)).save(f'bit_{ch_name}_{bit}.png')
102
+
103
+ # Combined LSB across all channels
104
+ lsb_img = np.zeros_like(pixels)
105
+ for ch in range(3):
106
+ lsb_img[:, :, ch] = (pixels[:, :, ch] & 1) * 255
107
+ Image.fromarray(lsb_img).save('lsb_all.png')
108
+ ```
109
+
110
+ **Full attack chain:**
111
+ 1. Extract bitplanes → find QR code in specific bitplane (often bit 1, not bit 0)
112
+ 2. Scan QR with `zbarimg bit_G_1.png` → get steghide password
113
+ 3. `steghide extract -sf challenge.bmp -p <password>` → extract hidden file
114
+
115
+ **Key insight:** Standard LSB (least significant bit) tools check bit 0 only. Hidden QR codes may be in bit 1 or bit 2 — always check multiple bitplanes systematically. BMP format preserves exact pixel values (no compression artifacts).
116
+
117
+ ---
118
+
119
+ ## Image Jigsaw Puzzle Reassembly via Edge Matching (BYPASS CTF 2025)
120
+
121
+ **Pattern (Jigsaw Puzzle):** Archive containing multiple puzzle piece images that must be reassembled into the original image. Reassembled image contains the flag (possibly ROT13 encoded).
122
+
123
+ **Technique:** Compute pixel intensity differences at shared edges between all piece pairs, then greedily place pieces to minimize total edge difference.
124
+
125
+ ```python
126
+ from PIL import Image
127
+ import numpy as np
128
+ import os
129
+
130
+ # Load all pieces
131
+ pieces = {}
132
+ for f in sorted(os.listdir('pieces/')):
133
+ pieces[f] = np.array(Image.open(f'pieces/{f}'))
134
+
135
+ piece_list = list(pieces.keys())
136
+ n = len(piece_list)
137
+ grid_size = int(n ** 0.5) # e.g., 25 pieces → 5x5
138
+
139
+ # Calculate edge compatibility
140
+ def edge_diff(img1, img2, direction):
141
+ if direction == 'right':
142
+ return np.sum(np.abs(img1[:, -1].astype(int) - img2[:, 0].astype(int)))
143
+ elif direction == 'bottom':
144
+ return np.sum(np.abs(img1[-1, :].astype(int) - img2[0, :].astype(int)))
145
+
146
+ # Build compatibility matrices
147
+ right_compat = np.full((n, n), float('inf'))
148
+ bottom_compat = np.full((n, n), float('inf'))
149
+ for i in range(n):
150
+ for j in range(n):
151
+ if i != j:
152
+ right_compat[i, j] = edge_diff(pieces[piece_list[i]], pieces[piece_list[j]], 'right')
153
+ bottom_compat[i, j] = edge_diff(pieces[piece_list[i]], pieces[piece_list[j]], 'bottom')
154
+
155
+ # Greedy placement
156
+ grid = [[None] * grid_size for _ in range(grid_size)]
157
+ used = set()
158
+ for row in range(grid_size):
159
+ for col in range(grid_size):
160
+ best_piece, best_diff = None, float('inf')
161
+ for idx in range(n):
162
+ if idx in used:
163
+ continue
164
+ diff = 0
165
+ if col > 0:
166
+ diff += right_compat[grid[row][col-1], idx]
167
+ if row > 0:
168
+ diff += bottom_compat[grid[row-1][col], idx]
169
+ if diff < best_diff:
170
+ best_diff, best_piece = diff, idx
171
+ grid[row][col] = best_piece
172
+ used.add(best_piece)
173
+
174
+ # Reassemble
175
+ piece_h, piece_w = pieces[piece_list[0]].shape[:2]
176
+ final = Image.new('RGB', (grid_size * piece_w, grid_size * piece_h))
177
+ for row in range(grid_size):
178
+ for col in range(grid_size):
179
+ final.paste(Image.open(f'pieces/{piece_list[grid[row][col]]}'),
180
+ (col * piece_w, row * piece_h))
181
+ final.save('reassembled.png')
182
+ ```
183
+
184
+ **Post-processing:** Check if reassembled image text is ROT13 encoded. Decode with `tr 'A-Za-z' 'N-ZA-Mn-za-m'`.
185
+
186
+ **Key insight:** Edge-matching works by minimizing pixel differences at shared borders. The greedy approach (place piece with smallest total edge difference to already-placed neighbors) works well for most CTF puzzles. For harder puzzles, add backtracking.
187
+
188
+ ---
189
+
190
+ ## F5 JPEG DCT Coefficient Ratio Detection (ApoorvCTF 2026)
191
+
192
+ **Pattern (Engraver's Fault):** Detect F5 steganography in JPEG images by analyzing DCT coefficient distributions. F5 decrements ±1 AC coefficients toward 0, creating a measurable ratio shift.
193
+
194
+ **Detection metric — ±1/±2 AC coefficient ratio:**
195
+ ```python
196
+ import numpy as np
197
+ from PIL import Image
198
+ import jpegio # or use jpeg_toolbox
199
+
200
+ def f5_ratio(jpeg_path):
201
+ """Ratio below 0.15 indicates F5 modification; above 0.20 indicates clean."""
202
+ jpg = jpegio.read(jpeg_path)
203
+ coeffs = jpg.coef_arrays[0].flatten() # Luminance Y channel
204
+ coeffs = coeffs[coeffs != 0] # Remove DC/zeros
205
+ count_1 = np.sum(np.abs(coeffs) == 1)
206
+ count_2 = np.sum(np.abs(coeffs) == 2)
207
+ return count_1 / max(count_2, 1)
208
+ ```
209
+
210
+ **Sparse image edge case:** Images with >80% zero DCT coefficients give misleading ±1/±2 ratios. Use a secondary metric:
211
+ ```python
212
+ def f5_sparse_check(jpeg_path):
213
+ """For sparse images, ±2/±3 ratio below 2.5 indicates modification."""
214
+ jpg = jpegio.read(jpeg_path)
215
+ coeffs = jpg.coef_arrays[0].flatten()
216
+ count_2 = np.sum(np.abs(coeffs) == 2)
217
+ count_3 = np.sum(np.abs(coeffs) == 3)
218
+ return count_2 / max(count_3, 1)
219
+
220
+ # Combined classifier:
221
+ r12 = f5_ratio(path)
222
+ r23 = f5_sparse_check(path)
223
+ is_modified = r12 < 0.15 or (r12 < 0.25 and r23 < 2.5)
224
+ ```
225
+
226
+ **Key insight:** F5 steganography shifts ±1 coefficients toward 0, reducing the ±1/±2 ratio. Natural JPEGs have ratio 0.25-0.45; F5-modified drop below 0.10. Sparse images (mostly flat/white) need the secondary ±2/±3 metric because their ±1 counts are inherently low.
227
+
228
+ ---
229
+
230
+ ## PNG Unused Palette Entry Steganography (ApoorvCTF 2026)
231
+
232
+ **Pattern (The Gotham Files):** Paletted PNG (8-bit indexed color) hides data in palette entries that no pixel references. The image uses indices 0-199 but the PLTE chunk has 256 entries — indices 200-255 contain hidden ASCII in their red channel values.
233
+
234
+ ```python
235
+ from PIL import Image
236
+ import struct
237
+
238
+ def extract_unused_plte(png_path):
239
+ img = Image.open(png_path)
240
+ palette = img.getpalette() # Flat list: [R0,G0,B0, R1,G1,B1, ...]
241
+ pixels = list(img.getdata())
242
+ used_indices = set(pixels)
243
+
244
+ # Extract red channel from unused palette entries
245
+ flag = ''
246
+ for i in range(256):
247
+ if i not in used_indices:
248
+ r = palette[i * 3] # Red channel
249
+ if 32 <= r <= 126:
250
+ flag += chr(r)
251
+ return flag
252
+ ```
253
+
254
+ **Key insight:** PNG palette can have up to 256 entries but images typically use fewer. Unused entries are invisible to viewers but persist in the file. Metadata hints like "collector", "the entries that don't make it to the page", or "red light" point to this technique. Always check which palette indices are actually referenced vs. allocated.
255
+
256
+ ---
257
+
258
+ ## QR Code Tile Reconstruction (UTCTF 2026)
259
+
260
+ **Pattern (QRecreate):** QR code split into tiles/pieces that must be reassembled. Tiles may be scrambled, rotated, or have missing alignment patterns.
261
+
262
+ **Reconstruction workflow:**
263
+ ```python
264
+ from PIL import Image
265
+ import numpy as np
266
+
267
+ # Load scrambled tiles
268
+ tiles = []
269
+ for i in range(N_TILES):
270
+ tile = Image.open(f'tile_{i}.png')
271
+ tiles.append(np.array(tile))
272
+
273
+ # Strategy 1: Edge matching (like jigsaw puzzle)
274
+ # Each tile edge has a unique bit pattern — match adjacent edges
275
+ def edge_signature(tile, side):
276
+ if side == 'top': return tuple(tile[0, :].flatten())
277
+ if side == 'bottom': return tuple(tile[-1, :].flatten())
278
+ if side == 'left': return tuple(tile[:, 0].flatten())
279
+ if side == 'right': return tuple(tile[:, -1].flatten())
280
+
281
+ # Strategy 2: QR structure constraints
282
+ # - Finder patterns (large squares) MUST be at 3 corners
283
+ # - Timing patterns (alternating B/W) run between finders
284
+ # - Use these as anchors to orient remaining tiles
285
+
286
+ # Strategy 3: Brute force small grids
287
+ # For 3x3 or 4x4 grids, try all permutations and scan with zbarimg
288
+ from itertools import permutations
289
+ import subprocess
290
+
291
+ grid_size = 3
292
+ tile_size = tiles[0].shape[0]
293
+ for perm in permutations(range(len(tiles))):
294
+ img = Image.new('L', (grid_size * tile_size, grid_size * tile_size))
295
+ for idx, tile_idx in enumerate(perm):
296
+ row, col = divmod(idx, grid_size)
297
+ img.paste(Image.fromarray(tiles[tile_idx]),
298
+ (col * tile_size, row * tile_size))
299
+ img.save('/tmp/qr_attempt.png')
300
+ result = subprocess.run(['zbarimg', '/tmp/qr_attempt.png'],
301
+ capture_output=True, text=True)
302
+ if result.stdout.strip():
303
+ print(f"DECODED: {result.stdout}")
304
+ break
305
+ ```
306
+
307
+ **Key insight:** QR codes have structural constraints (finder patterns, timing patterns, format info) that drastically reduce the search space. Use QR structure as anchors before brute-forcing tile positions.
308
+
309
+ ---
310
+
311
+ ## Seed-Based Pixel Permutation + Multi-Bitplane QR (L3m0nCTF 2025)
312
+
313
+ **Pattern (Lost Signal):** Image with randomized pixel colors hides a QR code. Pixels are visited in a seed-determined permutation order, and data is interleaved across multiple bitplanes of the luminance (Y) channel.
314
+
315
+ **Extraction workflow:**
316
+ 1. Convert image to YCbCr and extract Y (luminance) channel
317
+ 2. Generate the pixel visit order using the known seed
318
+ 3. Extract LSB bits from multiple bitplanes in interleaved order
319
+ 4. Reconstruct as a binary image and scan as QR code
320
+
321
+ ```python
322
+ from PIL import Image
323
+ import numpy as np
324
+
325
+ SEED = 739391 # Given or brute-forced
326
+
327
+ # 1. Extract Y channel
328
+ img = Image.open("challenge.png").convert("YCbCr")
329
+ Y = np.array(img.split()[0], dtype=np.uint8)
330
+ h, w = Y.shape
331
+
332
+ # 2. Generate deterministic pixel permutation
333
+ rng = np.random.RandomState(SEED)
334
+ perm = np.arange(h * w)
335
+ rng.shuffle(perm)
336
+
337
+ # 3. Extract bits from multiple bitplanes (interleaved)
338
+ bitplanes = [0, 1] # LSB0 and LSB1
339
+ total_bits = h * w
340
+ bits = np.zeros(total_bits, dtype=np.uint8)
341
+
342
+ for i in range(total_bits):
343
+ pix_idx = perm[i // len(bitplanes)]
344
+ bp = bitplanes[i % len(bitplanes)]
345
+ y, x = divmod(pix_idx, w)
346
+ bits[i] = (Y[y, x] >> bp) & 1
347
+
348
+ # 4. Reconstruct QR code
349
+ qr = bits.reshape((h, w))
350
+ qr_img = Image.fromarray((255 * (1 - qr)).astype(np.uint8))
351
+ qr_img.save("recovered_qr.png")
352
+ # zbarimg recovered_qr.png
353
+ ```
354
+
355
+ **Key insight:** The seed defines a deterministic pixel visit order (Fisher-Yates shuffle via `RandomState`). Without the correct seed, output is random noise. Bits from different bitplanes are interleaved (bit 0 from pixel N, bit 1 from pixel N, bit 0 from pixel N+1, ...), doubling the data density. Try the Y (luminance) channel first — it has the highest contrast for hidden binary data.
356
+
357
+ **Seed recovery:** If the seed is unknown, look for it in: EXIF metadata, filename, image dimensions, challenge description numbers, or brute-force small ranges.
358
+
359
+ **Detection:** Image appears as random colored noise but has suspicious dimensions (perfect square, power of 2). Challenge mentions "seed", "random", or "signal".
360
+
361
+ ---
362
+
363
+ ## JPEG Thumbnail Pixel-to-Text Mapping (RuCTF 2013)
364
+
365
+ **Pattern:** JPEG contains an embedded thumbnail where dark pixels map 1:1 to character positions in visible text on the main image.
366
+
367
+ ```python
368
+ from PIL import Image
369
+ # Extract thumbnail: exiftool -b -ThumbnailImage secret.jpg > thumb.jpg
370
+ thumb = Image.open('thumb.jpg')
371
+ text_lines = ["line1 of visible text...", "line2..."] # OCR or type from photo
372
+ result = ''
373
+ for y in range(thumb.height):
374
+ for x in range(thumb.width):
375
+ r, g, b = thumb.getpixel((x, y))[:3]
376
+ if r < 100 and g < 100 and b < 100: # Dark pixel = selected char
377
+ result += text_lines[y][x]
378
+ ```
379
+
380
+ **Key insight:** Extract thumbnails with `exiftool -b -ThumbnailImage`. Dark pixels act as a selection mask over the photographed text. Use OCR (ABBYY FineReader, Tesseract) to get the text grid, then map dark thumbnail pixels to character positions.
381
+
382
+ ---
383
+
384
+ ## Conditional LSB Extraction — Near-Black Pixel Filter (BaltCTF 2013)
385
+
386
+ **Pattern:** Only pixels with R<=1 AND G<=1 AND B<=1 carry steganographic data. Standard LSB tools miss the data because they process all pixels.
387
+
388
+ ```python
389
+ from PIL import Image
390
+ img = Image.open('image.png')
391
+ bits = ''
392
+ for pixel in img.getdata():
393
+ r, g, b = pixel[0], pixel[1], pixel[2]
394
+ if not (r <= 1 and g <= 1 and b <= 1):
395
+ continue # Skip non-carrier pixels
396
+ bits += str(r & 1) + str(g & 1) + str(b & 1)
397
+ # Convert bits to bytes
398
+ flag = bytes(int(bits[i:i+8], 2) for i in range(0, len(bits)-7, 8))
399
+ ```
400
+
401
+ **Key insight:** When standard `zsteg`/`stegsolve` find nothing, try filtering pixels by value range before LSB extraction. The carrier pixels may be restricted to near-black, near-white, or specific color ranges.
402
+
403
+ ---
404
+
405
+ ## JPEG Slack Space Steganography (BSidesSF 2025)
406
+
407
+ JPEG compression pads images to 8x8 pixel block boundaries. Data hidden in the padding pixels beyond the visible image dimensions:
408
+
409
+ 1. **Identify padded dimensions:** JPEG rounds up to nearest multiple of 8. A 253x195 image pads to 256x200
410
+ 2. **Extract slack pixels:** Use tools to extend visible region to true block dimensions
411
+
412
+ ```bash
413
+ # Extend image to see slack pixels
414
+ python3 jpeg_uncrop.py input.jpg --width 256 --height 200
415
+ # Or use ImageMagick to force full decode
416
+ magick input.jpg -define jpeg:size=256x200 extended.png
417
+ ```
418
+
419
+ 3. **Decode binary from slack pixels:** Black=0, white=1 in the padding region. Common encoding:
420
+ - 2 bytes: magic number
421
+ - 1 byte: key length
422
+ - N bytes: encryption key
423
+ - 1 byte: message length
424
+ - N bytes: encrypted message
425
+
426
+ **Key insight:** Most image editors and viewers crop to the stated dimensions, hiding the padding. Use `jpegtran -crop` or raw DCT decoders to access full block data.
427
+
428
+ ---
429
+
430
+ ## Nearest-Neighbor Interpolation Steganography (BSidesSF 2025)
431
+
432
+ Hidden data encoded as a pixel grid at regular intervals within a high-resolution image. Downscaling with nearest-neighbor interpolation extracts only the hidden pixels:
433
+
434
+ ```bash
435
+ # Hidden pixels spaced 16 apart in a 4096x3072 image
436
+ # Downscale by 16x with nearest-neighbor to recover 256x192 hidden image
437
+ magick flag.webp -interpolate nearest-neighbor -interpolative-resize 256x192 flag_visible.png
438
+ ```
439
+
440
+ **Key insight:** Nearest-neighbor interpolation selects exact pixel values (no blending), preserving the hidden data. Bilinear or bicubic interpolation would average surrounding pixels, destroying the message. The challenge name or description often hints at the interpolation method.
441
+
442
+ **Detection:** Open in image viewer and zoom to see repeating pixel patterns at regular intervals. Calculate GCD of image dimensions and suspected grid spacing.
443
+
444
+ ---
445
+
446
+ ## RGB Parity Steganography (Break In 2016)
447
+
448
+ Hidden image encoded in the parity of pixel RGB sums. Sum R+G+B per pixel -- even sum = white, odd sum = black. Renders a binary bitmap containing the hidden message.
449
+
450
+ ```python
451
+ from PIL import Image
452
+ img = Image.open('image.png')
453
+ out = Image.new('1', img.size)
454
+ for x in range(img.width):
455
+ for y in range(img.height):
456
+ r, g, b = img.getpixel((x, y))[:3]
457
+ out.putpixel((x, y), (r + g + b) % 2)
458
+ out.save('hidden.png')
459
+ ```
460
+
461
+ **Key insight:** Unlike LSB (Least Significant Bit) stego (single channel, single bit), parity stego uses the combined sum of all channels. Look for challenge hints about "pairs", "couples", or "adding colors".
462
+
463
+ **Detection:** Image appears normal but pixel RGB sums show non-random parity distribution.
464
+
465
+ ---
466
+
467
+ ## Pixel Coordinate Chain Steganography (H4ckIT CTF 2016)
468
+
469
+ Each pixel encodes a data byte in the red channel and the coordinates of the next pixel to read in the green and blue channels, forming a linked-list traversal through the image.
470
+
471
+ ```python
472
+ from PIL import Image
473
+
474
+ def extract_coordinate_chain(image_path, start_x=0, start_y=0):
475
+ """Follow coordinate chain: R=data, G=next_x, B=next_y"""
476
+ img = Image.open(image_path)
477
+ flag = ""
478
+ x, y = start_x, start_y
479
+ visited = set()
480
+
481
+ while (x, y) not in visited:
482
+ visited.add((x, y))
483
+ r, g, b = img.getpixel((x, y))[:3]
484
+
485
+ if r == 0: # null terminator
486
+ break
487
+
488
+ flag += chr(r)
489
+ x, y = g, b # next pixel coordinates from green and blue channels
490
+
491
+ return flag
492
+
493
+ # Variants:
494
+ # - (R,G) = coordinates, B = data byte
495
+ # - Coordinates stored as (G*256+B) for images wider than 256px
496
+ # - Starting pixel indicated by metadata or known offset
497
+ ```
498
+
499
+ **Key insight:** Linked-list pixel traversal hides both the message and the reading order. Standard LSB analysis misses this because only specific pixels carry data. Look for images where green/blue channels have suspiciously structured values (small numbers that could be coordinates).
500
+
501
+ ---
502
+
503
+ ## AVI Frame Differential Pixel Steganography (H4ckIT CTF 2016)
504
+
505
+ Compare consecutive video frames pixel-by-pixel. Pixels that increment by exactly 1 encode a "1" bit; unchanged pixels encode "0". Collect bits to form a Brainfuck program or binary message.
506
+
507
+ ```python
508
+ from PIL import Image
509
+ import subprocess
510
+
511
+ def extract_frame_differential(frame_dir, num_frames):
512
+ """Compare consecutive frames: incremented pixel = 1, same = 0"""
513
+ bits = ""
514
+
515
+ for i in range(num_frames - 1):
516
+ img1 = Image.open(f"{frame_dir}/frame_{i:04d}.png")
517
+ img2 = Image.open(f"{frame_dir}/frame_{i+1:04d}.png")
518
+
519
+ pixels1 = list(img1.getdata())
520
+ pixels2 = list(img2.getdata())
521
+
522
+ for p1, p2 in zip(pixels1, pixels2):
523
+ if p1 != p2:
524
+ # Pixel changed (incremented by 1) = bit "1"
525
+ bits += "1"
526
+ else:
527
+ bits += "0"
528
+
529
+ # Convert bits to ASCII or interpret as Brainfuck
530
+ message = ""
531
+ for i in range(0, len(bits), 8):
532
+ byte = int(bits[i:i+8], 2)
533
+ if 32 <= byte < 127:
534
+ message += chr(byte)
535
+
536
+ return message
537
+
538
+ # Extract frames from AVI first:
539
+ # binwalk video.avi (extracts embedded PNG/BMP frames)
540
+ # or: ffmpeg -i video.avi frame_%04d.png
541
+ ```
542
+
543
+ **Key insight:** Frame differential steganography hides data in the temporal domain rather than spatial. Standard image stego tools analyze single frames and miss inter-frame changes. Extract all frames, then diff consecutive pairs looking for single-pixel-value increments.
544
+
545
+ ---
546
+
547
+ ### JPEG Single-Bit-Flip Brute Force with OCR (SECCON 2017)
548
+
549
+ Corrupted JPEG with a single bitflip. Generate all single-bit variants and scan with OCR:
550
+
551
+ ```python
552
+ data = open('corrupted.jpg', 'rb').read()
553
+ for byte_pos in range(len(data)):
554
+ for bit in range(8):
555
+ candidate = data[:byte_pos] + bytes([data[byte_pos] ^ (1 << bit)]) + data[byte_pos+1:]
556
+ with open(f'attempt_{byte_pos}_{bit}.jpg', 'wb') as f:
557
+ f.write(candidate)
558
+ ```
559
+
560
+ ```bash
561
+ # Automated OCR scan for flag
562
+ for f in attempt_*.jpg; do
563
+ result=$(tesseract "$f" stdout 2>/dev/null)
564
+ if echo "$result" | grep -qi "flag\|ctf\|SECCON"; then
565
+ echo "FOUND in $f: $result"
566
+ fi
567
+ done
568
+ ```
569
+
570
+ **Key insight:** For small files (< 10KB), the total search space for single-bit flips is `8 * file_size` — typically under 80,000 candidates, easily brute-forceable. Use thumbnail generation as a fast validity check (corrupt JPEGs fail to decode), then OCR on survivors. JPEG compressed data rule: `0xFF` is always followed by `0x00` (stuffed byte) or a marker — violations indicate the corruption location.
571
+
572
+ ---
573
+
574
+ ## GIF Frame PLTE Chunk Concatenation to ELF (IceCTF 2018)
575
+
576
+ **Pattern:** A GIF hides a Linux ELF binary by breaking it into indexed PNG frames. Each frame's `PLTE` (palette) chunk holds the next slice of the binary — the actual pixel data is irrelevant. Extract with Pillow: iterate frames, convert each to PNG, walk the PNG chunks, concatenate every `PLTE` body, and the result is a valid ELF file.
577
+
578
+ ```python
579
+ from PIL import Image, ImagePalette
580
+ import struct
581
+
582
+ def read_png_plte(png_bytes):
583
+ i = 8 # skip PNG magic
584
+ while i < len(png_bytes):
585
+ length = struct.unpack(">I", png_bytes[i:i+4])[0]
586
+ ctype = png_bytes[i+4:i+8]
587
+ body = png_bytes[i+8:i+8+length]
588
+ if ctype == b"PLTE":
589
+ return body
590
+ i += 12 + length
591
+ return b""
592
+
593
+ payload = bytearray()
594
+ with Image.open("carrier.gif") as gif:
595
+ for frame in range(gif.n_frames):
596
+ gif.seek(frame)
597
+ png_buf = io.BytesIO()
598
+ gif.save(png_buf, "PNG")
599
+ payload += read_png_plte(png_buf.getvalue())
600
+
601
+ open("recovered.elf", "wb").write(payload)
602
+ ```
603
+
604
+ **Key insight:** GIF frames are internally stored with their own palettes. When you re-encode each frame as a PNG, the palette survives as a `PLTE` chunk — an ignored but byte-accurate container. Any stego carrier that uses a multi-frame format with per-frame metadata (GIF palettes, APNG frame data, PDF page streams, MKV tracks) lets you embed data in the *metadata channel* instead of the pixel channel, bypassing most LSB-style detection. When a GIF looks like a harmless animation but contains extra frames or palette entries, dump chunk-by-chunk before touching the pixels.
605
+
606
+ **References:** IceCTF 2018 — ilovebees, writeup 11418
607
+
608
+ ---
609
+
610
+ ## Nested-Resize QR Overlay at Survivor Pixels (SECCON 2018)
611
+
612
+ **Pattern:** Challenge PNG decodes to two different QR codes depending on how many times it is scaled down with nearest-neighbor interpolation (500 → 250 → 100 → 50). Track which source pixels survive every reduction: for a 10× chain with `PIL.Image.resize(size, Image.NEAREST)`, survivors sit at indices `(10i+7, 10j+7)`. Overlay a second QR at exactly those positions so it only emerges after the chained resize.
613
+
614
+ ```python
615
+ from PIL import Image
616
+ big = Image.open('qr1.png') # 500x500 visible QR
617
+ small = Image.open('qr2.png') # 50x50 hidden QR
618
+ px = big.load()
619
+ sx = small.load()
620
+ for i in range(50):
621
+ for j in range(50):
622
+ px[10*i+7, 10*j+7] = sx[i, j]
623
+ big.save('trap.png')
624
+ ```
625
+
626
+ **Key insight:** Nearest-neighbor resize keeps exactly one pixel per source block; its offset depends on rounding (PIL picks `floor(original*scale)+0.5`). Compute the survivor index once per resize step, then compose the nested stego at those indices. Works for any number of cascaded resizes as long as the interpolation is nearest-neighbor.
627
+
628
+ **References:** SECCON 2018 — QRChecker, writeup 12014
629
+
630
+ ---
631
+
632
+ ## ImageMagick +append Puzzle Stitching + gaps Solver (X-MAS CTF 2018)
633
+
634
+ **Pattern:** Disk image contains N puzzle-piece PNGs carved out by `foremost` or `scalpel`. Stitch all pieces horizontally with ImageMagick `convert +append`, then feed the strip to the `gaps` jigsaw solver (https://github.com/nemanja-m/gaps) with the known piece size (often stored in EXIF) to auto-reassemble.
635
+
636
+ ```bash
637
+ foremost -t png -i disk.img -o pieces
638
+ convert +append pieces/*.png strip.png
639
+ gaps --image=strip.png --size=273
640
+ ```
641
+
642
+ **Key insight:** CTF jigsaw challenges rarely require manual work. Carve pieces, stitch, run `gaps` — it uses a genetic algorithm to reassemble in minutes. Read `exiftool` on each piece for the size hint.
643
+
644
+ **References:** X-MAS CTF 2018 — Message from Santa, writeup 12662
645
+
646
+ ---
647
+
648
+ ## Steghide Passphrase in JPEG Header Metadata (Saudi/Oman CTF 2019)
649
+
650
+ **Pattern:** JPEG file with `steghide`-embedded payload whose passphrase is hidden in plain ASCII inside the JPEG header/metadata region. Standard tools (`exiftool`, `strings`) may miss it if the byte range isn't flagged as a proper EXIF/comment tag, but `xxd` on the first few hundred bytes reveals the string.
651
+
652
+ ```bash
653
+ # Scan header for suspicious ASCII
654
+ xxd info.jpg | head -20
655
+ # 00000010: ffdb 0043 0008 6261 6469 7362 6164 0008 ...C..badisbad..
656
+ # ^^^^^^^^^^^^^^^^^ passphrase at offset 0x18
657
+
658
+ # Confirm steghide payload with the passphrase
659
+ steghide --info info.jpg # prompts for passphrase
660
+ steghide extract -sf info.jpg -p badisbad
661
+ ```
662
+
663
+ **Key insight:** Always scan the first ~256 bytes of a JPEG with `xxd`/`hexdump -C` for ASCII runs — authors sometimes stuff passphrases into reserved areas of JFIF/APPn segments where `exiftool` doesn't surface them, but they're trivially visible in a hex view. Pair the leak with `steghide`, `outguess`, or `stegseek` wordlist seeding.
664
+
665
+ **References:** Quals Saudi and Oman National Cyber Security CTF 2019 — Hack a nice day, writeup 13232
666
+
667
+ ---
668
+
669
+ ## Corrupted PNG Magic and Lowercase Chunk Repair (Pragyan CTF 2019)
670
+
671
+ **Pattern:** PNG is unreadable because the 8-byte magic is tampered (e.g. `89 50 4E 47 2E 0A 2E 0A` instead of the correct `89 50 4E 47 0D 0A 1A 0A`) and critical chunk names are lowercased (`idat` instead of `IDAT`). PNG decoders treat lowercase chunk names as "ancillary" and skip them, so the image looks empty until the case is fixed. Metadata (e.g. `exiftool` Artist field) then yields the next step.
672
+
673
+ ```bash
674
+ # Step 1: patch magic bytes
675
+ printf '\x89PNG\r\n\x1a\n' | dd of=broken.png conv=notrunc bs=1 count=8
676
+
677
+ # Step 2: re-capitalise critical chunk names (IHDR, IDAT, IEND, PLTE)
678
+ python3 -c "
679
+ d = open('broken.png','rb').read()
680
+ d = d.replace(b'idat', b'IDAT').replace(b'iend', b'IEND')
681
+ open('fixed.png','wb').write(d)
682
+ "
683
+
684
+ # Step 3: pull hidden metadata
685
+ exiftool fixed.png | grep -Ei 'artist|comment|desc'
686
+ # Artist : md5_MEf89jf4h9 -> use md5(...) as zip password
687
+ ```
688
+
689
+ **Key insight:** PNG has two orthogonal parseability gates: the 8-byte signature and the case of each chunk name (first letter uppercase = critical). Fix both before concluding the file is empty. `pngcheck -v` flags exactly which byte/chunk is wrong. Once readable, treat EXIF `Artist`, `Description`, and `tEXt`/`iTXt` chunks as prime hiding spots.
690
+
691
+ **References:** Pragyan CTF 2019 — Magic PNGs, writeup 13833