Try   HackMD

[zer0pts CTF 2020] dirty laundry

tags: zer0pts CTF crypto

Challenge Summary

The challenge has the following code:

#!/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
ni

The paillier encryption has wierd modulis.

ni=next_prime(PRIME+noisei)qi
PRIME is 1024-bit, each noise is 256-bit, and
q
is 526-bit. If we define
piPRIME+noisei
, we can say that
ni=(PRIME+noisei)qi
. Since
{n1=(PRIME+noise1)q1n2=(PRIME+noise2)q2

as a consequence,
q1n2q2n1=q1q2noise2q1q2noise1

(left-hand side is big compared to right-hand side)
We can apply LLL.

Consider the following lattice:

(2768n2n3n4n50n100000n100000n100000n1)
As a result, the vector in the lattice is equal to
(2278q1,v2,v3,v4,v5)
where
vi=(noiseinoise1)q1qi
. We can get
q1
by using LLL.

Therefore, we can calculate

q2q5(
qi=gcd(q1,(noiseinoise1)q1qi)
).

get noise

By the public key of the paillier encryption,

gλ=1+kλn
g,λ,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:

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)))