ERC223及ERC827實現代碼欠缺安全考慮 —— ATN Token中的CUSTOM_CALL漏洞深入分析

本文部分內容基於安比(SECBIT)實驗室團隊與吳玉會(輕信科技)的討論

本文結論: ERC223, ERC827的部分實現代碼引入了任意函數調用缺陷,可能會對使用這部分代碼的合約帶來安全漏洞。如果需要實現上述規範接口,請仔細檢查實現代碼。這種合約本身允許用戶自定義call() 任意地址上任意函數的設計,十分危險。攻擊者可以很容易地借用當前合約的身份來進行任何操作,比如盜取Token或者繞開權限檢查等。

影響範圍:截止目前檢測到以太坊上部署的受影響的ERC20合約數量:146

最新更新:

  • 火幣網已經暫停了已經上線交易的相關問題Token[9][10]
  • ATN團隊已經修復漏洞[1]

CUSTOM_CALL 濫用事件回顧與分析

2018 年 6 月 20 日,AI Technology Network (ATN) 和慢霧團隊披露了一起針對 ATN 智能合約的攻擊事件,黑客於 2018 年 5 月 11 日利用 ATN Token 合約存在的漏洞,將自己地址設爲 owner 並增發獲利 1100 萬 ATN。ATN 技術團隊迅速發現問題、定位攻擊方法並完成合約的升級修復 [1]。黑客利用了 ERC223 合約可傳入自定義的接收調用函數與 ds-auth 權限校驗等特徵,在 ERC223 合約調用這個自定義函數時,合約調用自身函數從而造成內部權限控制失效。隨後,百度安全的“隱形人真忙”也在先知安全大會上進行了“以太坊智能合約 call 注入攻擊”的主題分享 [2]。這個漏洞源於一個較爲常見的做法:在調用合約函數之後,可以再次調用一次另一個合約的任意函數,並且這個任意函數可以由合約調用發起者指定。但是 ATN 的合約漏洞恰恰暴露了這一常見做法非常危險的一面:合約調用者可能通過該功能繞開權限檢查,或者以合約的身份發起對其它合約的攻擊等等。

有安全隱患代碼鏈接:
https://github.com/Dexaran/ERC223-token-standard/blob/16d350ec85d5b14b9dc857468c8e0eb4a10572d3/ERC223_Token.sol#L70
https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/token/ERC827/ERC827Token.sol

ATN 事件漏洞分析

ERC223 是由 Dexaran 於 2017 年 3 月 5 日提出的一個 Token 標準草案 [3],用於改進 ERC20,解決其無法處理髮往合約自身 Token 的這一問題。ERC20 有兩套代幣轉賬機制,一套爲直接調用transfer()函數,另一套爲調用 approve() + transferFrom()先授權再轉賬。當轉賬對象爲智能合約時,這種情況必須使用第二套方法,否則轉往合約地址的 Token 將永遠無法再次轉出。

下面代碼爲 ERC223 草案中的一段* 正確示例*,調用 transfer()函數時,合約判斷目標地址 to 是否是合約,如果是合約,則調用目標合約的tokenFallback()方法,從而實現合約對轉入 Token 的處理。這段代碼並沒有 CUSTOM_CALL 濫用的問題。

// 提案中的正確示例代碼
contract ERC223 {
  function transfer(address to, uint value, bytes data) {
        uint codeLength;
        assembly {
            codeLength := extcodesize(_to)
        }
        balances[msg.sender] = balances[msg.sender].sub(_value);
        balances[_to] = balances[_to].add(_value);
        if(codeLength>0) {
            // Require proper transaction handling.
            ERC223Receiver receiver = ERC223Receiver(_to);
            receiver.tokenFallback(msg.sender, _value, _data);
        }
    }
}

ERC223 合約是 ERC20 合約的超集,目標爲取代 ERC20 合約,成爲新的 Token 合約標準。但提出以來至今一年多的時間仍未得到廣泛接受,僅有少數項目採用了該提案。

下面是 ERC223 錯誤實現代碼,ATN Token不幸地採用了這一段代碼。用戶被允許傳入任意自定義的_custom_fallback,從而任意調用目標_to地址上的任意方法!

// 此代碼有 CUSTOM_CALL 濫用問題
function transferFrom(
    address _from, 
    address _to, 
    uint256 _amount, 
    bytes _data, 
    string _custom_fallback
    ) 
    public returns (bool success)
{
    ...
    ERC223ReceivingContract receiver = ERC223ReceivingContract(_to);
    receiving.call.value(0)(byte4(keccak256(_custom_fallback)), _from, amout, data);
    ...
}

ATN 的漏洞分析報告中稱其 Token 合約參考了 ERC223 標準的推薦實現 [4]。經過我們調查,發現其的確與 Dexaran 維護的 ERC223-token-standard 中 Recommended 分支的 transfer() 方法實現類似 [5]:

// 此代碼有 CUSTOM_CALL 濫用問題
function transfer(
    address _to, 
    uint _value, 
    bytes _data, 
    string _custom_fallback
    ) 
    public returns (bool success) 
{
    ...
    assert(_to.call.value(0)(bytes4(keccak256(_custom_fallback)), msg.sender, _value, _data));
    ...
}

這其實是很危險的行爲!ConsenSys 維護的「以太坊智能合約 —— 最佳安全開發指南」中曾明確提示,要儘量避免合約的外部調用。但在此次攻擊事件中,黑客傳入的 _custom_fallback 爲 setOwner(address),傳入的目標地址 _to 恰好是 ATN 合約本身,間接調用了 ATN 的 setOwner(address) 方法,使得 msg.sender 變爲 ATN Token 合約本身,從而通過 ds-auth 庫的 isAuthorized() 鑑權校驗。

EVM 讀取參數時並不會校驗參數個數,在上述例子中,黑客調用了 setOwner(adddress) 函數,EVM 僅會讀取最左邊的 _from 參數。因此使用底層 call() 方法傳參時,參數個數與函數所需不一致並不會引發報錯,黑客很容易精心構造出所需的攻擊參數。

CUSTOM_CALL 濫用的危害

再回到 _custom_fallback接口實現上。我們認爲作爲一個通用 Token 標準接口設計,設計者必須儘可能多地考慮整個系統生態的安全性,儘可能規避因使用不當引入的風險。倘若上面這種_custom_fallback接口設計得到廣泛採納,未來勢必會出現更多類似的安全性問題。一個良好的接口設計,最好能做到精簡、易用和無歧義。作者提案中 tokenFallback() 接口完全可以應對其原本想要解決的 ERC20 問題。而實現上引入自定義的_custom_fallback,很容易對開發者產生誤導並被濫用。

  function approveAndCall(
    address _spender,
    uint256 _value,
    bytes _data
  )
    public
    payable
    returns (bool)
  {
    // require(_spender != address(this));
    approve(_spender, _value);
    require(_spender.call.value(msg.value)(_data));
    return true;
  }

通常當我們調用 ERC20 的approve()函數給一個智能合約地址後,對方並不能收到相關通知進行下一步操作,常見做法是利用 接收通知調用(receiverCall)來解決無法監聽的問題。上面代碼是一種實現方式,很不幸這段代碼有嚴重的 CUSTOM_CALL 濫用漏洞。調用 approveAndCall()函數後,會接着執行 _spender上用戶自定義的其他方法來進行接收者的後續操作。

下面敲黑板!

【危害】:這種合約本身允許用戶自定義call()任意地址上任意函數的設計,十分危險。攻擊者可以很容易地借用當前合約的身份來進行任何操作

這通常會導致兩種危險的後果:

後果一:允許攻擊者以缺陷合約身份來盜走其它 Token 合約中的 Token
後果二:與 ds-auth 結合,繞過合約自身的權限檢查
後果三:允許攻擊者以缺陷合約身份來盜走其它 Token 賬戶所授權(Approve)的 Token

後果一舉例:假設缺陷 Token 合約 A 自身賬戶中擁有各種 Token B、 C、 D 等,攻擊者只需將 _spender 設爲想要盜取的目標 Token (如 B 的地址),再構造用於調用 transfer(address,uint256)的 data,即可輕鬆以合約 A 的身份將合約 A 中的各類 Token 轉走。上面代碼中對 _spender != address(this)的校驗,也僅能保護 A Token。

管理各種 Token 的智能合約,倘若也允許自定義call(),其合約上的各種 Token 就十分危險了。

後果二舉例:如 ATN 安全事件中,黑客也是藉此漏洞利用 ATN 合約的身份,繞過了 ds-auth 的權限控制。

後果三舉例:假設缺陷 Token 合約 A 被用戶 X 授權(Approve)管理 10,000 個Token B,那麼黑客也是藉此漏洞調用transferFrom()函數來盜取Token B。

ERC223 提案實現與接口定義不一致

進一步調查我們發現,ERC223 提案的文字接口描述中並沒有提到 _custom_fallback 這一參數的引入和使用。

以下是該提案規定的接口:

contract ERC223Interface {
    uint public totalSupply;
    function balanceOf(address who) constant returns (uint);
    function transfer(address to, uint value);
    function transfer(address to, uint value, bytes data);
    event Transfer(address indexed from, address indexed to, uint value, bytes data);
}

可以看到兩個transfer()接口定義中均沒有出現_custom_fallback參數。

我們在來看看提案的文字描述:

If the receiver is a contract ERC223 token contract will try to call tokenFallback function on receiver contract. If there is no tokenFallback function on receiver contract transaction will fail. tokenFallback function is analogue of fallback function for Ether transactions. It can be used to handle incoming transactions.

這段話的中心思想就是如果 Token 轉賬目標對象是 ERC223 合約,則嘗試調用其 tokenFallback()函數,如果目標對象不存在 tokenFallback() 函數,則讓交易 fail 掉。tokenFallback()在這裏充當的作用就是類似以太轉賬裏的默認fallback函數。

顯然,ERC223 提案的初衷十分清晰,就是約定一個tokenFallback()接口作爲 Token 合約標準,用於處理轉入的 Token。ERC223 提案主分支的代碼實現也沒有 _custom_fallback 的問題。而作者推薦的 Recommended 分支裏的代碼卻增加了一種引入_custom_fallbacktransfer() 實現,但是沒有進行任何風險提示。

ERC223 代碼實現的其他問題

事實上,ERC223 Recommended 分支代碼實現還存在其他問題。
call() 在處理 bytes 變量時會引發 evm 層面的 bug,導致數據不一致 [6]。

// ERC223_Token.sol#L70
assert(_to.call.value(0)(bytes4(keccak256(_custom_fallback)), msg.sender, _value, _data));
event 處理 indexed 的 bytes 變量,在特殊情況下也會引發報錯 [7]。

// ERC223_Interface.sol#L18
event Transfer(address indexed from, address indexed to, uint value, bytes indexed data); 

由此可以推斷 ERC223 的 Recommended 分支代碼是不成熟的,我們不推薦使用。

EVM 參數傳遞機制

下面我們解釋一下 EVM 中函數調用與參數傳遞的機制,以便於對這個安全隱患的理解。以如下合約爲例

contract A{
    function transfer(address to, uint256 value){
          return;
    }
}

首先了解一下EVM參數傳遞機制:在調用函數時,如果目標函數有參數,正常情況下我們需要根據ABI指定的參數類型來構造輸入。例如transfer(address to, uint256 value)在調用 transfer()時,以太坊使用函數簽名的哈希值前4字節作爲 function selector,計算 sha3(transfer(address,uint256))得到0xA9059CBB,再拼接上to地址,256位補全爲

0x0000000000000000000000003f5ce5fbfe3e9af3971dd833d26ba9b5c936f0be

緊隨其後拼接上value,同樣256位補全爲

0x000000000000000000000000000000000000000000000000000000e8d4a51000

最後得到完整的calldata:

0xa9059cbb0000000000000000000000003f5ce5fbfe3e9af3971dd833d26ba9b5c936f0be000000000000000000000000000000000000000000000000000000e8d4a51000

將這段 calldata 附加在交易中發送到目標智能合約地址即可實現函數調用。當以太坊節點收到交易時,將 calldata 與智能合約字節碼一同加載到 EVM 中,字節碼在編譯時生成,也意味着對參數的處理在編譯時也已經固定下來了。我們查閱以太坊黃皮書可以看到:
這裏寫圖片描述
現在我們來看一下實際編譯出的字節碼如何分離transferaddressvalue這三個參數,觀察如下字節碼片段:

PUSH 80
PUSH 40
MSTORE
PUSH 4
CALLDATASIZE 
LT 
PUSH [tag] 1
JUMPI 
PUSH 0
CALLDATALOAD
PUSH 100000000000000000000000000000000000000000000000000000000
SWAP1
DIV 
PUSH FFFFFFFF
AND
DUP1
PUSH A9059CBB
EQ          
PUSH 2
JUMPI
JUMPDEST
PUSH 0
DUP1
REVERT 

我們可以看到在字節碼中,出於動態數組的考慮,只會判斷calldata 是否小於某個最小長度,但是不會檢查參數是否過長。編譯器會生成一系列CALLDATALOAD 配合數學運算來分離出函數需要的參數。首先計算調用的目標函數:
CALLDATALOAD指令將交易中的 calldata(0xa9059cbb0000000000000000000000003f5ce5fbfe3e9af3971dd833d26ba9b5c936f0be000000000000000000000000000000000000000000000000000000e8d4a51000)加載到棧中,然後使用除法運算,將數據前256位除以 0x 100000000000000000000000000000000000000000000000000000000, 得到0xA9059CBB,以此類推,每個參數都會用類似的方法分離出來。但是當參數過多的時候,字節碼、EVM都不會處理,可以直接忽略,所以這個特性主要來源於編譯器。黑客利用這一特性可以很容易地針對 CUSTOM_CALL 構造攻擊參數。

ERC827 的有安全隱患代碼實現

類似的 ERC827 Token 提案也存在相同的問題 [8]。下面的代碼來自於 openzeppelin-solidity 的 ERC827 問題代碼實現:

function transferAndCall(address _to, uint256 _value,bytes _data)
    public payable returns (bool)
{
    require(_to != address(this));
    super.transfer(_to, _value);
    require(_to.call.value(msg.value)(_data));
    return true;
}

該代碼在transferAndCall()中轉賬功能完成後,會調用_to地址上的任意函數,並且參數由調用者任意指定。由於該函數檢查了_to != address(this),因此代碼不會產生 與 ds-auth 庫結合後繞開權限檢查的安全漏洞(後果二),但是可能會引入前文所提到的 後果一與後果三,即可以任意支配問題合約所擁有或管理的 Token(即以 this 合約爲跳板攻擊其它合約)。

此外,還有不少 ERC20 Token 加入了類似的 call()自定義數據的實現。這種允許自定義 call()任意地址上任意函數的設計,十分危險。在特殊情況下,甚至允許攻擊者盜走合約中的各種 Token,以及繞過合約本身的權限控制。

ERC20, ERC721 中關於“接收通知調用”正確的代碼實現

正確的代碼實現中,對於“接收通知調用”的處理應該將被通知函數的簽名(signature)寫死爲固定值,避免由攻擊者來任意指定的任何可能性。下面舉兩個例子說明正確的通知調用的寫法:

1.聲明Receiver函數,並通過聲明的函數進行接收通知調用

例如在以太坊官網(ethereum.org)維護的 ERC20 代碼中關於通知調用的代碼片段:

function approveAndCall(address _spender, uint256 _value, bytes _extraData)
    public returns (bool success) 
{
    tokenRecipient spender = tokenRecipient(_spender);
    if (approve(_spender, _value)) {
        spender.receiveApproval(msg.sender, _value, this, _extraData);
        return true;
    }
}

調用通知採用了正常的函數調用方式。

2.通過 Receiver 函數的簽名常量進行接收通知調用

下面一段正確實現代碼來自於 Consensys 維護的 Token-Factory 項目

/* Approves and then calls the receiving contract */
function approveAndCall(address _spender, uint256 _value, bytes _extraData) returns (bool success) {
    allowed[msg.sender][_spender] = _value;
    Approval(msg.sender, _spender, _value);

    //call the receiveApproval function on the contract you want to be notified. This crafts the function signature manually so one doesn't have to include a contract in here just for this.
    //receiveApproval(address _from, uint256 _value, address _tokenContract, bytes _extraData)
    //it is assumed that when does this that the call *should* succeed, otherwise one would use vanilla approve instead.
    if(!_spender.call(bytes4(bytes32(sha3("receiveApproval(address,uint256,address,bytes)"))), msg.sender, _value, this, _extraData)) { throw; }
    return true;
    }

下面是正確實現“接收通知調用”的代碼實現庫

總結與反思

  • ERC223 標準的實現與接口定義脫節,實現倉庫存在兩個分支接口不一致,使用權威代碼時不能放鬆警惕
  • ERC827 代碼同樣存在隱患
  • 用 low-level call 一定要小心
  • 需要深刻理解 EVM 中關於合約函數調用的實現機制

Reference

[1] ATN抵禦合約攻擊的報告
[2] 以太坊智能合約call注入攻擊 (https://github.com/dapphub/ds-auth/blob/c0050bbb6807027c623b1a1ee7afd86515cdb004/src/auth.sol#L52)
[3] ERC-223 Token Standard Proposal Draft
[4] ATN.sol transferFrom()
[5] ERC223_Token.sol transfer() function
[6] ERC223-token-standard Issue 50
[7] ERC223-token-standard Issue 51
[8] ERC827Token.sol
[9] New evilReflex Bug Identified in Multiple ERC20 Smart Contracts (CVE-2018-12702, CVE-2018-12703)
[10] HADAX Suspends 18T and GVE Deposits and Withdrawals

發佈了42 篇原創文章 · 獲贊 8 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章