以太坊智能合約安全


以太坊智能合約安全


引言

智能合約就是自主執行的合約,其條款是用代碼規定的。

雖然這個概念已經存在一段時間了,但至少從1996年Nick Szabo提出了這一概念以來,直到圖靈完備的以太坊區塊鏈來臨,智能合約的使用才變得普遍。

以太坊的智能合約存在於合約地址裡,能以交易命令來調用。用代碼編寫並存放在不可更改的公鏈上的合約執行起來會產生一定的風險與安全問題。我們將會在本文中討論這些問題和可能的緩解措施。

代碼即法律?

對智能合約理念的字面解釋造成了“代碼即法律” ( “Code is Law” ) 的範式理解,意思是那些智能合約具有約束力,並被詮釋為合法文件。所有意識到無法創建完全無錯代碼的軟件工程師在想到計算機程序具有合法約束力的時候,手心都會捏一把汗。這裡存在很多明顯的問題:

  1. 代碼有漏洞。寫無bug代碼是一件非常困難的事情,即使做好了所有可能的預防措施,在相當複雜的軟件中也總會存在意想不到的執行路徑或者可能的漏洞。
  2. 法律合約也需要解釋與仲裁。創建毫無漏洞法律合約是非常困難的。在任何大型合約中,拼寫錯誤可能出現,一些條款需要解釋和仲裁。這就是為什麼在出現爭議時我們需要法庭。如果在某個法律合約中,40頁中有39頁標定的銷售價格為100美金,而在某一頁上的價格中多了個“0”,法院將以“合約精神”為準進行裁定。而計算機只會執行寫好的條款,區塊鏈的不變性增加了這個問題,因為合約無法輕易修改。
  3. 軟件工程師不是法律專家,反之亦然。起草一份好的合約需要與編程不同的技能,一名能夠編寫非常完善計算機程序的人員不一定會寫法律合約。

兩起著名的利用智能合約漏洞的事件

The DAO 攻擊

這件事很多人都早已談了許多,我們就不在這重複了。您可以在這裡找到關於攻擊和善後的完整概述。

簡單來說,在 2016 年 6 月,一名黑客企圖轉移一大筆眾籌資金 (350 萬個 ETH, 佔當時ETH總數的15%)至他自己的子合約,這筆資金被鎖定在該子合約中 28 天,因而大家要與時間競賽尋找解決方案。

這事件要注意的重點在於,攻擊者是通過使合約以意料之外的方式運行而發起的攻擊。這個事件中,攻擊者利用了 可重入漏洞Reentrancy Vulnerabilities )。我們將會在這篇文章中對可重入進行深入討論。

黑客攻擊Parity事件

事實上,這是第二次 Parity 所提供的的多重簽名錢包合約受到黑客入侵了。眾多創業公司使用的多重簽名錢包的邏輯大多通過庫合約實現。每個錢包都包含一個輕量級的客戶端合約,連接到這個單點故障(譯註:即上述庫合約)。


以太坊智能合約安全


-Parity 多重簽名錢包結構-

這個庫合約中存在一個重大的漏洞,問題在於其中一個初始化函數只能被調用一次。

2017年11月,一名男子通過實施合約初始化,將自己變成了合約所有者。這允許他調用所有者才能調用的 函數,他利用這一特權調用了以下的函數:

// kills the contract sending everything to `_to`.
function kill(address _to) onlymanyowners(sha3(msg.data)) external {
suicide(_to);
}

這相當於一個能使合約無效的自毀按鈕。調用這個函數會導致客戶合約的所有資金有可能被永久凍結。


以太坊智能合約安全


直到撰寫本文之時,該事件到底是黑客造成的故意攻擊還是意外仍然是個謎,儘管肇事者聲稱這是意外行為。

兩次攻擊都說明了即使是由以太坊生態系統的大佬來寫相對簡單的合約,也容易出現基本的錯誤,帶來嚴重的後果。

已知的漏洞與陷阱

私鑰外洩

使用不安全的私鑰純粹是用戶的失誤,而不是一個漏洞。然而,儘管如此,我們還是要指出來,因為私鑰外洩總是意外地發生,有些玩家專門從不安全的地址中竊取資金。

把開發地址(如 Ganache/TestPRC 使用的地址)用於生產的事情經常發生。這些地址是由公開的私鑰生成。一些用戶甚至無意識地把這些私鑰導入到錢包軟件,因為他們使用 Ganache 的種子詞(seed words)生成了相同的私鑰。攻擊者則監視著 TestPRC 地址,不管多少數量的以太幣只要轉移到以太坊主鏈上的 TestPRC 地址都會立刻消失(在2個區塊內)。一項有趣的研究對這一十分有利可圖的“清掃(sweeping)”活動進行了調查,並發現每個清掃者(sweeper)的賬戶都設法累積了高達2300萬美元的資金。

可重入與競態條件(Race Conditions)

如果一個函數在執行完成前被調用了數次,發生意料不到的行為時,可重入漏洞就可能出現。

讓我們看看下面這個函數,它可以用於從合約中提取調用者的總餘額:

mapping (address => uint) private balances;
function payOut () {
require(msg.sender.call.value(balances[msg.sender])());
balances[msg.sender] = 0;
}

調用 call.value() 會導致合約外部代碼的執行。若調用者是另一份合約,這就意味著合約回退措施的執行。這可能會在餘額調整為 0 之前再次調用 payOut(), 從而獲得比可用資金更多的資金。

這種情況下的解決方法就是使用替代函數 send()transfer() 後兩者函數能為基礎運作提供足夠的 Gas,而想要再次調用 p*ayOut()* 時 Gas 就不足了。

若合約含有兩個共享狀態的函數,那麼不需要重複調用函數也可能會發生相似的競態條件(Race Conditions)。因此,最好的做法是在轉賬前更改狀態,即轉移資金前,在上述代碼中把餘額設為 0。

The DAO 攻擊利用了該漏洞的一種變體。

下/上溢

餘額一般用無符號的整數表示,在 Solidity 語言中通常為 256 位數字。當無符號整數上溢(overflow)或下溢(underflow)時,其數值會發生明顯變化。讓我們看看下面一個比較常見的下溢例子 (為了清晰一點我把數字縮短了):

 0x0003
- 0x0004
--------
0xFFFF

這裡很容易看出問題,減去一個比可用餘額大的值便導致下溢。得到的餘額是一個很大的數字。

還要注意的是由於舍入誤差(Rounding Errors),在整數中算術分割(Arithmetics Division)是很麻煩的。

解決方法是時刻對代碼進行下溢、上溢檢查。使用安全數字庫能協助檢查,比如 OpenZeppelin 的 SafeMath

交易順序假設

交易進入未確認的交易池,並可能被礦工無序地包含在區塊中,這取決於礦工的交易選擇標準,有可能是一些旨在從交易費中獲取最大收益的算法,但也可以是其它任何標準。因此,打包在區塊中的交易順序與交易生成的順序完全不同。因此,合約代碼無法對交易順序作出任何假設。

因為交易在記憶池(Mempool)是可見的,其執行是可預測的,所以除了合約執行出現意外結果的情況,還有一個可能的攻擊面。交易打包中可能出現的一個問題就是,延遲某個交易可能被流氓礦工用作個人利益。事實上,能夠在交易執行前意識到某些交易(的存在)對任何人來說都是有利的,而不僅僅是礦工。

對時間戳的依賴

時間戳(Timestamps)是由礦工生成。因此,合約不應該讓關鍵操作依賴於區塊時間戳,例如把時間戳用作一個生成隨機數的種子。Consensys 在他們的指導手冊中給出了“12分鐘規定”,表明如果你依賴時間戳的代碼能夠處理 12 分鐘的誤差,那麼使用block.timestamp 是安全的。

短地址攻擊

Golem team 揭露了一個有趣的攻擊,詳情請看這裡。該漏洞影響了 ERC20 代幣傳輸和一些類似的合約,該漏洞的問題在於交易字節代碼可以是任意大小,而以太坊虛擬機(Ethereum virtual machine,簡稱EVM)會在其尾部缺失的字節填充0。

實施該攻擊需要找到一個以十六進制(hex)形式表示且結尾為若干個 0 的地址,並在提幣請求中省略這些結尾的 0。當該合約發起一個轉賬請求時,短地址被插入,其餘的交易字節代碼被移位。

舉個例子,省略結尾的兩個 0 會導致交易數據中地址之後的字節發生 1 個字節的移位。地址後面是交易數據中的參數,通常是無符號的前置 0 的 256 位整數。這些前置的 0 會移入地址字段,使地址有效並確保交易目的地是正確的。

參數字段中一個字節的移位也很容易導致提幣量變為原來的256倍。在EVM用 0 填充缺失的結尾字節後,交易成功,然後轉走 256 倍的金額。

因此,利用省略兩個十六進制0的地址的漏洞使攻擊者可以從一個餘額為 1000 個代幣的賬戶中提取 256000 個代幣,以此類推。省略 4 個結尾的 0 則是 2^16 倍。

為了避免這種攻擊,你的合約應該驗證地址。

拒絕服務攻擊(DoS Attacks)

有時通過使合約交易超過能夠包含在一個區塊中的最大 Gas 量,來迫使合約交易失敗。在這篇拍賣合約的解讀中解釋了這個經典例子。迫使合約退還大量沒有接受的小投標會增加 Gas 消耗量,如果能耗超過了區塊 Gas 上限,那麼整個交易失敗。

這個問題的解決方法是避免許多交易調用可能由相同的函數調用引起的情況,尤其是如果調用次數會受到外部影響。

推薦的付款模式是讓客戶請求轉賬,而不是一次性轉賬出去,如 Solidity 官方文件所述。

緩解措施與結論

為了強調“代碼即法律”這一範式理解的危害,本文我們闡述了可能發生的漏洞以及過去攻擊者是如何利用這些漏洞的例子。

最近的歷史事件表明在公鏈上執行圖靈完備的智能合約是危險的,其安全性遠不足以取代傳統法律系統的語言準確度與解釋和仲裁空間。

但這並不意味著我們應該拋棄智能合約。智能合約是非常有用的工具,能開發出有趣的應用程序。然而,我們不能認為智能合約能取代具有法律約束力的合約,它只是用於自動化的補充工具。

另外,我們應該做好預防措施去避免漏洞:

  • 使用開放的資源與社區接受的庫合約的實質標準 (de facto standards),例如 Open Zeppelin’s contracts。
  • 使用推薦的模式與最優操作指導手冊,例如 Consensys 提供的。
  • 考慮由信譽好的供應商審核您的智能合約。

原文鏈接: https://medium.com/cryptronics/ethereum-smart-contract-security-73b0ede73fa8

翻譯&校對: 楊哲 & Elisa

稿源:以太坊愛好者(https://ethfans.org/posts/ethereum-smart-contract-security)


分享到:


相關文章: