关卡链接: 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**256i = 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。在智能合约的世界中,最简单的漏洞往往隐藏着最深刻的安全教训。 🎓