本文結論: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 的合約漏洞恰恰暴露了這一常見做法非常危險的一面:合約調用者可能通過該功能繞開權限檢查,或者以合約的身份發起對其它合約的攻擊等等。
有安全隱患代碼鏈接:
ATN 事件漏洞分析
ERC223 是由 Dexaran 于 2017 年 3 月 5 日提出的一個 Token 標準草案 [3],用于改進 ERC20,解決其無法處理發往合約自身 Token 的這一問題。ERC20 有兩套代幣轉賬機制,一套為直接調用 transfer()
函數,另一套為調用 approve()
+ transferFrom()
先授權再轉賬。當轉賬對象為智能合約時,這種情況必須使用第二套方法,否則轉往合約地址的 Token 將永遠無法再次轉出。
下面代碼為 ERC223 草案中的一段 正確示例,調用 transfer()
函數時,合約判斷目標地址 to
是否是合約,如果是合約,則調用目標合約的 tokenFallback()
方法,從而實現合約對轉入 Token 的處理。這段代碼并沒有 CUSTOM_CALL 濫用的問題。
ERC223 合約是 ERC20 合約的超集,目標為取代 ERC20 合約,成為新的 Token 合約標準。但提出以來至今一年多的時間仍未得到廣泛接受,僅有少數項目采用了該提案。
下面是 ERC223 錯誤實現代碼,ATN Token不幸地采用了這一段代碼。用戶被允許傳入任意自定義的 _custom_fallback
,從而任意調用目標 _to
地址上的任意方法!
ATN 的漏洞分析報告中稱其 Token 合約參考了 ERC223 標準的推薦實現 [4]。經過我們調查,發現其的確與 Dexaran 維護的 ERC223-token-standard 中 Recommended 分支的 transfer()
方法實現類似 [5]:
這其實是很危險的行為!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
,很容易對開發者產生誤導并被濫用。
通常當我們調用 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
這一參數的引入和使用。
以下是該提案規定的接口:
可以看到兩個 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_fallback
的 transfer()
實現,但是沒有進行任何風險提示。
ERC223 代碼實現的其他問題
事實上,ERC223 Recommended 分支代碼實現還存在其他問題。
call()
在處理 bytes 變量時會引發 evm 層面的 bug,導致數據不一致 [6]。
event 處理 indexed 的 bytes 變量,在特殊情況下也會引發報錯 [7]。
由此可以推斷 ERC223 的 Recomm* 分支代碼是不成熟的,我們不推薦使用。
EVM 參數傳遞機制
下面我們解釋一下 EVM 中函數調用與參數傳遞的機制,以便于對這個安全隱患的理解。以如下合約為例
首先了解一下EVM參數傳遞機制:在調用函數時,如果目標函數有參數,正常情況下我們需要根據ABI指定的參數類型來構造輸入。例如 transfer(address to, uint256 value)
在調用 transfer()
時,以太坊使用函數簽名的哈希值前4字節作為 function selector
,計算 sha3(transfer(address,uint256))
得到 0xA9059CBB
,再拼接上to
地址,256位補全為
緊隨其后拼接上value
,同樣256位補全為
最后得到完整的calldata:
將這段 calldata 附加在交易中發送到目標智能合約地址即可實現函數調用。當以太坊節點收到交易時,將 calldata 與智能合約字節碼一同加載到 EVM 中,字節碼在編譯時生成,也意味著對參數的處理在編譯時也已經固定下來了。我們查閱以太坊黃皮書可以看到:
現在我們來看一下實際編譯出的字節碼如何分離 transfer
、address
、value
這三個參數,觀察如下字節碼片段:
我們可以看到在字節碼中,出于動態數組的考慮,只會判斷 calldata
是否小于某個最小長度,但是不會檢查參數是否過長。編譯器會生成一系列 CALLDATALOAD
配合數學運算來分離出函數需要的參數。首先計算調用的目標函數:
CALLDATALOAD
指令將交易中的 calldata(0xa9059cbb0000000000000000000000003f5ce5fbfe3e9af3971dd833d26ba9b5c936f0be000000000000000000000000000000000000000000000000000000e8d4a51000
)加載到棧中,然后使用除法運算,將數據前256位除以 0x 100000000000000000000000000000000000000000000000000000000
, 得到0xA9059CBB
,以此類推,每個參數都會用類似的方法分離出來。但是當參數過多的時候,字節碼、EVM都不會處理,可以直接忽略,所以這個特性主要來源于編譯器。黑客利用這一特性可以很容易地針對 CUSTOM_CALL 構造攻擊參數。
ERC827 的有安全隱患代碼實現
類似的 ERC827 Token 提案也存在相同的問題 [8]。下面的代碼來自于 openzeppelin-solidity的 ERC827 問題代碼實現:
該代碼在 transferAndCall()
中轉賬功能完成后,會調用_to
地址上的任意函數,并且參數由調用者任意指定。由于該函數檢查了 _to != address(this)
,因此代碼不會產生 與 ds-auth 庫結合后繞開權限檢查的安全漏洞(后果二
),但是可能會引入前文所提到的 后果一
與后果三
,即可以任意支配問題合約所擁有或管理的 Token(即以 this 合約為跳板攻擊其它合約)。
此外,還有不少 ERC20 Token 加入了類似的 call()
自定義數據的實現。這種允許自定義 call()
任意地址上任意函數的設計,十分危險。在特殊情況下,甚至允許攻擊者盜走合約中的各種 Token,以及繞過合約本身的權限控制。
ERC20, ERC721 中關于“接收通知調用”正確的代碼實現
正確的代碼實現中,對于“接收通知調用”的處理應該將被通知函數的簽名(signature)寫死為固定值,避免由攻擊者來任意指定的任何可能性。下面舉兩個例子說明正確的通知調用的寫法:
- 聲明Receiver函數,并通過聲明的函數進行接收通知調用
例如在以太坊官網(ethereum.org)維護的 ERC20 代碼中關于通知調用的代碼片段:
調用通知采用了正常的函數調用方式。
- 通過 Receiver 函數的簽名常量進行接收通知調用
下面一段正確實現代碼來自于 Consensys 維護的 Token-Factory 項目
下面是正確實現“接收通知調用”的代碼實現庫
- https://github.com/svenstucki/ERC677
- https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/token/ERC721/ERC721BasicToken.sol#L349
- https://github.com/ConsenSys/Token-Factory/blob/master/contracts/HumanStandardToken.sol
- https://github.com/ethereum/ethereum-org/blob/b46095815f52cf328ecf7676b2b38284d48fba58/solidity/token-advanced.sol#L138