以太坊合約審計 CheckList 之“以太坊智能合約編碼設計問題”影響分析報告

作者:LoRexxar'@知道創宇404區塊鏈安全研究團隊
時間:2018年9月21日
系列文章:

一、簡介

在知道創宇404區塊鏈安全研究團隊整理輸出的《知道創宇以太坊合約審計CheckList》中,把“地址初始化問題”、“判斷函數問題”、“餘額判斷問題”、“轉賬函數問題”、“代碼外部調用設計問題”、“錯誤處理”、“弱隨機數問題”等問題統一歸類爲“以太坊智能合約編碼設計問題”。

“昊天塔(HaoTian)”是知道創宇404區塊鏈安全研究團隊獨立開發的用於監控、掃描、分析、審計區塊鏈智能合約安全自動化平臺。我們利用該平臺針對上述提到的《知道創宇以太坊合約審計CheckList》中“以太坊智能合約編碼設計”類問題在全網公開的智能合約代碼做了掃描分析。詳見下文:

二、漏洞詳情

以太坊智能合約是以太坊概念中非常重要的一個概念,以太坊實現了基於solidity語言的以太坊虛擬機(Ethereum Virtual Machine),它允許用戶在鏈上部署智能合約代碼,通過智能合約可以完成人們想要的合約。

這次我們提到的編碼設計問題就和EVM底層的設計有很大的關係,由於EVM的特性,智能合約有很多與其他語言不同的特性,當開發者沒有注意到這些問題時,就容易出現潛在的問題。

1、地址初始化問題

在EVM中,所有與地址有關的初始化時,都會賦予初值0。

如果一個address變量與0相等時,說明該變量可能未初始化或出現了未知的錯誤。

如果開發者在代碼中初始化了某個address變量,但未賦予初值,或用戶在發起某種操作時,誤操作未賦予address變量,但在下面的代碼中需要對這個變量做處理,就可能導致不必要的安全風險。

2、判斷函數問題

在智能合約中,有個很重要的校驗概念。下面這種問題的出現主要是合約代幣的內部交易。

但如果在涉及到關鍵判斷(如餘額判斷)等影響到交易結果時,當交易發生錯誤,我們需要對已經執行的交易結果進行回滾,而EVM不會檢查交易函數的返回結果。如果我們使用return false,EVM是無法獲取到這個錯誤的,則會導致在之前的文章中提到的 假充值問題

在智能合約中,我們需要拋出這個錯誤,這樣EVM才能獲取到錯誤觸發底層的revert指令回滾交易。

而在solidity扮演這一角色的,正是require函數。而有趣的是,在solidity中,還有一個函數叫做assert,和require不同的是,它底層對應的是空指令,EVM執行到這裏時就會報錯退出,不會觸發回滾。

轉化到直觀的交易來看,如果我們使用assert函數校驗時,assert會消耗掉所有剩餘的gas。而require會觸發回滾操作。

assert在校驗方面展現了強一致性,除了對固定變量的檢查以外,require更適合這種情況下的使用。

3、餘額判斷問題

在智能合約中,經常會出現對用戶餘額的判斷,尤其是賬戶初建時,許多合約都會對以合約創建時餘額爲0來判斷合約的初建狀態,這是一種錯誤的行爲。

在智能合約中,永遠無法阻止別人向你的強制轉賬,即使fallback函數throw也不可以。攻擊者可以創建帶有餘額的新合約,然後調用 selfdestruct(victimAddress) 銷燬,這樣餘額就會強制轉移給目標,在這個過程中,不會調用目標合約的代碼,所以無法從代碼層面阻止。

值得注意的是,在打包的過程中,攻擊者可以通過條件競爭來在合約創建前轉賬,這樣在合約創建時餘額就爲0了。

4、轉賬函數問題

在智能合約中,涉及到轉賬的操作最常見不過了。而在solidity中,提供了兩個函數用於轉賬tranfer/send。

當tranfer/send函數的目標是合約時,會調用合約內的fallback函數。但當fallback函數執行錯誤時,transfer函數會拋出錯誤並回滾,而send則會返回false。如果在使用send函數交易時,沒有及時做判斷,則可能出現轉賬失敗卻餘額減少的情況。

<span class="kd">function</span> <span class="nx">withdraw</span><span class="p">(</span><span class="nx">uint256</span> <span class="nx">_amount</span><span class="p">)</span> <span class="kr">public</span> <span class="p">{</span>
    <span class="nx">require</span><span class="p">(</span><span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">>=</span> <span class="nx">_amount</span><span class="p">);</span>
    <span class="nx">balances</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">-=</span> <span class="nx">_amount</span><span class="p">;</span>
    <span class="nx">etherLeft</span> <span class="o">-=</span> <span class="nx">_amount</span><span class="p">;</span>
    <span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">_amount</span><span class="p">);</span>  
<span class="p">}</span>

上面給出的代碼中使用 send() 函數進行轉賬,因爲這裏沒有驗證 send() 返回值,如果msg.sender 爲合約賬戶 fallback() 調用失敗,則 send() 返回false,最終導致賬戶餘額減少了,錢卻沒有拿到。

5、代碼外部調用設計問題

在智能合約的設計思路中,有一個很重要的概念爲外部調用。或是調用外部合約,又或是調用其它賬戶。這在智能合約的設計中是個很常見的思路,最常見的便是轉賬操作,就是典型的外部調用。

但外部調用本身就是一個容易發生錯誤的操作,誰也不能肯定在和外部合約/用戶交互時能確保順利,舉一個合約代幣比較常見的例子

contract auction {
    address highestBidder;
    uint highestBid;
    function bid() payable {
        if (msg.value < highestBid) throw;
        if (highestBidder != 0) {
            if (!highestBidder.send(highestBid)) { // 可能會發生錯誤
                throw;
            }
        }
       highestBidder = msg.sender;
       highestBid = msg.value;
    }
}

上述代碼當轉賬發生錯誤時可能會導致進一步其他的錯誤,如果碰到循環調用bid函數時,更可能導致循環到中途發生錯誤,在之前提到的 ddos優化問題 中,這也是一個很典型的例子。

而這就是一個典型的push操作,指合約主動和外部進行交互,這種情況容易出現問題是難以定位難以彌補,導致潛在的問題。

6、錯誤處理

智能合約中,有一些涉及到address底層操作的方法

address.call()
address.callcode()
address.delegatecall()
address.send()

他們都有一個典型的特點,就是遇到錯誤並不會拋出錯誤,而是會返回錯誤並繼續執行。

且作爲EVM設計的一部分,下面這些函數如果調用的合約不存在,將會返回True。如果合約開發者沒有注意到這個問題,那麼就有可能出現問題。

call、delegatecall、callcode、staticcall

http://rickgray.me/2018/05/26/ethereum-smart-contracts-vulnerabilities-review-part2/#4-Unchecked-Return-Values-For-Low-Level-Calls

7、弱隨機數問題

智能合約是藉助EVM運行,跑在區塊鏈上的合約代碼。其最大的特點就是公開和不可篡改性。而如何在合約上生成隨機數就成了一個大問題。

Fomo3D合約在空投獎勵的隨機數生成中就引入了block信息作爲隨機數種子生成的參數,導致隨機數種子只受到合約地址影響,無法做到完全隨機。

<span class="kd">function</span> <span class="nx">airdrop</span><span class="p">()</span>
    <span class="kr">private</span> 
    <span class="nx">view</span> 
    <span class="nx">returns</span><span class="p">(</span><span class="kt">bool</span><span class="p">)</span>
<span class="p">{</span>
    <span class="nx">uint256</span> <span class="nx">seed</span> <span class="o">=</span> <span class="nx">uint256</span><span class="p">(</span><span class="nx">keccak256</span><span class="p">(</span><span class="nx">abi</span><span class="p">.</span><span class="nx">encodePacked</span><span class="p">(</span>
        <span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">timestamp</span><span class="p">).</span><span class="nx">add</span>
        <span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">difficulty</span><span class="p">).</span><span class="nx">add</span>
        <span class="p">((</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">keccak256</span><span class="p">(</span><span class="nx">abi</span><span class="p">.</span><span class="nx">encodePacked</span><span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">coinbase</span><span class="p">))))</span> <span class="o">/</span> <span class="p">(</span><span class="nx">now</span><span class="p">)).</span><span class="nx">add</span>
        <span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="nx">gaslimit</span><span class="p">).</span><span class="nx">add</span>
        <span class="p">((</span><span class="nx">uint256</span><span class="p">(</span><span class="nx">keccak256</span><span class="p">(</span><span class="nx">abi</span><span class="p">.</span><span class="nx">encodePacked</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">))))</span> <span class="o">/</span> <span class="p">(</span><span class="nx">now</span><span class="p">)).</span><span class="nx">add</span>
        <span class="p">(</span><span class="nx">block</span><span class="p">.</span><span class="kt">number</span><span class="p">)</span>
    <span class="p">)));</span>
    <span class="k">if</span><span class="p">((</span><span class="nx">seed</span> <span class="o">-</span> <span class="p">((</span><span class="nx">seed</span> <span class="o">/</span> <span class="mi">1000</span><span class="p">)</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">))</span> <span class="o"><</span> <span class="nx">airDropTracker_</span><span class="p">)</span>
        <span class="k">return</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
    <span class="k">else</span>
        <span class="k">return</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
<span class="p">}</span>

上述這段代碼直接導致了Fomo3d薅羊毛事件的誕生。真實世界損失巨大,超過數千eth。

8萬筆交易「封死」以太坊網絡,只爲搶奪Fomo3D大獎? Last Winner

三、漏洞影響範圍

使用Haotian平臺智能合約審計功能可以準確掃描到該類型問題。

基於Haotian平臺智能合約掃描功能規則,我們對全網的公開的共42538個合約代碼進行了掃描,其中35107個合約存在地址初始化問題,4262個合約存在判斷函數問題,173個合約存在餘額判斷問題,930個合約存在轉賬函數問題, 349個合約存在弱隨機數問題,2300個合約調用了block.timestamp,過半合約涉及到這類安全風險。

1、地址初始化問題

截止2018年9月21日,我們發現了35107個存在地址初始化問題的合約代碼,存在潛在的安全隱患。

2、判斷函數問題

截止2018年9月21日,我們發現了4262個存在判斷函數問題的合約代碼,存在潛在的安全隱患。

3、餘額判斷問題

截止2018年9月21日,我們發現了173個存在餘額判斷問題的合約代碼,其中165個仍處於交易狀態,其中交易量最高的10個合約情況如下:

4、轉賬函數問題

截止2018年9月21日,我們發現了930個存在轉賬函數問題的合約代碼,其中873個仍處於交易狀態,其中交易量最高的10個合約情況如下:

5、弱隨機數問題

截止2018年9月21日,我們發現了349個存在弱隨機數問題的合約代碼,其中272個仍處於交易狀態,其中交易量最高的10個合約情況如下:

截止2018年9月21日,我們發現了2300個存在調用了block.timestamp的合約代碼,其中2123個仍處於交易狀態,其中交易量最高的10個合約情況如下:

四、修復方式

1、地址初始化問題

涉及到地址的函數中,建議加入require(_to!=address(0))驗證,有效避免用戶誤操作或未知錯誤導致的不必要的損失

2、判斷函數問題

對於正常的判斷來說,優先使用 require 來判斷結果。

而對於固定變量的檢查,使用assert函數可以避免一些未知的問題,因爲他會強制終止合約並使其無效化,在一些固定條件下,assert更適用

3、餘額判斷問題

不要在合約任何地方假設合約的餘額,尤其是不要通過創建時合約爲0來判斷合約初建狀態,攻擊者可以使用多種方式強制轉賬。

4、轉賬函數問題

在完成交易時,默認推薦使用transfer函數而不是send完成交易。

5、代碼外部調用設計問題

對於外部合約優先使用pull而不是push。如上述的轉賬函數,可以通過賦予提取權限來將主動行爲轉換爲被動行爲

contract auction {
    address highestBidder;
    uint highestBid;
    mapping(address => uint) refunds;
    function bid() payable external {
        if (msg.value < highestBid) throw;
        if (highestBidder != 0) {
            refunds[highestBidder] += highestBid; // 記錄在refunds中
        }
        highestBidder = msg.sender;
        highestBid = msg.value;
    }
    function withdrawRefund() external {
        uint refund = refunds[msg.sender];
        refunds[msg.sender] = 0;
        if (!msg.sender.send(refund)) {
            refunds[msg.sender] = refund; // 如果轉賬錯誤還可以挽回
        }
    }
}

通過構建withdraw來使用戶來執行合約將餘額取出。

6、錯誤處理

合約中涉及到call等在address底層操作的方法時,做好合理的錯誤處理

if(!someAddress.send(55)) {
    // Some failure code
}

包括目標合約不存在時,也同樣需要考慮。

7、弱隨機數問題

智能合約上隨機數生成方式需要更多考量

在合約中關於這樣的應用時,考慮更合適的生成方式和合理的利用順序非常重要。

這裏提供一個比較合理的隨機數生成方式 hash-commit-reveal ,即玩家提交行動計劃,然後行動計劃hash後提交給後端,後端生成相應的hash值,然後生成對應的隨機數reveal,返回對應隨機數commit。這樣,服務端拿不到行動計劃,客戶端也拿不到隨機數。

有一個很棒的實現代碼是 dice2win 的隨機數生成代碼。

當然 hash-commit 在一些簡單場景下也是不錯的實現方式。即玩家提交行動計劃的hash,然後生成隨機數,然後提交行動計劃。

五、一些思考

在探索智能合約最佳實踐的過程中,逐漸發現,在智能合約中有很多隻有智能合約纔會出現的問題,這些問題大多都是因爲EVM的特殊性而導致的特殊特性,但開發者並沒有對這些特性有所瞭解,導致很多的潛在安全問題誕生。

我把這一類問題歸結爲編碼設計問題,開發者可以在編碼設計階段注意這些問題,可以避免大多數潛在安全問題。


智能合約審計服務

針對目前主流的以太坊應用,知道創宇提供專業權威的智能合約審計服務,規避因合約安全問題導致的財產損失,爲各類以太坊應用安全保駕護航。

知道創宇404智能合約安全審計團隊: https://www.scanv.com/lca/index.html
聯繫電話:(086) 136 8133 5016(沈經理,工作日:10:00-18:00)

歡迎掃碼諮詢: 區塊鏈行業安全解決方案

黑客通過DDoS攻擊、CC攻擊、系統漏洞、代碼漏洞、業務流程漏洞、API-Key漏洞等進行攻擊和入侵,給區塊鏈項目的管理運營團隊及用戶造成巨大的經濟損失。知道創宇十餘年安全經驗,憑藉多重防護+雲端大數據技術,爲區塊鏈應用提供專屬安全解決方案。

歡迎掃碼諮詢:


Paper 本文由 Seebug Paper 發佈,如需轉載請註明來源。本文地址: https://paper.seebug.org/707/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章