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,528 @@
1
+ # CTF Crypto - Exotic Algebraic Structures
2
+
3
+ ## Table of Contents
4
+ - [Braid Group DH — Alexander Polynomial Multiplicativity (DiceCTF 2026)](#braid-group-dh--alexander-polynomial-multiplicativity-dicectf-2026)
5
+ - [Monotone Function Inversion with Partial Output](#monotone-function-inversion-with-partial-output)
6
+ - [Tropical Semiring Residuation Attack (BearCatCTF 2026)](#tropical-semiring-residuation-attack-bearcatctf-2026)
7
+ - [Paillier Cryptosystem Attack (SECCON 2015)](#paillier-cryptosystem-attack-seccon-2015)
8
+ - [Hamming Code Error Correction with Helical Interleaving (Sharif CTF 2016)](#hamming-code-error-correction-with-helical-interleaving-sharif-ctf-2016)
9
+ - [ElGamal Universal Re-encryption (Sharif CTF 2016)](#elgamal-universal-re-encryption-sharif-ctf-2016)
10
+ - [Paillier Oracle Size Bypass via Ciphertext Factoring (BSidesSF 2025)](#paillier-oracle-size-bypass-via-ciphertext-factoring-bsidessf-2025)
11
+ - [Format-Preserving Encryption Feistel Brute-Force (BSidesSF 2026)](#format-preserving-encryption-feistel-brute-force-bsidessf-2026)
12
+ - [Icosahedral Symmetry Group Cipher (BSidesSF 2026)](#icosahedral-symmetry-group-cipher-bsidessf-2026)
13
+ - [Goldwasser-Micali Ciphertext Replication Oracle (BSidesSF 2026)](#goldwasser-micali-ciphertext-replication-oracle-bsidessf-2026)
14
+
15
+ ---
16
+
17
+ ## Braid Group DH — Alexander Polynomial Multiplicativity (DiceCTF 2026)
18
+
19
+ **Pattern (Plane or Exchange):** Diffie-Hellman key exchange built over mathematical braids. Public keys are derived by connecting a private braid to public info, then scrambled with Reidemeister-like moves. Shared secret = `sha256(normalize(calculate(connect(my_priv, their_pub))))`. The `calculate()` function computes the Alexander polynomial of the braid.
20
+
21
+ **Protocol structure:**
22
+ ```python
23
+ import sympy as sp
24
+ import hashlib
25
+
26
+ t = sp.Symbol('t')
27
+
28
+ def compose(p1, p2):
29
+ return [p1[p2[i]] for i in range(len(p1))]
30
+
31
+ def inverse(p):
32
+ inv = [0] * len(p)
33
+ for i, j in enumerate(p):
34
+ inv[j] = i
35
+ return inv
36
+
37
+ def connect(g1, g2):
38
+ """Concatenate two braids with a swap at the junction."""
39
+ x1, o1 = g1
40
+ x2, o2 = g2
41
+ l = len(x1)
42
+ new_x = list(x1) + [v + l for v in x2]
43
+ new_o = list(o1) + [v + l for v in o2]
44
+ # Swap at junction
45
+ new_x[l-1], new_x[l] = new_x[l], new_x[l-1]
46
+ return (new_x, new_o)
47
+
48
+ def sweep(ap):
49
+ """Compute winding number matrix from arc presentation."""
50
+ l = len(ap)
51
+ current_row = [0] * l
52
+ matrix = []
53
+ for pair in ap:
54
+ c1, c2 = sorted(pair)
55
+ diff = pair[1] - pair[0]
56
+ s = 1 if diff > 0 else (-1 if diff < 0 else 0)
57
+ for c in range(c1, c2):
58
+ current_row[c] += s
59
+ matrix.append(list(current_row))
60
+ return matrix
61
+
62
+ def mine(point):
63
+ x, o = point
64
+ return sweep([*zip(x, o)])
65
+
66
+ def calculate(point):
67
+ """Compute Alexander polynomial from braid."""
68
+ mat = sp.Matrix([[t**(-x) for x in y] for y in mine(point)])
69
+ return mat.det(method='bareiss') * (1 - t)**(1 - len(point[0]))
70
+
71
+ def normalize(calculation):
72
+ """Convert Laurent polynomial to standard form."""
73
+ poly = sp.expand(sp.simplify(calculation))
74
+ all_exp = [term.as_coeff_exponent(t)[1] for term in poly.as_ordered_terms()]
75
+ min_exp = min(all_exp)
76
+ poly = sp.expand(sp.simplify(poly * t**(-min_exp)))
77
+ if poly.coeff(t, 0) < 0:
78
+ poly *= -1
79
+ return poly
80
+
81
+ # Key exchange:
82
+ # alice_pub = scramble(connect(pub_info, alice_priv), 1000)
83
+ # bob_pub = scramble(connect(pub_info, bob_priv), 1000)
84
+ # shared = sha256(str(normalize(calculate(connect(alice_priv, bob_pub)))))
85
+ ```
86
+
87
+ **The fatal vulnerability — Alexander polynomial multiplicativity:**
88
+
89
+ The Alexander polynomial satisfies `Δ(β₁·β₂) = Δ(β₁) × Δ(β₂)` under braid concatenation. This makes the scheme abelian:
90
+
91
+ ```python
92
+ # Eve computes shared secret from public values only:
93
+ calc_pub = normalize(calculate(pub_info))
94
+ calc_alice = normalize(calculate(alice_pub))
95
+ calc_bob = normalize(calculate(bob_pub))
96
+
97
+ # Recover Alice's private polynomial
98
+ calc_alice_priv = sp.cancel(calc_alice / calc_pub) # exact division
99
+
100
+ # Shared secret = calc(alice_priv) * calc(bob_pub) = calc(bob_priv) * calc(alice_pub)
101
+ shared_poly = normalize(sp.expand(calc_alice_priv * calc_bob))
102
+ shared_hex = hashlib.sha256(str(shared_poly).encode()).hexdigest()
103
+
104
+ # Decrypt XOR stream cipher
105
+ key = bytes.fromhex(shared_hex)
106
+ while len(key) < len(ciphertext):
107
+ key += hashlib.sha256(key).digest()
108
+ plaintext = bytes(a ^ b for a, b in zip(ciphertext, key))
109
+ ```
110
+
111
+ **Computational trick for large matrices:**
112
+
113
+ Direct sympy Bareiss on rational-function matrices (e.g., 30×30 with entries `t^(-w)`) is extremely slow. Clear denominators first:
114
+
115
+ ```python
116
+ # Winding numbers range from w_min to w_max (e.g., -1 to 5)
117
+ # Multiply all entries by t^w_max to get polynomial matrix
118
+ k = max(abs(w) for row in winding_matrix for w in row)
119
+ n = len(winding_matrix)
120
+
121
+ # Original: M[i][j] = t^(-w[i][j])
122
+ # Scaled: M'[i][j] = t^(k - w[i][j]) (all non-negative powers)
123
+ mat_poly = sp.Matrix([[t**(k - w) for w in row] for row in winding_matrix])
124
+ det_scaled = mat_poly.det(method='bareiss') # Much faster!
125
+
126
+ # Recover true determinant: det(M) = det(M') / t^(k*n)
127
+ det_true = sp.cancel(det_scaled / t**(k * n))
128
+ # Then: (1-t)^(n-1) divides det_true (topological property)
129
+ result = sp.cancel(det_true * (1 - t)**(1 - n))
130
+ ```
131
+
132
+ **Validation — palindromic property:**
133
+ All valid Alexander polynomials are palindromic (coefficients read the same forwards and backwards). Use this as a sanity check on intermediate results:
134
+ ```python
135
+ def is_palindromic(poly, var=t):
136
+ coeffs = sp.Poly(poly, var).all_coeffs()
137
+ return coeffs == coeffs[::-1]
138
+ ```
139
+
140
+ **When to recognize:** Challenge mentions braids, knots, permutation pairs, winding numbers, Reidemeister moves, or "topological key exchange." The key mathematical insight is that the Alexander polynomial — while a powerful knot/braid invariant — is multiplicative, making it fundamentally unsuitable as a one-way function for Diffie-Hellman.
141
+
142
+ **Key lessons:**
143
+ - **Diffie-Hellman requires non-abelian hardness.** If the invariant used for the shared secret is multiplicative/commutative under the group operation, Eve can compute it from public values.
144
+ - **Scrambling (Reidemeister moves) doesn't help** — the Alexander polynomial is an invariant, so scrambled braids produce the same polynomial.
145
+ - **Large symbolic determinants** need the denominator-clearing trick: multiply by `t^k` to get polynomials, compute det, divide back.
146
+
147
+ **References:** DiceCTF 2026 "Plane or Exchange"
148
+
149
+ ---
150
+
151
+ ## Monotone Function Inversion with Partial Output
152
+
153
+ **Pattern:** A flag is converted to a real number, pushed through an invertible/monotone function (e.g., iterated map, spiral), then some output digits are masked/erased. Recover the masked digits to invert and get the flag.
154
+
155
+ **Identification:**
156
+ - Output is a high-precision decimal number with some digits replaced by `?`
157
+ - The transformation is smooth/monotone (invertible via root-finding)
158
+ - Flag format constrains the input to a narrow range
159
+ - Challenge hints like "brute won't cut it" or "binary search"
160
+
161
+ **Key insight:** For a monotone function `f`, knowing the flag format (e.g., `0xL4ugh{...}`) constrains the output to a tiny interval. Many "unknown" output digits are actually **fixed** across all valid inputs and can be determined immediately.
162
+
163
+ **Attack: Hierarchical Digit Recovery**
164
+
165
+ 1. **Determine fixed digits:** Compute `f(flag_min)` and `f(flag_max)` for all valid flags. Digits that are identical in both outputs are fixed regardless of flag content.
166
+
167
+ 2. **Sequential refinement:** Determine remaining unknown digits one at a time (largest contribution first). For each candidate value (0-9), invert `f` and check if the result is a valid flag (ASCII, correct format).
168
+
169
+ 3. **Validation:** The correct digit produces readable ASCII text; wrong digits produce garbage bytes in the flag.
170
+
171
+ ```python
172
+ import mpmath
173
+
174
+ # Match SageMath's RealField(N) precision exactly:
175
+ # RealField(256) = 256-bit MPFR mantissa
176
+ mpmath.mp.prec = 256 # BINARY precision (not decimal!)
177
+ # For decimal: mpmath.mp.dps = N sets decimal places
178
+
179
+ phi = (mpmath.mpf(1) + mpmath.sqrt(mpmath.mpf(5))) / 2
180
+
181
+ def forward(x0):
182
+ """The challenge's transformation (e.g., iterated spiral)."""
183
+ x = x0
184
+ for i in range(iterations):
185
+ r = mpmath.mpf(i) / mpmath.mpf(iterations)
186
+ x = r * mpmath.sqrt(x*x + 1) + (1 - r) * (x + phi)
187
+ return x
188
+
189
+ def invert(y_target, x_guess):
190
+ """Invert via root-finding (Newton's method)."""
191
+ def f(x0):
192
+ return forward(x0) - y_target
193
+ return mpmath.findroot(f, x_guess, tol=mpmath.mpf(10)**(-200))
194
+
195
+ # Hierarchical search: determine unknown digits sequentially
196
+ masked = "?7086013?3756162?51694057..."
197
+ unknown_positions = [0, 8, 16, 25, 33, ...]
198
+
199
+ # Step 1: Fix digits that are constant across all valid flags
200
+ # (compute forward for min/max valid flag, compare)
201
+
202
+ # Step 2: For each remaining unknown (largest positional weight first):
203
+ for pos in remaining_unknowns:
204
+ for digit in range(10):
205
+ # Set this digit, others to middle value (5)
206
+ output_val = construct_number(known_digits | {pos: digit})
207
+ x_inv = invert(output_val, x_guess=0.335)
208
+ flag_int = int(x_inv * mpmath.power(10, flag_digits))
209
+ flag_bytes = flag_int.to_bytes(30, 'big')
210
+
211
+ # Check: starts with prefix? Ends with suffix? All ASCII?
212
+ if is_valid_flag(flag_bytes):
213
+ known_digits[pos] = digit
214
+ break
215
+ ```
216
+
217
+ **Why it works:** Each unknown digit affects a different decimal scale in the output number. The largest unknown (earliest position) shifts the inverted value by the most, determining several bytes of the flag. Fixing it and moving to the next unknown reveals more bytes. Total work: `10 * num_unknowns` inversions (linear, not exponential).
218
+
219
+ **Precision matching:** SageMath's `RealField(N)` uses MPFR with N-bit mantissa. In mpmath, set `mp.prec = N` (NOT `mp.dps`). The last few output digits are precision-sensitive and will only match with the correct binary precision.
220
+
221
+ **Derivative analysis:** For the spiral-type map `x → r*sqrt(x²+1) + (1-r)*(x+φ)`, the per-step derivative is `r*x/sqrt(x²+1) + (1-r) ≈ 1`, so the total derivative stays near 1 across all 81 iterations. This means precision is preserved through inversion — 67 known output digits give ~67 digits of input precision.
222
+
223
+ **References:** 0xL4ugh CTF "SpiralFloats"
224
+
225
+ ---
226
+
227
+ ## Tropical Semiring Residuation Attack (BearCatCTF 2026)
228
+
229
+ **Pattern (Tropped):** Diffie-Hellman key exchange using tropical matrices (min-plus algebra). Per-character shared secret XOR'd with encrypted flag.
230
+
231
+ **Tropical algebra:**
232
+ - Addition = `min(a, b)`
233
+ - Multiplication = `a + b`
234
+ - Matrix multiply: `(A*B)[i,j] = min_k(A[i,k] + B[k,j])`
235
+
236
+ **Tropical residuation recovers shared secret from public data:**
237
+ ```python
238
+ def tropical_residuate(M, Mb, aM, n):
239
+ """Recover shared secret from public matrices.
240
+ M = public matrix, Mb = M*b (Bob's public), aM = a*M (Alice's public)
241
+ """
242
+ # Right residual: b*[j] = max_i(Mb[i] - M[i][j])
243
+ b_star = [max(Mb[i] - M[i][j] for i in range(n)) for j in range(n)]
244
+ # Shared secret: aMb = min_j(aM[j] + b*[j])
245
+ aMb = min(aM[j] + b_star[j] for j in range(n))
246
+ return aMb
247
+
248
+ # Decrypt per-character: key = aMb % 32; plaintext = key ^ ciphertext
249
+ for i, enc_char in enumerate(encrypted):
250
+ key = shared_secret % 32
251
+ plaintext_char = chr(key ^ ord(enc_char))
252
+ ```
253
+
254
+ **Key insight:** Tropical DH is broken because the min-plus semiring lacks cancellation — given `M` and `M*b`, the "residual" `b*` can be computed directly via `max(Mb[i] - M[i][j])`. Unlike standard DH where recovering `b` from `g^b` is hard, tropical residuation recovers enough of `b`'s effect to compute the shared secret. This makes tropical matrix DH insecure for any matrix size.
255
+
256
+ **Detection:** Challenge mentions "tropical", "min-plus", "exotic algebra", or defines custom matrix multiplication using `min` and `+`.
257
+
258
+ ---
259
+
260
+ ## Paillier Cryptosystem Attack (SECCON 2015)
261
+
262
+ The Paillier cryptosystem is a homomorphic encryption scheme where `c = g^m * r^n mod n^2`. When given oracle equations involving c, o, h values:
263
+
264
+ 1. **Recover n:** Compute lower bound `sqrt(max(c, o, h))` to approximate n, then brute-force nearby values
265
+ 2. **Validate n:** Check equation `h = (c * o) % (n^2)` for correctness
266
+ 3. **Factor n:** Use standard methods (e.g., factordb) to find p, q
267
+ 4. **Decrypt:** Apply Paillier decryption:
268
+
269
+ ```python
270
+ from sympy import lcm, mod_inverse
271
+
272
+ # n = p * q (factored)
273
+ lam = lcm(p - 1, q - 1) # Carmichael function
274
+ n2 = n * n
275
+
276
+ def L(x):
277
+ return (x - 1) // n
278
+
279
+ # Compute mu
280
+ g_lam = pow(g, lam, n2)
281
+ mu = mod_inverse(L(g_lam), n)
282
+
283
+ # Decrypt
284
+ c_lam = pow(c, lam, n2)
285
+ m = (L(c_lam) * mu) % n
286
+ ```
287
+
288
+ **Key insight:** Paillier operates mod n^2, so ciphertext values are much larger than RSA. The homomorphic property `E(m1) * E(m2) = E(m1 + m2)` can leak relationships between plaintexts.
289
+
290
+ ---
291
+
292
+ ## Hamming Code Error Correction with Helical Interleaving (Sharif CTF 2016)
293
+
294
+ When data is protected by Hamming(31,26) codes with helical scan interleaving:
295
+
296
+ 1. **Determine matrix dimensions:** Brute-force width/height (30x30 search space) by testing which dimensions produce valid Hamming codewords
297
+ 2. **Read data in helical pattern:** Extract bits diagonally from the interleaved matrix
298
+ 3. **Apply Hamming parity check:** Multiply codeword by parity check matrix H to detect/correct errors
299
+
300
+ ```python
301
+ import numpy as np
302
+
303
+ def check_hamming(codeword, H):
304
+ """Syndrome = H * c^T; zero syndrome means valid codeword"""
305
+ syndrome = np.dot(H, codeword) % 2
306
+ return np.all(syndrome == 0)
307
+
308
+ # Brute-force dimensions
309
+ for w in range(1, 31):
310
+ for h in range(1, 31):
311
+ # Reshape data into w x h matrix
312
+ matrix = data[:w*h].reshape(h, w)
313
+ # Read diagonals (helical scan)
314
+ bits = read_helical(matrix)
315
+ # Check if bits form valid Hamming codewords
316
+ if validate_hamming_stream(bits, H):
317
+ print(f"Dimensions: {w}x{h}")
318
+ ```
319
+
320
+ **Key insight:** Try 8 different bit alignment offsets when the start position is unknown. Valid Hamming codewords have zero syndrome under multiplication by the parity check matrix.
321
+
322
+ ---
323
+
324
+ ## ElGamal Universal Re-encryption (Sharif CTF 2016)
325
+
326
+ Given an ElGamal-like ciphertext tuple (a, b, c, d) = (g^r, h^r, g^s, m*h^s), produce a different valid ciphertext decrypting to the same message without knowing the private key:
327
+
328
+ Transform exponents r -> 2r, s -> r+s:
329
+
330
+ ```python
331
+ def reencrypt(a, b, c, d, p):
332
+ return [
333
+ (a * a) % p, # g^(2r)
334
+ (b * b) % p, # h^(2r)
335
+ (a * c) % p, # g^(r+s)
336
+ (d * b) % p # m*h^(r+s)
337
+ ]
338
+ ```
339
+
340
+ **Key insight:** ElGamal's homomorphic property allows re-randomizing ciphertexts by multiplying components. The relationship between exponents must remain consistent: both pairs must share the same exponent offset.
341
+
342
+ ---
343
+
344
+ ## Paillier Oracle Size Bypass via Ciphertext Factoring (BSidesSF 2025)
345
+
346
+ When a Paillier decryption oracle rejects messages exceeding a size limit (e.g., >2000 bits), exploit the homomorphic property to factor the encrypted flag into smaller pieces:
347
+
348
+ 1. **Paillier additive homomorphism:** `E(m1) * E(m2) mod n^2 = E(m1 + m2 mod n)`
349
+ 2. **Multiplicative (scalar):** `E(m)^k mod n^2 = E(k*m mod n)`
350
+ 3. **Factoring ciphertext:** Divide n into small ranges, query oracle with `E(flag) * E(-offset)^1` to determine which range contains the flag
351
+ 4. **Chunk extraction:** Split the flag value into pieces that each fit within the oracle's size limit, decrypt individually, sum to recover original
352
+
353
+ ```python
354
+ from Crypto.Util.number import inverse
355
+
356
+ def paillier_sub(c, plaintext_sub, n):
357
+ """Compute E(m - plaintext_sub) from E(m) using homomorphic property"""
358
+ n2 = n * n
359
+ # E(-plaintext_sub) = E(n - plaintext_sub) = (n+1)^(n-plaintext_sub) * r^n mod n^2
360
+ neg_enc = pow(n + 1, n - plaintext_sub, n2)
361
+ return (c * neg_enc) % n2
362
+
363
+ # Binary search for flag value using oracle
364
+ def recover_flag(enc_flag, n, oracle_decrypt):
365
+ low, high = 0, n
366
+ while high - low > 1:
367
+ mid = (low + high) // 2
368
+ test_ct = paillier_sub(enc_flag, mid, n)
369
+ result = oracle_decrypt(test_ct)
370
+ if result < n // 2: # Positive (flag > mid)
371
+ low = mid
372
+ else: # Negative (flag < mid, wraps around)
373
+ high = mid
374
+ return low
375
+ ```
376
+
377
+ **Key insight:** Paillier's additive homomorphism allows computing `E(flag - offset)` without decryption. If the oracle reveals whether the decrypted value is "small" (within limit) or "large" (rejected/wraps), binary search recovers the flag in O(log n) queries.
378
+
379
+ ---
380
+
381
+ ## Format-Preserving Encryption Feistel Brute-Force (BSidesSF 2026)
382
+
383
+ **Pattern (tokencrypt):** Format-preserving encryption (FPE) using a Feistel network with a small round key. The 96-bit key splits into three components with different roles: a brute-forceable core, a GF(2) mixing matrix, and an affine offset.
384
+
385
+ **Key structure:**
386
+ - `s` (16 bits): Feistel round subkey — only 2^16 = 65536 possibilities
387
+ - `seed56` (56 bits): Generates an invertible GF(2) affine mixing matrix `M` (24x24)
388
+ - `b24` (24 bits): Affine offset applied after mixing
389
+
390
+ **Attack:**
391
+ 1. **Collect encrypt pairs:** Get multiple `(plaintext, ciphertext)` pairs from the FPE oracle
392
+ 2. **Brute-force `s`:** For each of 65536 candidate round keys, run the Feistel network on known plaintexts. If the Feistel core is correct, the remaining transformation is affine over GF(2)
393
+ 3. **Solve linear system:** With correct `s`, the relationship `ciphertext = M * feistel_output XOR b24` is linear. Collect 24+ pairs, build a GF(2) matrix equation, solve for `M` and `b24` via Gaussian elimination
394
+
395
+ ```python
396
+ import numpy as np
397
+
398
+ def feistel_encrypt(pt_24bit, s, rounds=3):
399
+ """24-bit Feistel with 16-bit round key s."""
400
+ L, R = pt_24bit >> 12, pt_24bit & 0xFFF
401
+ for r in range(rounds):
402
+ f = (R * s + r) & 0xFFF # Round function (example)
403
+ L, R = R, L ^ f
404
+ return (L << 12) | R
405
+
406
+ # Brute-force s (16-bit)
407
+ for s_candidate in range(1 << 16):
408
+ feistel_outputs = [feistel_encrypt(pt, s_candidate) for pt in known_pts]
409
+ # Check if feistel_outputs -> known_cts is affine over GF(2)
410
+ # Build system: for each bit position, collect equations
411
+ # If consistent -> found correct s, solve for M and b24
412
+ ```
413
+
414
+ **When to recognize:** Challenge mentions "format-preserving encryption", "FPE", or uses a Feistel structure with suspiciously small key components. Any round key under 32 bits is brute-forceable.
415
+
416
+ **Key lessons:**
417
+ - FPE with small Feistel round keys is trivially broken despite the total key looking large (96 bits)
418
+ - After recovering the Feistel core, the remaining affine layer is solvable as a linear system over GF(2)
419
+ - Collect enough plaintext-ciphertext pairs to overdetermine the linear system
420
+
421
+ **References:** BSidesSF 2026 "tokencrypt"
422
+
423
+ ---
424
+
425
+ ## Icosahedral Symmetry Group Cipher (BSidesSF 2026)
426
+
427
+ **Pattern (dodecacrypt):** Encryption maps message bytes to face permutations of a dodecahedron. The icosahedral symmetry group has order 120 (the rotation group of a regular dodecahedron/icosahedron), so each "digit" in base-120 encodes one group element as a specific arrangement of 12 face labels.
428
+
429
+ **How it works:**
430
+ 1. Message is converted to a large integer and expressed in base 120
431
+ 2. Each base-120 digit selects one of 120 possible face permutations
432
+ 3. The dodecahedron is rendered from a fixed viewing angle, showing only 6 of 12 faces
433
+ 4. Despite only 6 faces being visible, collisions between the 120 permutations are rare enough for unique recovery
434
+
435
+ **Attack:**
436
+ 1. **Build lookup table:** Probe the encryption API with all 120 single-digit inputs (0-119 in base 120), capture the rendered face arrangement for each
437
+ 2. **Match visible faces:** For each encrypted symbol in the ciphertext, compare the visible face pattern against the lookup table to recover the base-120 digit
438
+ 3. **Reconstruct message:** Convert the sequence of base-120 digits back to an integer, then to bytes
439
+
440
+ ```python
441
+ import itertools
442
+
443
+ # Build lookup: probe API with single-digit values
444
+ lookup = {}
445
+ for digit in range(120):
446
+ # Send digit, capture 6 visible face labels from rendered image
447
+ visible = get_visible_faces(encrypt_single(digit))
448
+ lookup[tuple(visible)] = digit
449
+
450
+ # Decrypt ciphertext
451
+ base120_digits = []
452
+ for symbol in ciphertext_symbols:
453
+ visible = get_visible_faces(symbol)
454
+ base120_digits.append(lookup[tuple(visible)])
455
+
456
+ # Convert base-120 to bytes
457
+ value = sum(d * 120**i for i, d in enumerate(reversed(base120_digits)))
458
+ plaintext = value.to_bytes((value.bit_length() + 7) // 8, 'big')
459
+ ```
460
+
461
+ **When to recognize:** Challenge involves polyhedra, dodecahedra, icosahedra, or mentions "120 rotations", "symmetry group", or shows 3D-rendered geometric objects with labeled faces.
462
+
463
+ **Key insight:** The icosahedral rotation group is small enough (order 120) that a complete lookup table fits easily in memory. Even with partial information (only 6 of 12 faces visible), the permutations are sufficiently distinct to avoid collisions in practice.
464
+
465
+ **References:** BSidesSF 2026 "dodecacrypt"
466
+
467
+ ---
468
+
469
+ ## Goldwasser-Micali Ciphertext Replication Oracle (BSidesSF 2026)
470
+
471
+ **Pattern (kproof):** A "proof of knowledge" protocol encrypts a user-chosen AES key using Goldwasser-Micali (GM) bit-by-bit encryption. The service decrypts GM ciphertext bits to reconstruct the AES key, then uses it to decrypt and hash a probe payload. The vulnerability: individual GM ciphertext values can be replayed, and 128 copies of the same GM-encrypted bit produce an AES key of either `0x00...00` or `0xFF...FF`.
472
+
473
+ **Goldwasser-Micali basics:**
474
+ - Encrypts one bit at a time: bit 0 → quadratic residue mod n, bit 1 → non-residue
475
+ - Decryption tests whether each ciphertext value is a quadratic residue
476
+ - Each ciphertext value independently encodes exactly one bit
477
+
478
+ **The vulnerability:**
479
+ The service accepts 128 GM ciphertext lines as the AES key. By sending the SAME GM ciphertext value 128 times, the decrypted key is either all-zeros (if the bit was 0) or all-ones (if the bit was 1). Since you control the probe plaintext and IV, you can precompute both possible SHA-256 hashes and compare against the service response.
480
+
481
+ **Attack (128 oracle queries for full key recovery):**
482
+
483
+ ```python
484
+ from Crypto.Cipher import AES
485
+ import hashlib
486
+
487
+ def recover_bit(gm_ciphertext_line, probe_ct, probe_iv, oracle):
488
+ """Determine if a single GM ciphertext encodes 0 or 1."""
489
+ # Replicate the single GM bit 128 times as the AES key
490
+ key_all_zero = b'\x00' * 16
491
+ key_all_ones = b'\xff' * 16
492
+
493
+ # Precompute expected hashes for both possible keys
494
+ hash0 = hashlib.sha256(
495
+ AES.new(key_all_zero, AES.MODE_CBC, probe_iv).decrypt(probe_ct)
496
+ ).hexdigest()
497
+ hash1 = hashlib.sha256(
498
+ AES.new(key_all_ones, AES.MODE_CBC, probe_iv).decrypt(probe_ct)
499
+ ).hexdigest()
500
+
501
+ # Query oracle with replicated GM line
502
+ result_hash = oracle.query(gm_ciphertext_line, copies=128)
503
+
504
+ if result_hash == hash0:
505
+ return 0
506
+ elif result_hash == hash1:
507
+ return 1
508
+
509
+ # Recover all 128 bits of the AES key
510
+ captured_gm_lines = parse_transcript(transcript) # 128 GM ciphertext values
511
+ key_bits = [recover_bit(line, probe_ct, probe_iv, oracle)
512
+ for line in captured_gm_lines]
513
+
514
+ # Reconstruct AES key and decrypt the captured payload
515
+ aes_key = bits_to_bytes(key_bits)
516
+ plaintext = AES.new(aes_key, AES.MODE_CBC, captured_iv).decrypt(captured_ct)
517
+ ```
518
+
519
+ **Key insight:** Goldwasser-Micali's bit-by-bit encryption means each ciphertext value independently encodes one bit. If a protocol allows replaying individual GM values as components of a larger key, each bit can be isolated and determined via a distinguishing oracle (here, SHA-256 hash comparison). This reduces key recovery from 2^128 brute-force to 128 linear queries.
520
+
521
+ **When to recognize:** Challenge uses bit-by-bit public-key encryption (GM, Rabin) combined with a symmetric key derivation step. The service decrypts individual ciphertext values without binding them to a position or preventing replay.
522
+
523
+ **Broader principle:** Any protocol that (1) encrypts a key bit-by-bit and (2) provides an oracle on the reconstructed key is vulnerable to bit-by-bit recovery via replication. The specific oracle (hash, decryption check, timing) varies but the attack structure is the same.
524
+
525
+ **References:** BSidesSF 2026 "kproof"
526
+
527
+
528
+ See [exotic-crypto-2.md](exotic-crypto-2.md) for 2017+ era exotic crypto attacks (BB-84, ElGamal variants, Paillier oracles, Cayley-Purser, BIP39, Asmuth-Bloom, Rabin polynomial, Vandermonde).
@@ -0,0 +1,113 @@
1
+ # CTF Crypto - Historical Ciphers
2
+
3
+ ## Table of Contents
4
+ - [Lorenz SZ40/42 (Tunny) Cipher](#lorenz-sz4042-tunny-cipher)
5
+ - [Book Cipher Brute Force (Nullcon 2026)](#book-cipher-brute-force-nullcon-2026)
6
+
7
+ ---
8
+
9
+ ## Lorenz SZ40/42 (Tunny) Cipher
10
+
11
+ The Lorenz cipher uses 12 wheels to encrypt 5-bit ITA2/Baudot characters. With known plaintext, a structured attack recovers all wheel settings.
12
+
13
+ **Machine structure:**
14
+ - 5 χ (chi) wheels: periods 41, 31, 29, 26, 23 — advance every step
15
+ - 5 Ψ (psi) wheels: periods 43, 47, 51, 53, 59 — advance only when μ37=1
16
+ - μ61 wheel: period 61 — advances every step, controls μ37 stepping
17
+ - μ37 wheel: period 37 — advances only when μ61=1, controls Ψ stepping
18
+
19
+ **Encryption:** `ciphertext[i] = plaintext[i] XOR chi[i] XOR psi[i]` (per 5-bit character)
20
+
21
+ **CRITICAL: The delta (Δ) approach is the fundamental technique:**
22
+
23
+ ```python
24
+ # Step 1: Get keystream from known plaintext
25
+ key_stream = [pt[i] ^ ct[i] for i in range(N)]
26
+
27
+ # Step 2: Compute delta keystream (THE key insight)
28
+ delta_k = [key_stream[i] ^ key_stream[i+1] for i in range(N-1)]
29
+ # delta_k = delta_chi XOR delta_psi
30
+ # Since psi only moves ~25% of the time, delta_k BIASES toward delta_chi
31
+
32
+ # Step 3: Recover delta_chi via majority vote at each wheel position
33
+ # Assume wheels start at position 1
34
+ for bit in range(5):
35
+ P = chi_periods[bit] # [41, 31, 29, 26, 23]
36
+ delta_chi = []
37
+ for phase in range(P):
38
+ # Collect all delta_k values at this wheel phase
39
+ vals = [delta_k_bit[i] for i in range(phase, len(delta_k_bit), P)]
40
+ delta_chi.append(1 if sum(vals) > len(vals)/2 else 0)
41
+
42
+ # Step 4: Integrate delta_chi to get chi (2 candidates per wheel, start 0 or 1)
43
+ chi = [start] # start = 0 or 1
44
+ for i in range(P-1):
45
+ chi.append(chi[-1] ^ delta_chi[i])
46
+ # Circular consistency: chi[0] ^ chi[-1] should equal delta_chi[P-1]
47
+
48
+ # Step 5: Subtract chi from keystream to get psi contribution
49
+ # Identify when psi steps: delta_psi = delta_k XOR delta_chi
50
+ # When ALL 5 bits of delta_psi are 0 → μ37 was off (psi didn't step)
51
+ # (Statistically very rare for all 5 cams to not change when stepping)
52
+
53
+ # Step 6: From stepping pattern, determine μ61 (period 61)
54
+ # μ61[pos] = 1 when we see psi resume stepping after being stopped
55
+
56
+ # Step 7: Cross-reference to get μ37 (period 37)
57
+ # μ37 position advances only when μ61=1
58
+
59
+ # Step 8: Determine psi wheels from delta_psi values when stepping occurs
60
+ # Look for repeating patterns with periods 43, 47, 51, 53, 59
61
+
62
+ # Step 9: Brute force remaining ambiguity
63
+ # Total candidates: 2^5 (chi) × 2^5 (psi) × 61×37 (μ positions) = 2,313,472
64
+ # Trivially brutable - decrypt and check if known plaintext matches
65
+ ```
66
+
67
+ **Common mistakes to avoid:**
68
+ - Do NOT assume psi is "period 2" or just alternating — it has real wheels with periods 43-59
69
+ - Do NOT spend time on statistical period-finding for the motor — just use the structured Δ approach
70
+ - Do NOT try LFSR analysis on the step sequence — the stepping is from mechanical wheels, not LFSRs
71
+ - The "step rate" (~35%) is a consequence of μ37 being on ~50% and μ61 on ~50% = ~25% psi stepping
72
+ - Always assume standard wheel periods unless evidence says otherwise
73
+ - Total brute force space is tiny (<3M) — don't over-optimize
74
+
75
+ **ITA2/Baudot encoding (5-bit):**
76
+ ```python
77
+ # Standard ITA2 mapping used in Lorenz challenges
78
+ char_to_code = {
79
+ 'A': 24, 'B': 19, 'C': 14, 'D': 18, 'E': 16, 'F': 22, 'G': 11,
80
+ 'H': 5, 'I': 12, 'J': 26, 'K': 30, 'L': 9, 'M': 7, 'N': 6,
81
+ 'O': 3, 'P': 13, 'Q': 29, 'R': 10, 'S': 20, 'T': 1, 'U': 28,
82
+ 'V': 15, 'W': 25, 'X': 23, 'Y': 21, 'Z': 17,
83
+ '9': 4, '5': 27, '8': 31, '3': 8, '4': 2, '/': 0,
84
+ }
85
+ # Code 27 = FIGS shift, Code 31 = LTRS shift
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Book Cipher Brute Force (Nullcon 2026)
91
+
92
+ **Pattern (Booking Key):** Book cipher encodes password as list of "steps forward" in reference text.
93
+
94
+ **Key insight:** Charset constraint drastically reduces candidate starting positions:
95
+ ```python
96
+ def decode_book_cipher(cipher_distances, book_text, valid_chars):
97
+ """Brute-force starting position; filter by charset."""
98
+ candidates = []
99
+ for start_key in range(len(book_text)):
100
+ pos = start_key
101
+ password = []
102
+ valid = True
103
+ for dist in cipher_distances:
104
+ pos = (pos + dist) % len(book_text)
105
+ ch = book_text[pos]
106
+ if ch not in valid_chars:
107
+ valid = False
108
+ break
109
+ password.append(ch)
110
+ if valid:
111
+ candidates.append((start_key, ''.join(password)))
112
+ return candidates # Typically 3-4 candidates out of ~56k positions
113
+ ```