相关信息
攻击者地址
0xa0c7BD318D69424603CBf91e9969870F21B8ab4c
攻击交易
0xfefd829e246002a8fd061eede7501bccb6e244a9aacea0ebceaecef5d877a984
0x3c09c6306b67737227edc24c663462d870e7c2bf39e9ab66877a980c900dd5d5
0x4227bca8ed4b8915c7eec0e14ad3748a88c4371d4176e716e8007249b9980dc9
攻击合约
0xa62c3ced6906b188a4d4a3c981b79f2aabf2107f
0xbdbb5945f252bc3466a319cdcc3ee8056bf2e569
被攻击合约Governance
0x35dd16dfa4ea1522c29ddd087e8f076cad0ae5e8
漏洞简析(节选自xyyme.eth mirror)
这个漏洞主要是代理合约内存插槽冲突导致的,学到了新知识,这位博主一系列文章写得比较详细,不在赘述
AudiusAdminUpgradeabilityProxy(代理合约,节选)
1 | contract AudiusAdminUpgradeabilityProxy is UpgradeabilityProxy { |
在代理合约中,slot 0
的位置是 proxyAdmin
Governance(逻辑合约,节选)
1 | contract Initializable { |
按照合约继承的内存分布规则,Initializable
合约中的 initialized
和 initializing
这两个变量分别位于逻辑合约 Governance
的 slot 0
和 slot 1
中。
看到这里,大家是不是已经发现了问题。如果按照这种写法,那么代理合约和逻辑合约的内存槽位不是冲突了吗?没错,但还有一点要注意的是,由于 initialized
和 initializing
都是 bool
类型变量,因此他们各自都只占据一字节(注意,是 1 byte,不是 1 bit),所以说它们俩实际上是被打包放在了 slot 0
中。也就是说,slot 0
的结构是:
上图是逻辑合约的 slot 0
内存分布。由于与代理合约的 ProxyAdmin
冲突,且 ProxyAdmin
的值为:
0x4DEcA517D6817B6510798b7328F2314d3003AbAC
因此,对应的 slot 0
槽位图示为:
这说明 initialized
和 initializing
这两个变量的值使用了 ProxyAdmin
实际值的最后两个字节!而恰好最后两个字节(0xAB, 0xAC)都是非零值,这也就造成在实际可升级合约的数据读取中,initialized
和 initializing
的值总是 true
。而这个巧合其实也取决于 ProxyAdmin
的最后两个字节是什么,如果它的地址最后两字节都是零: 0x4DEcA517D6817B6510798b7328F2314d30030000
,那么 initialized
和 initializing
便都是 false
了。
冲突原因已经找到了,我们来看看这个冲突会造成什么。再次看看逻辑合约:
1 | contract Initializable { |
对于 initializer
修饰符,由于 initializing
为 true
,因此可以通过 require
校验。而下面的 isTopLevelCall
会被赋值为 false
,造成 if
语句无法执行,那么 initializing
将永远为 true
,也就是说 initializer
已经起不到限制作用了。
黑客就是利用了这个 bug,从而可以调用各种被 initializer
修饰的方法。这些方法中包含一些特权方法,本来只能被管理员调用一次,这下被黑客调用,损失惨重。
参考链接
https://learnblockchain.cn/article/4454
https://learnblockchain.cn/article/4441
https://mirror.xyz/xyyme.eth/IQ8uMgQ11S7YK_Tt4sR3E1iPq6MBrdu5WqHwdFPwWuw