Small bruteforce space? Just try all combinations (e.g., 100 for 2 unknown digits)
Weak Hash Functions
Linear permutations (only XOR, rotations) are algebraically attackable
Build transformation matrix and solve over GF(2)
GF(2) Gaussian Elimination
import numpy as np
defsolve_gf2(A, b):
"""Solve Ax = b over GF(2)."""
m, n = A.shape
Aug = np.hstack([A, b.reshape(-1, 1)]) % 2
pivot_cols, row = [], 0for col inrange(n):
pivot = next((r for r inrange(row, m) if Aug[r, col]), None)
if pivot isNone: continue
Aug[[row, pivot]] = Aug[[pivot, row]]
for r inrange(m):
if r != row and Aug[r, col]: Aug[r] = (Aug[r] + Aug[row]) % 2
pivot_cols.append((row, col)); row += 1ifany(Aug[r, -1] for r inrange(row, m)): returnNone
x = np.zeros(n, dtype=np.uint8)
for r, c inreversed(pivot_cols):
x[c] = Aug[r, -1] ^ sum(Aug[r, c2] * x[c2] for c2 inrange(c+1, n)) % 2return x
RSA Attacks
Small e with small message: take eth root
Common modulus: extended GCD attack
Wiener's attack: small d
Fermat factorization: p and q close together
Pollard's p-1: smooth p-1
Hastad's broadcast attack: same message, multiple e=3 encryptions
RSA with Consecutive Primes
Pattern (Loopy Primes): q = next_prime(p), making p ≈ q ≈ sqrt(N).
Factorization: Find first prime below sqrt(N):
from sympy import nextprime, prevprime, isqrt
root = isqrt(n)
p = prevprime(root + 1)
while n % p != 0:
p = prevprime(p)
q = n // p
Multi-layer variant: 1024 nested RSA encryptions, each with consecutive primes of increasing bit size. Decrypt in reverse order.
Multi-Prime RSA
When N is product of many small primes (not just p*q):
# Factor N (easier when many primes)from sympy import factorint
factors = factorint(n) # Returns {p1: e1, p2: e2, ...}# Compute phi using all factors
phi = 1for p, e in factors.items():
phi *= (p - 1) * (p ** (e - 1))
d = pow(e, -1, phi)
plaintext = pow(ciphertext, d, n)
AES Attacks
ECB mode: block shuffling, byte-at-a-time oracle
CBC bit flipping: modify ciphertext to change plaintext
Padding oracle: decrypt without key
AES-CFB-8 Static IV State Forging
Pattern (Cleverly Forging Breaks): AES-CFB with 8-bit feedback and reused IV allows state reconstruction.
Key insight: After encrypting 16 known bytes, the AES internal shift register state is fully determined by those ciphertext bytes. Forge new ciphertexts by continuing encryption from known state.
Classic Ciphers
Caesar: frequency analysis or brute force 26 keys
Vigenere: Kasiski examination, index of coincidence
Substitution: frequency analysis, known plaintext
Vigenère Cipher
Known Plaintext Attack (most common in CTFs):
defvigenere_decrypt(ciphertext, key):
result = []
key_index = 0for c in ciphertext:
if c.isalpha():
shift = ord(key[key_index % len(key)].upper()) - ord('A')
base = ord('A') if c.isupper() elseord('a')
result.append(chr((ord(c) - base - shift) % 26 + base))
key_index += 1else:
result.append(c)
return''.join(result)
defderive_key(ciphertext, plaintext):
"""Derive key from known plaintext (e.g., flag format CCOI26{)"""
key = []
for c, p inzip(ciphertext, plaintext):
if c.isalpha() and p.isalpha():
c_val = ord(c.upper()) - ord('A')
p_val = ord(p.upper()) - ord('A')
key.append(chr((c_val - p_val) % 26 + ord('A')))
return''.join(key)
When standard keys don't work:
Key may not repeat - could be as long as message
Key derived from challenge theme (character names, phrases)
Key may have "padding" - repeated letters (IICCHHAA instead of ICHA)
Try guessing plaintext words from theme, derive full key
Elliptic Curve Attacks (General)
Small subgroup attacks:
Check curve order for small factors
Pohlig-Hellman: solve DLP in small subgroups, combine with CRT
Invalid curve attacks:
If point validation missing, send points on weaker curves
Craft points with small-order subgroups
Singular curves:
If discriminant Δ = 0, curve is singular
DLP becomes easy (maps to additive/multiplicative group)
Smart's attack:
For anomalous curves (order = field size p)
Lifts to p-adics, solves DLP in O(1)
# SageMath ECC basics
E = EllipticCurve(GF(p), [a, b])
G = E.gens()[0] # generator
order = E.order()
ECC Fault Injection
Pattern (Faulty Curves): Bit flip during ECC computation reveals private key bits.
Attack: Compare correct vs faulty ciphertext, recover key bit-by-bit:
# For each key bit position:# If fault at bit i changes output → key bit i affects computation# Binary distinguisher: faulty_output == correct_output → bit is 0
Useful Tools
# Python setup
pip install pycryptodome z3-solver sympy gmpy2
# SageMath for advanced math (required for ECC)
sage -python script.py
Common Patterns
from Crypto.Util.number import *
# RSA basics
n = p * q
phi = (p-1) * (q-1)
d = inverse(e, phi)
m = pow(c, d, n)
# XORfrom pwn import xor
xor(ct, key)
Z3 SMT Solver
Z3 solves constraint satisfaction - useful when crypto reduces to finding values satisfying conditions.
Basic usage:
from z3 import *
# Boolean variables (for bit-level problems)
bits = [Bool(f'b{i}') for i inrange(64)]
# Integer/bitvector variables
x = BitVec('x', 32) # 32-bit bitvector
y = Int('y') # arbitrary precision int
solver = Solver()
solver.add(x ^ 0xdeadbeef == 0x12345678)
solver.add(y > 100, y < 200)
if solver.check() == sat:
model = solver.model()
print(model.eval(x))
BPF/SECCOMP filter solving:
When challenges use BPF bytecode for flag validation (e.g., custom syscall handlers):
from z3 import *
# Model flag as array of 4-byte chunks (how BPF sees it)
flag = [BitVec(f'f{i}', 32) for i inrange(14)]
s = Solver()
# Constraint: printable ASCIIfor f in flag:
for byte inrange(4):
b = (f >> (byte * 8)) & 0xff
s.add(b >= 0x20, b < 0x7f)
# Extract constraints from BPF dump (seccomp-tools dump ./binary)
mem = [BitVec(f'm{i}', 32) for i inrange(16)]
# Example BPF constraint reconstruction
s.add(mem[0] == flag[0])
s.add(mem[1] == mem[0] ^ flag[1])
s.add(mem[4] == mem[0] + mem[1] + mem[2] + mem[3])
s.add(mem[8] == 4127179254) # From BPF if statementif s.check() == sat:
m = s.model()
flag_bytes = b''for f in flag:
val = m[f].as_long()
flag_bytes += val.to_bytes(4, 'little')
print(flag_bytes.decode())
Converting bits to flag:
from Crypto.Util.number import long_to_bytes
if solver.check() == sat:
model = solver.model()
flag_bits = ''.join('1'if model.eval(b) else'0'for b in bits)
print(long_to_bytes(int(flag_bits, 2)))
When to use Z3:
Type system constraints (OCaml GADTs, Haskell types)
Custom hash/cipher with algebraic structure
Equation systems over finite fields
Boolean satisfiability encoded in challenge
Constraint propagation puzzles
Cascade XOR (First-Byte Brute Force)
Pattern (Shifty XOR): Each byte XORed with previous ciphertext byte.
# c[i] = p[i] ^ c[i-1] (or similar cascade)# Brute force first byte, rest follows deterministicallyfor first_byte inrange(256):
flag = [first_byte]
for i inrange(1, len(ct)):
flag.append(ct[i] ^ flag[i-1])
ifall(32 <= b < 127for b in flag):
print(bytes(flag))
ECB Pattern Leakage on Images
Pattern (Electronic Christmas Book): AES-ECB on BMP/image data preserves visual patterns.
Exploitation: Identical plaintext blocks produce identical ciphertext blocks, revealing image structure even when encrypted. Rearrange or identify patterns visually.
Padding Oracle Attack
Pattern (The Seer): Server reveals whether decrypted padding is valid.
Byte-by-byte decryption:
defdecrypt_byte(block, prev_block, position, oracle):
for guess inrange(256):
modified = bytearray(prev_block)
# Set known bytes to produce valid padding
pad_value = 16 - position
for j inrange(position + 1, 16):
modified[j] = known[j] ^ pad_value
modified[position] = guess
if oracle(bytes(modified) + block):
return guess ^ pad_value
Atbash Cipher
Simple substitution: A↔Z, B↔Y, C↔X, etc.
defatbash(text):
return''.join(
chr(ord('Z') - (ord(c.upper()) - ord('A'))) if c.isalpha() else c
for c in text
)
Pattern (Wheel of Mystery): Physical cipher wheel with inner/outer alphabets.
Brute force all rotations:
outer = "ABCDEFGHIJKLMNOPQRSTUVWXYZ{}"
inner = "QNFUVWLEZYXPTKMR}ABJICOSDHG{"# Givenfor rotation inrange(len(outer)):
rotated = inner[rotation:] + inner[:rotation]
mapping = {outer[i]: rotated[i] for i inrange(len(outer))}
decrypted = ''.join(mapping.get(c, c) for c in ciphertext)
if decrypted.startswith("METACTF{"):
print(decrypted)