2024年熵密杯 Flag2 精讲:SM2协同签名-随机值复用 2024年熵密杯 Flag2 精讲SM2协同签名 — 随机值复用阅读指引本文是熵密杯2024年真题精析密码系统系列的第二篇。Flag2 考察的是SM2两方协同签名中随机数的不当复用——服务端在生成两个独立随机数 k₂ 和 k₃ 时错误地将 k₃ 设为 k₂ 的拷贝BN_copy(k3, k2)。这导致 s₂ 和 s₃ 之间存在线性关系攻击者从网络截获 (s₂, s₃, r) 后仅需一步模乘即可恢复服务端完整私钥 d₂无需任何暴力破解。 目录题目场景还原两方协同签名协议剖析漏洞定位BN_copy(k3, k2)数学推导d₂的一步恢复攻击实现一行代码与2025年Flag2的对比攻击原理总结与防御1. 题目场景还原题目描述某系统实现了SM2两方协同签名协议客户端Vue 3 element-plus elliptic库持有 d₁服务端C/OpenSSL持有 d₂。双方交互完成签名。攻击者可以截获服务端发回的 (s₂, s₃, r)请恢复服务端私钥 d₂ 获取 flag2具体流程详见下图。2. 两方协同签名协议剖析2.1 客户端第一阶段 — clientSign1// d₁ SM3(password) → 哈希口令得私钥分量letd1newBN(sm3(password),16);// k₁ CSPRNG → 密码学安全随机数k1.valuenewBN(cryptoRandomStringAsync({length:64}),16).mod(n);// P₁ d₁⁻¹ · G → 私钥分量的公钥化letP1G.mul(d1.invm(n));// Q₁ k₁ · G → 临时公钥letQ1G.mul(k1.value);// r₁ x_Q₁ mod nletr1newBN(Q1.getX().toString(16),16).mod(n);// s₁ k₁⁻¹ · (e d₁⁻¹ · r₁) mod n → 部分签名lets1k1.value.invm(n).mul(e.add(d1.invm(n).mul(r1))).mod(n);2.2 服务端处理 — server()// ② 解析客户端参数BN_hex2bn(e,str_e);BN_hex2bn(p1x,str_p1x);/* ... */BN_hex2bn(d2,XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX);// 隐藏的私钥// ③ ★★★ 漏洞核心: k3 k2 ★★★BN_rand_range(k2,n);// k2 random ∈ [1, n-1]BN_copy(k3,k2);// k3 k2 ← 应该是独立的 BN_rand_range!// ④ 验证 r₁ 正确性: u₁ e·s₁⁻¹, u₂ r₁·s₁⁻¹// (x, y) u₁·G u₂·P₁, 验证 r₁ x mod n// ⑤ (x₁, y₁) k₂·G k₃·Q₁ k₂·(G Q₁) ← 因 k₃k₂EC_POINT_mul(group,TMP,k2,Q1,k3,bn_ctx);// r (e x₁) mod n// ⑥ s₂ d₂ · k₃ mod n ( d₂·k₂)// s₃ d₂ · (r k₂) mod nBN_mod_mul(s2,d2,k3,n,bn_ctx);BN_mod_mul(s3,d2,tmp1,n,bn_ctx);// tmp1 r k₂2.3 客户端第二阶段 — clientSign2// s d₁·k₁·s₂ d₁·s₃ - r (mod n)lettmp1d1.mul(k1.value).mod(n);// d₁·k₁lettmp2tmp1.mul(s2).mod(n);// d₁·k₁·s₂lettmp3d1.mul(s3).mod(n);// d₁·s₃tmp1tmp2.add(tmp3).mod(n);letstmp1.sub(r).mod(n);3. 漏洞定位BN_copy(k3, k2)在server_sign.c中服务端的核心漏洞只有一行代码!BN_rand_range(k2,n)||!BN_copy(k3,k2)// ← ★ 漏洞! k3 应该独立随机 ★正确的代码应该是!BN_rand_range(k2,n)||!BN_rand_range(k3,n)// ← k3 独立随机为什么这很严重在两方签名协议中k₂ 和 k₃ 承担着不同的安全职责随机数作用出现在独立性的安全意义k₂服务端临时私钥1r 和 s₃与 k₃ 独立 → s₂ 和 s₃ 无关联k₃服务端临时私钥2s₂与 k₂ 独立 → s₂ 不能从 s₃ 推导当 k₃ k₂ 时s 2 d 2 ⋅ k 3 d 2 ⋅ k 2 s_2 d_2 \cdot k_3 d_2 \cdot k_2s2​d2​⋅k3​d2​⋅k2​s 3 d 2 ⋅ ( r k 2 ) d 2 ⋅ r d 2 ⋅ k 2 d 2 ⋅ r s 2 s_3 d_2 \cdot (r k_2) d_2 \cdot r d_2 \cdot k_2 d_2 \cdot r s_2s3​d2​⋅(rk2​)d2​⋅rd2​⋅k2​d2​⋅rs2​s₂ 和 s₃ 形成了线性关系s₃ - s₂ d₂·r4. 数学推导d₂ 的一步恢复正常协议的安全性正常两方签名中攻击者截获 (s₂, s₃, r) 后面对三个未知量 (d₂, k₂, k₃){ s 2 d 2 ⋅ k 3 ( m o d n ) s 3 d 2 ⋅ ( r k 2 ) ( m o d n ) \begin{cases} s_2 d_2 \cdot k_3 \pmod{n} \\ s_3 d_2 \cdot (r k_2) \pmod{n} \end{cases}{s2​d2​⋅k3​(modn)s3​d2​⋅(rk2​)(modn)​2个方程3个未知数 →无唯一解→ 无法恢复 d₂。漏洞后的脆弱性当 k₃ k₂ 时未知量减少到 2 个 (d₂, k₂){ s 2 d 2 ⋅ k 2 ( m o d n ) s 3 d 2 ⋅ r d 2 ⋅ k 2 d 2 ⋅ r s 2 ( m o d n ) \begin{cases} s_2 d_2 \cdot k_2 \pmod{n} \\ s_3 d_2 \cdot r d_2 \cdot k_2 d_2 \cdot r s_2 \pmod{n} \end{cases}{s2​d2​⋅k2​(modn)s3​d2​⋅rd2​⋅k2​d2​⋅rs2​(modn)​2个方程2个未知数 →唯一解s 3 − s 2 d 2 ⋅ r ( m o d n ) s_3 - s_2 d_2 \cdot r \pmod{n}s3​−s2​d2​⋅r(modn)d 2 ( s 3 − s 2 ) ⋅ r − 1 ( m o d n ) d_2 (s_3 - s_2) \cdot r^{-1} \pmod{n}d2​(s3​−s2​)⋅r−1(modn)不需要任何暴力破解一步模乘即可。5. 攻击实现一行代码// ★ 攻击者只需在 clientSign2 内部加入这一行 ★letd2s3.sub(s2).mod(n).mul(r.invm(n)).mod(n);letflag2flag2{d2.toString(16,64)};等价 Python 实现defrecover_d2(s2_hex,s3_hex,r_hex):n0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123s2int(s2_hex,16)s3int(s3_hex,16)rint(r_hex,16)# d₂ (s₃ - s₂) × r⁻¹ mod nd2((s3-s2)*pow(r,-1,n))%nreturnd2攻击所需的全部信息信息来源备注s₂网络截获服务端→客户端明文传输s₃网络截获服务端→客户端明文传输r网络截获服务端→客户端明文传输n公开参数SM2 曲线阶标准值已知6. 攻击原理总结与防御漏洞根因服务端代码意图: k₂ random() // 独立的临时私钥 k₃ random() // 另一个独立的临时私钥 服务端代码实际: k₂ random() k₃ k₂ // ← CtrlC / CtrlV 少改了变量名这是一个典型的复制粘贴型漏洞——开发者复制了生成 k₂ 的代码但忘记将变量名从 k₂ 改为 k₃。防御措施说明✅ 独立的随机数BN_rand_range(k3, n)不要拷贝✅ 代码审查关注BN_copy等变量赋值操作✅ 形式化验证协议安全需要 k₂ 和 k₃ 独立随机✅ 零知识证明服务端向客户端证明 s₂, s₃ 是正确计算的