TinyNTRU 解题思路 将公钥关系变形:
构造 NTRU 格:$$\begin{bmatrix} qI & 0\ H & I \end{bmatrix}$$
其中 (H) 为由多项式 (h) 构造的循环矩阵。
真实私钥对应格中的短向量:(f,g)
因此使用 LLL/BKZ 约减恢复短向量。
解题步骤
读取题目给出的公钥参数。
根据 (h) 构造 NTRU 格。
使用 LLL 进行格基约减。
从短向量中筛选满足:$$f_h \equiv g \pmod q$$
的候选。 5. 利用恢复出的私钥解密密文。 6. 得到 flag。
完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 from ast import literal_evalfrom pathlib import Pathimport reN = 127 p = 3 q = 12289 chunk_bytes = 24 def trim (poly, n ): return (poly + [0 ] * n)[:n] def mod_center (x, modulus ): x %= modulus if x > modulus // 2 : x -= modulus return x def center_lift (poly, modulus ): return [mod_center(x, modulus) for x in poly] def poly_add (a, b, n, modulus=None ): a = trim(a, n) b = trim(b, n) out = [a[i] + b[i] for i in range (n)] if modulus is not None : out = [x % modulus for x in out] return out def poly_mul (a, b, n, modulus=None ): a = trim(a, n) b = trim(b, n) out = [0 ] * n for i, ai in enumerate (a): if ai == 0 : continue for j, bj in enumerate (b): if bj: out[(i + j) % n] += ai * bj if modulus is not None : out = [x % modulus for x in out] return out def decrypt (ciphertext, f ): lifted = center_lift(poly_mul(f, ciphertext, N, q), q) return [x % p for x in lifted] def poly_blocks_to_bytes (blocks, chunk_bytes ): raw = bytearray () for block in blocks: value = 0 for coeff in reversed (block): value = value * 3 + (coeff % 3 ) raw.extend(value.to_bytes(chunk_bytes, "little" )) size = int .from_bytes(raw[:2 ], "big" ) return bytes (raw[2 :2 + size]) pub_text = Path("public_key.txt" ).read_text() out_text = Path("output.txt" ).read_text() h = literal_eval(re.search(r"h = (\[.*\])" , pub_text).group(1 )) ciphertexts = literal_eval(re.search(r"ciphertexts = (\[.*\])" , out_text).group(1 )) B = Matrix(ZZ, 2 * N, 2 * N) for i in range (N): B[i, i] = q for i in range (N): for j in range (N): B[N + i, j] = h[(j - i) % N] B[N + i, N + i] = 1 L = B.LLL(delta=0.99 ) f = None candidate_g = None for row in L.rows(): vec = [int (x) for x in row] a = vec[:N] b = vec[N:] ones = [i for i, x in enumerate (b) if abs (x) == 1 ] threes = [i for i, x in enumerate (b) if abs (x) == 3 ] if len (ones) == 1 and len (threes) == 7 : idx = ones[0 ] s = b[idx] f_try = [s * b[(j + idx) % N] for j in range (N)] g_try = [s * a[(j + idx) % N] for j in range (N)] if poly_mul(f_try, h, N, q) == [x % q for x in g_try]: f = f_try candidate_g = g_try break if f is None : raise ValueError("failed to recover private key" ) print ("[+] recovered f nonzero:" )print ([(i, x) for i, x in enumerate (f) if x])print ("[+] recovered g nonzero:" )print ([(i, x) for i, x in enumerate (candidate_g) if x])print ("[+] verify f*h == g mod q:" , poly_mul(f, h, N, q) == [x % q for x in candidate_g])blocks = [decrypt(c, f) for c in ciphertexts] flag = poly_blocks_to_bytes(blocks, chunk_bytes) print ("[+] flag:" , flag.decode())
Cold Forge 思路
FROST nonce 泄露恢复 share
FROST 签名份额满足:$$z_i=k_i+c\lambda_i s_i \pmod q$$
其中:
$s_i$ 为 signer 私钥 share
$\lambda_i$ 为拉格朗日系数
c 为 challenge
$k_i$ 为 nonce
泄露信息:$$k_i=(nonce_msb_i<<104)+\epsilon_i$$
代入:$$z_i-(nonce_msb_i<<104)=c\lambda_i s_i+\epsilon_i\pmod q$$
令:$$a_i=c\lambda_i$$ $$b_i=z_i-(nonce_msb_i<<104$$
得到:$$b_i=a_i s_i+\epsilon_i\pmod q$$
因为:$$|\epsilon_i|<2^{105}$$
这是 Hidden Number Problem,利用 LLL + Babai 格攻击恢复私钥 share。
LLL 恢复结果
合成阈值私钥
拉格朗日系数:
$$\lambda_1=2$$
$$\lambda_2=-1$$
FROST 私钥:$$sk=\lambda_1share_1+\lambda_2share_2\pmod q$$
计算公钥与题目 group_public 一致。
重建 BIP340 签名
使用恢复出的私钥,对消息生成确定性 BIP340 签名:
ChaCha20-Poly1305 解密
密钥派生:$$key=SHA256(b”cold-forge/release/v1”||signature)$$
使用 sealed_release.json 中 nonce、aad、ciphertext 解密:
1 flag{bd9b026e-b58e-416c-bf8f-d815573c35a6}
Invalid Echo 思路 题目实现 ECDH:Q=dG
$共享密钥:S=dP,然后:key=SHA256(S_x||S_y)$
$但是服务器接收点时未检查:P\in E,导致可以提交非法曲线点。$
漏洞利用 :
目标曲线:$E^2=x^3+7$
构造非法曲线:$E_c^2=x^3+c,寻找小阶点:ord(P)=r,则:dP=(d\bmod r)P$
$服务器 oracle 返回已知明文:m,枚举:k\in[0,r-1],匹配 AES:k=d\bmod r$
$恢复私钥得到:d\equiv a_i\pmod {r_i},使用 CRT:d=CRT(a_i,r_i),恢复服务器私钥。$
$计算:S=dB_{pub},得到:key=SHA256(S_x||S_y),AES-CBC 解密得到 flag。$
解答 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 import base64import hashlibimport jsonimport sysimport tempfileimport zipfilefrom decimal import Decimal, ROUND_HALF_EVEN, getcontextfrom pathlib import Pathfrom cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305from sympy import MatrixP_FIELD = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 GX = 55066263022277343669578718895168534326250603453777594175500187360389116729240 GY = 32670510020758816978083085130507043184471273380659243275938904335757337482424 G = (GX, GY) getcontext().prec = 220 def i2b32 (x ): return int (x).to_bytes(32 , "big" ) def tagged_hash (tag, data ): h = hashlib.sha256(tag.encode()).digest() return hashlib.sha256(h + h + data).digest() def inv_mod (x, m ): return pow (x % m, -1 , m) def point_add (A, B ): if A is None : return B if B is None : return A x1, y1 = A x2, y2 = B if x1 == x2 and (y1 + y2) % P_FIELD == 0 : return None if A == B: k = (3 * x1 * x1) * inv_mod(2 * y1, P_FIELD) % P_FIELD else : k = (y2 - y1) * inv_mod(x2 - x1, P_FIELD) % P_FIELD x3 = (k * k - x1 - x2) % P_FIELD y3 = (k * (x1 - x3) - y1) % P_FIELD return x3, y3 def point_mul (k, point=G ): k %= N R = None Q = point while k: if k & 1 : R = point_add(R, Q) Q = point_add(Q, Q) k >>= 1 return R def is_even_y (P ): return P[1 ] % 2 == 0 def bip340_sign_deterministic (msg, sk ): P = point_mul(sk) d = sk if is_even_y(P) else N - sk px = i2b32(P[0 ]) aux = b"\x00" * 32 t = bytes (a ^ b for a, b in zip (i2b32(d), tagged_hash("BIP0340/aux" , aux))) k0 = int .from_bytes(tagged_hash("BIP0340/nonce" , t + px + msg), "big" ) % N R = point_mul(k0) k = k0 if is_even_y(R) else N - k0 rx = i2b32(R[0 ]) e = int .from_bytes(tagged_hash("BIP0340/challenge" , rx + px + msg), "big" ) % N s = (k + e * d) % N return rx + i2b32(s) def centered_mod (x, q ): x %= q return x if x <= q // 2 else x - q def gram_schmidt_decimal (B ): n = len (B) m = len (B[0 ]) B_star = [[Decimal(0 ) for _ in range (m)] for _ in range (n)] norm = [Decimal(0 ) for _ in range (n)] for i in range (n): v = [Decimal(x) for x in B[i]] for j in range (i): if norm[j] == 0 : continue dot = sum (Decimal(B[i][k]) * B_star[j][k] for k in range (m)) mu = dot / norm[j] for k in range (m): v[k] -= mu * B_star[j][k] B_star[i] = v norm[i] = sum (x * x for x in v) return B_star, norm def babai_closest_vector (reduced_basis, target ): n = len (reduced_basis) m = len (reduced_basis[0 ]) B_star, norm = gram_schmidt_decimal(reduced_basis) y = [Decimal(x) for x in target] for i in reversed (range (n)): if norm[i] == 0 : continue c = sum (y[k] * B_star[i][k] for k in range (m)) / norm[i] ci = int (c.to_integral_value(rounding=ROUND_HALF_EVEN)) if ci: for k in range (m): y[k] -= Decimal(ci) * Decimal(reduced_basis[i][k]) closest = [target[k] - int (y[k]) for k in range (m)] return closest def recover_share_for_signer (transcripts, signer_id, lagrange, q, low_bits, drift ): rows = [t for t in transcripts if int (t["signer" ]) == signer_id] a = [] b = [] for t in rows: c = int (t["challenge" ], 16 ) z = int (t["z" ], 16 ) nonce_msb = int (t["nonce_msb" ], 16 ) K = nonce_msb << low_bits a_j = c * lagrange % q b_j = (z - K) % q a.append(a_j) b.append(b_j) m = len (a) inv_a0 = inv_mod(a[0 ], q) u = [1 ] + [(a[i] * inv_a0) % q for i in range (1 , m)] basis = [u] for i in range (1 , m): row = [0 ] * m row[i] = q basis.append(row) print (f"[*] signer {signer_id} : LLL on {m} -dim lattice" ) reduced = Matrix(basis).lll(delta=0.99 ) reduced_basis = [[int (reduced[i, j]) for j in range (m)] for i in range (m)] closest = babai_closest_vector(reduced_basis, b) share = closest[0 ] * inv_a0 % q residuals = [centered_mod(b_j - a_j * share, q) for a_j, b_j in zip (a, b)] max_abs = max (abs (x) for x in residuals) bound = (drift + 1 ) * (1 << low_bits) print (f"[+] signer {signer_id} share = {share:064x} " ) print (f" max |eps| = {max_abs} ({max_abs.bit_length()} bits)" ) print (f" bound = {bound} ({bound.bit_length()} bits)" ) if max_abs >= bound: raise ValueError("share recovery failed" ) return share def load_challenge (path ): path = Path(path) if path.is_file() and path.suffix.lower() == ".zip" : tmp = tempfile.TemporaryDirectory() with zipfile.ZipFile(path, "r" ) as zf: zf.extractall(tmp.name) root = Path(tmp.name) else : tmp = None root = path frost_file = list (root.rglob("frost_telemetry.json" ))[0 ] sealed_file = list (root.rglob("sealed_release.json" ))[0 ] frost = json.loads(frost_file.read_text()) sealed = json.loads(sealed_file.read_text()) return frost, sealed, tmp def main (): if len (sys.argv) != 2 : print (f"Usage: python3 {sys.argv[0 ]} <cold_forge.zip | cold_forge_dir>" ) sys.exit(1 ) frost, sealed, tmp = load_challenge(sys.argv[1 ]) q = int (frost["group_order" ], 16 ) low_bits = int (frost["leak_model" ]["discarded_low_bits" ]) drift = int (frost["leak_model" ]["maximum_bucket_drift" ]) participants = [int (x) for x in frost["participants" ]] lambdas = {int (k): int (v, 16 ) for k, v in frost["lagrange_coefficients" ].items()} shares = {} for signer_id in participants: shares[signer_id] = recover_share_for_signer( frost["transcripts" ], signer_id, lambdas[signer_id], q, low_bits, drift, ) sk = sum (lambdas[i] * shares[i] for i in participants) % q print (f"[+] group secret sk = {sk:064x} " ) pub = point_mul(sk) want_x = int (frost["group_public" ]["x" ], 16 ) want_y = int (frost["group_public" ]["y" ], 16 ) assert pub == (want_x, want_y) print ("[+] public key check passed" ) msg = bytes .fromhex(frost["message_hex" ]) sig = bip340_sign_deterministic(msg, sk) print (f"[+] deterministic BIP340 signature = {sig.hex ()} " ) key = hashlib.sha256(b"cold-forge/release/v1" + sig).digest() print (f"[+] release key = {key.hex ()} " ) nonce = base64.b64decode(sealed["nonce" ]) aad = base64.b64decode(sealed["aad" ]) ciphertext_and_tag = base64.b64decode(sealed["ciphertext" ]) plaintext = ChaCha20Poly1305(key).decrypt(nonce, ciphertext_and_tag, aad) print ("[+] plaintext =" , plaintext.decode()) if tmp is not None : tmp.cleanup() if __name__ == "__main__" : main()
slashKEM 解题思路 1.功耗侧信道恢复部分密钥
题目泄露模型:
$$L(s,u)=HW(Mont(su)\oplus Mont(7u+0x1234))$$
其中: s为 secret coefficient u为公开输入 HW为汉明重量
由于:$$s\in{-2,-1,0,1,2}$$
因此枚举所有可能值,通过 CPA 计算相关系数$$\rho(X,Y)= \frac{Cov(X,Y)} {\sqrt{Var(X)Var(Y)}}$$
选择相关性最高的猜测,即可恢复大部分 secret coefficient。
注意 trace 中 coefficient 顺序经过 bit-reversal,需要根据:
$$order=(0,64,32,96,16,\cdots)$$还原真实位置。
2、LWE 格攻击恢复剩余系数
ML-KEM 公钥关系:$$t=As+e \pmod q$$
其中:$$q=3329$$
并且误差:$$e_i\in{-1,0,1}$$
CPA 得到部分:
$$s=s_{known}+s_{unknown}$$
$$由于误差很小,将问题转化为格上的 CVP。 构造 lattice:$$
$$L= \begin{bmatrix} qI&0\ A_{unknown}^T&I \end{bmatrix}$$
恢复剩余 secret coefficient。
3.密钥派生
恢复完整 secret polynomial 后:
$$K=SHA256(encode(s))$$
得到 AES-GCM 解密密钥。
进行 AES-GCM 解密:
$$flag=AES_GCM^{-1}(K,nonce,ciphertext,tag)$$
得到:
1 flag{708d7b97-0b8d-4d99-aa1a-b23d3b13cc51}
解答代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 from sage.all import *import numpy as npfrom hashlib import sha256from Crypto.Cipher import AESimport jsonq = 3329 N = 64 with open ("output.json" ) as f: data=json.load(f) traces=np.load("traces.npz" )["traces" ] us=np.load("traces.npz" )["u_samples" ] def hw (x ): return bin (x&0xffff ).count("1" ) def leak (s,u ): return hw((s*u)%q ^ ((7 *u+0x1234 )%q)) def corr (x,y ): x=x-np.mean(x) y=y-np.mean(y) return abs (np.dot(x,y)/ np.sqrt(np.dot(x,x)*np.dot(y,y))) b=f"{i:07b} " [::-1 ] order.append(int (b,2 )) for pos,idx in enumerate (order): best=(-1 ,0 ) for s in [-2 ,-1 ,0 ,1 ,2 ]: hyp=[ leak( s, int (u[idx]) ) for u in us ] c=corr( hyp, traces[:,96 +pos] ) if c>best[0 ]: best=(c,s) if best[0 ]>0.5 : secret[idx]=best[1 ] print ("[+] CPA recovered" )A=data["A" ] t=vector( ZZ, sum (data["t" ],[]) ) def column (i ): poly=i//N pos=i%N ret=[] for row in range (2 ): v=[0 ]*N v[pos]=1 ret+=list ( vector(GF(q),A[row][poly]) * vector(GF(q),v) ) return vector(ZZ,ret) known=[i for i in range (128 ) if secret[i]!=None ] unknown=[i for i in range (128 ) if secret[i]==None ] rhs=t for i in known: rhs-=secret[i]*column(i) M=Matrix( ZZ, [column(i) for i in unknown] ).transpose() m=len (unknown) L=Matrix( ZZ, 128 +m, 128 +m ) for i in range (128 ): L[i,i]=q for i in range (m): for j in range (128 ): L[128 +i,j]=M[j,i] L[128 +i,128 +i]=1 L=L.LLL() v=L.closest_vector( vector(ZZ,list (rhs)+[0 ]*m) ) for i,x in zip (unknown,v[-m:]): secret[i]=int (x) print ("[+] secret:" )print (secret)key=sha256( bytes ([x+2 for x in secret]) ).digest() cipher=AES.new( key, AES.MODE_GCM, nonce=bytes .fromhex(data["nonce" ]) ) flag=cipher.decrypt_and_verify( bytes .fromhex(data["ciphertext" ]), bytes .fromhex(data["tag" ]) ) print (flag.decode())