By: Victory
概述
此前,俄罗斯开发者 poma 在 Semaphore 上发现了一个零知识证明验证合约存在双花漏洞(详见 https://github.com/semaphore-protocol/semaphore/issues/16)。出于兴趣,想先尝试复现一下该漏洞的 PoC,但由于漏洞代码是很久以前的代码,且该项目相对复杂,因此决定自己编写一个简单的 PoC 来复现漏洞。
零知识证明(ZKP)技术的核心是一个叫做「证明系统」的算法。该算法通过对消息进行一系列的计算,生成一个证明,用于证明消息的真实性。接收者无需拥有其它信息,只需验证证明,即可确认消息的真实性。
ZKP 的实现有许多种实现方案,我们在之前的文章《盘点 ZKP 主流实现方案技术特点》里为大家介绍了各种证明系统及编程平台,本次实验中使用的正是其中的 Circom 平台。
Circom 使用 Groth16 和 PlonK 作为其证明系统,在开发过程中我们可以任选其一,开发框架可以在不需要改变电路的情况下自动为开发者生成证明参数和验证合约。
简单来说,Circom 通过在客户端生成见证数据和证明数据,将这些数据提交到合约。verifier.sol 合约负责对提交的数据进行校验,以验证证明是否是在规定的规则下生成的。这种方法可以实现快速、高效和安全的验证,无需暴露消息的具体内容,保护消息的隐私性。
1、话不多说,我们直接上问题代码,请看下图的 verifyHash 函数。图中红框内的代码是记录某次见证数据是否有使用过,这种用法在防止双花上是比较常见的。但是此次漏洞的出现就是出现在这个见证数据 hash1 上。按照正常的理解一组 proof 数据应该只能匹配一组 hash1 进行验证。
2、在 verifier.sol 合约中,函数 verify(uint[] memory input, Proof memory proof) 的作用是对传入的数值进行椭圆曲线计算校验。该函数利用名为 scalar_mul() 的函数实现了椭圆曲线上的标量乘法。具体地,它会使用输入的参数对椭圆曲线进行计算,并比较计算结果与给定证明中的值是否相等,以确定输入值是否有效。
3、在 Solidity 智能合约中,需要使用 uint256 类型来编码 Fq。但是,由于 uint256 类型的最大值大于 q 值,可能会出现多个不同的整数在进行 mod 运算后会对应到同一个 Fq 值的情况。例如,s 和 s+q 实际上表示同一个点,即第 s 个点。同样的,s+2q 等等也都对应到点 s。这种现象被称为「Input Aliasing」,也就是这些数互为假名。
这里的 q 值是指循环群的阶数,也就是可以输入多个大整数会对应到同一个 Fq 中的值的数量。简单来说,即使将 hash 加上一个 q 值,仍然可以通过验证。在 uint256 类型的范围内,最多有 uint256_max/q 个不同的整数可以表示同一个点。这意味着一组证明最多可以有 5 个匹配的 hash1 能够通过合约的验证。
1、实现一个简单的电路输入 2 个数据返回一个见证数据,就是在合约里面用到的 hash1。
2、对电路进行编译生成 circuit_final.zkey, circuit.wasm 和 verifier.sol。接着生成一组 proof,一个正常的 hash,一个攻击 hash。
3、随后部署合约,使用前面生成的 checkHash 进行一次验证,验证通过。
4、接下来在使用相同的见证数据与前面生成的 attackHash,发现验证一样是通过的。这说明了一组 proof 可以有多个匹配的 hash 能通过合约的校验。至此 Circom 验证合约输入假名漏洞复现成功。
漏洞的解决方案
此次漏洞是由于一组证明可以有最多 5 个匹配的 hash 能在合约上通过验证。所以漏洞修复也很简单,就是限制所有输入的 hash 都要小于 q 值。
输入假名漏洞在零知识证明及密码学实现里是一个比较通用的漏洞,本质原因是数值在有限域内取余相同,开发者在进行密码学开发时需要注意验证群的阶数。
参考链接:
[1]. https://github.com/kobigurk/semaphore/issues/16
往期回顾
慢雾导航
慢雾科技官网
https://www.slowmist.com/
慢雾区官网
https://slowmist.io/
慢雾 GitHub
https://github.com/slowmist
Telegram
https://t.me/slowmistteam
https://twitter.com/@slowmist_team
Medium
https://medium.com/@slowmist
知识星球
https://t.zsxq.com/Q3zNvvF