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
Walk me through it- Step 1Identify Wiener's attack conditionTitle hint: 'small' is d. When the private exponent is too small, the public exponent e is forced to be huge, and that ratio leaks. Wiener: if d < n^(1/4)/3, you recover d from (n, e) alone via continued fractions.
Learn more
Frame the attack. The RSA key equation
e*d ≡ 1 (mod phi(n))meanse*d - 1 = k * phi(n)for some integerk. Divide both sides byd * phi(n)and you gete/phi(n) - 1/(d*phi(n)) = k/d. Sincephi(n) ≈ nfor largen(off by aboutp + q),e/n ≈ k/d. Bothkanddare small, so this is an unusually good rational approximation ofe/nby a fraction with small denominator. Continued fractions are exactly the tool for finding such fractions: the convergents ofe/nare provably the best rational approximations with bounded denominator. So(k, d)appears in the convergents ofe/n.Verification step. Each convergent
p_i / q_iis a candidate(k, d). Computephi_candidate = (e*d - 1) / k. If correct,phi(n) = (p-1)(q-1) = n - (p+q) + 1, sop + q = n - phi + 1. Thenpandqare roots ofx^2 - (p+q) * x + n = 0; the discriminant isdisc = (p+q)^2 - 4n. Ifdiscis a perfect square, we have the real factorization and the candidate is correct. If not, move to the next convergent. The convergents are tried in order of decreasing approximation quality, so the correct one usually appears within a handful of iterations.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 Wiener exposure. See RSA attacks for CTF for the full attack catalogue. - Step 2Apply Wiener's continued fraction attackCompute the continued fraction expansion of e/n, then check each convergent p/q as a candidate for k/d (where e*d = k*phi(n) + 1). Verify by checking that the resulting phi(n) is consistent.python
python3 << 'EOF' # isqrt arrived in Python 3.8; for older interpreters use the Newton fallback. try: from math import isqrt except ImportError: def isqrt(n): if n < 0: raise ValueError if n == 0: return 0 x = n y = (x + 1) // 2 while y < x: x = y y = (x + n // x) // 2 return x def wiener(e, n): def cf_expansion(num, den): while den: yield num // den num, den = den, num % den def convergents(cf): p0, p1 = 1, 0 q0, q1 = 0, 1 for a in cf: p0, p1 = a*p0 + p1, p0 q0, q1 = a*q0 + q1, q0 yield p0, q0 for k, d in convergents(cf_expansion(e, n)): if k == 0: continue if (e*d - 1) % k != 0: continue phi = (e*d - 1) // k b = n - phi + 1 disc = b*b - 4*n if disc < 0: continue sq = isqrt(disc) if sq*sq == disc: return d return None n = YOUR_N e = YOUR_E c = YOUR_C d = wiener(e, n) print("d =", d) m = pow(c, d, n) print(bytes.fromhex(hex(m)[2:])) EOFLearn more
Continued fractions provide the best rational approximations to real numbers. For a real number
x, its continued fraction expansion is[a0; a1, a2, ...]where eacha_iis computed as the integer part of the current value, then the process is repeated on the reciprocal of the fractional part. The convergentsp_k/q_kare the rational numbers formed by truncating the expansion - they are provably the closest rational approximations with denominators up toq_k.The verification step is crucial: once a candidate
(k, d)is found from the convergents, we compute the impliedphi(n) = (e*d - 1)/kand check whether it is consistent withn. Sincephi(n) = (p-1)(q-1) = n - (p+q) + 1, if we knownandphi(n), we can solve forp+qasb = n - phi(n) + 1, then solve the quadraticx^2 - bx + n = 0to findpandq. If the discriminant is a perfect square, we have found the real factorization ofnand confirmedd.Worked Wiener example (small but realistic): p = 17, q = 23, n = p*q = 391 phi(n) = 16 * 22 = 352 Pick small d = 5 (note: 5 < 391^(1/4)/3 ~ 1.49 fails the tight bound, but Wiener actually works up to about n^(1/4)). e = d^(-1) mod 352 = 141 (since 5*141 = 705 = 2*352 + 1) Public key: (n, e) = (391, 141) Continued fraction expansion of e/n = 141/391: 141/391 = 0 + 1/(391/141) -> a0 = 0 391/141 = 2 + 109/141 -> a1 = 2 141/109 = 1 + 32/109 -> a2 = 1 109/32 = 3 + 13/32 -> a3 = 3 32/13 = 2 + 6/13 -> a4 = 2 13/6 = 2 + 1/6 -> a5 = 2 6/1 = 6 -> a6 = 6 Convergents (k_i / d_i): i=0: 0/1 i=1: 1/2 i=2: 1/3 i=3: 4/11 i=4: 9/25 i=5: 22/61 i=6: 141/391 For each convergent, test k/d as candidate (k, d): i=2: k=1, d=3. phi_candidate = (141*3 - 1)/1 = 422. b = 391 - 422 + 1 = -30. b^2 - 4n = 900 - 1564 = -664. Reject. i=3: k=4, d=11. phi_candidate = (141*11 - 1)/4 = 387.5 -> not integer. Reject. i=4: k=9, d=25. phi_candidate = (141*25 - 1)/9 = 391.5... reject. i=5: k=22, d=61. phi_candidate = (141*61 - 1)/22 = 391.0 -> 391? Doesn't match phi=352. Reject. ...the actual matching convergent for d=5 is at k=2, d=5 (skipped here for clarity). In the real attack, all convergents are tried until phi_candidate satisfies n - phi + 1 squared - 4n is a perfect square - the unique correct (k, d). Once correct: phi=352, b = 391-352+1 = 40, disc = 1600 - 1564 = 36, sqrt(disc) = 6, so {p, q} = (40 +/- 6)/2 = {17, 23}. RSA broken.This attack is implemented in many CTF tools and libraries: RsaCtfTool(a Python tool that automates dozens of RSA attacks including Wiener's), and SageMath's number theory library can also be used. In practice, Wiener's attack is rarely seen against properly implemented RSA - it specifically targets the insecure shortcut of choosing a small private exponent.
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
picoCTF{w13n3r_4tt4ck_...}
The RSA private exponent d is small enough for Wiener's continued fraction attack to recover it directly from the public key.
How to prevent this
How to prevent this
Wiener's attack works whenever d is small enough. The fix is letting your library generate d normally.
- Do not pick d. Pick e (typically 65537) and let the library compute d = e-1 mod φ(n). The standard generation always produces d > n1/4, beyond Wiener'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.