This is a continuous challenge. The first 2 challenges are in 0CTF 2021 Finals ezRSA ezRSA+.

I'll walkthrough all these three challenges and see the connection between them.

ezRSA

Challenge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from Crypto.Util.number import *
from secret import flag
from os import urandom

def gen(n_size, m_size):
alpha = 0.5
delta = 0.03
d_size = int(delta * n_size)
k_size = int((alpha + delta - 0.5) * n_size)
c_size = int(n_size * (1 - alpha - 2 * delta))
while True:
while True:
d_p = getRandomNBitInteger(d_size)
d_q = getRandomNBitInteger(d_size)
k = getRandomNBitInteger(k_size)
l = getRandomNBitInteger(k_size)
if GCD(k ,l) == 1 and GCD(d_p, k) == 1 and GCD(d_q, l) == 1:
break
e = inverse(d_p, k) * inverse(l, k) * l + inverse(d_q, l) * inverse(k, l) * k
c = getRandomNBitInteger(c_size)
e += c * k * l
assert e * d_q % l == 1
assert e * d_p % k == 1
p = (e * d_p - 1) // k + 1
q = (e * d_q - 1) // l + 1
if isPrime(p) and isPrime(q):
magic = 1337 * k ** 4 + 7331 * l ** 3 + 73331 * k ** 2 + 13337 * l ** 2 + 7 * k * l + 2 * k + l
mask = 2 ** m_size - 1
return (p * q, e), (d_p, d_q, p, q), (magic, d_p & mask, d_q & mask)

def encrypt(m, pk):
n, e = pk
return pow(m, e, n)

n_size = 2000
m_size = 10
pk, sk, hint = gen(n_size, m_size)
flag = urandom(n_size // 8 - len(flag) - 1) + flag
enc = encrypt(int(flag.hex(), 16), pk)

print(pk)
print(hint)
print(enc)
'''
(13144833961692953638155744717380612667335058302310815242506755676885208234342620331186804951145894484501542968789132832800279633590988848298405521677820600481054741175400784558190943019903268095468121342412114428860754522164657102624139527993254089574309927288457799155130004731846999722554981630609692264462023821778810225493633789543259034893395115658330417361250466876018981150507377427664192443342394808337473089411393262018525828475108149889915075872592673448211565529063972264324533136645650169687118301014325354524932405270872098633633071371124551496573869700120350489760340226474892703585296623, 4976865541630914024304930292600669330017247151290783019063407119314069119952298933566289617702551408322779629557316539138884407655160925920670189379289389411163083468782698396121446186733546486790309424372952321446384824084362527492399667929050403530173432700957192011119967010196844119305465574740437)

(154118536863381755324327990994045278493514334577571515646858907141541837890, 431, 217)

12075538182684677737023332074837542797880423774993595442794806087281173669267997104408555839686283996516133283992342507757326913240132429242004071236464149863112788729225204797295863969020348408992315952963166814392745345811848977394200562308125908479180595553832800151118160338048296786712765863667672764499042391263351628529676289293121487926074423104988380291130127694041802572569416584214743544288441507782008422389394379332477148914009173609753877263990429988651290402630935296993764147874437465394433756515223371180032964253037946818633821940103044535390973722964105390263537722948112571112911062
'''

Solution

Let's look at the running part first. With n_size = 2000, m_size = 10, pk,sk,hint are generated by calling gen(n_size, m_size). Thus, we look at this function in details.

Part 1 Parameters: \[ \begin{aligned} \alpha &= 0.5, \\ \delta &= 0.03, \\ d_s &= \delta \cdot n_s = 60, \\ k_s &= (\alpha+\delta-0.5)\cdot n_s = 60, \\ c_s &= n_s \cdot (1-\alpha-2\cdot \delta)=880. \end{aligned}\]

Part 2 Condition on parameters: \(d_p, d_q\) are random \(d_s=60\) bits integer; \(k,l\) are random \(k_s=60\) bits integer. And we have the constraint: \[ \gcd(k,l) = \gcd(d_p,k) = \gcd(d_q,l)=1. \]

Part 3 Condition on \(e\): observing line 19, \(l\) and \(k\) are factors of two components in RHS equation. Thus, we can actually \(\bmod l\) and \(\bmod k\) to obtain the condition on Chinese Remainder Theorem (CRT): \[ \left\{ \begin{aligned} e &\equiv d_q^{-1} \bmod l\\ e &\equiv d_p^{-1} \bmod k \end{aligned} \right. \] \(c\) is a random \(c_s = 880\) bits integer (very large). And \(e = e + ckl\). (will not change the property of \(\bmod l\) or \(\bmod k\).)

Then calculate \(p,q\) as below: \[ \begin{aligned} p &= \lfloor\frac{e\cdot d_p - 1}{k}\rfloor +1,\\ q &= \lfloor\frac{e\cdot d_q - 1}{l}\rfloor +1. \end{aligned} \] \(p,q\) are prime.

Part 4 Generate and return:

pk: \((pq, e)\)

sk: \((d_p, d_q, p, q)\)

hint: (magic, d_p & mask, d_q & mask)

where: \[ \begin{aligned} \text{magic}&=1337\cdot k^{4} + 7331 \cdot l^3 + 73331 \cdot k^2 + 13337 \cdot l ^2 + 7 \cdot k \cdot l + 2 \cdot k + l,\\ \text{mask}&= 2^{m_s} - 1. \end{aligned} \] This indicates that d_p & mask and d_q & mask are last \(m_s=10\) bits of \(d_p,d_q\).

We also want to look at how the program encrypts data:

input: \((m,pk=(pq, e))\) output: \[ c = m ^ e \bmod n \]

Now, let's look at the main function again.

flag is imported from secret, and is concatenated by a random string with length related to flag itself. Finally, we encrypt the hexed flag with public key.

Our solution will be divided into 3 parts: 1) Finding \(k,l\); 2) Using \(k,l\) and conditions on \(d_p,d_q\) to find \(d_p, d_q\); 3) Using \(d_p,d_q\) to find \(p,q\); 4) decrypt

Step 1: Finding \(k,l\)

Expected solution is posted on here. My attempt also starts from taking the 4th root of magic then iterates a small range of integers, but I know this quite depends on luck. The idea is: \[ k'=(k^4+\frac{res}{1337})^{1/4}>k\text{, but also } k'\approx k, \] Thus, we let \[ \hat{k}=k'-i,i\in\text{ a small range of integers}. \] But the problem is: how small should this integer set be? For an accurate solution, we need further investigation on the value of \(k'-k\), which probably involves property of inequality of \((a^n+b^n)^{1/n} \leq a+b\).

Proof of this inequality is simple, involving binomial expansion: \[ \begin{aligned} (a^{1/n}+b^{1/n})^{n} &= a + \sum_{i=1}^{n-1}C_n^{i}(a^{1/n})^i(b^{1/n})^{n-i}+b\\ &\geq a+b=((a+b)^{1/n})^n \end{aligned} \] \[ \begin{aligned} &\Longrightarrow (a+b)^{1/n}\leq a^{1/n}+b^{1/n}\\ &\Longrightarrow (a^n+b^n)^{1/n}\leq a+b. \end{aligned} \] Thus, let \(r = (\frac{res}{1337})^{1/4}\), we have: \[ k'=(k^4+r^4)^{1/4} \leq k+r \] The upper bound of \(k'-k\) is \(r\).

Since \(l\) is 60 bits, the range is actually very large. So I'm not sure why the official solution succeed from a theoretical perspective. But I can neither find another way to solve this challenge. I just adapt the way it uses. So the way is to have: \[\hat{k}=k'-i\text{ and }\hat{l} = (\frac{\text{magic}-1337\hat{k}^4}{7331})^{1/3},\] and iterate them in two loop until find a pair of \((\hat{k},\hat{l})\) such that: \[ 1337\cdot \hat{k}^{4} + 7331 \cdot \hat{l}^3 + 73331 \cdot \hat{k}^2 + 13337 \cdot \hat{l} ^2 + 7 \cdot \hat{k} \cdot \hat{l} + 2 \cdot \hat{k} + \hat{l}. \] And this is what we want for \((k,l)\).

Note:

When we write code to calculate \(k'\), pow(a, e, m) will not work for such large calculation. Thus, we have to use gmpy2 and iroot function.

Step 2: Finding \(d_p,d_q\)

We go back to line 19 and see the equation of \(e\). When we \(\bmod k\) and \(\bmod l\) obtained in step 1, we can obtain \(d_p^{-1},d_q^{-1}\), then we can use inverse function to obtain \(d_p,d_q\).

Step 3: Finding \(p,q\)

Look at equation of \(p,q\). Once we have \(d_p,d_q\), we can simply plug in all the values and obtain \(p,q\), or we can use \(n=pq\) to calculate the other factor for convenience.

Now we demonstrate the solution code.

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from math import gcd
from Crypto.Util.number import *
from z3 import *
import gmpy2

N, e = (13144833961692953638155744717380612667335058302310815242506755676885208234342620331186804951145894484501542968789132832800279633590988848298405521677820600481054741175400784558190943019903268095468121342412114428860754522164657102624139527993254089574309927288457799155130004731846999722554981630609692264462023821778810225493633789543259034893395115658330417361250466876018981150507377427664192443342394808337473089411393262018525828475108149889915075872592673448211565529063972264324533136645650169687118301014325354524932405270872098633633071371124551496573869700120350489760340226474892703585296623, 4976865541630914024304930292600669330017247151290783019063407119314069119952298933566289617702551408322779629557316539138884407655160925920670189379289389411163083468782698396121446186733546486790309424372952321446384824084362527492399667929050403530173432700957192011119967010196844119305465574740437)
g, dp_mask, dq_mask = (154118536863381755324327990994045278493514334577571515646858907141541837890, 431, 217)
c = 12075538182684677737023332074837542797880423774993595442794806087281173669267997104408555839686283996516133283992342507757326913240132429242004071236464149863112788729225204797295863969020348408992315952963166814392745345811848977394200562308125908479180595553832800151118160338048296786712765863667672764499042391263351628529676289293121487926074423104988380291130127694041802572569416584214743544288441507782008422389394379332477148914009173609753877263990429988651290402630935296993764147874437465394433756515223371180032964253037946818633821940103044535390973722964105390263537722948112571112911062

def magic(k, l):
return 1337 * k ** 4 + 7331 * l ** 3 + 73331 * k ** 2 + 13337 * l ** 2 + 7 * k * l + 2 * k + l

k_approx = gmpy2.iroot(g//1337, 4)[0]

for i in range(50):
k_bar = k_approx - i
t1_approx = 1337 * k_bar ** 4
l_approx = gmpy2.iroot((g-t1_approx)//7331, 3)[0]
for j in range(50):
l_bar = l_approx-j
if magic(k_bar, l_bar) == g:
if gcd(k_bar, l_bar) == 1:
k = k_bar
l = l_bar
break

mask = 2 ** 10 - 1
d_q = inverse(e%l, l)
assert d_q & mask == dq_mask
q = (e * d_q - 1)//l + 1
p = N//q

d = inverse(e, (p-1)*(q-1))
m = pow(c, d, N)
print(m)
print(bytes.fromhex(hex(m)[2:]))

result:

1
2
3
4
b'\xcd]\xf8\xe7\x97\xe2\xfds\x1c\x84\xd3/\x8e\xba}\xaeU[_\x12a\x9f\x07+\xe7\x15L\x97\x8e\x99\x1f\n\xa1Lq\xf3\x06^\xa9\xc7\xf0\xeb\x93BNfHk\xc61t\xc0\x02\xcf\xaf[Z\x9dMOo"\xf2\xa5\xa9o\xb6l9\xab\xc3\xe7\x15O\r,
<~\xb51\xea\xfaD\xa7n\x80\xb7\x97\x83[\xfag\xe1\xc3\xa3+\x9b\x91\xf9\x16\xd6\xef}\xf9\xf7\xb5i\x06\xdd\x19\xef5\xc9\xd28(\x7f\'dp]\xd8\x85\x07\x911\xd0\xa7\x12\xb3X\xe0\xa7\x193j
7\x931\xe3\xc2\x0e\xf7B\xf2\xf6\x1a\x05&\r\x01\xa2\xe2dd\xb6\xbe\x9b\x81V\xaa\xf4\x99\xc7Wa\xbcf\xb9\xd4\xee\xee\'\xdb\x02\xdb\x83Y\xb9\xc1w\xf5\xb7\x8a\xb6\x19\xb3\x13\x0f\xe2H\xce\xe9\xcc\xb7\xf2\xc6\x8at
flag{laTTice_a77ack_on_RSA_h0VV_far_can_w3_Go???}'

Reference

This paper may also help, but I haven't read it: Finding Small Roots of Bivariate Integer
Polynomial Equations: a Direct Approa

ezRSA+

Challenge

The code is pretty much similar to the previous one, but 2 things changed:

  1. no magic any more;
  2. m_size = 12

Output from the challenge becomes:

1
2
3
N, e = (5943169364392579648240628105465400265561630477719849140342288893646282358845864829196464904298425034495515703590715696166689341849788423790118035115884268058450057766891418761627136386260375534474238287294722575087291704432681906513559934960801746158191355141430407698622886747610818853554584519369492697299961587570531415598410602926850787615266600194130088653542080297272898142822126215764285317511778718862825024939241749996811603610592132845393755564618871967716819173935553139267269362451423802419847919801412517294612793840767037662589233422389282092051866024514599213445596030685325227269609799, 1762727270442607836236621349004505613506359415168929966540357011133047321101203605340201016757203407375680206339341465088639129039278027484128644822299382121442456525803635369125157261704416172232695058332167310707999473221769390010733123910955508477009411113166141188309425895082608534271594076218827)
dp_m, dq_m = (1361, 1475)
c = 4531542437692818645025309324015912433184165181252393791711464775823247402127139569010657935303362440253951226047224610112010632226435610975118324658493911658016955717228741291266124372004503735693124810068041692730706167827666860062121413284053921548345924563903211959003376490264228814415500106847482893238907846512437694006785527867729127228361388592714377847012086312471372061723017560705890925568994607972832373362380226566506368932968108567819188587148829190811891555283157485379418388715910299951228404648535235675640369275644493614250037857212768450860647393276921502407057557208885073276533644

Solution

Although it just removed one line and changed m size, the influence is huge. We cannot obtain \(k,l\) from the magic any more. So our method to solve this problem should be completely changed. This is where lattice attack involves.

I have studied two methods to solve this, but I'll save the official one for ezRSA++ later. The method I used to solve this one is using LLL to get the values of \(k,l\), and the rest steps are pretty the same. This method is proposed by Bleichenbacher and May [^1][^2].

We still look back at equation for \(p,q\), which can be rewritten as: \[ \left\{ \begin{aligned} (p-1)k=ed_p-1\\ (q-1)l=ed_q-1\\ \end{aligned} \right. \] \[ \Longrightarrow \left\{ \begin{aligned} kp&=ed_p+(k-1)\\ lq&=ed_q+(l-1)\\ \end{aligned} \right. \] Multiply these two equations to obtain: \[ e^2X+eY+(1-N)Z = W, \] where: \[ \begin{aligned} X &= d_pd_q\\ Y &= d_p(l-1)+d_q(k-1)\\ Z &= kl\\ W &= k+l-1 \end{aligned} \] We write the above in the matrix form: \[ \begin{bmatrix} X & Y & Z \end{bmatrix} \cdot \begin{bmatrix} e^2 & 1 & 0 & 0 \\ e & 0 & 1 & 0 \\ 1-N & 0 & 0 & 1 \end{bmatrix} = \begin{bmatrix} W & X & Y & Z \end{bmatrix} \] Since \(d_p, d_q, k, l\) are not very large (only 60 bits), we can actually construct a lattice to solve this linear relationship and obtain the values of \(X,Y,Z,W\). Then, we put all the constraints into z3 solver and obtain \(d_p,d_q,k,l\).

The rest will be the same as ezRSA.

Note:

Here's one thing that's interesting. When I use LLL() from sage, it actually calculated and output \(\begin{bmatrix}-W & -X & -Y & -Z\end{bmatrix}\). So I have to add another step to make them positive.

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from sage.all import *
from z3 import *
from Crypto.Util.number import *

N, e = (5943169364392579648240628105465400265561630477719849140342288893646282358845864829196464904298425034495515703590715696166689341849788423790118035115884268058450057766891418761627136386260375534474238287294722575087291704432681906513559934960801746158191355141430407698622886747610818853554584519369492697299961587570531415598410602926850787615266600194130088653542080297272898142822126215764285317511778718862825024939241749996811603610592132845393755564618871967716819173935553139267269362451423802419847919801412517294612793840767037662589233422389282092051866024514599213445596030685325227269609799, 1762727270442607836236621349004505613506359415168929966540357011133047321101203605340201016757203407375680206339341465088639129039278027484128644822299382121442456525803635369125157261704416172232695058332167310707999473221769390010733123910955508477009411113166141188309425895082608534271594076218827)
dp_m, dq_m = (1361, 1475)
c = 4531542437692818645025309324015912433184165181252393791711464775823247402127139569010657935303362440253951226047224610112010632226435610975118324658493911658016955717228741291266124372004503735693124810068041692730706167827666860062121413284053921548345924563903211959003376490264228814415500106847482893238907846512437694006785527867729127228361388592714377847012086312471372061723017560705890925568994607972832373362380226566506368932968108567819188587148829190811891555283157485379418388715910299951228404648535235675640369275644493614250037857212768450860647393276921502407057557208885073276533644

M = Matrix(ZZ, [[ e*e, 1, 0, 0],
[ e, 0, 1, 0],
[ 1-N, 0, 0, 1]])

v = M.LLL()[0]
v0, v1, v2, v3 = [int(-v[i]) for i in range(4)]

sol = Solver()
dp, dq, k, l = Ints('dp dq k l')
# k, l = Ints('k l')
sol.add(k + l - 1 == v0)
sol.add(dp * dq == v1)
sol.add(dp * (l - 1) + dq * (k - 1) == v2)
sol.add(k * l == v3)

sol.check()

dp, dq, k, l = sol.model()[dp].as_long(), sol.model()[dq].as_long(), sol.model()[k].as_long(), sol.model()[l].as_long()

p = (e * dp - 1) // k + 1
q = N // p

d = inverse(e, (p-1)*(q-1))
m = pow(c, d, N)
print(m)
print(bytes.fromhex(hex(m)[2:]))

result:

1
b'O@]T<\xbazZ\xed\xfe\xfeQm>g\x0b\x9d9\xac\x14\xc2\x04\xf4/\xa5\xcb\x91\xc2\x82u]\xd3\x03\xec\xcc\xa64_\xbfko,\xe1a\x99G\xe0\xb8\xd6\xe2\x02\xe1T\xaan\x0b\xeeu\x9e\x12\xf2\x9bLS6\xf7\x91\xa0 v\x863\x80\xda.\xb0N\x8b\xcdl\x96\xc4\x00\xbb^uL\xa2\x13>%8+y\xd8{F\xdd\xc9\x0ct\xce\xd6\x01\xa1\xe8\x86!.d\x9d\x88\xd4"\x90\xd7\r\x1e{t\x14wp\xa4\x99\x83\xa1\x19\xaf\x1a\x00\xe8_\xd1\x8b\xa9S\xb8\x17\r\xf1{\x9a\xd8\xb7\x1eI\tT-[s\x8a)}\xb1i\xf2.7\x9e\xa2\xd9dyQL\x05}\xc8S]C\xef\x0c63u)v\x86\x19\xba\x93\xf2[\xb3\xa6\xe7E)\x9f\x1e\x9075]\x81\xfd,\xbc\xf1Xw\xfb\xf1\xf6\x86\xf9d\xf6m\xdcp\xafflag{s0rry_4_the_si11y_mis7ak3!!!}'

Reference

[^1] New Attacks on RSA with Small Secret CRT-Exponents: paper [^2] A Polynomial Time Attack on RSA with Private CRT-Exponents Smaller Than \(N^{0.073}\): paper

ezRSA++

Based on 0CTF 2021 Finals "ezRSA+", the intended solution is "Partial Key Exposure Attack on Short Secret Exponent CRT-RSA". Maybe the method in "Small CRT-exponent RSA revisited" also works

Challenge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from Crypto.Util.number import *
from os import urandom
from secret import flag

def ExGCD(a, b):
if 0 == b:
return 1, 0, a
x, y, q = ExGCD(b, a % b)
x, y = y, (x - a // b * y)
return x, y, q

def gen(p_size, d_size, l_size):
while True:
p = getPrime(p_size)
q = getPrime(p_size)
if GCD(p - 1, q - 1) == 2:
break
d_p = getPrime(d_size)
d_q = getPrime(d_size)
s, t, g = ExGCD(p - 1, q - 1)
if s < 0:
s += q - 1
else:
t += p - 1
n = p * q
phi = (p - 1) * (q - 1)
e = (inverse(d_p, p - 1) * t * (q - 1) + inverse(d_q, q - 1) * s * (p - 1)) // g % phi
assert (e * d_q % (q - 1) == 1)
assert (e * d_p % (p - 1) == 1)
k = (e * d_p - 1) // (p - 1)
l = (e * d_q - 1) // (q - 1)
return (n, e), (d_p, d_q, p, q), (d_p % (2**l_size), d_q % (2**l_size))

def encrypt(m, pk):
n, e = pk
return pow(m, e, n)

p_size = 1000
d_size = 105
l_size = 55
pk, sk, hint = gen(p_size, d_size, l_size)
flag = urandom(2 * p_size // 8 - len(flag) - 1) + flag
enc = encrypt(int(flag.hex(), 16), pk)
print(enc)
print(pk)
print(hint)

'''
35558284230663313298312684064040643811204702946900174110911295087662938676356112802781671547473910691476600838877279843972105403072929243674403244286458898562457747942651643439624568905004454158744508429126554955023110569348839934098381885098523538078300248638407684468503519326866276798222721018258242443186786917829878515320321445508466038372324063139762003962072922393974710763356236627711414307859950011736526834586028087922704589199885845050751932885698053938070734392371814246294798366452078193195538346218718588887085179856336533576097324041969786125752304133487678308830354729347735644474025828
(44774502335951608354043148360684114092901940301155357314508676399067538307546121753785009844275454594381602690061553832466871574728524408152400619047820736137949166290404514747591817206669966103047443912935755873432503095952914080827536130899968275165557303493867755627520568588808534411526058896791373252974606364861105086430729757064078675811147972536205086402752245214343186536177015741922559575575911278873118556603923689408629477875537332177644886701517140711134017511229202430437068095342526435886609381176269251580339549071944830141516001532825295594908434587285225415103472279090325281062442217, 29624366183227462965645558392954094074485353876807451497147549927093025197118051280445930543762170853769573962200247669305286333212410439624262142109295839433584663989554419810341266820063074908743295553517790354149623873028162282751352613333181218478850463012413786673509078012976454604598813805735677104174112776060905225493357010861225261560490401501912259585922988353328944884443953564154752191932500561561256069872534626325000901099904014035414792860997025614313564862063784602254606240743545483125618939111639728114664995759380293512809125885893543730614962375399353971677980309835647540883700977)
(5013415024346389, 4333469053087705)
'''

Solution

Although I solved the previous two challenges, I didn't solve this one. I was thinking about Bleichenbacher-May attack and hope that with several modifications I can solve it, but the fact is

(to be finished)

Code

Reference

Partial Key Exposure Attack on Short Secret Exponent CRT-RSA: paper

Small CRT-exponent RSA revisited: paper