@程式設計師,如何淋漓盡致地敲出Solidity安全程式碼?
安全是區塊鏈領域舉足輕重的話題。本期咱們聊聊合約規範問題引起的嚴重安全後果。
「區塊鏈大本營」攜手「成都鏈安科技」團隊重磅推出「合約安全漏洞解析連載」,以講故事的方式帶你回顧區塊鏈安全走過的歷程;分析漏洞背後的玄機。讓開發者在趣味中學習,寫出更加牢固的合約,防患於未然。
當然,這些文章並不是專為開發者而作的,即使你不是開發者,當你讀完本連載,相信再有安全問題爆出時,你會有全新的理解。
以戒為基,止觀相踵。大矩崇規,鏈金烹礦——《頭陀贊》 宋·黃庭堅
上回書說到
可見修飾字斟句酌
函式呼叫約法三章
區塊鏈技術的發展要與安全掛鉤,齊頭並進,讓迅速的發展約束在可靠的範圍之內,才能真正讓新科技穩步推廣,深入人心。
在合約開發過程中,正確精準的新增可見性說明符以及函式修飾符,讓相關函式在可控的範圍之內最大地發揮效用,是安全開發中控制理念的核心。
將細微之處,尤其是敏感和重要函式的呼叫控制做到最嚴謹,可以避免許多導致資金流失、許可權被盜的安全隱患。
本期話題
官方標準用心良苦,編寫合規不容小覷
秋去冬來令又更,蕭蕭風籟助清吟。
我們在以太坊智慧合約漏洞分析已經走過了14期,將以太坊自興起以來發生的大大小小的攻擊事件以及漏洞發現進行了歸類的分析和討論。
從資料型別到變數,從重要函式到程式碼邏輯,從代幣合約到區塊鏈遊戲,安全漏洞不僅僅產生於開發過程中的失誤,其源頭在一定程度上來源於開發者安全意識的淡薄。
同時,這也體現了區塊鏈行業目前的一個痛點,許多開發者在合約編寫的過程中並沒有注意相應的安全規範,或者並不明確相應的函式和功能實現中官方給出的標準。
這期我們針對官方給出的標準協議和編寫規範,補充相應的安全檢查,旨在將安全在具體的程式碼層面體現得淋漓盡致。
知識鋪墊
什麼是以太坊代幣介面標準?
ERC,是Ethereum Request for Comments的縮寫,字面意思是以太坊註釋請求,其本身是一個包含結構化資訊的網路文件。ERC標準由以太坊社群定義,目的是制定使用者和以太坊互動的規則。
這個標準不是一層不變的,社群開發者可以向官方提出自己定義的新標準,但是這個新標準需要被整個以太坊社群接受採納才能應用在以太坊網路上。從以太坊誕生以來,被採納並且要求代幣遵守的標準出現了幾種:ERC-20,ERC-223,ERC-621,ERC-721,ERC-827。
在這些標準的建議下,開發者需要對型別、可見性、返回值、事件、變數與函式命名等方面實現與介面的相容,也就是按照建議去書寫程式碼。從ERC-20啟動了區塊鏈代幣專案的繁榮開始,之後的標準都是在其基礎上進行進一步的補充和完善,便於開發者按照專案需求實現相關的功能。
為了實現安全功能的附加程式碼
自從臭名昭著的The DAO事件發生後,以太坊社群開始重視合約的安全問題,並且許多開發者和研究者想要合力解決和防範類似的攻擊事件,於是自發的開始研究程式碼編寫過程中可以調整和完善的地方。
於是接踵而來的一次次安全事件催生了各種附加安全檢查程式碼,比較著名的是針對BEC事件中整型溢位漏洞出現的SafeMath程式碼庫,開發者可以在編寫過程中使用現成的程式碼來實現安全防護,很好的幫助消除了惡性漏洞帶來的影響。
不怕知識不全,就怕視而不見
這些介面標準和安全程式碼庫都是為了幫助專案方保障專案的長久穩健執行而設計的。可以說,以太坊社群已經並仍在用民主和科學的方式建立一個行業規範。即便如此,仍有很多開發者在編寫合約,實現專案功能的過程中,選擇坐井觀天,對於安全規範的日新月異疏於關注和學習。
這樣一來,開發出來的合約就缺失相關的安全檢查,甚至出現完全與介面不相容的問題,直接導致的後果就是漏洞被攻擊者利用,專案上鍊就胎死腹中。下面我們就對目前出現的比較重要的編寫規範和建議進行分類討論。
合約編寫規範建議
1、ERC20介面標準檢查
最新的ERC20介面規範如下,建議代幣合約開發者按照以下介面規範進行實現,包括型別、可見性、返回值、事件、變數與函式命名等。
如果代幣合約的開發者未完全按照ERC20標準進行實現,那麼將會對去中心化交易所(DEX)和使用ERC20 Token開發的DAPP產生如下影響:
-
transfer、transferFrom、approve函式未宣告返回值:合約無法正常完成交易與轉賬、部分由合約管理的Token可能永遠被鎖定在合約中
-
transfer、transferFrom、approve操作未觸發Transfer或者Approval事件:目前區塊鏈瀏覽器(eg:etherscan)通過監控事件記錄交易,如果該交易未觸發事件,區塊鏈瀏覽器將無法準確記錄代幣交易
-
name/symbol/decimals使用其他寫法,例如全部大寫(NAME/SYMBOL/DECIMALS):使用ERC20 標準的DAPP(eg:METAMASK)將無法讀取這3個變數的對應值
2、transfer/transferFrom執行失敗未丟擲異常導致假充值
某些交易所確認機制不完善以及相關合約程式碼未能嚴格遵循標準而引發的問題,攻擊者可實現假充值,情況分析如下:
以ERC-20 Token為例,某些交易所可能僅依賴交易狀態(TxReceiptStatus)和鏈上確認次數(Block Confirmation Number)對交易結果作判斷,忽略了對Token balance的檢查。以下列程式碼為例:
如果發起者賬戶為0且_value為大於0的某個值,那麼呼叫後函式會返回false,但該筆交易的交易狀態是Success:
有問題的交易所在確認轉賬狀態時,如前所述只讀取交易狀態和鏈上確認次數,就會承認該筆轉賬,導致虛假轉賬問題的產生。
合約程式碼也存在一定問題,以上述程式碼為例,按照ERC-20的標準,transfer、transferFrom函式在Token賬戶轉賬額度不足的條件下應該丟擲異常:
正確的程式碼應該是:
這樣交易狀態自然會變為Fail。換言之,即便交易所依賴交易狀態做最終確認,也可確保自身安然無虞。
3、目標地址非零檢查
在 transfer、transferFrom、transferOwnership 等敏感函式中,使用者操作不可逆,所有建議開發者在這些函式實現中增加目標地址非零檢查:
4、Pausable模組繼承
建議主合約繼承 Pausable Ownable ERC20 標準模組,當出現重大異常時可以暫停所有交易:
5、以太坊最新安全規範
Solidity 0.4.22以及以上的編譯器版本,建構函式建議宣告方式:constructor() public {};
Solidity 0.4.21以及以上的編譯器版本,觸發事件建議採用:emit Transfer()。
6、對編譯器版本的說明
建議固定編譯器版本,即pragma solidity 0.4.8,然後使用對應編譯器版本編譯釋出合約。固定編譯器版本有助於確保合約不會被用於最新的可能還有bug未被發現的編譯器去部署。智慧合約也可能會由他人部署,而pragma標明瞭合約作者希望使用哪個版本的編譯器來部署合約。
7、棄用項
Solidity處於不斷的更新迭代中,在此過程存在部分表示式棄用,開發者不應在棄用之後的版本使用它們。
-
suicide 在0.4.3版本已棄用,使用selfdestruct 替代
-
callcode在0.4.12版本已棄用
-
throw在0.4.13版本已棄用,使用revert替代
-
sha3在0.4.17版本會彈出已棄用警告,使用keccak256 替代
-
var在0.4.20版本已棄用
-
msg.gas在0.4.22已棄用,使用gasleft()替代
-
constant 作為函式狀態修飾符在0.4.24已棄用,使用view代替years在0.4.24已 棄用
8、誤用assert、require、revert、throw
Solidity官方對assert、require、revert、throw的介紹如下:
Solidity 使用狀態恢復異常來處理錯誤。這種異常將撤消對當前呼叫(及其所有子呼叫)中的狀態所做的所有更改,並且還向呼叫者標記錯誤。
函式 assert 和 require 可用於檢查條件並在條件不滿足時丟擲異常。assert 函式只能用於測試內部錯誤,並檢查非變數。require 函式用於確認條件有效性,例如輸入變數,或合約狀態變數是否滿足條件,或驗證外部合約呼叫返回的值。如果使用得當,分析工具可以評估你的合約,並標示出那些會使 assert 失敗的條件和函式呼叫。
正常工作的程式碼不會導致一個 assert 語句的失敗;如果這發生了,那就說明出現了一個需要你修復的 bug。
還有另外兩種觸發異常的方法:revert 函式可以用來標記錯誤並恢復當前的呼叫。revert 呼叫中包含有關錯誤的詳細資訊是可能的,訊息會被返回給呼叫者。已不推薦的關鍵字 throw 也可以用來替代 revert() (但無法返回錯誤訊息)。
在內部, Solidity 對一個 require 式的異常執行回退操作(指令 0xfd )並執行一個無效操作(指令 0xfe )來引發 assert 式異常。 在這兩種情況下,都會導致 EVM 回退對狀態所做的所有更改。回退的原因是不能繼續安全地執行,因為沒有實現預期的效果。
因為我們想保留交易的原子性,所以最安全的做法是回退所有更改並使整個交易(或至少是呼叫)不產生效果。 請注意, assert 式異常消耗了所有可用的呼叫 gas ,而從Metropolis 版本起 require 式的異常不會消耗任何 gas。
適合使用Require的場景:
-
驗證使用者輸入:require(_to != address(0));
-
驗證外部合約返回值:require(external.send(amount)) ;
-
驗證執行程式碼的前提條件:require(allowed[_from][msg.sender] >= _value);
-
require應該經常使用;
-
require一般位於函式的開頭處。
適合用assert的場景:
-
溢位檢查:c=a+b;assert(c>=a);
-
檢查常數:assert(this.balance >= totalBalance);
-
執行操作後驗證狀態;
-
避免絕對不應該出現的狀況;
-
assert不應經常使用(觸發異常會消耗所有gas);
-
assert一般位於函式結尾處;
revert和require類似,可以用於複雜邏輯的場景,throw已棄用。
9、SafeMath使用建議
為了避免開發人員忽略對溢位的檢查,建議使用SafeMath。
下面是OpenZeppelin編寫的SafeMath庫:
在合約中使用SafeMath示例:
安全開發 穩步發展
至此,我們已經完成了對以太坊智慧合約重點漏洞分析和修復的整合,總的來說,以太坊作為區塊鏈專案火遍全球的起飛平臺,為智慧合約概念的實現和區塊鏈技術的普及做出了巨大的貢獻。
但是安全問題在發展迅速、市場不夠冷靜的前提下層出不窮,與財產緊密掛鉤的以太坊加密貨幣因為這些問題白白流失,令整個行業人心惶惶。普及安全知識,倡導安全開發與審計,是我們展開漏洞分析連載的初衷。
這一期的合約編寫規範為以太坊漏洞分析做了一個小結,希望能幫助更多的開發者重視開發的過程中的問題,關注以太坊社群動向,做好專案部署上鍊前的安全審計,讓整個區塊鏈生態進入發展與安全交融的良性迴圈。
參考文章
-
https://www.jianshu.com/p/6ed6aab514d6
-
https://mp.weixin.qq.com/s/3cMbE6p_4qCdVLa4FNA5-A
-
https://mp.weixin.qq.com/s/1MB-t_yZYsJDTPRazD1zAA
-
https://github.com/sec-bit/badERC20Fix/blob/master/README_CN.md
-
https://solidity.readthedocs.io/en/v0.4.24/control-structures.html#error-handling-assert-require-revert-and-exceptions
-
https://medium.com/blockchannel/the-use-of-revert-assert-and-require-in-solidity-and-the-new-revert-opcode-in-the-evm-1a3a7990e06e
-
https://github.com/ConsenSys/smart-contract-best-practices/blob/master/README-zh.md
-
https://github.com/ethereum/wiki/wiki/Safety
推薦閱讀