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 = m ^ e \pmod n$$

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 \times e \pmod n$$

This implies that the decryption is made like this: $$m = c \times d \pmod n$$

Where \(x^2 + y^2 = z^2\) is the modular multiplicative inverse of $e \pmod n$. 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: $\frac{1}{2^{(2 \times 1024)}} \iff \frac{1}{2^{1024}}$, 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) \rightarrow m \times e \pmod n$$ $$decrypt(c) \rightarrow c \times d \pmod n$$

Therefore: $$m \pmod n = decrypt(encrypt(m))$$ $$\iff m \pmod n = (m \times e \pmod n) \times d \pmod n$$ $$\iff m \pmod n = m \times e \times d \pmod n$$

To satisfy this equation, we must have $e \times d \equiv 1 \pmod n$.

Which is the same as: $$d \equiv e^-1 \pmod n$$

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) \in \mathbb Z$$: $$e \times u + n \times v = gcd(e,n) \iff e \times u + n \times 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 \pmod n$; and we know for a fact that the inverse is defined by this expression: $$u \equiv e^-1 \pmod n$$

This is exactly what we were searching for. Replacing $u$ by $d$, we get our solution: $$d \equiv e^-1 \pmod n$$

Solution

We compute $d$ this way: $$d \equiv e^-1 \pmod n$$

Once we’ve got $d$, we decrypt our ciphertext (and thus, retrieve $m$): $$m \equiv c \times d \pmod n$$

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) \in \mathbb Z$ for diophantines equations such as this one : $$e \times u + n \times 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 \times u + n \times v = gcd(e,n)$ works : $$7 \times -14 + 33 \times 3 = 1$$

But also, we notice that the definition of $u$ being the inverse of $e \pmod n$ (only if $gcd(e,n)=1$) works: $$e \times u \equiv 1 \pmod n$$ $$7 \times -14 \equiv 1 \pmod {33}$$

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 :)