# [zer0pts CTF 2020] dirty laundry
###### tags: `zer0pts CTF` `crypto`
## Challenge Summary
The challenge has the following code:
```python=
#!/usr/bin/sage
from sage.all import *
from Crypto.Util.number import getStrongPrime, bytes_to_long
from secret import flag
class PRNG256(object):
def __init__(self, seed):
self.mask = (1 << 256) - 1
self.seed = seed & self.mask
def _pick(self):
b = ((self.seed>>0)^(self.seed>>2)^(self.seed>>5)^(self.seed>>10)^1)&1
self.seed = ((self.seed>>1)|(b<<255)) & self.mask
return b
def rand(self):
x = 0
for i in range(256):
x = (x << 1) | self._pick()
return x
PRIME = getStrongPrime(1024)
prng = PRNG256(PRIME)
def paillier_enc(m, p, noise):
p = next_prime(p + noise)
q = getStrongPrime(512)
n = p * q
g = (1 + prng.rand() * n) % n**2
c = pow(g, m, n**2) * pow(prng.rand(), n, n**2) % n**2
return n, g, c
def make_shares(secret, k, shares, prime=PRIME):
PR, x = PolynomialRing(GF(prime), name='x').objgen()
f = PR([secret] + [ZZ.random_element(prime) for _ in range(k-1)])
xy = []
pubkey = []
for x in range(1, shares+1):
noise = prng.rand()
n, g, y = paillier_enc(f(x) + noise, prime, noise)
pubkey.append([n, g])
xy.append([x, y])
return pubkey, xy
secret = bytes_to_long(flag)
pubkey, shares = make_shares(secret, 3, 5)
print("[+] len(flag):", len(flag))
print("[+] pubkey:", pubkey)
print("[+] shares:", shares)
```
The challenge is the secret sharing scheme with the paillier encryption. We need to get the PRIME and pure $f(x)$ velues to solve this.
## How to solve
First, we crack the paillier encryption, because it has wierd modulis.
### factoring $n_i$
The paillier encryption has wierd modulis.
$$
n_i = next\_prime(PRIME + noise_i) q_i
$$
PRIME is 1024-bit, each noise is 256-bit, and $q$ is 526-bit. If we define $p_i \approx PRIME + noise_i$, we can say that $n_i = (PRIME + noise_i) q_i$. Since
$$
\begin{cases}
n_1 = (PRIME + noise_1) q_1 \\
n_2 = (PRIME + noise_2) q_2
\end{cases}
$$
as a consequence, $q_1 n_2 - q_2 n_1 = q_1 q_2 noise_2 - q_1 q_2 noise_1$
(left-hand side is big compared to right-hand side)
We can apply LLL.
Consider the following lattice:
\begin{pmatrix}
2^{768} & n_2 & n_3 & n_4 & n_5 \\
0 & -n_1 & 0 & 0 & 0\\
0 & 0 & -n_1 & 0 & 0\\
0 & 0 & 0 & -n_1 & 0\\
0 & 0 & 0 & 0 & -n_1
\end{pmatrix}
As a result, the vector in the lattice is equal to $(2^{278}q_1, v_2, v_3, v_4, v_5)$ where $v_i = (noise_i - noise_1) q_1 q_i$. We can get $q_1$ by using LLL.
Therefore, we can calculate $q_2 \sim q_5$($q_i = gcd(q_1, (noise_i - noise_1)q_1 q_i)$).
### get noise
By the public key of the paillier encryption,
$$g^{\lambda} = 1 + k\lambda n$$
$g, \lambda, n$ are known values so we can get $k$. Since we were able to find the value of prng, we could extract noise before it was used in paillier (note the order)
### get flag
Now, we have noise, so we can calculate pure $f(x)$ and the correct $PRIME$. Finally, we can the flag by using lagrange interpolation.
Script:
```python=
from sage.all import *
from Crypto.Util.number import inverse, long_to_bytes
from re import compile
with open("./../distfiles/output.txt") as f:
output = f.read()
pubkey = eval(compile(r"pubkey: (.+)\n").findall(output)[0])
shares = eval(compile(r"shares: (.+)\n").findall(output)[0])
ns = []
gs = []
for n, g in pubkey:
ns.append(n)
gs.append(g)
# factoring
print("[+] n bits:", [n.bit_length() for n in ns])
m = matrix(ZZ, 5, 5)
m[0, 0] = 2**768
for i in range(1, m.dimensions()[0]):
m[0, i] = ns[i]
m[i, i] = -ns[0]
print("[+] matrix")
for i in range(m.dimensions()[0]):
print(i, [int(x).bit_length() for x in m[i]])
ml = m.LLL()
print("[+] LLL done")
print("[+] matrix")
for i in range(ml.dimensions()[0]):
print(i, [int(x).bit_length() for x in ml[i]])
# decrypt and get samples of prng
def dec(c, p, q, g):
n = int(p) * int(q)
l = int(lcm(p-1, q-1))
m = ((pow(c, l, n**2)-1) // n) * inverse((pow(g, l, n**2)-1) // n, n)
k = ((pow(g, l, n**2) - 1) // n) * inverse(l, n)
return m % n, k % n
q0 = gcd(abs(ml[0][0]), ns[0])
p0 = ns[0] // q0
qs = [q0]
ps = [p0]
res = dec(shares[0][1], p0, q0, gs[0])
ms = [res[0]]
samples = [res[1]]
for i in range(1, 5):
qi = gcd(abs(ml[0][i])//q0, ns[i])
pi = ns[i] // qi
mi, ki = dec(shares[i][1], pi, qi, gs[i])
qs.append(qi)
ps.append(pi)
ms.append(mi)
samples.append(ki)
# find noises
def bit_reverse(x):
y = 0
for i in range(256):
y = (y << 1) | ((x >> i) & 1)
return y
s = bit_reverse(samples[-1])
noises = []
idx = 3
for j in range(13):
x = 0
for i in range(256):
msb = s >> 255
s <<= 1
lsb = (msb^(s>>0)^(s>>2)^(s>>5)^(s>>10)^1)&1
s = (s|lsb) & ((1<<256)-1)
x = (x << 1) | lsb
noise = bit_reverse(x)
if j % 3 == 0:
noises.append(noise)
elif j % 3 == 2:
assert noise == samples[idx]
idx -= 1
noises.reverse()
print("[+] found all noises")
xy = []
for i in range(5):
xy.append((i+1, ms[i] - noises[i]))
# find PRIME
prev = previous_prime(ps[0])
for x in range(prev, ps[0]):
prime = x - noises[0]
if is_prime(prime):
R = PolynomialRing(GF(prime), name='x')
f = R.lagrange_polynomial(xy)
print("[+] flag:", long_to_bytes(f(0)))
```