Description
An RSA challenge where the prime p was generated with a 'smooth' p-1 value - all prime factors of p-1 are small. This makes N vulnerable to Pollard's p-1 factoring algorithm.
Pollard's p-1 exploits the structure: if p-1 is B-smooth (all prime factors <= B), then for any base a, a^(B!) ≡ 1 (mod p), and gcd(a^(B!) - 1, N) = p.
Download the challenge files to get N and e.
Implement Pollard's p-1 in Python to factor N.
Use p and q to compute the private key d and decrypt the ciphertext.
wget https://artifacts.picoctf.net/c/195/gen.pycat gen.pypip install pycryptodomeSolution
Walk me through it- Step 1Understand the smooth p-1 weaknessIf all prime factors of p-1 are small (say <= 10^6), then B! is divisible by p-1. By Fermat's little theorem, a^(p-1) ≡ 1 (mod p), so a^(B!) ≡ 1 (mod p), meaning p divides a^(B!)-1.
Learn more
Fermat's Little Theorem: For prime p and any a not divisible by p:
a^(p-1) ≡ 1 (mod p).A number M is B-smooth if every prime factor of M is at most B. If p-1 is B-smooth, then p-1 divides B! (because B! contains every prime up to B raised to a high power). Therefore
a^(B!) ≡ a^(k*(p-1)) ≡ (a^(p-1))^k ≡ 1 (mod p), sop | a^(B!) - 1. As long as q-1 is NOT B-smooth, q does not dividea^(B!) - 1, andgcd(a^(B!) - 1, N) = pexactly.Toy walkthrough with N = pq, p = 331, q = 271: p - 1 = 330 = 2 * 3 * 5 * 11 (all small -> 11-smooth) q - 1 = 270 = 2 * 3^3 * 5 (also smooth, but suppose only p-1 were) Use bound B = 11. Then 11! is a multiple of 330 = p-1. Compute a = 2^(11!) mod N incrementally: a = 2 a = a^2 mod N (now 2^2) a = a^3 mod N (now 2^6) a = a^4 mod N (now 2^24) ... a = a^11 mod N (now 2^(11!) mod N) At each step, check g = gcd(a - 1, N). As soon as the running exponent becomes a multiple of p-1 = 330, we have a ≡ 1 (mod p) but a != 1 (mod q), so gcd(a - 1, N) returns p (a non-trivial factor of N).The crucial efficiency trick: a^(B!) mod N is built incrementally - one short modular exponentiation per j - so we never construct the astronomical number B! itself. Each step is
a = pow(a, j, N), which uses square-and-multiply in O(log j * log^2 N) bit operations. - Step 2Implement Pollard's p-1Start with a=2 and repeatedly compute a = a^j mod N for j=2..B. After each step, check if gcd(a-1, N) is a non-trivial factor.python
python3 -c " from math import gcd # Replace with actual values from the challenge files n = 0xYOUR_N_VALUE e = 65537 c = 0xYOUR_CIPHERTEXT def pollard_p_minus_1(n, B): a = 2 for j in range(2, B + 1): a = pow(a, j, n) # accumulate: after iter j, a = 2^(j!) mod n if j % 10000 == 0 or j == B: g = gcd(a - 1, n) if 1 < g < n: return g return None for B in (10**4, 10**5, 10**6, 10**7): print(f'Trying B = {B}') p = pollard_p_minus_1(n, B) if p is not None: q = n // p print(f'Found: p = {p}') print(f' q = {q}') phi = (p-1)*(q-1) d = pow(e, -1, phi) flag_int = pow(c, d, n) print('Flag:', flag_int.to_bytes((flag_int.bit_length()+7)//8, 'big').decode()) break else: print('no smooth factor found up to B = 10^7; switch attacks (Fermat, Wiener, ECM)') "Learn more
Why the loop is one accumulator, not pow each time. The standard Pollard p-1 loop computes
a = 2^(B!) mod nincrementally: after iterationj, the running value ofaequals2^(j!) mod n, becausea = pow(a, j, n)raises the previous2^((j-1)!)to thej-th power. This matches the toy walkthrough above. Recomputingpow(a, j!, n)from scratch each step would multiply the work by anotherlog(j)factor for nothing.Heuristic for choosing B. Start at
B = 10^4. If the loop completes without finding a non-trivial factor, double an order of magnitude:10^5, then10^6, then10^7. ByB = 10^7the prime is unlikely to be smooth at all, and you should switch attacks (Fermat for close primes, Wiener for small private exponent, ECM for medium-smooth factors). The script above iterates this ladder for you.Non-trivial-factor check. The
1 < g < nguard is essential:gcd(a - 1, n) = 1means we have not yet hit a multiple of p-1, andgcd(a - 1, n) = nmeans both p-1 and q-1 are smooth and we wrapped past both factors. The script returnsNonein those cases instead of failing silently.The algorithm runs in O(B * log(B) * log^2(N)) time, which is very fast for small B. Checking gcd every 10,000 steps rather than every step is an optimization: the gcd check is cheap, but checking every iteration still adds visible overhead at large B.
The
pow(a, j, n)call uses Python's built-in three-argument modular exponentiation, which uses fast square-and-multiply and stays within modular bounds. This is far faster than computing the full exponentiation then taking mod. - Step 3Decrypt the ciphertext with the recovered factorsWith p and q known, compute phi(N) = (p-1)(q-1), then d = e^-1 mod phi(N), then flag = c^d mod N.python
python3 -c " from Crypto.Util.number import long_to_bytes p = 0xYOUR_P q = 0xYOUR_Q e = 65537 c = 0xYOUR_CIPHERTEXT n = p * q phi = (p-1) * (q-1) d = pow(e, -1, phi) m = pow(c, d, n) print(long_to_bytes(m).decode()) "Learn more
Standard RSA decryption:
m = c^d mod Nwhered = e^-1 mod phi(N)andphi(N) = (p-1)(q-1). Python 3.8+ supportspow(e, -1, phi)for modular inverse directly.To prevent Pollard's p-1 attack, RSA key generation must ensure that p-1 (and q-1) have at least one large prime factor - ideally close to the size of p itself. Such primes are called safe primes: p is safe if p = 2q + 1 where q is also prime. OpenSSL's
genrsacommand generates safe primes by default for this reason.
Flag
picoCTF{...}
gen.py constructs primes p, q where p-1 and q-1 are smooth (each factor is a product of 16-17 bit primes plus 1). Sieve primes up to ~200000, build the prime-power exponent ladder, and Pollard's p-1 finds p in seconds. Standard RSA decryption then yields the flag.