关卡链接: Ethernaut Level 19 - Alien Codex
攻击类型: 存储操纵 / 整数下溢
难度: ⭐⭐⭐⭐⭐
本关的目标是获取 AlienCodex
合约的所有权。这是一个继承了 Ownable
的合约,owner
存储在 slot 0。
AlienCodex
合约使用了一个旧的 Solidity 版本 (^0.5.0
),这意味着整数操作不会进行溢出检查。这是本关的核心漏洞。合约的存储布局如下:
Slot | 变量名 | 类型 | 说明 |
---|---|---|---|
0 | contact |
bool |
与 owner 打包在同一个槽位 |
0 | owner |
address |
继承自 Ownable ,位于 slot 0 |
1 | codex |
bytes32[] |
动态数组,slot 1 存储其长度 |
合约中的函数都受到 contacted
修饰符的限制,我们必须先调用 makeContact()
将 contact
设置为 true
。
关键漏洞在 retract()
函数中:
1 | // From AlienCodex.sol (Solidity v0.5.0) |
由于没有溢出检查,如果 codex.length
为0,执行 codex.length--
会导致整数下溢,使其长度变为 2**256 - 1
。一个长度为 2**256 - 1
的动态数组可以覆盖整个合约的存储空间!
拥有一个可以写到任意存储位置的数组后,我们的目标是覆盖 slot 0 中的 owner
变量。我们需要找到哪个数组索引 i
对应于存储槽 0
。
动态数组的数据存储位置是从 keccak256(p)
开始的,其中 p
是数组长度所在的槽位。在本例中,codex
的长度存储在 slot 1,所以它的数据起始位置是 keccak256(1)
。
codex[0]
存储在 keccak256(1)
codex[i]
存储在 keccak256(1) + i
我们想写入的位置是 slot 0。因此,我们需要找到一个索引 i
,使得 keccak256(1) + i
在 2**256
的模运算下等于 0
。
keccak256(1) + i = 2**256
i = 2**256 - keccak256(1)
一旦我们计算出这个索引 i
,我们就可以调用 revise(i, our_address)
来将 owner
修改为我们自己的地址。
攻击合约将执行上述的三个步骤:建立联系、触发下溢、计算索引并修改 owner
。
1 | // SPDX-License-Identifier: Unlicense |
makeContact()
: 解除对其他函数的调用限制。retract()
: 在 codex
数组为空时调用,利用整数下溢将数组长度变为 type(uint256).max
。i = 2**256 - keccak256(1)
。revise()
: 使用计算出的索引和 player
的地址作为参数调用 revise
,这会覆盖 slot 0 的内容,从而改变 owner
。0.8.0
版本开始,Solidity 默认会对所有算术运算进行上溢和下溢检查。这是防止此类漏洞最简单、最有效的方法。SafeMath
库: 如果必须使用旧版本的Solidity,应始终使用 SafeMath
或类似的库来执行所有算术运算,以防止溢出。keccak256
: EVM中用于计算哈希的核心函数,它在确定存储位置时扮演着重要角色。unchecked
: 在Solidity ^0.8.0
中,可以使用 unchecked
块来故意允许溢出,这在复现旧版本漏洞或进行特定位操作时很有用。核心概念:
keccak256
哈希进行确定性计算。攻击向量:
owner
变量所在存储槽(slot 0)的数组索引。revise
)来覆盖 owner
。防御策略:
SafeMath
。在智能合约的世界中,最简单的漏洞往往隐藏着最深刻的安全教训。 🎓