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 .
Setup
Download message.txt and encryption.py.
Inspect the RSA parameters: n, e, and ciphertext c.
cat message.txtcat encryption.pySolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Identify the small-private-exponent (Wiener) conditionObservationI noticed that e is nearly the same size as n in the message.txt parameters, which is a classic indicator that d was chosen to be small and suggested a small-private-exponent attack rather than attempting to factor n directly.Title hint: 'small' is d. Here d is a 256-bit prime while N is ~2000 bits. Wiener's attack requires d < N^(1/4)/3; for a 2000-bit N that ceiling is roughly 498 bits (N^(1/4) is 500 bits), so a 256-bit d sits comfortably inside the bound and continued fractions recover d directly. The flag itself confirms the intended technique: picoCTF{w13n3r_4tt4ck_...}. (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 a 2000-bit N,N^(1/4)is about 500 bits, so the ceiling is roughly 498 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 584 bits for a 2000-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 2
Recover d with Wiener's continued-fraction attackObservationI noticed that d is a 256-bit prime and N is approximately 2000 bits, placing d well within Wiener's bound (~498 bits), which indicated that the continued-fraction convergents of e/N would expose k/d and recover 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{w13n3r_4tt4ck_...}'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 challenge needs. Wiener's bound for a 2000-bit N is roughly N^(1/4)/3 ~ 498 bits, and d here is only 256 bits, so the simple continued-fraction attack recovers d directly. The flag (picoCTF{w13n3r_4tt4ck_...}) confirms Wiener is the intended path. Boneh-Durfee would also succeed (its N^0.292 bound subsumes Wiener's), but setting up SageMath and a lattice script is wasted effort here.
Tried: Try to factor N directly with a tool like factordb or msieve before checking the exponent size.
N is a product of two strong ~1000-bit primes, so direct factorization is infeasible. The vulnerability is not in N at all; it is the deliberately small private exponent d. Spotting that e is nearly as large as N (a small-d fingerprint) points you to Wiener's attack, which never factors N - it derives d from the continued-fraction structure of e/N.
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{w13n3r_4tt4ck_...}
The RSA private exponent d is a 256-bit prime, well inside Wiener's bound (d < N^(1/4)/3, roughly 498 bits for a 2000-bit N), so the continued-fraction attack recovers d from the public key. The flag itself names the technique.
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.