apbq-rsa-1

\( \newcommand{\R}{\mathbb{R}} \newcommand{\Q}{\mathbb{Q}} \newcommand{\Z}{\mathbb{Z}} \)

Challenge description

In this challenge, we face a standard RSA implementation:

1
2
3
4
p = getPrime(1024)
q = getPrime(1024)
n = p * q
e = 0x10001

p and q are two big primes, and e = 0x10001 is the standard choice for public exponent. These parameters are used to encrypt the flag:

1
2
3
4
FLAG = open('flag.txt', 'rb').read().strip()
c = pow(bytes_to_long(FLAG), e, n)
print(f'{n = }')
print(f'{c = }')

Together with the ciphertext c and the base n we are also given some hints:

1
2
3
4
5
6
hints = []
for _ in range(2):
    a, b = randint(0, 2**12), randint(0, 2**312)
    hints.append(a * p + b * q)

print(f'{hints = }')

Solution

We know that to decrypt RSA it is enough to factor n. However n = p*q where p and q are 1024-bits primes, and hence direct factorization is not an option. We must find a way to use the hints instead.

The first observation is the following. Let us call our hints h1 = a1*p + b1*q and h2 = a2*p + b2*q. Then if only we knew a1 and a2 we could compute

\[H = a_1 h_2 - a_2 h_1 = (a_1b_2 - a_2b_1)q\]

Now this number shares a factor q with our public modulus n; we can efficently compute gcd(n, H) = q, and then recover p = n // q. Of course the same reasoning applies to b1*h2 - b2*h1 which gives us p.

This would allow us to recover the flag. However we do not know either a1 and a2 or b1 and b2. Looking at the code b1 and b2 are two random integers between 0 and 2**312, but a1 and a2 are both lower than 2**12 = 8192, which means we can brute-force it. The total number of possible combinations of (a1, a2) is 2**26 = 67108864, which is expensive but still doable. However, we can easily reduce it further by looking at coprime a1 and a2 (meaning that gcd(a1, a2) = 1). This is because if a1 and a2 have one factor in common we can divide the whole expression by this common factor and get another (duplicate) solution. For every possible pair of coprime a1 and a2 we just compute H and check gcd(n, H). If this is 1 it means we picked the wrong pair. Otherwise we got a factor of n, and we can use it to solve the challenge.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from sage.all import gcd
import time

def bf_hints(hints, n):
	h1, h2 = hints
	tic = time.time()

	for a1 in range(2**12):
		if a1 % 100 == 0:
			print(f'{a1 = }/{2**12} [t = {round(time.time()-tic, 2)} s]') # Print elapsed time
			tic = time.time()

		for a2 in range(2**12):
			if gcd(a1, a2) != 1:
				# Skip if a1 and a2 are not coprime
				continue

			guess = a1 * h1 - a2 * h2
			q = gcd(n, guess)
			if q != 1:
				# Win
				print(f'Hit: {a1 = } {a2 = }')
				return int(q)

Notice that this may still take a couple of minutes: for this reason I added a line of code to check our progress once in a while and print the elapsed time. The function gcd is taken from SageMath since it’s faster, but math.gcd would work as well. Now we can load n and hints from the output we are given, and run this function.

1
2
3
4
q = bf_hints(hints, n)

assert n % q == 0
p = n // q

Once we have p and q we can compute

\[\varphi = (p-1)(q-1),\]

and then

\[d = e^{-1} \bmod \varphi,\]

the private exponent we need to invert the ciphertext. In this way we finally recover the flag:

1
2
3
4
5
6
phi = (p-1)*(q-1)

d = pow(e, -1, phi)
m = pow(c, d, n)

print(m.to_bytes((m.bit_length() + 7) // 8, "big")) # b'DUCTF{gcd_1s_a_g00d_alg0r1thm_f0r_th3_t00lbox}'

Stellar Vector is powered by the DistriNet research group and the KU Leuven.

© 2022-2023 All content published on this site are protected under copyright of the respective authors.