返回论坛

Groth16 证明延展性攻击:原理、实现与防御

查找币 漏洞披露 安全研究 Web3安全 区块链安全

查找币安全研究院

钱包恢复评估 | 链上取证分析 | Web3 事件响应
以合法授权、证据保全、隐私保护和可复核流程为前提,不要求用户在线提交完整私钥或助记词。

查看研究院 研究报告中心
> 作者:查找币安全团队 ## 前言 在之前的ZKP系列文章中,我们盘点了主流零知识证明实现方案的技术特点,并提及部分ZKP算法存在**证明延展性风险**。本篇我们将从实战角度,深入剖析Groth16证明系统的延展性攻击原理、工程实现方法,并提供详细的防御建议。 ## 漏洞概述 ### 什么是证明延展性攻击? **证明延展性攻击**是指:攻击者在获取一个合法证明后,即使不知道原始见证(witness),也能通过数学变换生成新的合法证明。这种攻击的核心在于证明系统的代数结构存在可操作空间。 ### 为什么关注Groth16? 目前主流证明系统中,**Groth16**是受延展性攻击影响最显著的方案。尽管存在多种更安全的证明系统,Groth16凭借其**极小的证明体积**和**极快的验证速度**,在计算成本高昂的区块链场景中仍是最优选择。 ### 攻击后果 以一个基于ZKP的身份验证存款系统为例: - 用户提交证明验证身份后即可提款 - 证明值被用作提款登记凭证 - 攻击者获取合法证明后,通过数学变换生成新证明 - 系统会将新证明视为不同用户的合法凭证,导致**多次提款** 这本质上是一种**双花攻击**,在DeFi、跨链桥等场景中尤为危险。 ## 数学原理 ### 理解验证函数 Groth16的验证核心公式如下(简化表示): ``` e(A, B) = e(α, β) · e(C, δ) · e(∑(ai·ui), γ) ``` 其中: - `A`, `B`, `C` 是证明的三个关键元素 - `e` 表示双线性配对运算 - `α, β, γ, δ` 是公共参考字符串中的参数 ### 攻击核心:代数变形 攻击的数学基础在于**双线性配对的可交换性**。我们可以利用群的结合律进行如下变换: ``` e(A·x, B·x⁻¹) = e(A, B)ˣ·ˣ⁻¹ = e(A, B) ``` 其中 `x` 是任意非零域元素,`x⁻¹` 是其逆元。这个等式成立的关键在于: 1. 双线性配对满足 `e(a·P, b·Q) = e(P, Q)^(a·b)` 2. 当 `a·b = 1` 时,配对结果不变 因此,我们可以构造: - `A' = x · A` - `B' = x⁻¹ · B` 使得 `e(A', B') = e(A, B)`,从而绕过验证。 ## 工程实现 ### 环境准备 选择支持BN128曲线的JavaScript库:**ffjavascript** ```javascript const F = require('ffjavascript').F; const BN128 = require('ffjavascript').bn128; ``` ### 攻击步骤 1. **获取目标证明** ```javascript const proof = { pi_a: ['175662...', '136538...', '1'], pi_b: [['149061...', '152890...'], ['188412...', '683528...'], ['1', '0']], pi_c: ['216418...', '208258...', '1'] }; ``` 2. **构造变换因子** ```javascript const X = F.e("123456"); // 任意非零域元素 const invX = F.inv(X); // 计算逆元 ``` 3. **生成新证明** ```javascript const A = curve.G1.fromObject(proof.pi_a); const B = curve.G2.fromObject(proof.pi_b); const new_pi_a = curve.G1.timesScalar(A, X); const new_pi_b = curve.G2.timesScalar(B, invX); const new_proof = { pi_a: new_pi_a, pi_b: new_pi_b, pi_c: proof.pi_c // C保持不变 }; ``` 4. **验证新证明** 将 `new_proof` 提交至验证合约,验证函数返回 `true`,攻击成功。 ### 攻击效果 - **证明大小不变**:仍为3个群元素 - **验证时间不变**:双线性配对运算复杂度相同 - **成功率100%**:只要原始证明合法,变换后的证明必然合法 ## 防御方案 ### 方案一:签名绑定 **原理**:要求证明提交者对proof进行数字签名,验证者同时验证证明和签名。 **优点**:实现简单,兼容现有系统 **缺点**:增加链上验证成本,需管理签名密钥 ### 方案二:Nullifier机制(推荐) **原理**:在电路公开输入中增加nullifier值,系统记录已使用的nullifier。 **实现示例**(参考TornadoCash): ```solidity mapping(uint256 => bool) public nullifierHashes; function withdraw(uint256 nullifierHash, Groth16Proof memory proof) { require(!nullifierHashes[nullifierHash], "Already spent"); require(verify(proof, nullifierHash), "Invalid proof"); nullifierHashes[nullifierHash] = true; // 执行提款 } ``` ### 方案三:身份绑定 **原理**:将证明者身份信息(如msg.sender)作为公开输入加入电路。 ```solidity function verifyWithAddress(Groth16Proof memory proof, address user) { require(verify(proof, [user]), "Invalid proof"); require(msg.sender == user, "Address mismatch"); // 执行操作 } ``` ### 方案四:更换证明系统 - **Plonk**:支持通用设置,抗延展性更强 - **Marlin**:基于多项式承诺,安全性更高 - **Sonic**:透明设置,无需可信初始化 ## 总结 Groth16证明延展性攻击是一种**低门槛、高危害**的安全漏洞。攻击者仅需简单的代数变换即可生成新的合法证明,在未采取防护措施的场景中可直接导致双花攻击。 **核心防护原则**: 1. **绑定唯一标识**:将证明与特定上下文(nullifier、签名、身份)绑定 2. **状态追踪**:记录已使用的证明,防止重复提交 3. **系统升级**:在条件允许时迁移至更安全的证明系统 **查找币安全团队建议**:所有使用Groth16的DApp和协议应优先采用**Nullifier机制**进行防护,该方案在安全性和实现复杂度之间取得了最佳平衡。 --- *本文由查找币安全团队整理发布* **参考链接**: [1] How to Generate a Groth16 Proof for Forgery - PPIO Blog **往期ZKP系列文章**: - [盘点ZKP主流实现方案技术特点](https://example.com/zkp-overview) - [Circom验证合约输入假名漏洞复现](https://example.com/circom-vuln)
在论坛中查看和回复