28^{th} 2023 / Document No. D22.102.16
Prepared By: aris
Challenge Author(s): aris
Difficulty: Easy
Classification: Official
- This challenge teaches players about the Mignotte Secret Sharing scheme. In this scheme, we are able to obtain the secret modulo
$n$ distinct relatively prime moduli and combine these partial solutions with CRT to get the entire secret$a_0$ .
- The military possesses a server containing crucial data about the virus and potential cures, secured with encryption and a key distributed using a secret sharing scheme. However, authorized members holding parts of the key are infected, preventing access to the research. Fueled by your cryptography passion, you and your friends aim to hack into the server and recover the key. Can you succeed in this challenging mission?
- Basic knowledge of Secret Sharing schemes.
- Familiar with polynomials.
- Know how to combine partial solutions to obtain a full solution.
- Learn how to apply CRT to combine multiple partial solutions.
- Learn about the Mignotte Secret Sharing.
In this challenge, we are provided with one file server.py
which is the main script that runs when we connect to the remote instance.
From the welcome message, we understand that the challenge is about secret sharing schemes. Such a scheme usually requires two parameters having been set:
- The finite field
$GF(p)$ in which all the operations will be performed. - The degree of the polynomial to be interpolated, say
d
. - The number of users in the scheme
n
(or equivalently, the number of shares required to interpolate the polynomial).
Before moving on, it is important to recall how secret sharing schemes work. The purpose of such a scheme is key distribution among a group of users where each user contributes to this distrubution by submitting their share; as it is called. First, a
Back to our problem, there are two things that stand out.
- The polynomial being used is not defined in a finite field
$GF(p)$ . This is trivial to see from thepoly
function which substitutes the polynomial with the value of$x$ and the result is not reduced modulo any prime number$p$ .
def poly(self, x):
return sum([self.coeffs[i] * x**i for i in range(self.d+1)])
- The degree of the polynomial is
$30$ but the maximum number of shares is only$19$ which initially might make us think that it is not possible to interpolate the polynomial. This is trivial to see from the constructor of the MSS class.
class MSS:
def __init__(self, BITS, d, n):
self.d = d
self.n = n
self.BITS = BITS
...
def main():
mss = MSS(256, 30, 19)
...
Our final task is to recover the
self.key = bytes_to_long(os.urandom(BITS//8))
self.coeffs = [self.key] + [bytes_to_long(os.urandom(self.BITS//8)) for _ in range(self.d)]
def encrypt_flag(self, m):
key = sha256(str(self.key).encode()).digest()
iv = os.urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
ct = cipher.encrypt(pad(m, 16))
return {'iv': iv.hex(), 'enc_flag': ct.hex()}
Therefore we can deduce the following about the secret sharing scheme.
- The polynomial is defined over the integers and is of degree
$d = 30$ . - The maximum number of users in this scheme is
$n = 19$ . - The size of each coefficient is 256 bits.
- All coefficients but
$a_0$ are random 256-bit integers.$a_0$ itself is the key that we have to recover.
Now let us examine the flow of the application and how we can interact with it.
- We can send our ID to the server and receive our share back.
- We can receive the encrypted flag from the server.
Since the key is unknown and AES is considered secure, we will experiment with the first option. The first option has the following restrictions.
def get_share(self, x):
if x > 2**15:
return {'approved': 'False', 'reason': 'This scheme is intended for less users.'}
elif self.n < 1:
return {'approved': 'False', 'reason': 'Enough shares for today.'}
else:
self.n -= 1
return {'approved': 'True', 'x': x, 'y': self.poly(x)}
- Our ID must not be greater than 15 bits.
- Each time we send an ID, the number of shares is decreased by
$1$ so we are allowed to send only 19 requests. - If we attempt to send more, our request is not accepted.
This challenge demonstrates why it is important to use secure parameters for secret sharing schemes and to define polynomials in finite fields. Firstly, the problem with recovering the coefficients directly is that there are 31 unknowns but we are able to obtain only 19 relations with these variables. Let us redefine the polynomial
Note that this challenge can become significantly easier if the player is already aware of the well known secret sharing scheme known as Mignotte Secret Sharing scheme (MSS).
Since we are limited to send at most 15-bit IDs, we would need at least
Let us adjust the Chinese Remainder Theorem to our challenge data. The idea is to send 18 distinct
Let us write a function that randomly selects 18 15-bit primes, sends them to the server as the user ID and receives the corresponding share.
from Crypto.Util.number import getPrime
def obtain_shares():
X = [getPrime(15) for _ in range(n)]
RK = [] # reduced keys
for x in X:
payload = json.dumps({'command': 'get_share', 'x': x})
io.sendlineafter(b'query = ', payload.encode())
share = json.loads(io.recvline().strip())['y']
RK.append(share % x)
return X, RK
d = 30
n = 19
Having obtained the shares, we can use Sympy's implementation of the CRT and solve for the key.
from sympy.ntheory.modular import crt
def solve_crt(X, rk):
return int(crt(X, rk)[0])
Finally, we can hash the key as seen in the challenge source, request the encrypted flag and decrypt it.
from hashlib import sha256
def calculate_decryption_key(key):
return sha256(str(key).encode()).digest()
def request_encrypted_flag():
payload = json.dumps({'command': 'encrypt_flag'})
io.sendlineafter(b'query = ', payload.encode())
io.recvuntil(b'flag : ')
data = json.loads(io.recvuntil(b'}').strip())
iv = bytes.fromhex(data['iv'])
encflag = bytes.fromhex(data['enc_flag'])
return iv, encflag
from Crypto.Util.Padding import unpad
from Crypto.Cipher import AES
def decrypt_flag(key, iv, enc_flag):
cipher = AES.new(key, AES.MODE_CBC, iv)
return unpad(cipher.decrypt(encflag), 16).decode()
A final summary of all that was said above:
- Notice that the degree of the polynomial
$d = 30$ is greater than the number of shares we are allowed to obtain$n = 19$ . - Write down how
$x_i$ are substituted in the polynomial$P$ and take advantage of the modular arithmetic properties to get the key$a_0$ reduced modulo different relatively prime moduli. - Having obtained enough modular congruences, apply the CRT to find the whole key.
- Recalculate the decryption key
- Request the encrypted flag and decrypt it.
These steps can be represented by code with the pwn()
function:
def pwn():
X, RK = obtain_shares()
key = solve_crt(X, RK)
aes_key = calculate_decryption_key(key)
iv, enc_flag = request_encrypted_flag()
flag = decrypt_flag(key, iv, enc_flag)
print(flag)
pwn()