c0w5lip@wired:~$

🏷️ write-up cryptography

Write Up - IrisCTF 2023: babynotrsa | Why you should beware tweaking RSA

Informations

Video version (French)

Sincere acknowledgements to aaSSfxxx who helped me a bit with mathematical demonstrations and kindly read my write up before I posted it.

This challenge is one of the ones I flagged during the IrisCTF 2023. Thanks to the organizers, I didn’t tried lots challenges but that one was particulary fun as a beginner.

Challenge

Name: babynotrsa

Author: sera

Description:

Everyone knows RSA, but everyone also knows that RSA is slow. Why not just use a faster operation than exponentiation?

Files:

chal.py

from Crypto.Util.number import getStrongPrime

# We get 2 1024-bit primes
p = getStrongPrime(1024)
q = getStrongPrime(1024)

# We calculate the modulus
n = p*q

# We generate our encryption key
import secrets
e = secrets.randbelow(n)

# We take our input
flag = b"irisctf{REDACTED_REDACTED_REDACTED}"
assert len(flag) == 35
# and convert it to a number
flag = int.from_bytes(flag, byteorder='big')

# We encrypt our input
encrypted = (flag * e) % n

print(f"n: {n}")
print(f"e: {e}")
print(f"flag: {encrypted}")

output.txt :

n: 21429933885346644587620272790089165813353259223649897308397918491861562279767580488441831451651834802520437234248670652477414296159324726172158330221397420877323921934377321483041598028053870169281419856238830264612049920637819183013812186448416408328958360799645342598727238977986741643705720539702955864527935398839069236768630867447760912744208154645904678859979378604386855741350220991958191408182147658532111413386776058224418484895056146180001830405844881486308594953615999140110712045286000170660686758188247928230655746746482354748673482506070246808187808961599576834080344066055446605664648340486804023919467
e: 10788856448030235429585145974385410619185237539198378911887172763282204686697141640582780419040340318300048024100764883750608733331571719088729202796193207904701854848679412033514037149161609202467086017862616635522167577463675349103892366486246290794304652162107619408011548841664240624935414339021041162505899467159623692906986841033101688573177710503499081107294555688550493634416552587963816327790111808356639558596438537569271043190414208204773219496030644456745185896540608008662177117212000718802474957268532153146989410300300554162811564064457762004188326986236869603714437275058878379647196886872404148116134
flag: 3954523654845598592730156937269688140867480061118457307435945875579028695730063528424973907208923014508950419982702682082417623843946231057553311028711409093751376287876799688357176816093484535703797332422565021382453879908968161161537921292725907853309522100738603080298951279637316809695591295752657105226749125868510570125512146397480808774515489938198191435285342823923715673372695893409325086032930406554421670815433958591841773705563688270739343539481283865883427560667086249616210745997056621098406247201301461721906304555526293017773805845093545204570993288514598261070097976786800172141678030841959348372097

First approach

Let be m the plaintext flag and c the encrypted flag (from output.txt).

Reminder: RSA cryptosystem is based on modular exponentation, and compute messages this way: c=me(modn)

The algorithm looks almost like a classic RSA encryption. However, unlike RSA, this algorithm uses multiplication instead of exponentation as a way to compute c:c=m×e(modn)$

This implies that the decryption is made like this: m=c×d(modn)

Where x2+y2=z2 is the modular multiplicative inverse of e(modn). This number, also called “secret exponent”, is usually part of the RSA private key (d,n), used for decrypting messages.

Throughout all this write up, we’ll admit that e and n are coprimes, because we can tell from the piece of chal.py below that the chance for them to not being coprime (if the randbelow(n) fonction returns p or q) is equal to: 12(2×1024)121024, which is honestly quite low.

# We get 2 1024-bit primes
p = getStrongPrime(1024)
q = getStrongPrime(1024)

# We calculate the modulus
n = p*q

# We generate our encryption key
import secrets
e = secrets.randbelow(n)

So, what’s the matter ?

To decrypt c, we have to retrieve d. But as we’ll see, due to the fact that the algorithm uses multiplication instead of exponentation, d is actually easy to compute.

First method

Let’s define two functions: encrypt(m)m×e(modn) decrypt(c)c×d(modn)

Therefore: m(modn)=decrypt(encrypt(m)) m(modn)=(m×e(modn))×d(modn) m(modn)=m×e×d(modn)

To satisfy this equation, we must have e×d1(modn).

Which is the same as: de1(modn)

We got d.

Second method (Bézout’s identity and extended Euclidean algorithm)

Admiting e and n are coprimes, we have: gcd(e,n)=1. Yet, we know from the Bézout’s identity that the following diophantine equation admits a solution couple (u,v)Z:e×u+n×v=gcd(e,n)e×u+n×v=1$

From there, and because gcd(e,n)=1, the extended Euclidean algorithm tells us that in this equation, u is the inverse of e(modn); and we know for a fact that the inverse is defined by this expression: ue1(modn)

This is exactly what we were searching for. Replacing u by d, we get our solution: de1(modn)

Solution

We compute d this way: de1(modn)

Once we’ve got d, we decrypt our ciphertext (and thus, retrieve m): mc×d(modn)

from Crypto.Util.number import *

n = 21429933885346644587620272790089165813353259223649897308397918491861562279767580488441831451651834802520437234248670652477414296159324726172158330221397420877323921934377321483041598028053870169281419856238830264612049920637819183013812186448416408328958360799645342598727238977986741643705720539702955864527935398839069236768630867447760912744208154645904678859979378604386855741350220991958191408182147658532111413386776058224418484895056146180001830405844881486308594953615999140110712045286000170660686758188247928230655746746482354748673482506070246808187808961599576834080344066055446605664648340486804023919467
e = 10788856448030235429585145974385410619185237539198378911887172763282204686697141640582780419040340318300048024100764883750608733331571719088729202796193207904701854848679412033514037149161609202467086017862616635522167577463675349103892366486246290794304652162107619408011548841664240624935414339021041162505899467159623692906986841033101688573177710503499081107294555688550493634416552587963816327790111808356639558596438537569271043190414208204773219496030644456745185896540608008662177117212000718802474957268532153146989410300300554162811564064457762004188326986236869603714437275058878379647196886872404148116134
flag = 3954523654845598592730156937269688140867480061118457307435945875579028695730063528424973907208923014508950419982702682082417623843946231057553311028711409093751376287876799688357176816093484535703797332422565021382453879908968161161537921292725907853309522100738603080298951279637316809695591295752657105226749125868510570125512146397480808774515489938198191435285342823923715673372695893409325086032930406554421670815433958591841773705563688270739343539481283865883427560667086249616210745997056621098406247201301461721906304555526293017773805845093545204570993288514598261070097976786800172141678030841959348372097

d = pow(e, -1, n)
decrypted_flag = flag * d % n

print(long_to_bytes(decrypted_flag).decode())
irisctf{discrete_divide_isn't_hard}

Bonus : small example on the extended Euclidean algorithm

The extended Euclidean algorithm allows us to not only to find the gcd of two number, but also to find Bézout coefficients (u,v)Z for diophantines equations such as this one : e×u+n×v=gcd(e,n)

The book Introduction to Algorithms (written in part by one of the inventors of RSA; for the fun fact) gives a pseudocode recursive version of the algorithm. Here is a Python code snippet I wrote from it to fit our example :

def egcd(e, n):
    if n == 0:
        return e, 1, 0
    
    gcd, u, v = egcd(n, e % n)
    return gcd, v, u - (e//n)*v

e, n = 7, 33

result = egcd(e, n)
print(f"gcd({e},{n}) = {result[0]}; u = {result[2]}; v = {result[1]}")

With e=7 and n=33, we get this output: gcd(7,33)=1;u=14;v=3. And we notice that: e×u+n×v=gcd(e,n) works : 7×14+33×3=1

But also, we notice that the definition of u being the inverse of e(modn) (only if gcd(e,n)=1) works: e×u1(modn) 7×141(mod33)

I won’t explain much about this algorithm because it would weigh down the article (and I couldn’t see myself explaining it in details in this write up to be honest).

I hope you learnt something (that modifiying just a tiny part of a cryptosystem can be crucial, for example) reading this wu/article. Give me feedback on Discord (REDACTED) or my other social medias if you liked it; I would really appreciate it.

Thanks again to aaSSfxxx and the IrisSec team :)