关卡链接: 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
的目标合约具有兼容的存储布局。在智能合约的世界中,最简单的漏洞往往隐藏着最深刻的安全教训。 🎓