5th February 2024 / Document No. D24.102.30
Prepared By: aris
Challenge Author(s): aris
Difficulty: Medium
Classification: Official
- In this challenge, the player is called to factor an RSA modulus
$n= p \cdot q$ , given alternate base-10 digits of the primes$p,q$ . The task is to implement a custom algorithm to retrieve the$i$ -th redacted digit by checking if$n$ is equal to$p \cdot q$ modulo$10^i$ .
- You find yourself in a labyrinthine expanse where movement is restricted to forward paths only. Each step presents both opportunity and uncertainty, as the correct route remains shrouded in mystery. Your mission is clear: navigate the labyrinth and reach the elusive endpoint. However, there's a twist—you have just one chance to discern the correct path. Should you falter and choose incorrectly, you're cast back to the beginning, forced to restart your journey anew. As you embark on this daunting quest, the labyrinth unfolds before you, its twisting passages and concealed pathways presenting a formidable challenge. With each stride, you must weigh your options carefully, considering every angle and possibility. Yet, despite the daunting odds, there's a glimmer of hope amidst the uncertainty. Hidden throughout the labyrinth are cryptic clues and hints, waiting to be uncovered by the keen-eyed. These hints offer glimpses of the correct path, providing invaluable guidance to those who dare to seek them out. But beware, for time is of the essence, and every moment spent deliberating brings you closer to the brink of failure. With determination and wit as your allies, you must press onward, braving the twists and turns of the labyrinth, in pursuit of victory and escape from the labyrinth's confounding embrace. Are you tenacious enough for that?
- Basic Python source code analysis.
- Basic understanding of the RSA cryptosystem.
- Familiar with modular arithmetic.
- Gain more experience with modular arithmetic. More specifically, if two numbers are equal, then they are equal modulo any number
$m$ .
In this challenge, we are provided with two files:
source.py
: This is the main script that encrypts the flag.output.txt
: This is a text file which contains some values from the source script.
Let us first take a look at the source script which is short and easy-to-follow.
from secret import FLAG
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
class RSACipher:
def __init__(self, bits):
self.key = RSA.generate(bits)
self.cipher = PKCS1_OAEP.new(self.key)
def encrypt(self, m):
return self.cipher.encrypt(m)
def decrypt(self, c):
return self.cipher.decrypt(c)
cipher = RSACipher(1024)
enc_flag = cipher.encrypt(FLAG)
with open('output.txt', 'w') as f:
f.write(f'n = {cipher.key.n}\n')
f.write(f'ct = {enc_flag.hex()}\n')
f.write(f'p = {str(cipher.key.p)[::2]}\n')
f.write(f'q = {str(cipher.key.q)[1::2]}')
There is a standard RSA implementation that pads the message with the PKCS#1 OAEP padding scheme. As in any RSA challenge, we are given the modulus
So far, assuming that RSA is secure, there is no vulnerability to exploit. However we are also provided with two values
Before moving on, let us write a function that loads the data from output.txt
.
def load_data():
with open('output.txt') as f:
n = int(f.readline().split(' = ')[1])
ct = bytes.fromhex(f.readline().split(' = ')[1])
hint_p = int(f.readline().split(' = ')[1])
hint_q = int(f.readline().split(' = ')[1])
return n, ct, hint_p, hint_q
Generally, in most CTF challenges, if any portion of the private key is leaked then the solution will most likely be related to recovering the private key. This challenge verifies this observation.
The Base-10 version of the RSA primes
For simplicity, we will use two binary masks for each of
For our example above, the corresponding masks would be:
$$
p_{mask} = 1010101010\
q_{mask} = 0101010101
$$
Let us write a function that creates
def create_masks(primelen):
pmask = ''.join(['1' if i % 2 == 0 else '0' for i in range(primelen)])
qmask = ''.join(['1' if i % 2 == 1 else '0' for i in range(primelen)])
return pmask, qmask
Recall that the whole product
Let
Right now, there are four candidates for the last digit of
Back to our example, due to
So far, we know:
- The last two digits of
$p$ are$41$ - The last digit of
$q$ is$1$ - The last two digits of
$n$ are$21$ . - The second-to-last digit of
$q$ can be either$3$ or$8$ .
We know we have found the correct candidate of
Let us compute the products
For candidate
The algorithm can be summarized as follows:
- For the digit at position
$i$ , we can extract the$i$ -th character of$p_{mask}$ and$q_{mask}$ . - If
$p_{mask}[i] = 1$ , then we know the$i$ -th digit of$p$ so extract it.- Bruteforce all 10 possible candidates of
$q$ . - Check whether
$n \pmod {10^i} == (p \cdot q) \pmod {10^i}$ . - The correct candidate for
$q[i]$ will satisfy this relation.
- Bruteforce all 10 possible candidates of
- Else, we know the
$i$ -th digit of$q$ so extract it and repeat the same steps until all the bits of$p, q$ are recovered.
Let us write a function that brute forces the
def bruteforce_digit(i, n, known_prime, prime_to_check, hint_prime):
msk = 10**(i+1)
known_prime = 10**i * (hint_prime % 10) + known_prime
for d in range(10):
test_prime = 10**i * d + prime_to_check
if n % msk == known_prime * test_prime % msk:
updated_prime_to_check = test_prime # correct candidate! update the unknown prime
updated_hint_prime = hint_prime // 10 # move on to the next digit
return known_prime, updated_prime_to_check, updated_hint_prime
The variable known_prime
corresponds to the prime whose prime_to_check
will be set to the other one. Similarly with hint_prime
.
Let us write a function that iterates over bruteforce_digit
with the corresponding arguments.
def factor(n, p, q, hp, hq, pmask, qmask):
for i in range(prime_len):
if pmask[-(i+1)] == '1':
p, q, hp = bruteforce_digit(i, n, p, q, hp)
else:
q, p, hq = bruteforce_digit(i, n, q, p, hq)
assert n == p * q
return p, q
Having factored
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
def decrypt(p, q, n, ct):
e = 0x10001
d = pow(e, -1, (p-1)*(q-1))
key = RSA.construct((n, e, d))
flag = PKCS1_OAEP.new(key).decrypt(ct)
return flag
A final summary of all that was said above:
- Notice that we know alternate digits of the decimal representation of
$p$ and$q$ . - Create two masks consisting of
$0,1$ and show which digits of the two primes are known and which are redacted. - Realize that knowing the
$i$ -th digit of$p$ is enough to recover the$i$ -th digit of$q$ by verifying whether$n$ is equal to$p \cdot q$ modulo$10^i$ . - Write a custom algorithm that recovers the primes' digits one by one.
- Having factored
$n$ , decrypt the flag using RSA along with the PKCS#1 OAEP padding scheme.
This recap can be represented by code with the pwn()
function:
from math import sqrt
def pwn():
n, ct, hint_p, hint_q = load_data()
prime_len = len(str(int(sqrt(n))))
pmask, qmask = create_masks(prime_len)
p, q = factor(n, 0, 0, hint_p, hint_q, pmask, qmask)
flag = decrypt(p, q, n, ct)
print(flag)
if __name__ == '__main__':
pwn()