关卡链接: Ethernaut Level 16 - Preservation
攻击类型:delegatecall存储布局操纵
难度: ⭐⭐⭐⭐☆
本关的目标是获取 Preservation 合约的所有权,即成为该合约的 owner。

Preservation 合约的 owner 是私有的,并且没有直接的函数来修改它。漏洞隐藏在使用 delegatecall 的 setFirstTime 和 setSecondTime 函数中。
1 | contract Preservation { |
delegatecall 是一个非常危险的操作码。它会在调用者合约的上下文中执行另一个合约的代码。这意味着,被调用合约(library)的代码可以修改调用者合约(Preservation)的存储。
当 setFirstTime 通过 delegatecall 调用 timeZone1Library 的 setTime 函数时,setTime 函数修改的存储槽位是 Preservation 合约的槽位。
让我们比较一下 Preservation 和 LibraryContract 的存储布局:
| Slot | Preservation 合约 |
LibraryContract 合约 |
|---|---|---|
| 0 | timeZone1Library |
storedTime |
| 1 | timeZone2Library |
(未使用) |
| 2 | owner |
(未使用) |
| 3 | storedTime |
(未使用) |
当 LibraryContract.setTime(uint) 被 delegatecall 调用时,它以为自己在修改 storedTime(位于 slot 0)。但实际上,它修改的是 Preservation 合约的 slot 0,也就是 timeZone1Library 的地址!
这就给了我们一个攻击路径:
setFirstTime: 我们传入一个精心构造的 _timeStamp,这个 _timeStamp 其实是我们的攻击合约的地址。这次调用会把 Preservation 合约的 timeZone1Library (slot 0) 修改为我们的攻击合约地址。setTime(uint) 函数。但是,这个函数的实现不是为了设置时间,而是为了修改 owner。为了能修改 owner(位于 slot 2),我们的攻击合约需要有与 Preservation 相似的存储布局,使得 owner 变量也位于 slot 2。setFirstTime: 现在 timeZone1Library 已经指向我们的攻击合约。我们再次调用 setFirstTime,这次传入我们自己的地址(player)作为 _timeStamp。delegatecall 会执行我们攻击合约的 setTime 函数,该函数会将传入的 _timeStamp (我们的地址) 写入 owner 变量(slot 2),从而使我们成为 owner。攻击合约的存储布局必须与 Preservation 兼容,至少在前三个槽位上是这样。它的 setTime 函数被设计用来修改 owner。
1 | // SPDX-License-Identifier: MIT |
1 | // SPDX-License-Identifier: Unlicense |
setTime 函数和兼容存储布局的攻击合约。setFirstTime 调用: 调用 setFirstTime,参数为攻击合约的地址。这会劫持 timeZone1Library 指针。setFirstTime 调用: 再次调用 setFirstTime,参数为 player 的地址。这会执行攻击合约的代码,将 player 的地址写入 Preservation 合约的 owner 存储槽。library 关键字: Solidity 的 library 类型是专门为此类功能设计的。库是无状态的,并且不能被 delegatecall 直接调用来修改状态(除非使用了特定的技巧)。它们可以防止存储布局冲突。delegatecall 到一个非库合约,请务必确保两个合约具有完全相同且兼容的存储布局。任何差异都可能导致严重的安全漏洞。delegatecall 暴露给用户输入: 避免让用户控制 delegatecall 的目标地址或参数。delegatecall 应该只用于与受信任和经过验证的代码进行交互。call 而不是 delegatecall: 如果只是想调用另一个合约的函数,而不需要在当前合约的上下文中执行,请使用标准的 call。call 会在被调用合约自己的上下文中执行,不会影响调用者的存储。delegatecall: EVM 中最强大的操作码之一,也是最危险的。它允许代码重用,但也带来了存储操纵的风险。forge inspect <Contract> storage-layout 是一个非常有用的工具。address 转换为 uint 是本次攻击的关键。uint256(uint160(address)) 是实现这一点的标准方法。核心概念:
delegatecall 在调用者的上下文中执行代码,这意味着它可以修改调用者的存储。delegatecall 会导致意想不到的、灾难性的状态损坏。delegatecall 影响的关键。攻击向量:
delegatecall 和不匹配的存储布局来覆盖合约的关键状态变量(如指针或所有者地址)。防御策略:
delegatecall 的使用。library 关键字来创建无状态的辅助合约。delegatecall 的目标合约具有兼容的存储布局。在智能合约的世界中,最简单的漏洞往往隐藏着最深刻的安全教训。 🎓