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.
- package/AGENTS.md +131 -0
- package/Dockerfile +192 -0
- package/README.md +522 -0
- package/agents/ctf-analyzer.md +58 -0
- package/agents/ctf-controller.md +151 -0
- package/agents/ctf-speedrun.md +74 -0
- package/bin/ctf-agent.js +119 -0
- package/docker-compose.yml +13 -0
- package/mcp_config.json +19 -0
- package/package.json +44 -0
- package/references/ctf-triage-ladder.md +68 -0
- package/references/exploit-databases.md +575 -0
- package/references/llm-safety-and-policy-compliance.md +173 -0
- package/references/multi-agent-orchestration-and-policy-routing.md +278 -0
- package/references/security-events-and-intelligence.md +566 -0
- package/references/version-matrix.md +59 -0
- package/rules/ctf-execution-rules.md +44 -0
- package/rules/ctf-safety-framing-rules.md +140 -0
- package/rules/flag-validation-rules.md +25 -0
- package/scripts/__init__.py +1 -0
- package/scripts/ctf-tools.lock +205 -0
- package/scripts/ctf_init.py +1026 -0
- package/scripts/ctf_update.py +583 -0
- package/scripts/ctfd_client.py +81 -0
- package/scripts/cve_lookup.py +453 -0
- package/scripts/extract_flags.py +155 -0
- package/scripts/install_as_agent.py +278 -0
- package/scripts/install_ctf_tools.sh +603 -0
- package/scripts/parallel_triage.py +395 -0
- package/scripts/prompt_policy_sanitizer.py +283 -0
- package/scripts/scope_guard.py +444 -0
- package/scripts/skill_validator.py +504 -0
- package/scripts/workspace_cleaner.py +141 -0
- package/skills/ctf-ai-ml/SKILL.md +120 -0
- package/skills/ctf-ai-ml/adversarial-ml.md +623 -0
- package/skills/ctf-ai-ml/llm-attacks.md +487 -0
- package/skills/ctf-ai-ml/model-attacks.md +422 -0
- package/skills/ctf-crypto/SKILL.md +320 -0
- package/skills/ctf-crypto/advanced-math.md +798 -0
- package/skills/ctf-crypto/classic-ciphers.md +651 -0
- package/skills/ctf-crypto/ecc-attacks.md +347 -0
- package/skills/ctf-crypto/exotic-crypto-2.md +380 -0
- package/skills/ctf-crypto/exotic-crypto.md +528 -0
- package/skills/ctf-crypto/historical.md +113 -0
- package/skills/ctf-crypto/lattice-and-lwe.md +524 -0
- package/skills/ctf-crypto/modern-ciphers-2.md +563 -0
- package/skills/ctf-crypto/modern-ciphers-3.md +453 -0
- package/skills/ctf-crypto/modern-ciphers.md +649 -0
- package/skills/ctf-crypto/prng-attacks.md +257 -0
- package/skills/ctf-crypto/prng.md +664 -0
- package/skills/ctf-crypto/rsa-attacks-2.md +792 -0
- package/skills/ctf-crypto/rsa-attacks.md +487 -0
- package/skills/ctf-crypto/stream-ciphers.md +390 -0
- package/skills/ctf-crypto/zkp-and-advanced.md +456 -0
- package/skills/ctf-forensics/3d-printing.md +121 -0
- package/skills/ctf-forensics/SKILL.md +379 -0
- package/skills/ctf-forensics/disk-advanced.md +497 -0
- package/skills/ctf-forensics/disk-and-memory.md +491 -0
- package/skills/ctf-forensics/disk-recovery.md +699 -0
- package/skills/ctf-forensics/linux-forensics.md +511 -0
- package/skills/ctf-forensics/network-advanced.md +583 -0
- package/skills/ctf-forensics/network.md +645 -0
- package/skills/ctf-forensics/peripheral-capture.md +287 -0
- package/skills/ctf-forensics/signals-and-hardware.md +713 -0
- package/skills/ctf-forensics/steganography.md +694 -0
- package/skills/ctf-forensics/stego-advanced-2.md +475 -0
- package/skills/ctf-forensics/stego-advanced.md +481 -0
- package/skills/ctf-forensics/stego-image.md +691 -0
- package/skills/ctf-forensics/windows.md +625 -0
- package/skills/ctf-malware/SKILL.md +181 -0
- package/skills/ctf-malware/c2-and-protocols.md +274 -0
- package/skills/ctf-malware/pe-and-dotnet.md +108 -0
- package/skills/ctf-malware/scripts-and-obfuscation.md +449 -0
- package/skills/ctf-misc/SKILL.md +498 -0
- package/skills/ctf-misc/bashjails.md +323 -0
- package/skills/ctf-misc/ctfd-navigation.md +465 -0
- package/skills/ctf-misc/dns.md +255 -0
- package/skills/ctf-misc/encodings-advanced.md +504 -0
- package/skills/ctf-misc/encodings.md +431 -0
- package/skills/ctf-misc/games-and-vms-2.md +254 -0
- package/skills/ctf-misc/games-and-vms-3.md +690 -0
- package/skills/ctf-misc/games-and-vms-4.md +229 -0
- package/skills/ctf-misc/games-and-vms.md +529 -0
- package/skills/ctf-misc/linux-privesc.md +333 -0
- package/skills/ctf-misc/pyjails.md +671 -0
- package/skills/ctf-misc/rf-sdr.md +91 -0
- package/skills/ctf-osint/SKILL.md +198 -0
- package/skills/ctf-osint/geolocation-and-media.md +464 -0
- package/skills/ctf-osint/social-media.md +312 -0
- package/skills/ctf-osint/web-and-dns.md +341 -0
- package/skills/ctf-pwn/SKILL.md +214 -0
- package/skills/ctf-pwn/advanced-exploits-2.md +579 -0
- package/skills/ctf-pwn/advanced-exploits-3.md +598 -0
- package/skills/ctf-pwn/advanced-exploits-4.md +590 -0
- package/skills/ctf-pwn/advanced-exploits-5.md +119 -0
- package/skills/ctf-pwn/advanced-exploits.md +773 -0
- package/skills/ctf-pwn/advanced.md +326 -0
- package/skills/ctf-pwn/field-notes.md +245 -0
- package/skills/ctf-pwn/format-string.md +694 -0
- package/skills/ctf-pwn/heap-fsop.md +285 -0
- package/skills/ctf-pwn/heap-techniques-2.md +333 -0
- package/skills/ctf-pwn/heap-techniques.md +513 -0
- package/skills/ctf-pwn/kernel-bypass.md +421 -0
- package/skills/ctf-pwn/kernel-techniques.md +366 -0
- package/skills/ctf-pwn/kernel.md +636 -0
- package/skills/ctf-pwn/overflow-basics.md +611 -0
- package/skills/ctf-pwn/rop-advanced.md +725 -0
- package/skills/ctf-pwn/rop-and-shellcode.md +659 -0
- package/skills/ctf-pwn/sandbox-escape.md +313 -0
- package/skills/ctf-reverse/SKILL.md +163 -0
- package/skills/ctf-reverse/anti-analysis-ctf.md +204 -0
- package/skills/ctf-reverse/anti-analysis.md +693 -0
- package/skills/ctf-reverse/field-notes.md +376 -0
- package/skills/ctf-reverse/languages-compiled.md +666 -0
- package/skills/ctf-reverse/languages-platforms.md +592 -0
- package/skills/ctf-reverse/languages.md +553 -0
- package/skills/ctf-reverse/patterns-ctf-2.md +397 -0
- package/skills/ctf-reverse/patterns-ctf-3.md +797 -0
- package/skills/ctf-reverse/patterns-ctf.md +670 -0
- package/skills/ctf-reverse/patterns-runtime.md +274 -0
- package/skills/ctf-reverse/patterns.md +572 -0
- package/skills/ctf-reverse/platforms-hardware.md +387 -0
- package/skills/ctf-reverse/platforms.md +664 -0
- package/skills/ctf-reverse/tools-advanced-2.md +421 -0
- package/skills/ctf-reverse/tools-advanced.md +407 -0
- package/skills/ctf-reverse/tools-dynamic.md +679 -0
- package/skills/ctf-reverse/tools-emulation.md +319 -0
- package/skills/ctf-reverse/tools.md +573 -0
- package/skills/ctf-web/SKILL.md +153 -0
- package/skills/ctf-web/auth-and-access-2.md +82 -0
- package/skills/ctf-web/auth-and-access.md +783 -0
- package/skills/ctf-web/auth-infra.md +321 -0
- package/skills/ctf-web/auth-jwt.md +186 -0
- package/skills/ctf-web/client-side-advanced.md +739 -0
- package/skills/ctf-web/client-side.md +529 -0
- package/skills/ctf-web/cves.md +373 -0
- package/skills/ctf-web/field-notes.md +482 -0
- package/skills/ctf-web/node-and-prototype.md +200 -0
- package/skills/ctf-web/server-side-2.md +337 -0
- package/skills/ctf-web/server-side-advanced-2.md +559 -0
- package/skills/ctf-web/server-side-advanced-3.md +125 -0
- package/skills/ctf-web/server-side-advanced-4.md +480 -0
- package/skills/ctf-web/server-side-advanced.md +378 -0
- package/skills/ctf-web/server-side-deser.md +443 -0
- package/skills/ctf-web/server-side-exec-2.md +799 -0
- package/skills/ctf-web/server-side-exec.md +457 -0
- package/skills/ctf-web/server-side.md +629 -0
- package/skills/ctf-web/sql-injection.md +790 -0
- package/skills/ctf-web/web3.md +374 -0
- package/skills/ctf-writeup/SKILL.md +90 -0
- package/skills/solve-challenge/SKILL.md +269 -0
- 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
|