Cryptography – 465 pts (21 solves) and 490 pts (11 solves) – Chall author: Polymero (me)
“What can I say, I just like squares.”
Files for 1: nonsquarefreedom_easy.py
Files for 2: nonsquarefreedom_hard.py
This challenge was part of our very first CTF, K3RN3LCTF 2021.
Exploration
On the surface players are presented with the output of some multi-prime RSA encryption.
# EASY
#----------------------------------------------------
# Output
#----------------------------------------------------
# M < N : True
# M < P**8 : True
# M < Q*R : True
#
# n = 68410735253478047532669195609897926895002715632943461448660159313126496660033080937734557748701577020593482441014012783126085444004682764336220752851098517881202476417639649807333810261708210761333918442034275018088771547499619393557995773550772279857842207065696251926349053195423917250334982174308578108707
# e = 65537
# c = 4776006201999857533937746330553026200220638488579394063956998522022062232921285860886801454955588545654394710104334517021340109545003304904641820637316671869512340501549190724859489875329025743780939742424765825407663239591228764211985406490810832049380427145964590612241379808722737688823830921988891019862
#
# D(C) = 58324527381741086207181449678831242444903897671571344216578285287377618832939516678686212825798172668450906644065483369735063383237979049248667084304630968896854046853486000780081390375682767386163384705607552367796490630893227401487357088304270489873369870382871693215188248166759293149916320915248800905458
#
# HARD
#----------------------------------------------------
# Output
#----------------------------------------------------
# M < N : True
# M < P**8 : False
# M < Q*R : False
#
# n = 4575829597858799419341358254099339062313194353857263690760863923804026600749439184775330442983454068796504813860650114437843860719657927599984274468923063336628389598105328878858384622054430674402403839270645603835832233587365339188250968852039492773992195746788936530150586208030748428985765114934032164269
# e = 65537
# c = 3704469765736808327150183385593470573779546046957340283734683699989596150060156191944264331756779846153685794890755215418575392561313787296737832446637414912621671452320923899214051811828161280972929008735017749715723409961801337999696046444992388364931185375322735030067979993625538441901181791804857243883
#
# D(C) = 3808886831337667850991686443291003571625029443294253379657761379567000961486361549315830406668585035951634722009857007794996473504806639953404195426190048807121464518905063268090265455608765879999313973046043954153720466695054029750321369389019308205783219802884185884371070551851548317370008588012333323252
#
However, it seems we are given some additional hints: some clues regarding the size of the plaintext message, and even the decrypted ciphertext! Although something seems to be wrong with it… Let us take a look at the provided source code.
# Imports
from Crypto.Util.number import getPrime
import os
# Key gen
P = getPrime(512//8)
Q = getPrime(256)
R = getPrime(256)
N = P**8 * Q * R
E = 0x10001
def pad_easy(m):
m <<= (512//8)
m += (-(m % (P**2)) % P)
return m
def pad_hard(m):
m <<= (512//2)
m += int.from_bytes(os.urandom(256//8),'big')
m += (-(m % (P**2)) % (P**2))
m += (-(m % (P**3)) % (P**3))
return m
# Pad FLAG
M = pad_hard(int.from_bytes(b'flag{"""REDACTED"""}','big')) # <--- pad_easy for EASY, pad_hard for HARD
print('M < N :',M < N)
print('M < P**8 :',M < (P**8))
print('M < Q*R :',M < (Q*R))
# Encrypt FLAG
C = pow(M,E,N)
print('\nn =',N)
print('e =',E)
print('c =',C)
# Hint
F = P**7 * (P-1) * (Q-1) * (R-1)
D = inverse(E,F)
print('\nD(C) =',pow(C,D,N))
The following is not strictly necessary to recover either of the 2 flags, but provides interesting background into this challenge and explains why the provided decryption is faulty.
As it turns out, a composite modulus that is NOT square-free will result in some interesting RSA behaviour. More specifically, a non-square-free modulus implies that for at least one of the factors we have
\[N = 0 \mod{P^s}\]for some $s >= 2$. If this is the case then the textbook RSA equation breaks such that
\[m^{e^d} \neq m \mod{N} \forall m \in \left\{ m \mod{P^r} \in \left\{ k P\ |\ \forall k \in (1, ..., P-1) \right\}\ |\ \forall r \in (2, ..., s) \right\}.\]This implies that a total fraction of
\[\sum_{r=2}^{s} \frac{p-1}{p^r}\]of the messages will be invalid. We can see that both padding functions will make sure our message belongs to this invalid group, and as a hint we are given the faulty decryption of the ciphertext.
These or some of the key observations:
- The modulus contains a small prime raised to a power, such that we can recover P, and Q*R with standard factorisation techniques.
- The modulus is non-square-free and hence contains messages that are invalid using the textbook RSA equation.
- The used padding schemes make sure the flags belong to this invalid group.
- As a hint, we are given the faulty decryption of the encrypted flag.
Exploitation
Non-Square-Freedom 1
Although the faulty decryption seems to be useless, in this case it actually contains the full message. The only thing we have to do is to recover Q*R such that we can simply take the mod of the faulty decryption and find our flag! Because the size of P is only 64 bits we can simply using any standard factorisation method (including online programs) to recover P and Q*R and we are set to go.
# Solution Easy!
win = long_to_bytes(DC_easy % (Q*R))
print(win)
Ta-da!
flag{y34_th1s_1s_n0t_h0w_mult1pr1m3_RS4_w0rks_buddy}
Non-Square-Freedom 2
This time we can apply a similar technique but because $M > Q R$ we need to use the Chinese Remainder Theorem to recover the full original message. In short, we have
\[M = 0 \mod{P^3}\] \[M = \mathrm{hint} \mod{Q R}\]such that we can use the extended euclidean algorithm to recover $a$ and $b$ in
\[a P^3 + b Q R = 1.\]Finally, we can recover the full message as
\[M = 0 \cdot b Q R + \mathrm{hint} \cdot a P^3 = \mathrm{hint} \cdot a P^3 \mod{Q R P^3}.\]# Solution Hard!
def gcdExtended(a, b):
if a == 0 :
return b,0,1
gcd,x1,y1 = gcdExtended(b%a, a)
x = y1 - (b//a) * x1
y = x1
return gcd,x,y
win = long_to_bytes(((DC_hard % (Q*R)) * P**2 * gcdExtended(P**2,Q*R)[1]) % (Q*R*P**2))
print(win)
Ta-da!
flag{1_th1nk_1_m1ght_b3_squ4r3_fr33_1nt0l3r4nt}
Thanks for reading! <3
~ Polymero