from time import time import random import os import sys import hashlib
import random as py_random
defsample(tau,eta,lens,B,s=None): lower = -B + tau * eta upper = B - tau * eta c_rows = [] for _ inrange(lens): row = [0] * 128 for idx in py_random.sample(range(128), tau): row[idx] = py_random.choice((-1, 1)) c_rows.append(row) c = matrix(ZZ, c_rows)
if s isNone: s = vector(ZZ, [py_random.randint(-eta, eta) for _ inrange(128)]) cs = c * s
# If one coordinate can never satisfy the bound for any y in [-B, B], # rejection sampling would not terminate. count = 0 whileTrue: y = vector(ZZ, [py_random.randint(-B, B) for _ inrange(lens)]) z = cs + y ifall(lower <= zi <= upper for zi in z): return (c_rows, list(z)), (list(s), list(y)) count+=1 if count>10000: raise Exception("Rejection sampling failed after 10000 attempts. Please check the parameters.") exit(0)
tau=39 eta=2 B=2*tau*eta s= vector(ZZ, [py_random.randint(-eta, eta) for _ inrange(128)])
s[0]=eta print("the first stage") cs=[] leaks=[] count=0 while count<15000: print("how many samples do you want to get? (0 to exit)") try: n=int(input()) except: print("[-] Invalid input!") continue
if n == 0: break pk,sk=sample(tau,eta,n,B,s) cs+=pk[0] leaks+=[bin(i&0xffffffff).count('1') for i in sk[1]] print(pk[1]) count+=n print("the second stage") count=0 while count<5000: print("which samples do you interest in more? (-1 to exit)") try: indices=input() indices = [int(item.strip()) for item in indices.split(',')] assertall(-1 <= idx < len(cs) for idx in indices) except: print("[-] Invalid input!") continue if -1in indices: break ans=[(cs[i],leaks[i]) for i in indices] print(ans) count+=len(indices)
print("the third stage") print("plz input the guess secret s:") try: guess_s = [int(item.strip()) for item ininput().split(',')] assertlen(guess_s) == 128 except: print("[-] Invalid input!") sys.exit(1) if guess_s == list(s): flag=open("./flag.txt").read().strip() print(f"[+] Congratulation! Here is your flag: {flag}") else: print("[-] Wrong guess! Better luck next time!")
分析
题目分析
这道题本质上是一个带泄露的稀疏秘密恢复问题,灵感来自 FLIP(Fast Lattice-based Instantiation and Primitive)加密方案。目标是通过服务端给的一些有限信息,把一个 128 维的秘密向量 s 给恢复出来。
参数设置
参数
值
含义
N
128
秘密向量的维度
tau
39
每行 c 的非零元素个数
eta
2
秘密 s 每个分量的范围 [-2, 2]
B
2×39×2 = 156
噪声 y 的范围 [-B, B]
秘密向量 s
1 2
s = vector(ZZ, [py_random.randint(-eta, eta) for _ inrange(128)]) s[0] = eta # s[0] = 2,这是已知条件
lower = -B + tau * eta # = -156 + 78 = -78 upper = B - tau * eta # = 156 - 78 = 78
y = vector(ZZ, [py_random.randint(-B, B) for _ inrange(lens)]) # y ∈ [-156, 156] z = cs + y ifall(lower <= zi <= upper for zi in z): # 要求 z ∈ [-78, 78] return (c_rows, list(z)), (list(s), list(y))
所以最终每个样本满足:
zi=ci⋅s+yi
其中:
zi∈[−78,78](拒绝采样保证)
yi∈[−156,156](均匀随机生成)
ci⋅s∈[−78,78](稀疏性保证)
这里多说一句,为什么拒绝采样要限制 z ∈ [-78, 78]?
如果 z∈[−78,78],那么 y=z−c⋅s∈[−78−78,78+78]=[−156,156],自动满足 y 的范围约束。换句话说,这个约束确保了:给定 z 之后,y 的有效范围是 [z−78,z+78],宽度只有 156(而不是完整的 313)。当 |z| 比较小的时候,这个范围跟 [-156, 156] 的交集更小,HW 约束就更容易把 y 唯一确定下来。
defbuild_exact_equations(zs, selected, c_rows, leaks): """ 已知: z = c*s + y leak = HW32(y)
枚举 y in [-156,156] 且 HW32(y)=leak。 若满足 c*s = z-y in [-78,78] 的候选唯一, 则得到一条精确方程: c*s = z-y """ hw_map = defaultdict(list) for y inrange(-B, B + 1): hw_map[hw32(y)].append(y)
eq_rows = [] eq_rhs = []
for i inrange(len(c_rows)): z = int(zs[selected[i]]) leak = int(leaks[i])
candidates = [] for y in hw_map[leak]: cs = z - y if -CS_BOUND <= cs <= CS_BOUND: candidates.append(cs)
defsolve_by_lll(eq_rows, eq_rhs): """ 对每条方程: c_i * s = rhs_i
构造扩展矩阵: M_i = [c_i | -rhs_i]
则真实向量: v = (s_0, s_1, ..., s_127, 1)
满足: M * v = 0
因此 v 位于 M 的右核中。 求右核格后 LLL,短向量通常就是 ±k*(s,1)。 """ M = Matrix(ZZ, [list(row) + [-int(rhs)] for row, rhs inzip(eq_rows, eq_rhs)])
print(f"[+] rank(M) = {M.rank()}, kernel dim over QQ = {N + 1 - M.rank()}")
# 先在 QQ 上求右核 Kq = M.change_ring(QQ).right_kernel().basis_matrix()
if Kq.nrows() == 0: raise RuntimeError("empty kernel")
# 把有理基向量逐行清分母,转成整数格基 K_rows = [] for r in Kq.rows(): den = ZZ(1) for x in r: den = lcm(den, ZZ(x.denominator())) K_rows.append([ZZ(x * den) for x in r])
K = Matrix(ZZ, K_rows)
print(f"[+] integer kernel basis shape = {K.nrows()} x {K.ncols()}")