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,651 @@
1
+ # CTF Crypto - Classic Ciphers
2
+
3
+ ## Table of Contents
4
+ - [Vigenere Cipher](#vigenere-cipher)
5
+ - [Atbash Cipher](#atbash-cipher)
6
+ - [Polybius Square Cipher (Qiwi-Infosec 2016)](#polybius-square-cipher-qiwi-infosec-2016)
7
+ - [Substitution Cipher with Rotating Wheel](#substitution-cipher-with-rotating-wheel)
8
+ - [Kasiski Examination for Key Length](#kasiski-examination-for-key-length)
9
+ - [XOR Variants](#xor-variants)
10
+ - [Multi-Byte XOR Key Recovery via Frequency Analysis](#multi-byte-xor-key-recovery-via-frequency-analysis)
11
+ - [Cascade XOR (First-Byte Brute Force)](#cascade-xor-first-byte-brute-force)
12
+ - [XOR with Rotation: Power-of-2 Bit Isolation (Pragyan 2026)](#xor-with-rotation-power-of-2-bit-isolation-pragyan-2026)
13
+ - [Weak XOR Verification Brute Force (Pragyan 2026)](#weak-xor-verification-brute-force-pragyan-2026)
14
+ - [Deterministic OTP with Load-Balanced Backends (Pragyan 2026)](#deterministic-otp-with-load-balanced-backends-pragyan-2026)
15
+ - [OTP Key Reuse / Many-Time Pad XOR (BYPASS CTF 2025)](#otp-key-reuse--many-time-pad-xor-bypass-ctf-2025)
16
+ - [Book Cipher](#book-cipher)
17
+ - [Variable-Length Homophonic Substitution (ASIS CTF Finals 2013)](#variable-length-homophonic-substitution-asis-ctf-finals-2013)
18
+ - [Grid Permutation Cipher Keyspace Reduction (BSidesSF 2026)](#grid-permutation-cipher-keyspace-reduction-bsidessf-2026)
19
+ - [Image-Based Caesar Shift Ciphers (BSidesSF 2026)](#image-based-caesar-shift-ciphers-bsidessf-2026)
20
+ - [Variant A — Vertical Strip Shift (caesar1)](#variant-a--vertical-strip-shift-caesar1)
21
+ - [Variant B — Horizontal Shift with ASCII Encoding (caesar2)](#variant-b--horizontal-shift-with-ascii-encoding-caesar2)
22
+ - [XOR Key Recovery via File Format Headers (MetaCTF Flash 2026)](#xor-key-recovery-via-file-format-headers-metactf-flash-2026)
23
+ - [3D Vigenere Palindrome Symmetry Key Recovery (SECCON 2017)](#3d-vigenere-palindrome-symmetry-key-recovery-seccon-2017)
24
+ - [Nihilist Cipher Double-Crib Key Recovery (Security Fest CTF 2018)](#nihilist-cipher-double-crib-key-recovery-security-fest-ctf-2018)
25
+ - [16-Byte XOR Block Cipher Structural Reversal (h4ckc0n 2018)](#16-byte-xor-block-cipher-structural-reversal-h4ckc0n-2018)
26
+ - [Flag Semaphore Photo Decoding (DefCamp CTF 2018)](#flag-semaphore-photo-decoding-defcamp-ctf-2018)
27
+ - [Two-Byte Nibble Reassembly with Random Padding (Trend Micro 2018)](#two-byte-nibble-reassembly-with-random-padding-trend-micro-2018)
28
+
29
+ ---
30
+
31
+ ## Vigenere Cipher
32
+
33
+ **Known Plaintext Attack (most common in CTFs):**
34
+ ```python
35
+ def vigenere_decrypt(ciphertext, key):
36
+ result = []
37
+ key_index = 0
38
+ for c in ciphertext:
39
+ if c.isalpha():
40
+ shift = ord(key[key_index % len(key)].upper()) - ord('A')
41
+ base = ord('A') if c.isupper() else ord('a')
42
+ result.append(chr((ord(c) - base - shift) % 26 + base))
43
+ key_index += 1
44
+ else:
45
+ result.append(c)
46
+ return ''.join(result)
47
+
48
+ def derive_key(ciphertext, plaintext):
49
+ """Derive key from known plaintext (e.g., flag format CCOI26{)"""
50
+ key = []
51
+ for c, p in zip(ciphertext, plaintext):
52
+ if c.isalpha() and p.isalpha():
53
+ c_val = ord(c.upper()) - ord('A')
54
+ p_val = ord(p.upper()) - ord('A')
55
+ key.append(chr((c_val - p_val) % 26 + ord('A')))
56
+ return ''.join(key)
57
+ ```
58
+
59
+ ### Kasiski Examination for Key Length
60
+
61
+ When no known plaintext is available, determine the Vigenere key length using Kasiski examination: find repeated sequences in the ciphertext and compute the GCD of their distances.
62
+
63
+ ```python
64
+ from math import gcd
65
+ from functools import reduce
66
+ from collections import Counter
67
+
68
+ def kasiski_examination(ciphertext, min_seq=3):
69
+ """Find repeating sequences and compute likely key lengths."""
70
+ ct = ''.join(c.upper() for c in ciphertext if c.isalpha())
71
+ distances = []
72
+
73
+ # Find repeated trigrams and their distances
74
+ for seq_len in range(min_seq, 6):
75
+ seen = {}
76
+ for i in range(len(ct) - seq_len):
77
+ seq = ct[i:i+seq_len]
78
+ if seq in seen:
79
+ for prev_pos in seen[seq]:
80
+ distances.append(i - prev_pos)
81
+ seen[seq].append(i)
82
+ else:
83
+ seen[seq] = [i]
84
+
85
+ # Key length is likely the GCD of distances
86
+ if distances:
87
+ key_len = reduce(gcd, distances)
88
+ print(f"Likely key length: {key_len}")
89
+ print(f"All distances: {sorted(set(distances))}")
90
+ return key_len
91
+ return None
92
+
93
+ def frequency_attack(ciphertext, key_length):
94
+ """Break Vigenere by frequency analysis on each key-position group."""
95
+ ct = [c.upper() for c in ciphertext if c.isalpha()]
96
+ english_freq = [0.082,0.015,0.028,0.043,0.127,0.022,0.020,0.061,0.070,
97
+ 0.002,0.008,0.040,0.024,0.067,0.075,0.019,0.001,0.060,
98
+ 0.063,0.091,0.028,0.010,0.023,0.002,0.020,0.001]
99
+ key = []
100
+
101
+ for i in range(key_length):
102
+ group = [ct[j] for j in range(i, len(ct), key_length)]
103
+ # Try each shift, score by English letter frequency
104
+ best_shift, best_score = 0, -1
105
+ for shift in range(26):
106
+ decrypted = [chr((ord(c) - ord('A') - shift) % 26 + ord('A')) for c in group]
107
+ freq = Counter(decrypted)
108
+ score = sum(freq.get(chr(j+65), 0) / len(group) * english_freq[j]
109
+ for j in range(26))
110
+ if score > best_score:
111
+ best_score = score
112
+ best_shift = shift
113
+ key.append(chr(best_shift + ord('A')))
114
+
115
+ return ''.join(key)
116
+ ```
117
+
118
+ **Key insight:** Repeated sequences in Vigenere ciphertext occur at distances that are multiples of the key length. The GCD of all such distances reveals the key length, after which each position becomes a simple Caesar cipher solvable by frequency analysis.
119
+
120
+ **When standard keys don't work:**
121
+ 1. Key may not repeat - could be as long as message
122
+ 2. Key derived from challenge theme (character names, phrases)
123
+ 3. Key may have "padding" - repeated letters (IICCHHAA instead of ICHA)
124
+ 4. Try guessing plaintext words from theme, derive full key
125
+
126
+ ---
127
+
128
+ ## Atbash Cipher
129
+
130
+ Simple substitution: A<->Z, B<->Y, C<->X, etc.
131
+
132
+ ```python
133
+ def atbash(text):
134
+ return ''.join(
135
+ chr(ord('Z') - (ord(c.upper()) - ord('A'))) if c.isalpha() else c
136
+ for c in text
137
+ )
138
+ ```
139
+
140
+ **Identification:** Challenge name hints ("Abashed" = Atbash), preserves spaces/punctuation, 1-to-1 substitution.
141
+
142
+ ---
143
+
144
+ ## Polybius Square Cipher (Qiwi-Infosec 2016)
145
+
146
+ 5x5 grid cipher where each letter maps to a two-digit coordinate (row, column). I/J typically share a cell.
147
+
148
+ ```python
149
+ import string
150
+
151
+ def polybius_decrypt(ciphertext, key="ABCDEFGHIKLMNOPQRSTUVWXYZ"):
152
+ """Decrypt Polybius square cipher (pairs of digits 1-5)"""
153
+ grid = {}
154
+ for i, ch in enumerate(key):
155
+ row, col = i // 5 + 1, i % 5 + 1
156
+ grid[(row, col)] = ch
157
+
158
+ digits = [int(d) for d in ciphertext if d.isdigit()]
159
+ plaintext = ""
160
+ for i in range(0, len(digits), 2):
161
+ plaintext += grid.get((digits[i], digits[i+1]), '?')
162
+ return plaintext
163
+
164
+ # Example: "5211251521531412" -> pairs (5,2)(1,1)(2,5)(1,5)(2,1)(5,3)(1,4)(1,2)
165
+ print(polybius_decrypt("5211251521531412"))
166
+ ```
167
+
168
+ **Key insight:** Polybius ciphers produce digit-only ciphertext with values 1-5. The 5x5 grid merges I/J into one cell. Custom key alphabets change the grid layout but the two-digit coordinate structure remains constant.
169
+
170
+ ---
171
+
172
+ ## Substitution Cipher with Rotating Wheel
173
+
174
+ **Pattern (Wheel of Mystery):** Physical cipher wheel with inner/outer alphabets.
175
+
176
+ **Automated solver:** Use [quipqiup.com](https://quipqiup.com/) for general substitution ciphers — it uses word pattern matching and language entropy to solve without knowing the key.
177
+
178
+ **Brute force all rotations:**
179
+ ```python
180
+ outer = "ABCDEFGHIJKLMNOPQRSTUVWXYZ{}"
181
+ inner = "QNFUVWLEZYXPTKMR}ABJICOSDHG{" # Given
182
+
183
+ for rotation in range(len(outer)):
184
+ rotated = inner[rotation:] + inner[:rotation]
185
+ mapping = {outer[i]: rotated[i] for i in range(len(outer))}
186
+ decrypted = ''.join(mapping.get(c, c) for c in ciphertext)
187
+ if decrypted.startswith("METACTF{"):
188
+ print(decrypted)
189
+ ```
190
+
191
+ ---
192
+
193
+ ## XOR Variants
194
+
195
+ ### Multi-Byte XOR Key Recovery via Frequency Analysis
196
+
197
+ **Pattern:** Ciphertext XOR'd with a repeating multi-byte key. Key length unknown.
198
+
199
+ **Step 1 — Determine key length:** Try each candidate length, split ciphertext into groups by position modulo key length, score each group's byte frequency against English text (space = 0x20 is the most common byte).
200
+
201
+ **Step 2 — Recover each key byte:** For each position, brute-force all 256 byte values and select the one producing the most English-like decrypted text.
202
+
203
+ ```python
204
+ from collections import Counter
205
+
206
+ def score_english(data):
207
+ """Score how English-like a byte sequence is."""
208
+ freq = Counter(data)
209
+ # Space is the most common character in English text
210
+ return freq.get(ord(' '), 0) + sum(freq.get(c, 0) for c in range(ord('a'), ord('z')+1))
211
+
212
+ def find_key_length(ciphertext, max_len=40):
213
+ """Test key lengths by scoring single-byte XOR on each column."""
214
+ best_len, best_score = 1, 0
215
+ for kl in range(1, max_len + 1):
216
+ total = 0
217
+ for col in range(kl):
218
+ group = ciphertext[col::kl]
219
+ best_col_score = max(
220
+ score_english(bytes(b ^ k for b in group))
221
+ for k in range(256)
222
+ )
223
+ total += best_col_score
224
+ if total > best_score:
225
+ best_score = total
226
+ best_len = kl
227
+ return best_len
228
+
229
+ def recover_key(ciphertext, key_length):
230
+ """Recover each key byte via frequency analysis."""
231
+ key = []
232
+ for col in range(key_length):
233
+ group = ciphertext[col::key_length]
234
+ best_k = max(range(256), key=lambda k: score_english(bytes(b ^ k for b in group)))
235
+ key.append(best_k)
236
+ return bytes(key)
237
+
238
+ ct = open('encrypted.bin', 'rb').read()
239
+ kl = find_key_length(ct)
240
+ key = recover_key(ct, kl)
241
+ print(f"Key ({kl} bytes): {key}")
242
+ print(bytes(c ^ key[i % len(key)] for i, c in enumerate(ct)))
243
+ ```
244
+
245
+ **Key insight:** Multi-byte repeating XOR splits into `key_length` independent single-byte XOR problems. English text frequency (especially space = 0x20) reliably identifies correct key bytes. Works best with ciphertext longer than ~100 bytes.
246
+
247
+ ### Cascade XOR (First-Byte Brute Force)
248
+
249
+ **Pattern (Shifty XOR):** Each byte XORed with previous ciphertext byte.
250
+
251
+ ```python
252
+ # c[i] = p[i] ^ c[i-1] (or similar cascade)
253
+ # Brute force first byte, rest follows deterministically
254
+ for first_byte in range(256):
255
+ flag = [first_byte]
256
+ for i in range(1, len(ct)):
257
+ flag.append(ct[i] ^ flag[i-1])
258
+ if all(32 <= b < 127 for b in flag):
259
+ print(bytes(flag))
260
+ ```
261
+
262
+ ### XOR with Rotation: Power-of-2 Bit Isolation (Pragyan 2026)
263
+
264
+ **Pattern (R0tnoT13):** Given `S XOR ROTR(S, k)` for multiple rotation offsets k, recover S.
265
+
266
+ **Key insight:** When ALL rotation offsets are powers of 2 (2, 4, 8, 16, 32, 64), even-indexed and odd-indexed bits NEVER mix across any frame. This reduces N-bit recovery to just 2 bits of brute force.
267
+
268
+ **Algorithm:**
269
+ 1. Express every bit of S in terms of two unknowns (s_0 for even bits, s_1 for odd bits) using the k=2 frame
270
+ 2. Only 4 candidate states -> try all, verify against all frames
271
+ 3. XOR valid state with ciphertext -> plaintext
272
+
273
+ ### Weak XOR Verification Brute Force (Pragyan 2026)
274
+
275
+ **Pattern (Dor4_Null5):** Verification XORs all comparison bytes into a single byte instead of checking each individually.
276
+
277
+ **Vulnerability:** Any fixed response has 1/256 probability of passing. With enough interaction budget (e.g., 4919 attempts), brute-force succeeds with ~256 expected attempts.
278
+
279
+ ```python
280
+ for attempt in range(3000):
281
+ r.sendlineafter(b"prompt: ", b"00" * 8) # Fixed zero response
282
+ result = r.recvline()
283
+ if b"successful" in result:
284
+ break
285
+ ```
286
+
287
+ ---
288
+
289
+ ## Deterministic OTP with Load-Balanced Backends (Pragyan 2026)
290
+
291
+ **Pattern (DumCows):** Service encrypts data with deterministic keystream that resets per connection. Multiple backends with different keystreams behind a load balancer.
292
+
293
+ **Attack:**
294
+ 1. Send known plaintext (e.g., 18 bytes of 'A'), XOR with ciphertext -> recover keystream
295
+ 2. XOR keystream with target ciphertext -> decrypt secret
296
+ 3. **Backend matching:** Must connect to same backend for keystream to match. Retry connections until patterns align.
297
+
298
+ ```python
299
+ def recover_keystream(known, ciphertext):
300
+ return bytes(k ^ c for k, c in zip(known, ciphertext))
301
+
302
+ def decrypt(keystream, target_ct):
303
+ return bytes(k ^ c for k, c in zip(keystream, target_ct))
304
+ ```
305
+
306
+ **Key insight:** When encryption is deterministic per connection with no nonce/IV, known-plaintext attack is trivial. The challenge is matching backends.
307
+
308
+ ---
309
+
310
+ ## OTP Key Reuse / Many-Time Pad XOR (BYPASS CTF 2025)
311
+
312
+ **Pattern (Once More Unto the Same Wind):** Two ciphertexts encrypted with the same OTP key. Known plaintext for one message enables recovery of the other.
313
+
314
+ **XOR property:** `C1 XOR C2 = P1 XOR P2` (key cancels). When one plaintext (P1) is known, recover the other: `P2 = C1 XOR C2 XOR P1`.
315
+
316
+ ```python
317
+ from pwn import xor
318
+
319
+ c1 = bytes.fromhex("7713283f5e9979...")
320
+ c2 = bytes.fromhex("740b393f4c8b67...")
321
+
322
+ # If one plaintext is known (or guessable, e.g., padded 'A' chars)
323
+ known_plaintext = b"A" * len(c1)
324
+ flag = xor(xor(c1, c2), known_plaintext)
325
+ print(flag)
326
+ ```
327
+
328
+ **When plaintext is unknown — crib dragging:**
329
+ ```python
330
+ def crib_drag(c1, c2, crib, max_pos=None):
331
+ """Slide known word across XOR of two ciphertexts."""
332
+ xored = xor(c1[:min(len(c1), len(c2))], c2[:min(len(c1), len(c2))])
333
+ for pos in range(len(xored) - len(crib)):
334
+ candidate = xor(xored[pos:pos+len(crib)], crib)
335
+ if all(32 <= b < 127 for b in candidate):
336
+ print(f"pos {pos}: {candidate}")
337
+ ```
338
+
339
+ **Key insight:** OTP (One-Time Pad) XOR encryption is only secure when the key is truly one-time. Reusing the key on two messages leaks `P1 XOR P2` — exploit with known plaintext or crib dragging.
340
+
341
+ ---
342
+
343
+ ## Book Cipher
344
+
345
+ **Pattern (Booking Key, Nullcon 2026):** Book cipher with "steps forward" encoding. Brute-force starting position with charset filtering reduces ~56k candidates to 3-4.
346
+
347
+ See [historical.md](historical.md) for full implementation.
348
+
349
+ ---
350
+
351
+ ## Variable-Length Homophonic Substitution (ASIS CTF Finals 2013)
352
+
353
+ **Pattern (Rookie Agent):** Ciphertext uses alphanumeric characters grouped in blocks of 5. Single-character frequency analysis shows non-uniform distribution. N-gram analysis reveals repeated multi-character groups mapping to single plaintext characters, with different plaintext characters encoded by groups of different lengths (1-4 characters).
354
+
355
+ **Analysis workflow:**
356
+
357
+ 1. Collapse whitespace and compute n-gram frequencies (1 through 6):
358
+ ```python
359
+ from collections import Counter
360
+
361
+ ct = "6di16ovhtmnzslsxqcjo8fkdmtyrbn..." # cleaned ciphertext
362
+ for n in range(1, 7):
363
+ ngrams = [ct[i:i+n] for i in range(len(ct)-n+1)]
364
+ freq = Counter(ngrams).most_common(20)
365
+ print(f"{n}-grams: {freq[:10]}")
366
+ ```
367
+
368
+ 2. Identify constant-frequency groups — if `8f`, `fk`, and `kd` each appear exactly 36 times, check whether `8fkd` also appears 36 times. If so, it is a single substitution unit:
369
+ ```python
370
+ # Iteratively replace most-frequent fixed groups with single symbols
371
+ substitutions = {
372
+ '8fkd': 'E', '4bg9': 'I', 'lsxq': 'A', 'fmrk': 'B',
373
+ '9gle': 'C', 'mtyr': 'D', 'cjo': 'F', 'htm': 'G',
374
+ # ... continue for all identified groups
375
+ }
376
+ reduced = ct
377
+ for pattern, symbol in sorted(substitutions.items(), key=lambda x: -len(x[0])):
378
+ reduced = reduced.replace(pattern, symbol)
379
+ ```
380
+
381
+ 3. The reduced text is now a monoalphabetic substitution — solve with [quipqiup.com](https://quipqiup.com/) or statistical analysis on English.
382
+
383
+ 4. When some characters remain ambiguous after decryption, brute-force permutations against a known hash of the flag:
384
+ ```python
385
+ from itertools import permutations
386
+ from hashlib import sha256
387
+
388
+ partial_flag = '3c6a1c371b381c943065864b95ae5546'
389
+ ambiguous_chars = '12456789x' # chars with uncertain mapping
390
+ known_hash = '9f2a579716af14400c9ba1de8682ca52c17b3ed4235ea17ac12ae78ca24876ef'
391
+
392
+ for p in permutations(ambiguous_chars):
393
+ mapping = dict(zip(ambiguous_chars, p))
394
+ candidate = ''.join(mapping.get(c, c) for c in partial_flag)
395
+ if sha256(('ASIS_' + candidate).encode()).hexdigest() == known_hash:
396
+ print(f"Flag: ASIS_{candidate}")
397
+ break
398
+ ```
399
+
400
+ **Key insight:** Variable-length homophonic substitution hides letter frequencies by mapping common plaintext letters to longer codegroups. The attack reverses this: find n-grams that always appear as a unit (identical frequency for all sub-n-grams), replace them with single symbols, then solve the resulting monoalphabetic substitution. When the flag format provides a hash for verification, brute-force remaining ambiguous character permutations offline.
401
+
402
+ ---
403
+
404
+ ## Grid Permutation Cipher Keyspace Reduction (BSidesSF 2026)
405
+
406
+ **Pattern (ghostcrypt):** A substitution cipher built on a 5x5 grid where the key permutes rows and columns independently. Row permutations and column permutations commute — applying all row swaps then all column swaps gives the same result regardless of order. This collapses the keyspace from potentially huge to just 5! x 5! = 14,400 combinations, making brute-force trivial.
407
+
408
+ ```python
409
+ from itertools import permutations
410
+
411
+ # 5x5 grid substitution cipher — brute force row+column permutations
412
+ grid_size = 5
413
+ ciphertext = "..." # encrypted text
414
+ wordlist = set(open("/usr/share/dict/words").read().split())
415
+
416
+ for row_perm in permutations(range(grid_size)):
417
+ for col_perm in permutations(range(grid_size)):
418
+ # Apply inverse permutation to grid
419
+ decrypted = apply_grid_permutation(ciphertext, row_perm, col_perm)
420
+ words = decrypted.split()
421
+ if sum(1 for w in words if w.lower() in wordlist) > len(words) * 0.5:
422
+ print(f"Key: rows={row_perm}, cols={col_perm}")
423
+ print(decrypted)
424
+ break
425
+ ```
426
+
427
+ **Key insight:** Row and column permutations on a grid are independent operations that commute. The total keyspace is the product of row permutations x column permutations (n!^2), NOT the factorial of total cells. For a 5x5 grid: 120 x 120 = 14,400 — brute-forceable in milliseconds.
428
+
429
+ **When to recognize:** Challenge uses a grid-based cipher, mentions "row/column shuffling", or provides a substitution table that looks like a permuted matrix. Any grid cipher where rows and columns are shuffled independently has this n!^2 keyspace property.
430
+
431
+ ---
432
+
433
+ ## Image-Based Caesar Shift Ciphers (BSidesSF 2026)
434
+
435
+ Two variants of applying Caesar cipher concepts to 2D image data:
436
+
437
+ ### Variant A — Vertical Strip Shift (caesar1)
438
+
439
+ Each vertical strip of pixels is shifted downward by `(column / strip_width) * multiplier mod height`. The multiplier is a small integer (1-50), making it brute-forceable.
440
+
441
+ ```python
442
+ from PIL import Image
443
+ import sys
444
+
445
+ img = Image.open("shifted.png")
446
+ w, h = img.size
447
+ pixels = img.load()
448
+ strip_width = 10 # Determined by visual inspection
449
+
450
+ for multiplier in range(1, 51):
451
+ out = Image.new("RGB", (w, h))
452
+ out_px = out.load()
453
+ for x in range(w):
454
+ shift = (x // strip_width) * multiplier % h
455
+ for y in range(h):
456
+ out_px[x, (y - shift) % h] = pixels[x, y]
457
+ out.save(f"attempt_{multiplier}.png")
458
+ ```
459
+
460
+ ### Variant B — Horizontal Shift with ASCII Encoding (caesar2)
461
+
462
+ Each row is shifted horizontally by a different amount. The shift value for each strip directly encodes an ASCII character of the flag.
463
+
464
+ ```python
465
+ from PIL import Image
466
+
467
+ original = Image.open("original.png")
468
+ shifted = Image.open("shifted.png")
469
+ w, h = original.size
470
+
471
+ flag = ""
472
+ prev_shift = -1
473
+ for y in range(h):
474
+ orig_row = [original.getpixel((x, y)) for x in range(w)]
475
+ shift_row = [shifted.getpixel((x, y)) for x in range(w)]
476
+ # Find shift by comparing rows
477
+ for offset in range(128):
478
+ if all(orig_row[(x + offset) % w] == shift_row[x] for x in range(min(20, w))):
479
+ if offset != prev_shift:
480
+ flag += chr(offset)
481
+ prev_shift = offset
482
+ break
483
+ print(flag)
484
+ ```
485
+
486
+ **Key insight:** Image pixel shifts are a visual form of Caesar cipher. When comparing an original and shifted image, the shift amount per row/column directly encodes hidden data. Always compare row-by-row or column-by-column when given two versions of the same image.
487
+
488
+ **When to recognize:** Challenge provides one or two image files with visible horizontal or vertical "shearing" artifacts. If an original image is provided alongside a shifted version, compute per-row or per-column offsets and check if they decode as ASCII.
489
+
490
+ ---
491
+
492
+ ## XOR Key Recovery via File Format Headers (MetaCTF Flash 2026)
493
+
494
+ **Pattern (In The Door):** A file claims to be a known format (e.g., PDF, PNG, ZIP) but `file` reports it as "data". The file has been XOR-encrypted with a repeating key. Recover the key by XOR-ing the encrypted bytes against the expected file format header, then extend the key using known structural elements at the end of the file.
495
+
496
+ ```python
497
+ # Step 1: XOR first bytes against expected header to derive key start
498
+ encrypted = open('encrypted.pdf', 'rb').read()
499
+
500
+ # PDF files always start with %PDF-1.
501
+ expected_header = b'%PDF-1.'
502
+ key_start = bytes(a ^ b for a, b in zip(encrypted[:len(expected_header)], expected_header))
503
+ print(f"Key prefix: {key_start}") # e.g., b'h4ck4ll'
504
+
505
+ # Step 2: Extend key using known trailer structures
506
+ # PDF files end with %%EOF (possibly followed by newline)
507
+ # Try known trailer patterns at the end of the file
508
+ pdf_trailers = [b'%%EOF\n', b'%%EOF\r\n', b'%%EOF']
509
+ for trailer in pdf_trailers:
510
+ tail = encrypted[-len(trailer):]
511
+ key_tail = bytes(a ^ b for a, b in zip(tail, trailer))
512
+ print(f"Key tail candidate: {key_tail}")
513
+
514
+ # Step 3: Once key length is known, combine fragments
515
+ # Common structures to anchor: 'startxref', 'trailer', 'endobj'
516
+ key = b'h4ck4llth3cryp70' # 16-byte repeating key
517
+ key_len = len(key)
518
+
519
+ # Step 4: Decrypt entire file
520
+ decrypted = bytes(encrypted[i] ^ key[i % key_len] for i in range(len(encrypted)))
521
+ with open('decrypted.pdf', 'wb') as f:
522
+ f.write(decrypted)
523
+
524
+ # Verify
525
+ import subprocess
526
+ result = subprocess.run(['file', 'decrypted.pdf'], capture_output=True, text=True)
527
+ print(result.stdout) # Should show: PDF document
528
+ ```
529
+
530
+ **Key insight:** Every file format has known byte sequences at fixed positions -- magic bytes at the start, structural markers throughout, and trailer signatures at the end. XOR with a repeating key is fully recoverable when you know enough plaintext at known offsets. For a key of length N, you need N bytes of known plaintext at known positions (they need not be contiguous, but you must know their offset modulo the key length).
531
+
532
+ **Common file format anchors for key recovery:**
533
+
534
+ | Format | Header | Trailer/Footer |
535
+ |--------|--------|----------------|
536
+ | PDF | `%PDF-1.` | `%%EOF` |
537
+ | PNG | `\x89PNG\r\n\x1a\n` | `IEND\xaeB\x60\x82` |
538
+ | ZIP | `PK\x03\x04` | `PK\x05\x06` (EOCD) |
539
+ | JPEG | `\xff\xd8\xff\xe0` | `\xff\xd9` |
540
+ | ELF | `\x7fELF` | -- |
541
+ | GIF | `GIF89a` or `GIF87a` | `\x3b` (trailer) |
542
+
543
+ **When to recognize:** Challenge provides a file that should be a known format (filename extension or description says so) but `file` reports "data" or wrong type. Hex dump shows no recognizable magic bytes. XOR the first few bytes against the expected header -- if the result looks like an ASCII string or repeating pattern, it is a repeating XOR key.
544
+
545
+ **Determining key length:** If the header-derived key fragment repeats or the key is a readable string, try common lengths (8, 16, 32). Alternatively, XOR the file against itself shifted by candidate key lengths and look for low-entropy output (many null bytes indicate correct shift = key length).
546
+
547
+ **References:** MetaCTF Flash CTF 2026 "In The Door"
548
+
549
+ ---
550
+
551
+ ## 3D Vigenere Palindrome Symmetry Key Recovery (SECCON 2017)
552
+
553
+ **Pattern:** When k2 = reverse(k1) in a 3D Vigenere cipher, the encryption depends only on `k1[i] + k1[key_len-1-i]` (symmetric sums). Only half the key needs recovery:
554
+
555
+ ```python
556
+ # Encryption: ct[i] = table[k1[i%kl]][k2[i%kl]][pt[i]]
557
+ # With k2 = reverse(k1): ct[i] depends on k1[i%kl] + k1[(kl-1-i)%kl]
558
+ # Known-plaintext with flag prefix recovers kl/2 sum values
559
+ # Then brute-force one half of the key (the sums constrain the other)
560
+ for c1 in range(len(s)):
561
+ for c2 in range(len(s)):
562
+ if (c1 + c2) % len(s) == known_sum:
563
+ # test this key pair
564
+ ```
565
+
566
+ **Key insight:** Palindrome key structure (k2 = reverse(k1)) halves the effective keyspace. Each plaintext position depends on the sum of two key characters at mirrored positions. Known plaintext of length >= key_length/2 fully constrains these sums, reducing the remaining brute-force dramatically. This generalizes to any polyalphabetic cipher where key symmetry reduces independent key variables.
567
+
568
+ **References:** SECCON CTF 2017
569
+
570
+ ---
571
+
572
+ ## Nihilist Cipher Double-Crib Key Recovery (Security Fest CTF 2018)
573
+
574
+ **Pattern (Mission Impossible):** Nihilist cipher encrypts via Polybius square lookup + additive numeric key. Each plaintext letter becomes a two-digit Polybius coordinate, then the key's digit stream is added to produce ciphertext digits.
575
+
576
+ **Key insight:** The flag format `midnight{...}` contains two `i` characters at known positions. Both must produce the same Polybius coordinates (e.g., `24`), so their ciphertext differences directly reveal two key digit pairs — enough to constrain the additive key. Every valid Polybius coordinate must be in range 1-5, which prunes invalid key candidates aggressively.
577
+
578
+ **Recovery approach:**
579
+ ```python
580
+ # For each pair of key digits (k1, k2) in 1..9:
581
+ # for each ciphertext two-digit group:
582
+ # plain = ((c1 - k1) % 10, (c2 - k2) % 10)
583
+ # if plain[0] not in 1..5 or plain[1] not in 1..5: reject
584
+ # otherwise test frequency (most common letter => 'e') and look up Polybius
585
+ ```
586
+ Use the two repeated-character constraints from the flag prefix to cut the keyspace to a handful, then brute-force the Polybius square from remaining ciphertext frequency.
587
+
588
+ **References:** Security Fest CTF 2018 — writeup 10210
589
+
590
+ ---
591
+
592
+ ## 16-Byte XOR Block Cipher Structural Reversal (h4ckc0n 2018)
593
+
594
+ **Pattern (custom XOR block cipher):** Encryption operates on 16-byte blocks split into four 4-byte lanes. Each output byte is an XOR of several input bytes from the same block; one lane can be recovered as the XOR of the other three.
595
+
596
+ **Exploit:** Because every encrypted byte is a linear combination of plaintext bytes in the same block, XOR three lanes together to reconstruct the fourth. No key recovery needed — the algorithm is self-inverting once the linear structure is identified.
597
+
598
+ ```python
599
+ def decrypt(ciphertext):
600
+ out = bytearray()
601
+ for i in range(0, len(ciphertext), 16):
602
+ for j in range(4):
603
+ xorsum = 0
604
+ for k in range(4):
605
+ if k != j:
606
+ for l in range(i + k*4, i + k*4 + 4):
607
+ xorsum ^= ciphertext[l]
608
+ for m in range(i + j*4, i + j*4 + 4):
609
+ out.append(ciphertext[m] ^ xorsum)
610
+ return bytes(out)
611
+ ```
612
+
613
+ **Key insight:** Any unkeyed cipher built from XOR-only operations over fixed-size blocks is a linear system. If each output bit is a linear combination of input bits, invert by solving for the dependent lane — no key or brute force required.
614
+
615
+ **References:** h4ckc0n 2018 — writeup 10806
616
+
617
+ ---
618
+
619
+ ## Flag Semaphore Photo Decoding (DefCamp CTF 2018)
620
+
621
+ **Pattern:** Challenge provides photos of a person holding flags in fixed positions. Map each pose to a letter using the standard flag semaphore chart (8 compass directions × 2 arms = ~32 letters). Two special poses — `J` (shift to "letters") and `#` (shift to "numbers") — switch between modes; maintain a mode flag while decoding.
622
+
623
+ ```python
624
+ SEMAPHORE = {
625
+ ('NW','N'): 'A', ('NW','NE'): 'B', ('NW','E'): 'C', ('NW','SE'): 'D',
626
+ # ... full table on Wikipedia
627
+ ('N','NE'): 'J', # letters shift
628
+ ('NE','SE'): '#', # numbers shift
629
+ }
630
+ letters = [SEMAPHORE[pose] for pose in pose_sequence]
631
+ ```
632
+
633
+ **Key insight:** Semaphore is often disguised as "person dancing" or "art installation" photos. The presence of exactly two outstretched limbs per frame is the tell.
634
+
635
+ **References:** DefCamp CTF 2018 — Multiple Flags, writeup 12005
636
+
637
+ ---
638
+
639
+ ## Two-Byte Nibble Reassembly with Random Padding (Trend Micro 2018)
640
+
641
+ **Pattern:** Custom encoding outputs two bytes for every input byte, where each input byte is split into high and low nibbles, and each nibble is padded with a random high nibble. Recover the original by masking low nibbles and recombining.
642
+
643
+ ```python
644
+ def decode(src):
645
+ return bytes(((src[2*i] & 0xf) << 4) | (src[2*i+1] & 0xf)
646
+ for i in range(len(src)//2))
647
+ ```
648
+
649
+ **Key insight:** Random padding in the *high* nibble can be ignored entirely — only the low nibbles carry signal. Spot the pattern when the encoded length is exactly 2x the plaintext length and histograms show uniform high-nibble distribution.
650
+
651
+ **References:** Trend Micro CTF 2018 — J1, writeup 12874