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

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

一、簡介

在知道創宇404區塊鏈安全研究團隊整理輸出的《知道創宇以太坊合約審計CheckList》中,把“溢出問題”、“重入漏洞”、“權限控制錯誤”、“重放攻擊”等問題統一歸類爲“以太坊智能合約編碼安全問題”。

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

二、漏洞詳情

1、溢出問題

以太坊Solidity設計之初就被定位爲圖靈完備性語言。在solidity的設計中,支持int/uint變長的有符號或無符號整型。變量支持的步長以8遞增,支持從uint8到uint256,以及int8到int256。需要注意的是,uint和int默認代表的是uint256和int256。uint8的數值範圍與C中的uchar相同,即取值範圍是0到2^8-1,uint256支持的取值範圍是0到2^256-1。而當對應變量值超出這個範圍時,就會溢出至符號位,導致變量值發生巨大的變化。

(1) 算數溢出

在Solidity智能合約代碼中,在餘額的檢查中如果直接使用了加減乘除沒做額外的判斷時,就會存在算術溢出隱患

contract MyToken {
    mapping (address => uint) balances;

    function balanceOf(address _user) returns (uint) { return balances[_user]; }
    function deposit() payable { balances[msg.sender] += msg.value; }
    function withdraw(uint _amount) {
        require(balances[msg.sender] - _amount > 0);  // 存在整數溢出
        msg.sender.transfer(_amount);
        balances[msg.sender] -= _amount;
    }
}

在上述代碼中,由於沒有校驗 _amount 一定會小於 balances[msg.sender] ,所以攻擊者可以通過傳入超大數字導致溢出繞過判斷,這樣就可以一口氣轉走鉅額代幣。

2018年4月24日,SMT/BEC合約被惡意攻擊者轉走了50,659,039,041,325,800,000,000,000,000,000,000,000,000,000,000,000,000,000,000個SMT代幣。惡意攻擊者就是利用了 SMT/BEC合約的整數溢出漏洞 導致了這樣的結果。

2018年5月19日,以太坊Hexagon合約代幣被公開存在 整數溢出漏洞

(2) 鑄幣燒幣溢出問題

作爲一個合約代幣的智能合約來說,除了有其他合約的功能以外,還需要有鑄幣和燒幣功能。而更特殊的是,這兩個函數一般都爲乘法或者指數交易,很容易造成溢出問題。

function TokenERC20(
    uint256 initialSupply,
    string tokenName,
    string tokenSymbol
) public {
    totalSupply = initialSupply * 10 ** uint256(decimals);  
    balanceOf[msg.sender] = totalSupply;                
    name = tokenName;                                   
    symbol = tokenSymbol;                               
}

上述代碼未對代幣總額做限制,會導致指數算數上溢。

2018年6月21日,Seebug Paper公開了一篇關於整數溢出漏洞的分析文章 ERC20 智能合約整數溢出系列漏洞披露 ,裏面提到很多關於指數上溢的漏洞樣例。

2、call注入

Solidity作爲一種用於編寫以太坊智能合約的圖靈完備的語言,除了常見語言特性以外,還提供了調用/繼承其他合約的功能。在 call delegatecall callcode 三個函數來實現合約之間相互調用及交互。正是因爲這些靈活各種調用,也導致了這些函數被合約開發者“濫用”,甚至“肆無忌憚”提供任意調用“功能”,導致了各種安全漏洞及風險。

function withdraw(uint _amount) {
    require(balances[msg.sender] >= _amount);
    msg.sender.call.value(_amount)();
    balances[msg.sender] -= _amount;
}

上述代碼就是一個典型的存在call注入問題直接導致重入漏洞的demo。

2016年7月, The DAO 被攻擊者使用重入漏洞取走了所有代幣,損失超過60億,直接導致了eth的硬分叉,影響深遠。

2017年7月20日,Parity Multisig電子錢包版本1.5+的漏洞被發現,使得攻擊者從三個高安全的多重簽名合約中竊取到超過15萬ETH ,其事件原因是由於未做限制的 delegatecall 函數調用了合約初始化函數導致合約擁有者被修改。

2018年6月16日,「隱形人真忙」在先知大會上分享了 「智能合約消息調用攻防」 的議題,其中提到了一種新的攻擊場景——call注⼊,主要介紹了利用對call調用處理不當,配合一定的應用場景的一種攻擊手段。接着於 2018年6月20日,ATN代幣團隊發佈「ATN抵禦黑客攻擊的報告」,報告指出黑客利用call注入攻擊漏洞修改合約擁有者,然後給自己發行代幣,從而造成 ATN 代幣增發。

2018年6月26日,知道創宇區塊鏈安全研究團隊在Seebug Paper上公開了 《以太坊 Solidity 合約 call 函數簇濫用導致的安全風險》

3、權限控制錯誤

在智能合約中,合約開發者一般都會設置一些用於合約所有者,但如果開發者疏忽寫錯了函數權限,就有可能導致所有者轉移等嚴重後果。

function initContract() public {
    owner = msg.reader;
}

上述代碼函數就需要設置onlyOwner。

4、重放攻擊

2018年,DEFCON26上來自 360 獨角獸安全團隊(UnicornTeam)的 Zhenzuan Bai, Yuwei Zheng 等分享了議題 《Your May Have Paid More than You Imagine:Replay Attacks on Ethereum Smart Contracts》

在攻擊中提出了智能合約中比較特殊的委託概念。

在資產管理體系中,常有委託管理的情況,委託人將資產給受託人管理,委託人支付一定的費用給受託人。這個業務場景在智能合約中也比較普遍。

這裏舉例子爲transferProxy函數,該函數用於當user1轉token給user3,但沒有eth來支付gasprice,所以委託user2代理支付,通過調用transferProxy來完成。

function transferProxy(address _from, address _to, uint256 _value, uint256 _fee,
    uint8 _v, bytes32 _r, bytes32 _s) public returns (bool){
    if(balances[_from] < _fee + _value 
        || _fee > _fee + _value) revert();
    uint256 nonce = nonces[_from];
    bytes32 h = keccak256(_from,_to,_value,_fee,nonce,address(this));
    if(_from != ecrecover(h,_v,_r,_s)) revert();
    if(balances[_to] + _value < balances[_to]
        || balances[msg.sender] + _fee < balances[msg.sender]) revert();
    balances[_to] += _value;
    emit Transfer(_from, _to, _value);
    balances[msg.sender] += _fee;
    emit Transfer(_from, msg.sender, _fee);
    balances[_from] -= _value + _fee;
    nonces[_from] = nonce + 1;
    return true;
}

上述代碼nonce值可以被預測,而其他變量不變的情況下,可以通過重放攻擊來多次轉賬。

三、漏洞影響範圍

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

基於Haotian平臺智能合約審計功能規則,我們對全網的公開的共42538個合約代碼進行了掃描,其中共1852個合約涉及到這類問題。

1、溢出問題

截止2018年9月5日,我們發現了391個存在算數溢出問題的合約代碼,其中332個仍處於交易狀態,其中交易量最高的10個合約情況如下:

截止2018年9月5日,我們發現了1636個存在超額鑄幣銷幣問題的合約代碼,其中1364個仍處於交易狀態,其中交易量最高的10個合約情況如下:

2、call注入

截止2018年9月5日,我們發現了204個存在call注入問題的合約代碼,其中140個仍處於交易狀態,其中交易量最高的10個合約情況如下:

3、重放攻擊

截止2018年9月5日,我們發現了18個存在重放攻擊隱患問題的合約代碼,其中16個仍處於交易狀態,其中交易量最高的10個合約情況如下:

四、修復方式

1、溢出問題

1) 算術溢出問題

在調用加減乘除時,通常的修復方式都是使用openzeppelin-safeMath,但也可以通過對不同變量的判斷來限制,但很難對乘法和指數做什麼限制。

function transfer(address _to, uint256 _amount)  public returns (bool success) {
    require(_to != address(0));
    require(_amount <= balances[msg.sender]);
    balances[msg.sender] = balances[msg.sender].sub(_amount);
    balances[_to] = balances[_to].add(_amount);
    emit Transfer(msg.sender, _to, _amount);
    return true;
}
2)鑄幣燒幣溢出問題

鑄幣函數中,應對totalSupply設置上限,避免因爲算術溢出等漏洞導致惡意鑄幣增發。

在鑄幣燒幣加上合理的權限限制可以有效減少該問題危害。

contract OPL {
    // Public variables
    string public name;
    string public symbol;
    uint8 public decimals = 18; // 18 decimals
    bool public adminVer = false;
    address public owner;
    uint256 public totalSupply;
    function OPL() public {
        totalSupply = 210000000 * 10 ** uint256(decimals);      
        ...                                 
}

2、call注入

call函數調用時,應該做嚴格的權限控制,或直接寫死call調用的函數。避免call函數可以被用戶控制。

在可能存在重入漏洞的代碼中,經可能使用transfer函數完成轉賬,或者限制call執行的gas,都可以有效的減少該問題的危害。

contract EtherStore {

    // initialise the mutex
    bool reEntrancyMutex = false;
    uint256 public withdrawalLimit = 1 ether;
    mapping(address => uint256) public lastWithdrawTime;
    mapping(address => uint256) public balances;

    function depositFunds() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdrawFunds (uint256 _weiToWithdraw) public {
        require(!reEntrancyMutex);
        require(balances[msg.sender] >= _weiToWithdraw);
        // limit the withdrawal
        require(_weiToWithdraw <= withdrawalLimit);
        // limit the time allowed to withdraw
        require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
        balances[msg.sender] -= _weiToWithdraw;
        lastWithdrawTime[msg.sender] = now;
        // set the reEntrancy mutex before the external call
        reEntrancyMutex = true;
        msg.sender.transfer(_weiToWithdraw);
        // release the mutex after the external call
        reEntrancyMutex = false; 
    }
 }

上述代碼是一種用互斥鎖來避免遞歸防護方式。

3、權限控制錯誤

合約中不同函數應設置合理的權限

檢查合約中各函數是否正確使用了public、private等關鍵詞進行可見性修飾,檢查合約是否正確定義並使用了modifier對關鍵函數進行訪問限制,避免越權導致的問題。

function initContract() public OnlyOwner {
    owner = msg.reader;
}

4、重放攻擊

合約中如果涉及委託管理的需求,應注意驗證的不可複用性,避免重放攻擊。

其中主要的兩點在於: 1、避免使用transferProxy函數。採用更靠譜的簽名方式簽名。 2、nonce機制其自增可預測與這種簽名方式違背,導致可以被預測。儘量避免nonce自增。

五、一些思考

在完善智能合約審計checklist時,我選取了一部分問題將其歸爲編碼安全問題,這類安全問題往往是開發者疏忽導致合約代碼出現漏洞,攻擊者利用代碼中的漏洞來攻擊,往往會導致嚴重的盜幣事件。

在我們使用HaoTian對全網的公開合約進行掃描和監控時,我們發現文章中提到的幾個問題涉及到的合約較少。由於智能合約代碼公開透明的特性,加上這類問題比較容易檢查出,一旦出現就會導致對合約的毀滅性打擊,所以大部分合約開發人員都會注意到這類問題。但在不容易被人們發現的未公開合約中,或許還有大批潛在的問題存在。

這裏我們建議所有的開發者重新審視自己的合約代碼,檢查是否存在編碼安全問題,避免不必要的麻煩或嚴重的安全問題。


智能合約審計服務

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

知道創宇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/696/

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