Audius攻击事件简单分析

2022-10-11

相关信息

攻击者地址

0xa0c7BD318D69424603CBf91e9969870F21B8ab4c

攻击交易

0xfefd829e246002a8fd061eede7501bccb6e244a9aacea0ebceaecef5d877a984

0x3c09c6306b67737227edc24c663462d870e7c2bf39e9ab66877a980c900dd5d5

0x4227bca8ed4b8915c7eec0e14ad3748a88c4371d4176e716e8007249b9980dc9

攻击合约

0xa62c3ced6906b188a4d4a3c981b79f2aabf2107f

0xbdbb5945f252bc3466a319cdcc3ee8056bf2e569

被攻击合约Governance
0x35dd16dfa4ea1522c29ddd087e8f076cad0ae5e8

漏洞简析(节选自xyyme.eth mirror)

这个漏洞主要是代理合约内存插槽冲突导致的,学到了新知识,这位博主一系列文章写得比较详细,不在赘述

AudiusAdminUpgradeabilityProxy(代理合约,节选)

1
2
3
4
5
6
contract AudiusAdminUpgradeabilityProxy is UpgradeabilityProxy {
address private proxyAdmin;
string private constant ERROR_ONLY_ADMIN = (
"AudiusAdminUpgradeabilityProxy: Caller must be current proxy admin"
);
}

在代理合约中,slot 0 的位置是 proxyAdmin

Governance(逻辑合约,节选)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
contract Initializable {
/**
* @dev Indicates that the contract has been initialized.
*/
bool private initialized;

/**
* @dev Indicates that the contract is in the process of being initialized.
*/
bool private initializing;

/**
* @dev Modifier to use in the initializer function of a contract.
*/
modifier initializer() {
require(
initializing || isConstructor() || !initialized,
"Contract instance has already been initialized"
);

bool isTopLevelCall = !initializing;
if (isTopLevelCall) {
initializing = true;
initialized = true;
}

_;

if (isTopLevelCall) {
initializing = false;
}
}

///....

// Reserved storage space to allow for layout changes in the future.
uint256[50] private ______gap;
}

contract InitializableV2 is Initializable {
/// ....
}

contract Governance is InitializableV2 {
/// ....
}

按照合约继承的内存分布规则,Initializable 合约中的 initializedinitializing 这两个变量分别位于逻辑合约 Governanceslot 0slot 1 中。

看到这里,大家是不是已经发现了问题。如果按照这种写法,那么代理合约和逻辑合约的内存槽位不是冲突了吗?没错,但还有一点要注意的是,由于 initializedinitializing 都是 bool 类型变量,因此他们各自都只占据一字节(注意,是 1 byte,不是 1 bit),所以说它们俩实际上是被打包放在了 slot 0 中。也就是说,slot 0 的结构是:img

上图是逻辑合约的 slot 0 内存分布。由于与代理合约的 ProxyAdmin 冲突,且 ProxyAdmin 的值为:

0x4DEcA517D6817B6510798b7328F2314d3003AbAC

因此,对应的 slot 0 槽位图示为:

img

这说明 initializedinitializing 这两个变量的值使用了 ProxyAdmin 实际值的最后两个字节!而恰好最后两个字节(0xAB, 0xAC)都是非零值,这也就造成在实际可升级合约的数据读取中,initializedinitializing 的值总是 true。而这个巧合其实也取决于 ProxyAdmin 的最后两个字节是什么,如果它的地址最后两字节都是零: 0x4DEcA517D6817B6510798b7328F2314d30030000,那么 initializedinitializing 便都是 false 了。

冲突原因已经找到了,我们来看看这个冲突会造成什么。再次看看逻辑合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
contract Initializable {
///....
modifier initializer() {
require(
initializing || isConstructor() || !initialized,
"Contract instance has already been initialized"
);

bool isTopLevelCall = !initializing;
if (isTopLevelCall) {
initializing = true;
initialized = true;
}

_;

if (isTopLevelCall) {
initializing = false;
}
}

///....

// Reserved storage space to allow for layout changes in the future.
uint256[50] private ______gap;
}

对于 initializer 修饰符,由于 initializingtrue,因此可以通过 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