作者:LoRexxar’@知道創宇404區塊鏈安全研究團隊
時間:2018年9月21日
系列文章:
- 《以太坊合約審計 CheckList 之“以太坊智能合約規範問題”影響分析報告》
- 《以太坊合約審計 CheckList 之“以太坊智能合約設計缺陷問題”影響分析報告》
- 《以太坊合約審計 CheckList 之“以太坊智能合約編碼安全問題”影響分析報告》
一、簡介
在知道創宇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函數交易時,沒有及時做判斷,則可能出現轉賬失敗卻餘額減少的情況。
function withdraw(uint256 _amount) public { require(balances[msg.sender] >= _amount); balances[msg.sender] -= _amount; etherLeft -= _amount; msg.sender.send(_amount); }
上面給出的代碼中使用 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
7、弱隨機數問題
智能合約是藉助EVM運行,跑在區塊鏈上的合約代碼。其最大的特點就是公開和不可篡改性。而如何在合約上生成隨機數就成了一個大問題。
Fomo3D合約在空投獎勵的隨機數生成中就引入了block信息作為隨機數種子生成的參數,導致隨機數種子只受到合約地址影響,無法做到完全隨機。
function airdrop() private view returns(bool) { uint256 seed = uint256(keccak256(abi.encodePacked( (block.timestamp).add (block.difficulty).add ((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)).add (block.gaslimit).add ((uint256(keccak256(abi.encodePacked(msg.sender)))) / (now)).add (block.number) ))); if((seed - ((seed / 1000) * 1000)) < airDropTracker_) return(true); else return(false); }
上述這段代碼直接導致了Fomo3d薅羊毛事件的誕生。真實世界損失巨大,超過數千eth。
8萬筆交易「封死」以太坊網絡,只為搶奪Fomo3D大獎?
Last Winner
三、漏洞影響範圍
使用Haotian平台智能合約審計功能可以準確掃描到該類型問題。