# HTB University CTF 2023 - Brains & Bytes [CRYPTO] In this markdown, I will publish and explain all my solvers as I played for the team of Mohammed Premier University (UMP) ## [Easy] MSS ### Overview We are giving the following [server.py](https://github.com/hackthebox/uni-ctf-2023/blob/main/uni-ctf-2023/crypto/%5BEasy%5D%20MSS/challenge/server.py) script which contain the *MSS* class. ```py class MSS: def __init__(self, BITS, d, n): self.d = d self.n = n self.BITS = BITS 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 poly(self, x): return sum([self.coeffs[i] * x**i for i in range(self.d+1)]) 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)} 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()} ``` ### MSS Class analysis Let's examine the MSS class and discuss each of its functions: 1. `__init__()`: This is the class constructor. It uses the following parameters to initialise the MSS (Multi-Secret Sharing) scheme: `n` (number of shares permitted), `d` (degree of the polynomial), and `BITS` (bit length for randomness). It produces the polynomial's coefficients and a random key which is in the coefficient of $x^0 =1$. 2. `poly(x)`: This method evaluates the polynomial defined by the coefficients in `self.coeffs`   at the given a value `x`. 3. `get_share(x)`: For a given `x`, this method returns a share. It determines whether there are still enough shares and whether the supplied `x` less than `2^15`. It returns a share with the evaluated `y`-value of the polynomial at `x` if the necessary requirements are met. Shares that are available are reduced in number by one for each request. 4. `encrypt_flag(m)`: This method uses AES in CBC mode to encrypt a message `m` (in this case, the `FLAG`). It uses SHA-256 to extract a key from the starting key, creates a random initialization vector (IV), then encrypts the data. The end product is a dictionary that has both the encrypted message and the IV. ### Vulnerability Now let's talk about the weakness for `x=0`: Denoting the polynomial in the class by: $$p(x) = \sum_{i=0}^d p_i x^i = p_0 + p_1 x +p_2x^2 + \ldots p_dx^d$$ A check is made in the get_share function to see if `x` is bigger than $2^{15} =32768$. It passes the check and the share is given without any modular reduction if x is zero. The evaluation of the polynomial at `x=0` returns $p_0$, which is the key. This indicates that the key is directly visible by requesting a share with `x=0`. Since the key is used to derive the AES key for encrypting the `FLAG`, if an attacker requests a share with `x=0`, they can obtain the key directly. With this key, they can then decrypt the encrypted `FLAG` after using sha256 algorithm to derive the AES key. In summary, the vulnerability lies in the lack of preventing an attacker to ask a share for `x=0`, allowing them to retrieve the key and subsequently decrypt the encrypted FLAG. ### Python Script ```py from pwn import * import json from Crypto.Util.number import * from hashlib import sha256 from Crypto.Cipher import AES from Crypto.Util.Padding import unpad address = '83.136.251.50' port = 54878 r = remote(address, port) def json_recv(): line = r.recvline() return json.loads(line.decode()) def json_send(hsh): request = json.dumps(hsh).encode() r.sendline(request) r.recvuntil(b"query = ").decode() to_send ={"command":"get_share","x":str(0)} json_send(to_send) rvd = json.loads(r.recvline()[:-1]) r.recvuntil(b"query = ").decode() to_send ={"command":"encrypt_flag"} json_send(to_send) flag = r.recvline_contains(b"Here is").split(b" : ")[1][:-1] flag = json.loads(flag) iv = bytes.fromhex(flag["iv"]) enc = bytes.fromhex(flag["enc_flag"]) key = sha256(str(rvd["y"]).encode()).digest() cipher = AES.new(key, AES.MODE_CBC, iv) print(unpad(cipher.decrypt(enc),16).decode()) #HTB{thr3sh0ld_t00_sm4ll_______CRT_t00_str0nk!} ``` ## [Easy] MSS Revenge This challenges is a rectification for the previous challenge, that means our previous solution is not the intendend one (Clearly from the flag phrase). Let's analyze the changes and devise a strategy to obtain the secret key. ### Overview In this challenge we are given [server.py](https://github.com/hackthebox/uni-ctf-2023/blob/main/uni-ctf-2023/crypto/%5BEasy%5D%20MSS%20Revenge/challenge/server.py) which modifies the `get_share` method, now it includes an additional check for `x`, ensuring that it is both greater than 0 and less than $2^{15}$. ```py def get_share(self, x): if x < 1 or 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)} ``` ### Analysis To retrieve the secret key, a fresh approach is necessary. Because there are so few shares available (limited by $n<d$), we are unable to do a direct Lagrange interpolation. Alternatively, we can aggregate several evaluations of the polynomial at various primes by using the Chinese Remainder Theorem ([CRT](https://en.wikipedia.org/wiki/Chinese_remainder_theorem#:~:text=In%20mathematics%2C%20the%20Chinese%20remainder,are%20pairwise%20coprime%20(no%20two))). ### A solution: Here's a methodical approach: - Choose a collection of unique prime numbers that are all less than $2^{15}$ (15-bit primes). Since the hash size is 256-bit then we need $\lceil\frac{256}{15} \rceil = 18$ prime number and requested share. - Utilise these primes to determine the polynomial's evaluation points $(q,p(q))$ modulo $q$. We know that $$p(q)= p_0 + p_1 q +p_2q^2 +\ldots +p_d q^d \mod q =p_0 \mod q$$ for each prime number $q$. - Calculate the residues ($key = p_0 \mod M$) using the chinese remainder theorem such that $M = \prod_q q$. Since we have many primes such that $M \geq2^{255}$ then we are sure that we retrieved the right $key=p_0$. - hash the key and decrypt to get the flag. ### SageMath Script ```py from pwn import * # pip install pwntools import json from Crypto.Util.number import * from hashlib import sha256 from Crypto.Cipher import AES from Crypto.Util.Padding import pad,unpad def json_send(hsh): request = json.dumps(hsh).encode() def json_recv(): line = r.recvline() return json.loads(line.decode()) pr = [getPrime(15) for _ in range(18)] r = remote('94.237.48.55', int(45135)) welcome = r.recvline() points = [] for i in range(len(pr)): r.recvuntil(b"query = ").decode() to_send ={"command":"get_share","x":str(pr[i])} json_send(to_send) rvd = json.loads(r.recvline()[:-1]) points.append((rvd["x"],rvd["y"])) r.recvuntil(b"query = ").decode() to_send ={"command":"encrypt_flag"} json_send(to_send) flag = r.recvline_contains(b"Here is").split(b" : ")[1][:-1] flag = json.loads(flag) mods = [] res = [] for i in range(len(points)): mods.append(points[i][0]) res.append(lift(Mod(points[i][1],points[i][0]))) key = crt(res,mods) iv = bytes.fromhex(flag["iv"]) enc = bytes.fromhex(flag["enc_flag"]) key1 = sha256(str(int(key)).encode()).digest() cipher = AES.new(key1, AES.MODE_CBC, iv) print(unpad(cipher.decrypt(enc),16).decode()) #HTB{R3venge_0f_7he_sm4ll_thr3sh0ld_!} ``` ## [Medium] Mayday Mayday ### Overview ### Analysis ### Script ## [Hard] Zombie Rolled ### Overview ### Analysis ### Script