Description
Everything seems secure; strong numbers, familiar parameters but something small might ruin it all. Can you recover the message? Download the message.txt and source encryption.py.
cat message.txtcat encryption.pySolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Identify the small-private-exponent (Wiener) condition
ObservationIn message.txt, e is nearly as large as n, which is the classic sign that d was chosen small. That points at a small-private-exponent attack rather than factoring.Title hint: 'small' is d. encryption.py builds N from two 1048-bit primes (2096 bits total) and then sets d = getPrime(256) and e = inverse(d, phi), which is the wrong way round and leaves e almost as large as N. Wiener's attack requires d < N^(1/4)/3; for this 2096-bit N that ceiling is about 522 bits (N^(1/4) is 524 bits), so a 256-bit d sits comfortably inside the bound and continued fractions recover d directly. (Boneh-Durfee, a stronger lattice attack reaching d < N^0.292, would also work since it subsumes Wiener's range, but it is not needed here.)Learn more
Why Wiener works here. Wiener's 1990 attack requires
d < N^(1/4)/3. For this 2096-bit N,N^(1/4)is 524 bits, so the ceiling is about 522 bits. The challenge's d is a 256-bit prime, well under that bound, so the continued-fraction convergents ofe/Nland onk/dand recover d directly.How it works. From
e*d - k*phi(N) = 1andphi(N) ≈ N, the fractione/Nis a very close approximation tok/d. The theory of continued fractions guarantees thatk/dappears among the convergents ofe/Nwhen d is small enough, so you enumerate the convergents and test each candidate denominator as d.If d were larger. Past Wiener's ~N^(1/4) ceiling, the Boneh-Durfee (1999) lattice attack extends recovery to
d < N^0.292(about 612 bits for this 2096-bit N) using Coppersmith/LLL basis reduction. It is the right tool only when d is too big for Wiener; this challenge does not need it.The right way to make RSA fast is the Chinese Remainder Theorem (CRT) optimization: keep
dnormal-sized, stored_p = d mod (p-1)andd_q = d mod (q-1), decrypt modpand modqseparately, recombine. Roughly 4x faster than computingm = c^d mod ndirectly, and no small-d exposure. See RSA attacks for CTF for the full attack catalogue.Step 2Recover d with Wiener's continued-fraction attack
Observationd is 256 bits against a 2096-bit modulus, comfortably inside Wiener's bound of about 522. The continued-fraction convergents of e over N expose the ratio that yields d from the public key alone.Wiener's attack is pure Python: expand e/N as a continued fraction, walk its convergents, and for each convergent k/d check whether the implied phi(N) yields an integer factorization of N. The owiener library (pip install owiener) does exactly this. Once d is recovered, decrypt with m = pow(c, d, n), convert the decimal m to hex (Python's hex(m)), then decode the hex bytes to read the flag.bashpip3 install owienerpythonpython3 -c " import owiener n = YOUR_N e = YOUR_E c = YOUR_C d = owiener.attack(e, n) print('recovered d =', d) m = pow(c, d, n) print(bytes.fromhex(hex(m)[2:])) "Expected output
b'picoCTF{sm4ll_d_...}'What didn't work first
Tried: Reach straight for the Boneh-Durfee lattice attack (SageMath + LLL) assuming Wiener's bound is too tight.
Boneh-Durfee is heavier machinery than this needs. Wiener's bound here is about 522 bits and d is 256, so continued fractions recover it directly in a second of pure Python. Boneh-Durfee would also work, since its bound subsumes Wiener's, but setting up Sage and a lattice script buys nothing.
Tried: Try to factor N directly with a tool like factordb or msieve before checking the exponent size.
N is the product of two strong 1048-bit primes, so factoring is out of reach. The weakness is not in N at all but in the deliberately small private exponent. An e nearly as large as N is that fingerprint, and Wiener's attack derives d from the continued fractions of their ratio without factoring anything.
Learn more
Why continued fractions? When
dis small, the fractione/Nis an extremely close rational approximation tok/d, wherekcomes frome*d - k*phi(N) = 1. A theorem of continued fractions guarantees that any sufficiently good rational approximation appears among the convergents, sok/dis one of the convergents ofe/N. Enumerate them and you find d.Verifying a candidate. For each convergent
k/d, computephi = (e*d - 1)/kand check it is an integer; then solve the quadraticx^2 - (N - phi + 1)x + N = 0for the roots p and q. If they multiply back to N, d is correct. This is exactly whatowiener.attack(e, n)automates.Decryption after recovery. Once d is recovered, compute
m = pow(c, d, n)in Python. The result is a large decimal integer. Convert it to hex withhex(m)(drop the leading0x), then decode the hex bytes to ASCII using CyberChef's "From Hex" recipe orbytes.fromhex(...)in Python.
Interactive tools
- RSA CalculatorDecrypt RSA ciphertexts, factor n from the sum of primes, or generate key parameters. Handles arbitrarily large BigInt values.
Alternate Solution
Once Wiener's attack recovers the private exponent d, plug N, e, d, and the ciphertext into the RSA Calculator on this site to perform the final decryption in the browser without writing any additional code.
Flag
Reveal flag
picoCTF{sm4ll_d_...}
The RSA private exponent d is a 256-bit prime, well inside Wiener's bound (d < N^(1/4)/3, about 522 bits for this 2096-bit N), so the continued-fraction attack recovers d from the public key: d = 68887774502271338957062311167447501492711325582360363047266665739903259761543. The flag is shown abbreviated on this page; work the steps above to recover the full value.
Key takeaway
How to prevent this
How to prevent this
Wiener's attack works whenever d is small enough relative to N. The fix is letting your library generate d normally.
- Do not pick d. Pick e (typically 65537) and let the library compute d = e-1mod φ(n). The standard generation always produces d far above Wiener's bound (and Boneh-Durfee's), beyond the attack's reach.
- Use a vetted library's key generator:
cryptography.hazmat, OpenSSL'sEVP_PKEY_keygen, or RustCrypto'sRsaPrivateKey::new. Hand-rolling any RSA parameter is a high-risk path. - Migrate off RSA where you can. Ed25519 / X25519 sidestep this entire family of attacks (Wiener, Coppersmith, common modulus, low-exponent) because the math is fundamentally different.