shaneson.eth

Posted on Jun 17, 2022Read on Mirror.xyz

Opensea Wyvern 2.2 版本漏洞与分析

原文出处:

https://nft.mirror.xyz/VdF3BYwuzXgLrJglw5xF6CHcQfAVbqeJVtueCr4BUzs

背景

审计组和NFT组汇报一个非常重要的安全分享,这里收录一下。背景:一位白帽子发现了opensea一个发现关键的一个合约漏洞,并且已经修复。这个漏洞影响非常深远,所有组装calldata,利用functionSelector来做路由选择的合约函数都会收到影响。

发现该漏洞可被利用来窃取OpenSea市场上有活跃报价的一部分用户的钱包中的WETH。该漏洞不需要用户采取任何行动就可以利用--某些在过去签署了合法或报价的用户即使没有采取进一步行动,也会面临风险。

漏洞介绍

Wyvern Listing 包含许多不同的参数,用于指示列表或报价信息,并验证其他参与的智能合约调用,这些参数被汇总到一个单一的承诺中,由用户签署并由合约检查,从而确保只有在用户实际批准列表或报价的情况下才会转移项目。这些参数中有几个是可变长度的,Wyvern 2.2合约(在后来的ABI编码标准之前写的)将它们串联在一起,没有适当的域分离:(拼装calldata)

index = ArrayUtils.unsafeWriteAddress(index, order.target);
index = ArrayUtils.unsafeWriteUint8(index, uint8(order.howToCall));
index = ArrayUtils.unsafeWriteBytes(index, order.calldata);
index = ArrayUtils.unsafeWriteBytes(index,order.replacementPattern);
index = ArrayUtils.unsafeWriteAddress(index, order.staticTarget);
index = ArrayUtils.unsafeWriteBytes(index, order.staticExtradata);
index = ArrayUtils.unsafeWriteAddress(index, order.paymentToken);

在上面实现中,具有不同参数的订单--例如calldata = 0x01和replacementPattern = 0x0101以及calldata = 0x0101和replacementPattern = 0x01--会导致相同的计算承诺。由于这种碰撞,一个聪明的对手可以从一个列表或报价中获取签名,并提出一个不同的列表或报价--没有由用户签署,但导致相同的承诺--这将被智能合约认为是有效的。更具体地说,攻击者可以在order.calldata、order.replacementPattern,staticTarget和order.staticExtradata之间 "转移 "字节,以创建一个具有不同语义的报价或列表,从而产生相同的承诺。

攻击

卖家之前签好了一个卖单,签署的原数据可以利用字节偏移,重新解释成意义不同的新订单。调整字节偏移,各字段传到合约里,通过replacementPattern把function selector换掉,举的例子是把转移NFT的方法transferFrom换成getApproved方法,而且最后数据全拼起来order hash与之一致,用户签名依旧有效。另外,getApproved方法是view方法,保证可以成功调用。

攻击原理

这里核心关键是 replacePatter字段换成2进制后,为1的bit位必须包含 标识两个selector差别的 0010 1011 1010 0000 0110 0000 0010 0001这十个比特位,其他位不care,因为mask为1 就是表示从对方那这个数位的结果过来,所以只需要把10个不同的bit的值拿过来就行,其他位因为都相同,拿不拿都一样。

There is a 1 in 1024 chance that a random, 4 byte bitmask has ones in the correct places such that, when applied through the replacementPattern mechanism, the function selector would change from 0x23b872dd to 0x081812fc.

这里1/1024的概率计算,是4个字节32个比特位,要求指定的10个比特位出现1的概率是1/1024。

4个字节其他位都不care,所以指定10个比特位为1的概率,就是2^ 10=1024。

The 4 bytes 0x6bfa60bb in the maker's address, coincidentally, have 1 bits in the places corresponding to the bitwise difference between transferFrom and getApproved.

这里的 1 bits 不是表示1个比特位,是在说所有为1的比特位和表示两个selector不同10个比特位一致。

拿这个表示两个selector比特位区别的 010 1011 1010 0000 0110 0000 0010 0001,去calldata里面去移动匹配,匹配到包含这10个1的位置,进行截取就可以了。

Given that the attacker could shift the replacementPattern to start at any point in the address, the attacker could extract 16 different 4 byte starts to the replacementPattern. This means approximately 1 in 64 offers could have been exploitable.

上文中的1/64的概率,由于to地址是20个字节,selector的mask使用4个字节,所以20个字节可以有17种选择,4个字节概率是1/1024,文中按16种选择算,就是1/64,其实应该是 17/1024,即大约每60个用户就会存在一个用户可以被攻击。

解决方案

在CallData组装的时候,加上随机Nonce,极大加大用户碰撞calldata的难度。

OpenSea