six seven again — LA CTF 2026 Crypto Writeup
Category: Crypto
Difficulty: Medium
Points: 164
Solves: 114
Flag: lactf{n_h4s_1337_b1ts_b3c4us3_667+670=1337}
Solved by: Smothy @ 0xN1umb

"Six of one, seven of another... but Coppersmith don't care about the difference."
Challenge Description
six seven again — AVDestroyer LA CTF will take place on February 6 and February 7, 2026.
nc chall.lac.tf 31181
We're given chall.py — a textbook RSA setup, except p is generated in a very specific way. Each connection gives fresh n and c, so we need to solve it programmatically. Let's gooo.
TL;DR
RSA where p is a 201-digit prime made of 67 sixes, 67 random {6,7} digits, and 67 sevens. The unknown middle part is only ~220 bits — well within Coppersmith's N^(1/4) ≈ 334 bit bound. One small_roots() call and it's GG. Lattice go brrr.
Initial Recon
Download the source and take a look:
def generate_super_67_prime() -> int:
while True:
digits = ["6"] * 67
digits += [secrets.choice("67") for _ in range(67)]
digits += ["7"] * 67
test = int("".join(digits))
if isPrime(test, false_positive_prob=1e-12):
return test
p = generate_super_67_prime()
q = getPrime(670)
n = p * q
e = 65537
c = pow(m, e, n)So p is a 201-digit prime with a very constrained structure:
- First 67 digits: all
6s - Middle 67 digits: each randomly
6or7 - Last 67 digits: all
7s
And q is a standard 670-bit random prime. Classic RSA otherwise.
The first thing that jumps out: we know 2/3 of p's digits exactly. The only unknown is the middle 67 digits, and each of those can only be one of two values. That's only 2^67 possibilities — way too many to brute force, but suspiciously little entropy for a prime this size.
Step 1: Understanding the Prime Structure
Let's decompose p mathematically:
p = 666...6 × 10^134 + δ × 10^67 + 777...7
(67) (67)
Where δ is the unknown middle part. Each digit of δ (offset by 6) is either 0 or 1:
δ = Σ bᵢ·10^i where bᵢ ∈ {0,1} for i = 0,...,66
So δ_max = 111...1 (67 ones) = (10^67 - 1)/9 ≈ 10^66 ≈ 2^220
We can write: p = p₀ + δ × 10^67 where p₀ is completely known.
Step 2: The Coppersmith Insight
Here's where the magic happens. We have a linear polynomial modulo a factor of n:
f(x) = p₀ + 10^67 · x ≡ 0 (mod p)
with a small root x = δ where |δ| < X ≈ 2^220.
Coppersmith's theorem says: for a monic polynomial f(x) of degree d with a root modulo a factor b ≥ N^β of N, we can find roots bounded by |x| < N^(β²/d).
For our case:
d = 1(linear polynomial)β ≈ 0.5(sincep ≈ q ≈ √n)- Bound:
X < N^(0.25) ≈ 2^(1337/4) ≈ 2^334
Our unknown: δ < 2^220. Since 220 < 334, the bound is comfortably satisfied!
Step 3: Why 1337 Bits?
The flag basically spoils the joke, but it's still satisfying to verify:
| Component | Size |
|---|---|
p | ~667-668 bits (201 decimal digits) |
q | 670 bits (getPrime(670)) |
n = p × q | ~1337 bits |
667 + 670 = 1337. The whole challenge is a leet speak joke. Respect.
Step 4: The Solve
The actual solve is clean. Connect to the server, grab n and c, then run Coppersmith:
# Make the polynomial monic (required by SageMath)
P.<x> = PolynomialRing(Zmod(n))
mult_inv = pow(10**67, -1, n)
c_const = (p0 * mult_inv) % n
f = x + c_const
# Coppersmith magic — finds δ instantly
roots = f.small_roots(X=X, beta=0.499, epsilon=0.1)Under the hood, SageMath constructs a lattice from shift polynomials of f, runs LLL reduction, and extracts a short vector that encodes the small root. For a linear polynomial, the lattice is as small as 2×2 with the right parameters.
sage solve.sage
X bits: 220
n bits: 1337
Trying beta=0.499, eps=0.1...
Found roots: [110000011101110101010010011000010101001110110110101011000011111...]
Found p! (201 digits)
And just like that — p drops, q = n/p, standard RSA decryption, flag.
The Flag
lactf{n_h4s_1337_b1ts_b3c4us3_667+670=1337}
Breaking it down:
n_h4s_1337_b1ts→ n has 1337 bitsb3c4us3_667+670=1337→ because 667 + 670 = 1337
The Solve Script
#!/usr/bin/env sage
"""six seven again — LA CTF 2026
Coppersmith small roots on structured RSA prime
Smothy @ 0xN1umb"""
def long_to_bytes(n):
return n.to_bytes((n.bit_length() + 7) // 8, 'big')
n = <from server>
c = <from server>
e = 65537
# Known structure of p
sixes_67 = int("6" * 67)
sevens_67 = int("7" * 67)
p0 = sixes_67 * 10^134 + sixes_67 * 10^67 + sevens_67
mult = 10^67
X = (10^67 - 1) // 9 + 1 # ~2^220
# Monic polynomial: f(x) = x + p0/mult mod n
P.<x> = PolynomialRing(Zmod(n))
mult_inv = int(pow(int(mult), -1, int(n)))
c_const = (p0 * mult_inv) % n
f = x + c_const
# Coppersmith — finds the unknown middle digits
roots = f.small_roots(X=X, beta=0.499, epsilon=0.1)
for root in roots:
x0 = int(root)
p_cand = int(p0 + x0 * mult)
from math import gcd
g = gcd(p_cand, n)
if 1 < g < n:
p = g
q = n // p
phi = (p-1)*(q-1)
d = int(pow(e, -1, phi))
m_val = int(pow(c, d, n))
flag = long_to_bytes(m_val)
print(f"Flag: {flag}")The Graveyard of Failed Attempts
Attempt 1: fpylll without SageMath
Tried implementing Coppersmith manually using fpylll's LLL. Built the 2×2 lattice [[n, 0], [p0, mult*X]], ran LLL, tried extracting the root as x0 = -v0 * X / v1... got non-exact divisions every time. The lattice was finding short vectors, but root extraction from raw LLL output is finicky without SageMath's small_roots() doing the heavy lifting.
Attempt 2: Larger fpylll Lattices (Howgrave-Graham shifts)
Scaled up to 7×7, 9×9, 11×11 lattices with shift polynomials N^{m-i} * f(x)^i. LLL ran fine, but extracting polynomial roots from the reduced basis without a proper polynomial GCD implementation was painful. The polynomial root-finding over integers for degree > 1 needs either SymPy or SageMath.
Attempt 3: Neighborhood Search
After getting approximate roots from LLL, tried searching ±1000 around the approximation. Didn't find anything — the approximation wasn't close enough without proper Howgrave-Graham bounds checking.
The Fix: Docker SageMath
Realized the SageMath Docker image was already pulled. One docker run later, small_roots() found the root in under a second with beta=0.499, epsilon=0.1. Sometimes you just gotta use the right tool.
Key Takeaways
-
Structured primes = Coppersmith. If you know parts of a prime factor and the unknown is less than N^(1/4), Coppersmith's method is your best friend.
-
The monic polynomial requirement in SageMath's
small_roots()is a common gotcha. Divide by the leading coefficient mod n before calling it. -
Start with large epsilon.
epsilon=0.1creates a tiny lattice and runs fast. Only decrease it if you need to push closer to the theoretical bound. -
Don't fight without SageMath. For lattice-based crypto attacks, SageMath's
small_roots()saves hours of manual lattice construction. Keep a Docker image ready. -
Count the bits. 667 + 670 = 1337. The challenge author knew exactly what they were doing.
Tools Used
- SageMath (Docker:
sagemath/sagemath) — Coppersmithsmall_roots() - Python 3 + pwntools — Server interaction, PoW solving
- fpylll — Failed attempt at manual LLL (educational though)
- matplotlib — Screenshot generation
- Way too much caffeine
Writeup by Smothy from 0xN1umb team. Six of one, seven of another, but Coppersmith takes them all. GG.