Lattice Attacks
Lattices for CTF crypto: basis intuition, LLL reduction, Coppersmith small-roots (univariate/bivariate), the Hidden Number Problem for biased ECDSA nonces, knapsack/subset-sum, and approximate GCD - with runnable SageMath.
Lattice Attacks
A surprising fraction of CTF crypto reduces to one sentence: find a short vector, or a close vector, in a lattice you can build from the challenge data. Small RSA roots, biased ECDSA nonces, broken knapsack ciphers, and approximate-GCD problems all collapse onto the same machinery - construct a basis whose short vectors encode the secret, then call LLL.
This page is the toolbox behind RSA Attacks (Coppersmith, Boneh-Durfee) and ECC Attacks (the nonce Hidden Number Problem). If you have never seen a lattice, start at the intuition and work down.
Notation: a lattice is the set of integer combinations of basis vectors . The basis is written as the rows of a matrix . Its determinant (covolume) is for a full-rank lattice.
Lattice Intuition
A lattice is a regular grid of points in , but the grid need not be axis-aligned. The same lattice has infinitely many bases: any basis multiplied by an integer matrix of determinant (a unimodular matrix) generates the same point set. Some bases are "good" (short, nearly orthogonal vectors); most are "bad" (long, skewed vectors). Almost every lattice attack is the same move:
Encode the secret as a short vector
Build a basis whose lattice provably contains a vector whose entries are the unknown small quantities (a leaked difference, a short nonce, a small polynomial root).
Reduce the basis
Run LLL (or BKZ) to transform the bad basis into a good one. The first reduced vector is short, and if your target was the shortest, it pops out.
Read the secret back
Decode the recovered short (or close) vector into the plaintext / key.
Two hard problems sit underneath:
- SVP (Shortest Vector Problem): find the shortest non-zero .
- CVP (Closest Vector Problem): given a target , find the lattice point nearest to it.
Both are NP-hard in general, but in the low dimensions and with the gap typical of CTFs, LLL solves them in practice.
The length scale to keep in your head is the Gaussian heuristic: a random lattice's shortest vector has norm roughly
If the vector encoding your secret is much shorter than this, it is unusually short and LLL will find it.
LLL Reduction
The Lenstra-Lenstra-Lovász algorithm (1982) runs in polynomial time and returns a basis whose first vector satisfies
The exponential factor is a worst-case bound; on the structured lattices you build in a CTF, LLL routinely returns the shortest vector. That is the whole reason these attacks work.
# SageMath - the one line you will type most often
M = Matrix(ZZ, [
[b00, b01, b02],
[b10, b11, b12],
[b20, b21, b22],
])
reduced = M.LLL() # rows of `reduced` are a reduced basis
print(reduced[0]) # usually the shortest vectorLLL works on integer matrices. To use rationals or to weight some coordinates more than others, scale columns by a large constant (a "diagonal weight") so the quantity you care about dominates the norm; divide back out afterwards. This column-scaling trick is the secret sauce in HNP and Coppersmith lattices.
For tougher instances (higher dimension, smaller gap), use BKZ with a block size, which interpolates between LLL (block size 2) and full enumeration:
reduced = M.BKZ(block_size=20) # slower, stronger; raise block_size if LLL failsThe Python library fpylll exposes the same engine outside Sage and is what fplll / many PoCs call under the hood.
Coppersmith Small Roots
Coppersmith's method finds small integer roots of a polynomial modulo (or modulo an unknown divisor of ). It is the lattice engine behind the RSA stereotyped-message, partial-key, and Boneh-Durfee attacks.
Univariate: roots mod N
Given a monic of degree and a modulus , Coppersmith finds all roots with
The idea: build a lattice of polynomials that all vanish at modulo . A short vector in that lattice is a polynomial with small enough coefficients that holds over the integers, not just mod - so its integer roots (found with ordinary root-finding) include .
# SageMath - built in to PolynomialRing over Zmod(N)
N = ...
P.<x> = PolynomialRing(Zmod(N))
f = x^3 + a2*x^2 + a1*x + a0 # your monic polynomial, root x0 is small
f = f.monic()
roots = f.small_roots(X=2**200, beta=1.0, epsilon=1/20)
print(roots)The knobs:
X- an upper bound on . LargerXneeds a bigger lattice.beta- the root is taken mod a divisor with . Usebeta=1.0for roots mod itself;beta≈0.5when factoring given high bits of a prime.epsilon- controls lattice size vs. the achievable bound; smaller is stronger but slower.
The general bound for roots modulo a divisor is
which is exactly the "you need about half the bits of " rule for factoring: with and you can fill in up to .
Bivariate and beyond
small_roots is univariate-only. For two variables (Boneh-Durfee, short-pad, "two leaks"), use the well-known community helper defund/coppersmith, which builds the multivariate lattice and reduces it for you:
# SageMath - defund/coppersmith style multivariate small_roots
load("coppersmith.sage")
P.<x, y> = PolynomialRing(Zmod(N))
f = x*(N + 1 + y) + 1 # e.g. the Boneh-Durfee relation mod e
roots = small_roots(f, bounds=(X, Y), m=4, d=4)
print(roots)Bivariate Coppersmith is a heuristic - the two recovered polynomials must be algebraically independent for the resultant step to give a real root. If it returns nothing, raise the lattice parameters m/d, tighten your bounds, or re-derive the polynomial so the unknowns are as small as possible. Tighter bounds beat bigger lattices.
Hidden Number Problem (Biased ECDSA Nonces)
The Hidden Number Problem (HNP) is the single most lucrative lattice attack in CTF crypto, because it breaks ECDSA (and DSA) whenever the per-signature nonces are short or biased. It cross-links directly with ECC Attacks.
Recall the ECDSA signing relation, with secret key , nonce , hash :
Write and , so . If every nonce is small (say only the low bits are nonzero, ), then is the hidden number and each signature is an approximate equation with a small residue .
Stack these into a lattice. With signatures, build the basis
where scales the nonce coordinates. The lattice contains the vector , which is short precisely because every is small - so LLL surfaces it, and the last meaningful coordinate hands you .
# SageMath - HNP for short ECDSA nonces (top bits of k are zero)
# sigs: list of (h, r, s); n: group order; KB: bound on the nonce (e.g. 2^ell)
def recover_d(sigs, n, KB):
m = len(sigs)
t = [(Integer(s).inverse_mod(n) * r) % n for (h, r, s) in sigs]
u = [(Integer(s).inverse_mod(n) * h) % n for (h, r, s) in sigs]
B = Matrix(QQ, m + 2, m + 2)
for i in range(m):
B[i, i] = n
for i in range(m):
B[m, i] = t[i]
B[m + 1, i] = u[i]
B[m, m] = QQ(KB) / n
B[m + 1, m + 1] = KB
for row in B.LLL():
# look for the row whose last entry is +/- KB: it encodes -d
if abs(row[-1]) == KB:
d = (-row[-2] * n / KB) % n if False else None
# robust path: try every short row as a candidate for d directly
# In practice: read d from the vector, then verify Q == d*G.
return B.LLL()
# A clean, ready-made implementation lives in the lbumns / "lattice-based-cryptanalysis"
# and Cryptohack toolkits; the construction above is the standard EHNP/HNP form.How many signatures? Each leaked bit of bias is worth one signature's worth of constraint. A 1-bit MSB bias on a 256-bit curve needs on the order of a few hundred signatures; a strong bias (e.g. 8 known top bits) needs only a handful. If LLL fails, collect more signatures or switch to BKZ.
For the related case where the nonces themselves are partly known (leaked top bits rather than zero), the same lattice works with shifted by the known part; this is the Extended HNP (EHNP). The dedicated lattice-based-cryptanalysis repo packages both.
Knapsack / Subset-Sum
A subset-sum (knapsack) cipher hides a message bit-vector in a public weight set via . Recovering from is the subset-sum problem. When the density is low (, the Lagarias-Odlyzko / Coster-et-al bound), LLL solves it almost always.
Build the lattice
with a large multiplier . The short vector has tiny last coordinate (the subset-sum constraint is satisfied) and entries, so it is unusually short.
# SageMath - Coster-LaMacchia-Odlyzko-Schnorr-Stern low-density subset-sum
def solve_knapsack(a, S):
n = len(a)
N = ceil(sqrt(n)) + 1 # any sufficiently large weight works
B = Matrix(QQ, n + 1, n + 1)
for i in range(n):
B[i, i] = 1
B[i, n] = N * a[i]
for j in range(n):
B[n, j] = QQ(1) / 2
B[n, n] = N * S
for row in B.LLL():
bits = row[:n]
if all(x in (QQ(-1)/2, QQ(1)/2) for x in bits):
e = [0 if x == QQ(1)/2 else 1 for x in bits]
if sum(ei * ai for ei, ai in zip(e, a)) == S:
return e
# also try the complemented vector
e = [1 - b for b in e]
if sum(ei * ai for ei, ai in zip(e, a)) == S:
return e
return NoneThe classic Merkle-Hellman cryptosystem builds a superincreasing private knapsack and disguises it with modular multiplication; the Shamir attack (and the low-density LLL attack above) break it.
Approximate GCD
The Approximate-GCD problem underlies some homomorphic-encryption toy schemes and shows up directly in CTFs: you are given several near-multiples of a hidden , with small noise , and must recover . With exact multiples () it is just ; with noise, build a lattice.
For two samples with , the lattice
has a short vector ; reducing it reveals , and then rounded. With more samples, extend to an -dimensional "orthogonal lattice" version for robustness.
# SageMath - approximate GCD from many noisy multiples x_i = q_i*p + r_i
def approximate_gcd(xs, rho):
m = len(xs)
B = Matrix(ZZ, m, m)
B[0, 0] = 2**(rho + 1)
for i in range(1, m):
B[0, i] = xs[i]
B[i, i] = -xs[0]
short = B.LLL()[0]
q0 = short[0] // 2**(rho + 1)
p = round(xs[0] / q0)
return pThe parameter (the bit length of the noise) is the lever: if the challenge leaks noise much smaller than , a small lattice suffices. The orthogonal-lattice attack scales this to many samples and is the standard CTF approach when two samples are not enough.
Picking the Right Lattice
| You are given | Build a lattice over | Attack |
|---|---|---|
| Polynomial with a small root mod | shifts | Coppersmith univariate (small_roots) |
| Two small unknowns mod | multivariate shifts | Coppersmith bivariate (defund/coppersmith) |
| Many ECDSA sigs, short/biased | rows + scaled diagonal | HNP / EHNP |
| Subset-sum, low density | identity + weighted sum column | CLOS low-density knapsack |
| Noisy multiples of hidden | + sample columns | approximate-GCD |
| "Find small with " | rows , | direct short-vector search |
When in doubt: write the unknowns as a vector, find an integer relation they satisfy, scale the coordinates so the secret vector is short, and LLL.
Checklist
- Can the secret be written as a short vector of an integer relation? Then it is a lattice problem.
- Small root of a polynomial mod ? Coppersmith
small_roots(setX,beta,epsilon). - Factoring with ~half the bits of ? Coppersmith with
beta≈0.5. - Two or more small unknowns? defund/coppersmith multivariate.
- Short or biased ECDSA/DSA nonces across many signatures? HNP lattice, then verify .
- Subset-sum with density ? CLOS low-density knapsack lattice.
- Noisy near-multiples of a hidden modulus? approximate-GCD lattice.
- LLL returns nothing useful? scale columns harder, tighten bounds, collect more samples, or switch to BKZ.
See Also
- RSA Attacks - Coppersmith, Boneh-Durfee, partial-key exposure built on these lattices.
- ECC Attacks - the biased-nonce HNP attack in its native ECDSA setting.
- ECC Basics - ECDSA signing relation used by the HNP construction.
- RSA Basics - the modular arithmetic underpinning Coppersmith.
Last updated on
ECC Attacks
A working catalogue of elliptic-curve attacks for CTF crypto: ECDLP, Pohlig-Hellman on smooth order, MOV/Frey-Ruck pairing reduction, Smart's anomalous-curve attack, singular curves, invalid-curve and small-subgroup, twist attacks, ECDSA nonce reuse and biased-nonce HNP.
Diffie-Hellman Attacks
Break weak Diffie-Hellman key exchanges in CTF crypto challenges - small subgroup, logjam, and discrete log.