返回论坛

Circom 验证合约输入假名漏洞深度分析与复现

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

查找币安全研究院

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

查看研究院 研究报告中心
**作者:查找币安全团队** ## 概述 零知识证明(ZKP)技术作为Web3安全领域的重要基石,其实现方案的安全性直接影响着整个生态系统的信任基础。近日,查找币安全团队在技术研究中发现,Circom框架生成的验证合约存在一个经典的“输入假名”漏洞,该漏洞最早由俄罗斯开发者poma在Semaphore项目中披露([相关Issue](https://github.com/semaphore-protocol/semaphore/issues/16))。本文将从技术原理出发,详细解析该漏洞的成因、复现过程及修复方案,为开发者提供实践性安全指导。 ## 前置知识:Circom与ZKP验证机制 ### 零知识证明核心流程 零知识证明系统的核心是“证明系统”算法,其工作流程可概括为: 1. **证明生成**:证明者通过电路计算,生成见证数据(witness)和证明数据(proof) 2. **链上验证**:验证合约(verifier.sol)对提交的proof进行椭圆曲线校验 3. **结果确认**:验证通过即证明消息的真实性,无需暴露原始数据 ### Circom的技术特点 Circom作为主流的ZKP开发框架,支持Groth16和PlonK两种证明系统。其优势在于: - **自动化参数生成**:开发者无需手动配置证明参数 - **合约自动部署**:框架自动生成兼容Solidity的验证合约 - **隐私保护**:验证过程不泄露任何输入信息 ## 漏洞深度解析 ### 1. 问题代码定位 漏洞核心存在于验证合约中的`verifyHash`函数。如下图所示,该函数通过记录见证数据的哈希值(hash1)来防止双花攻击。然而,漏洞恰恰出现在这个hash1的匹配机制上。 ``` // 问题代码示意 function verifyHash(uint[] memory input, Proof memory proof) public returns (bool) { uint hash1 = input[0]; require(!usedHashes[hash1], "Hash already used"); usedHashes[hash1] = true; // ... 后续验证逻辑 } ``` 按照正常设计,一组proof数据应仅对应唯一一个hash1进行验证。但实际代码中,这个假设并不成立。 ### 2. 椭圆曲线校验机制 `verify`函数的核心逻辑是通过`scalar_mul()`函数实现椭圆曲线上的标量乘法。具体流程如下: 1. 接收输入数组`input`和证明结构体`Proof memory proof` 2. 对输入参数进行椭圆曲线计算 3. 比较计算结果与证明中的给定值是否相等 4. 若相等则验证通过 ### 3. 输入假名(Input Aliasing)漏洞原理 在Solidity智能合约中,所有数值以`uint256`类型存储。然而,椭圆曲线运算涉及有限域Fq,其模数q远小于`uint256`的最大值。这意味着: - 数值`s`和`s + q`在模运算后映射到相同的Fq值 - 同样地,`s + 2q`、`s + 3q`等也对应同一个域元素 - 这种现象称为“输入假名”(Input Aliasing) **关键数据**:在`uint256`范围内,最多有`uint256_max / q`个不同的整数可表示同一个点。对于常见的椭圆曲线,这个值约为5。这意味着同一组proof最多可以有5个不同的hash1通过合约验证。 ## 漏洞复现过程 ### 步骤1:构建简单电路 设计一个接收2个输入数据、返回1个见证数据的电路: ```circom pragma circom 2.0.0; template SimpleCircuit() { signal input a; signal input b; signal output hash1; hash1 <== a + b; } component main = SimpleCircuit(); ``` ### 步骤2:生成验证材料 执行以下命令生成所需文件: ```bash # 编译电路 circom circuit.circom --r1cs --wasm --sym # 生成zkey文件 snarkjs groth16 setup circuit.r1cs pot12_final.ptau circuit_final.zkey # 导出验证合约 snarkjs zkey export solidityverifier circuit_final.zkey verifier.sol ``` ### 步骤3:生成正常proof ```javascript // 生成正常证明 const { proof, publicSignals } = await snarkjs.groth16.fullProve( { a: 10, b: 20 }, "circuit.wasm", "circuit_final.zkey" ); // hash1 = 30 ``` ### 步骤4:构造攻击hash ```javascript // 构造假名hash const q = 21888242871839275222246405745257275088548364400416034343698204186575808495617n; const attackHash = 30n + q; // 与正常hash映射到相同Fq值 ``` ### 步骤5:验证结果 部署合约后,分别使用正常hash和攻击hash进行验证: - **正常验证**:使用hash1=30,验证通过 ✅ - **攻击验证**:使用attackHash=30+q,验证同样通过 ✅ **复现结论**:同一组proof可以对应多个不同的hash值通过合约验证,输入假名漏洞确认存在。 ## 漏洞修复方案 ### 根本原因 漏洞本质在于:一组proof最多可以有5个不同的hash通过合约验证,这为双花攻击提供了可能性。 ### 修复方法 限制所有输入的hash值必须小于有限域阶数q: ```solidity function verify(uint[] memory input, Proof memory proof) public returns (bool) { uint q = 21888242871839275222246405745257275088548364400416034343698204186575808495617; for (uint i = 0; i < input.length; i++) { require(input[i] < q, "Input exceeds field modulus"); } // ... 原有验证逻辑 } ``` ## 安全建议 1. **验证域阶数**:所有输入数值必须小于有限域阶数q 2. **使用安全库**:优先采用经过审计的密码学库 3. **单元测试覆盖**:包含边界值测试(如q-1、q、q+1) 4. **代码审计**:定期进行专业安全审计 ## 总结 输入假名漏洞是零知识证明和密码学实现中的经典问题,其本质源于有限域运算的模数特性。开发者在进行密码学相关开发时,必须高度重视验证群的阶数约束。本案例再次证明,即使是在成熟的ZKP框架中,细微的实现疏忽也可能导致严重的安全漏洞。 查找币安全团队将持续关注ZKP生态的安全动态,为Web3开发者提供专业的技术支持与安全指南。 --- *本文由查找币安全团队整理发布*
在论坛中查看和回复