zer0pts CTF 2020 - dirty laundry 题解
题目描述:Do you wanna air my dirty laundry?
附件里给了一份把 Shamir secret sharing 和 Paillier 混在一起的实现:先用一个二次多项式 f(x) = secret + ax + bx^2 (mod PRIME) 生成 5 份 share,再把每份 f(x) + noise 用不同的 Paillier 公钥加密后公开
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
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
Paillier 在题目的特殊生成方式下满足
因此可以直接恢复
而密文在去掉随机项以后会近似落成
这就把原本的 Paillier 乘法结构压回了一阶线性关系。
先从每组公钥里的 g 直接算出对应的 k = (g - 1) // n,然后顺着这些连续输出去恢复 PRNG 状态。状态一旦拿到,Paillier 里用到的随机量和额外加的 noise 也就都能一起复原。
接着把密文里的随机项剥掉,再利用 (1 + k * n)^m 的展开把它压回一阶线性关系,最后就能拿回每个点的真实多项式值。到这一步题目又变回一个普通二次多项式,最后常数项就是 flag。
def key_from_g(g, n):
return (g - 1) // n
def linearized_plain(c, n, k, rn_inv):
cleaned = (c * rn_inv) % (n * n)
return ((cleaned - 1) // n) * pow(k, -1, n) % n