In the task we get source code and its output. The code is simply RSA encryption of the flag.
The key observation from the code is:
def __str__(self):
return "Key([e = {0}, n = {1}, x = {2}, y = {3}])".format(self.e, self.d, self.iqmp, self.ipmq)
Second output parameter is self.d
even though it's labelled as n = {1}
.
This means that we know e, d, iqmp and ipmq
, but we don't know n
.
As name of the challenge suggests, they goal is to recover n
to complete the private key and decrypt the flag.
In order to do that we need to do a bit of math.
There is not much to go with, since the only values related to n
we have are iqmp
and ipmq
.
Let's use their definition:
ipmq = modinv(p,q)
ipmq*p == 1 mod q
ipmq*p = 1 + k*q [some integer k with bound k<q]
k*q = ipmq*p - 1 [some integer k with bound k<q]
Same for iqmp
:
iqmp = modinv(q,p)
iqmp*q == 1 mod p
iqmp*q = 1 + m*p [some integer m with bound m<p]
m*p = iqmp*q - 1 [some integer m with bound m<p]
Let's now multiply those 2 equations:
(ipmq*p) * (iqmp*q) = (1+k*q) * (1+m*p)
ipmq*iqmp*p*q = k*m*p*q + k*q + m*p + 1
ipmq*iqmp*N = k*m*N + k*q + m*p + 1
Let's move N
to one side:
N*(ipmq*iqmp - k*m) = k*q + m*p + 1
On the left side of the equation we have N*x
and since those are all integer equations, it means x
has to be integer as well.
This means that the right side of the equations has to also be a multiple of N.
Now the key observation in this task:
Let's remember the bounds for k
and m
-> k<q
and m<p
.
On the right side we have k*q + m*p + 1
.
What is the upper bound? It has to be smaller than q*q + p*p +1
.
We can assume, from the task setup, that p
and q
are of similar bitlength, and thus q*q
and p*p
can't be much bigger than N
, and therefore the whole right side of the equation can't be much bigger than 2*N
even in the most pessimistic case (with k=q
and m=p
), and since it's all integers it can be either N
or 2*N
and nothing more.
It's very unlikely that we hit the upper bound, so we can safely assume that in reality it's equal to N
.
So we have now:
k*q + m*p + 1 = N
Now let's substitute back k*q = ipmq*p - 1
and m*p = iqmp*q - 1
:
(ipmq*p - 1) + (iqmp*q - 1) + 1 = N
ipmq*p + iqmp*q - 1 = N
Let's now look into another relation:
N = p*q
phi(N) = (p-1)*(q-1)
From this we know that:
phi(N) = N - p - q + 1
N = phi(N)+p+q-1
Let's modify a bit our previous result:
ipmq*p + iqmp*q - 1 = N
ipmq*(p-1+1) + iqmp*(q+1-1) - 1 = N
ipmq*(p-1) + ipmq + iqmp*(q-1) + iqmp - 1 = N
And combine this with relation between N
and phi
:
ipmq*(p-1) + ipmq + iqmp*(q-1) + iqmp - 1 = phi(N) + p + q - 1
For simplicity let's introduce:
p-1 = X
q-1 = Y
phi(N) = phi
And thus:
phi = X*Y
X = phi/Y
And now:
phi = ipmq*X + ipmq + iqmp*Y + iqmp -1 -(X+1+Y)
phi = ipmq*(phi/Y) + ipmq + iqmp*Y + iqmp - 1 -(phi/Y + 1 + Y)
Let's now multiply this by Y
:
phi*Y = ipmq*phi + ipmq*Y + iqmp*Y^2 + iqmp*Y - Y -(phi + Y + Y^2)
phi*Y = ipmq*phi + ipmq*Y + iqmp*Y^2 + iqmp*Y - Y - phi - Y - Y^2
Move this to one side and combine:
Y^2*(iqmp-1) + Y(ipmq + iqmp - phi - 2) + ipmq*phi - phi = 0
Now we have simply an equation a*x^2 + bx + c == 0
and we can directly solve it, assuming we know the value of phi
.
Fortunately we can easily calculate phi
one using the fact that:
e*d mod phi == 1
e*d == 1+k*phi
k*phi == e*d-1
Now although the value of k
may be big, we can approximate it using some value which should be of similar bitlen to phi
we're looking for.
Once such value would of course be N
, but we don't know that.
Other such value is for example d
, since it was calculated as inverse(self.e, (self.p-1)*(self.q-1))
so it has to be smaller than phi
.
We iterate over possible phi using:
def find_phi(e, d):
kfi = e * d - 1
k = kfi / (int(d * 3))
print('start k', k)
while True:
fi = kfi / k
try:
d0 = gmpy2.invert(e, fi)
if d == d0:
yield fi
except:
pass
finally:
k += 1
Keep in mind there can be many values phi
where d == modinv(e,phi)
, so we need to test all of them, but with our start approximation for k
we should hit the right one quite fast.
Now that we can provide possible phi
values, we can get back to our quadratic equation and solve it:
def solve_for_phi(ipmq, iqmp, possible_phi):
a = iqmp - 1
b = ipmq + iqmp - 2 - possible_phi
c = ipmq * possible_phi - possible_phi
delta = b ** 2 - 4 * a * c
if delta > 0:
r, correct = gmpy2.iroot(delta, 2)
if correct:
x1 = (-b - r) / (2 * a)
x2 = (-b + r) / (2 * a)
if gmpy2.is_prime(x1 + 1):
q = x1 + 1
p = possible_phi / x1 + 1
return p, q
elif gmpy2.is_prime(x2 + 1):
q = x2 + 1
p = possible_phi / x2 + 1
return p, q
Now if we plug this all together:
def main():
e = 1048583
d = 20899585599499852848600179189763086698516108548228367107221738096450499101070075492197700491683249172909869748620431162381087017866603003080844372390109407618883775889949113518883655204495367156356586733638609604914325927159037673858380872827051492954190012228501796895529660404878822550757780926433386946425164501187561418082866346427628551763297010068329425460680225523270632454412376673863754258135691783420342075219153761633410012733450586771838248239221434791288928709490210661095249658730871114233033907339401132548352479119599592161475582267434069666373923164546185334225821332964035123667137917080001159691927
ipmq = 22886390627173202444468626406642274959028635116543626995297684671305848436910064602418012808595951325519844918478912090039470530649857775854959462500919029371215000179065185673136642143061689849338228110909931445119687113803523924040922470616407096745128917352037282612768345609735657018628096338779732460743
iqmp = 138356012157150927033117814862941924437637775040379746970778376921933744927520585574595823734209547857047013402623714044512594300691782086053475259157899010363944831564630625623351267412232071416191142966170634950729938561841853176635423819365023039470901382901261884795304947251115006930995163847675576699331
ct = 0x32074de818f2feeb788e36d7d3ee09f0000381584a72b2fba0dcc9a2ebe5fd79cf2d6fd40c4dbfea27d3489704f2c1a30b17a783baa67229d02043c5bc9bdb995ae984d80a96bd79370ea2c356f39f85a12d16983598c1fb772f9183441fea5dfeb5b26455df75de18ce70a6a9e9dbc0a4ca434ba94cf4d1e5347395cf7aafa756c8a5bd6fd166bc30245a4bded28f5baac38d024042a166369f7515e8b0c479a1965b5988b350064648738f6585c0a0d1463bd536d11a105bb926b44236593b5c6c71ef5b132cd9c211e8ad9131aa53ffde88f5b0df18e7c45bcdb6244edcaa8d386196d25297c259fca3be37f0f2015f40cb5423a918c51383390dfd5a8703
for potential_phi in find_phi(e, d):
res = solve_for_phi(ipmq, iqmp, potential_phi)
if res:
p, q = res
n = p * q
print(long_to_bytes(pow(ct, d, n)))
break
We get the flag: hitcon{1t_is_50_easy_t0_find_th3_modulus_back@@!!@!@!@@!}
Complete solver here