智能合約的常見漏洞

目錄: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且沒有轉帳功能。
相關文章
相關標籤/搜索