从 EIP-1967 再谈 “鱿鱼游戏SQUID” 安全风险
摘要: 最近在加密圈大火的 SQUID 游戏代币价格波动巨大!它真的安全吗?
前言
随着电视剧的爆火,鱿鱼游戏的热度也逐渐攀升,而 SQUID 在 10 月 21 日上线后,价格幅度变化巨大,但是现在的它真的安全吗?
Squid 从代码上来看其依赖于以太坊提案 EIP-1967,知道创宇区块链安全实验室 将从 EIP-1967 提案再谈 Squid 的安全风险。
SQUID 价格从 0.08 刀一路扶摇直上九万里,飙升到 3000 美元。然而令人唏嘘的是,价格随即闪崩,5 分钟从 3000 跌至 0.0007,究其原因,合约开发者在短短的几分钟内利用 7000w 枚 Squid 砸盘 ,导致流动性池被抽干,直至归零。
在经历了底部 0.0007 后,该游戏代币最近短短几天重新涨到了 0.4,涨幅达到了 50 倍以上,可谓势头不小。
EIP-1967
This EIP standardises the storage slot for the logic contract address, instead of a public method on the proxy contract as DelegateProxy (EIP897) does. The rationale for this is that proxies should never expose functions to end users that could potentially clash with those of the logic contract.
首先,EIP-1967 的目的是规定一个通用的存储插槽,用于在代理合约中的特定位置存放逻辑合约的地址。
在该提案中,对逻辑合约地址的存储槽进行了标准化,而不是像 DelegateProxy (EIP897) 一样在代理合约上使用公共方法。
通过跟进该提案,在提案中定义了如下的标准化存储槽。
bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)<br />
bytes32(uint256(keccak256("eip1967.proxy.beacon")) - 1)<br />
bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1)
最后通过计算,列出来了三个 byte32位 的 Hash 值作为推荐的存储槽。
bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)
//逻辑合约地址 存储槽
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
bytes32(uint256(keccak256("eip1967.proxy.beacon")) - 1)
//信标合约地址 存储槽
0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50
bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1)
//admin 地址 存储槽
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
EIP-1967 中有提到,对于一个代理来说,代理永远不应该向最终用户公开可能与逻辑合约功能冲突的方法。此处计算将 keccak256 结果进行减一再转化为 bytes32 来隐藏前像,以防止基于函数签名攻击。
针对该攻击,具体哈希后的前 4 个 bytes 函数签名的攻击思路如下:
- 由于 solidity 中识别一个函数,靠的是函数签名,而函数签名是函数哈希后的前 4 个 bytes,是非常容易碰撞出来的。而函数签名相关知识点可以参考实验室之前的文章。
- 在一个独立的 solidity 文件中,编译器自己会去检查所有的 external 和 public 函数是否存在函数签名碰撞,但对于代理模式的合约文件,可能存在 proxy 合约中的函数签名与 impl 合约中的函数签名碰撞。而一旦发生这种碰撞,proxy 合约中的函数就会被直接调用,而不是 impl 合约对应的函数。
除了以上用途,使用 EIP-1967 还有如下的好处
合约代理模式规范性:
由于代理合约中缺少用于获取代理的逻辑地址的通用接口,因此无法构建对这些信息进行操作的通用工具。比如在区块链浏览器中,最终用户想要与底层逻辑合约而不是代理本身进行交互。
如果拥有从代理检索逻辑合约地址的通用方法将可以允许区块浏览器去显示逻辑合约的 ABI 而不是代理的 ABI。浏览器可以还检查合约在不同槽位的存储,来确定它是否确实是一个代理。
Squid安全性
通过跟进 Squid 合约,虽然我们看到合约部署者
0x87230146e138d3f296a9a77e497a2a83012e9bc5
通过 renounceOwnership
方法将 _owner
设置为了 0 地址:
但是这样就可以高枕无忧了吗?在 Squid 的初始化中,部署者通过为 ApprovedEngine
传入逻辑函数地址并设置为接口实现。
但与此同时,初始化传入了参数_sir
,该参数从代码中可以发现,其被存储在了存储槽(storage['0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103'])
中。
该地址后续可成功通过ifSir
修饰器的检查并随时调用approveTo
函数来部署新的逻辑合约实现。
_sir
可通过升级对应的逻辑合约,进而改变了_implementation
返回的对于EIP-1967中提到的槽中 bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)
的值通过改变 Engine
收到调用的时候合约执行逻辑,进而修改逻辑合约的代码:
针对该修改逻辑合约进行攻击已屡见不鲜,在几天前 bzx 就曾因"开发方被钓鱼导致私钥被盗",进而代理合约被修改并影响到了流动性提供者,攻击者不仅转走了原本合约内的资金,还尝试转移对合约进行了approve
的账户的资产。
事件参考(https://bzx.network/blog/prelminary-post-mortem)
最后,通过追溯该Squid的部署合约,我们可以发现该合约的具体部署Transaction:
https://bscscan.com/tx/0xcaa4186f7e4dacd856835cb92a8518c88b098a33348a42168f99a0c57b7291c7
而在此 Transaction 中,传入合约的参数最后一个参数则为 _sir 地址:
0x6bdb3b0fd9f39427a07b8ab33bac32db67eb4e38
当然,还存在多种方法拿到该地址如:
并且该sir后门地址至今仍非常活跃:
后记
Squid 经过了暴跌与暴涨,但本质上仍没有完成去中心化,合约依然存在诸多安全风险。在频繁和大幅度的涨跌中,常有参与者忘记了本心,最后导致亏空的结局。
作者:创宇区块链安全实验室;来自链得得内容开放平台“得得号”,本文仅代表作者观点,不代表链得得官方立场凡“得得号”文章,原创性和内容的真实性由投稿人保证,如果稿件因抄袭、作假等行为导致的法律后果,由投稿人本人负责得得号平台发布文章,如有侵权、违规及其他不当言论内容,请广大读者监督,一经证实,平台会立即下线。如遇文章内容问题,请联系微信:chaindd123。
评论(0)
Oh! no
您是否确认要删除该条评论吗?