Crypto challenge Unbr34k4bl3 from the Cyber Grabs CTF.
No one can break my rsa encryption, prove me wrong !!
Flag Format:
cybergrabs{}
Author: Mritunjya
Looking at the source code, this challenge looks like a typical RSA challenge at first, but there are some important differences to note:
- (line 34). This is a twist but RSA strategies can easily be extended to 3 prime components.
- (line 19). This suggests that the cryptosystem is actually a Rabin cryptosystem.
- We’re not given the public keys and , but they are related through .
Finding and
We know that and are related through , which is some even number
greater than 2, but we’re not given any of their real values. We’re also given
through an oddly-named functor
function that:
Taking the entire equation gives us:
This means there are two possibilities: either or is even (since we know is a prime). The first case isn’t possible, because with , the geometric series equation would not be satisfied. So it must be true that , the only even prime.
Applying geometric series expansion, . We can rearrange this via the quadratic equation to . Trying out a few values we see that only and gives us a value that make prime.
Finding and
We’re not actually given or , but we are given and . In order words:
We can rewrite these equations without the mod by introducing variables and to be arbitrary constants that we solve for later:
We’ll be trying to use these formulas to create a quadratic that we can use to eliminate and . Multiplying these together gives:
I grouped and together here because it’s important to note that since we have , we know and thus . This means that for purposes of solving the equation, is a constant to us. This actually introduces an interesting structure on the right hand side, we can create 2 new variables:
Substituting this into our equation above we get:
Recall from whatever algebra class you last took that . Since we have both and in our equation, we can try to look for a way to isolate them in order to create our goal.
is basically , and since and are both smaller than or , then we’ll approximate this using . Now that has become a constant, we can create the coefficients we need:
Putting this into Python, looks like:
from decimal import Decimal
getcontext().prec = 3000 # To get all digits
k1k2 = ip * iq - 1
alpha_times_beta = k1k2 * pq
alpha_plus_beta = pq * ip * iq - 1 - k1k2 * pq
def quadratic(b, c):
b, c = Decimal(b), Decimal(c)
disc = b ** 2 - 4 * c
return (-b + disc.sqrt()) / 2, (-b - disc.sqrt()) / 2
alpha, beta = quadratic(-alpha_plus_beta, alpha_times_beta)
Now that we have and , we can try GCD’ing them against to get and :
from math import gcd
p = gcd(pq, int(alpha))
q = gcd(pq, int(beta))
assert p * q == pq # Success!
Alternative method
@sahuang used the sympy library to do this part instead, resulting in much less manual math. It’s based on this proof from Math StackExchange that .
from sympy import *
p,q = symbols("p q")
eq1 = Eq(ip * p + iq * q - pq - 1, 0)
eq2 = Eq(p * q, pq)
sol = solve((eq1, eq2), (p, q))
Decrypting the ciphertexts
Now that we know and , it’s time to plug them back into the cryptosystem and get our plaintexts. is actually easier than , because with we can just find the modular inverse:
phi = (p - 1) * (q - 1) * (r - 1)
d2 = pow(e2, -1, phi)
m2 = pow(c2, d2, n)
print(long_to_bytes(m2))
# ... The last part of the flag is: 8ut_num83r_sy5t3m_15_3v3n_m0r3_1nt3r35t1n6} ...
This trick won’t work with however:
d1 = pow(e1, -1, phi)
# ValueError: base is not invertible for the given modulus
Because is even (it’s the product of one less than 3 primes), there can’t possibly be a such that . According to Wikipedia, the decryption for a standard two-prime takes 3 steps:
- Compute the square root of and :
- Use the extended Euclidean algorithm to find and such that .
- Use the Chinese remainder theorem to find the roots of modulo :
- The real message could be any , but we don’t know which.
Converting this to work with , it looks like:
- Compute the square root of , , and :
- Using the variable names from AoPS’s definition of CRT:
- For .
- For .
- Let .
- The real message could be any , but we don’t know which.
In code this looks like:
# Step 1
mp = pow(c1, (p + 1) // 4, p)
mq = pow(c1, (q + 1) // 4, q)
mr = pow(c1, (r + 1) // 4, r)
# Step 2
bp = n // p
bq = n // q
br = n // r
ap = pow(bp, -1, p)
aq = pow(bq, -1, q)
ar = pow(br, -1, r)
# Step 3
from itertools import product
for sp, sq, sr in product((-1, 1), repeat=3):
m = (sp * ap * bp * mp + sq * aq * bq * mq + sr * ar * br * mr) % n
m = long_to_bytes(m)
# Step 4
# We know that the real flag starts with `cybergrabs{`...
if b"cybergrabs" in m: print(m)
# Congratulations, You found the first part of flag cybergrabs{r481n_cryp70sy5t3m_15_1nt3r35t1n6_ ...
The final flag, then, is:
cybergrabs{r481n_cryp70sy5t3m_15_1nt3r35t1n6_8ut_num83r_sy5t3m_15_3v3n_m0r3_1nt3r35t1n6}
Big thanks to @10, @sahuang, and @thebishop in the Project Sekai discord for doing a lot of the heavy-lifting to solve this challenge.