撰文:Cobo 安全團隊

原文:《 Cobo安全團隊:一文講透Solidity 編譯器漏洞

編譯器漏洞

編譯器[1]是現代計算機系統的基本組件之一。編譯器本身也是一種計算機程序,他的功能是將人類易於理解和編寫的高級程序語言源代碼轉化成計算機底層CPU 或字節碼虛擬機可以執行的指令代碼。

大多數開發者和安全人員通常會比較關注程序應用代碼的安全,但可能會忽略編譯器自身的安全。實際上編譯器也是計算機程序,因此也會存在安全漏洞,而編譯器產生的安全漏洞,在特定場景下也可以帶來嚴重的安全風險。比如瀏覽器在編譯並解析執行Javascript 前端代碼的過程中,就可能由於Javascript 解析引擎的漏洞[2],導致用戶在訪問惡意頁面時被攻擊者利用漏洞實現遠程代碼執行,最終完成對受害者瀏覽器甚至操作系統的控制。筆者在從事區塊鏈安全研究之前,在傳統安全研究工作中就曾發現多個Google Chrome、Microsoft Edge 瀏覽器的Javascript 腳本引擎的高危漏洞。而筆者曾經參與的另一項研究[3]也表明,Clang C++ 編譯器的bug 也可能導致遠程代碼執行這類嚴重後果。

Solidity 編譯器也不例外,根據Solidity 開發團隊的安全預警[4],在多個不同版本的Solidity 編譯器中都存在安全漏洞。

Solidity 編譯器漏洞

Solidity 編譯器的作用是將開發人員編寫的智能合約代碼轉化成以太坊虛擬機(EVM)指令代碼,這些EVM 指令代碼通過交易打包被上傳到以太坊上,最終通過EVM 進行解析執行。

這裡需要將Solidity 編譯器漏洞與EVM 自身的漏洞進行區分。 EVM 的漏洞是指虛擬機在執行指令時產生的安全漏洞。由於攻擊者可以上傳任意代碼到以太坊上,這些代碼最終將運行在每個以太坊P2P 客戶端程序中,如果EVM 存在安全漏洞,那麼將影響整個以太坊網絡,造成整個網絡的拒絕服務(DoS )甚至導致整個鏈完全被攻擊者接管。不過由於EVM 本身設計比較簡單,且核心代碼不會頻繁更新,因此產生上述問題的概率相對較低。

Solidity 編譯器漏洞是指編譯器將Solidity 轉化成EVM 代碼時存在漏洞。與瀏覽器這種會運行在用戶客戶端計算機編譯運行Javascript 的場景不同,Solidity 編譯過程只運行在智能合約開發者的計算機上,並不運行在以太坊上。因此Solidity 編譯器漏洞不會影響以太坊網絡本身。

一種攻擊場景是,攻擊者通過社會工程學等手段誘導Solidity 開發者下載編譯攻擊者惡意構造的Solidity 代碼,然後利用Solidity 編譯器漏洞實現代碼執行完成對受害者計算機的控制。這種攻擊的目標只針對智能合約開發者,不會影響普通以太坊用戶,因此本文將不會深入討論。

Solidity 編譯器漏洞的另一種危害在於,可以導致其Solidity 源碼生成的EVM 代碼與智能合約開發者的預期存在不一致的情況。由於以太坊上的智能合約通常與用戶的加密貨幣資產有關,因此編譯器導致智能合約產生的任何bug 都可能導致用戶資產受損,從而產生嚴重後果。

開發者和合約審計人員可能會重點關注合約代碼邏輯實現問題,以及重入、整數溢出等Solidity 層面的安全問題。而對於Solidity 編譯器的漏洞,僅通過對合約源碼邏輯的審計,是很難發現的。需要結合特定編譯器版本與特定的代碼模式共同分析,才能確定智能合約是否受編譯器漏洞的影響。

Solidity 編譯器漏洞示例

下面將以幾個真實的Solidity 編譯器漏洞為例,展示Solidity 編譯器漏洞的具體形式、成因及危害。

SOL-2016-9 HighOrderByteCleanStorage[5]

該漏洞存在於上古時期的Solidity 編譯器版本中(>=0.1.6 <0.4.4)。

考慮如下代碼

Solidity編譯器漏洞解析及應對措施

其storage 變量b 沒有經過任何修改,因此run()函數應該返回默認值0。但實際在漏洞版本編譯器生成的代碼run()中將返回1。

在不了解該編譯器漏洞的情況,普通開發者很難通過簡單的code review 發現上述代碼中存在的bug。上述代碼只是一個簡單示例,因此不會造成特別嚴重的危害。但如果上述b 變量被用於一些如權限驗證、資產記賬等用途時,這種與預期的不一致將可能導致十分嚴重的後果。

那麼為什麼會產生上述奇怪的現象呢?原因在於EVM 使用棧式虛擬機,棧中每個元素均為32 字節大小(即uint256 變量大小)。另一方面底層存儲storage 的每個slot 也為32 字節大小。而Solidity 語言層面支持uint32 等各類低於32 字節的數據類型,編譯器在處理這種類型的變量時,需要對其高位進行適當的清除操作(clean up)以保證數據的正確性。上述情況中,在加法產生整數溢出時,編譯器沒有正確地對結果高位進行clean up,導致溢出後高位的1 bit 被寫入storage 中,最終覆蓋a 變量後面的b 變量,使b 變量的值被修改成了1。

SOL-2022-4 InlineAssemblyMemorySideEffects[6]

考慮如下代碼:

Solidity編譯器漏洞解析及應對措施

該漏洞存在>=0.8.13 <0.8.15版本的編譯器中。 Solidity 編譯器在將Solidity 語言轉化成EVM 代碼的過程中,並不只是簡單的進行翻譯。還會進行深入的控制流與數據分析,實現各種編譯優化流程,以縮減生成代碼的體積,優化執行過程中的gas 消耗。這類優化操作在各種高級語言的編譯器中都十分常見,但由於這類優化要考慮的情況十分複雜,也十分容易出現bug 或安全漏洞。

上述代碼的漏洞就源於這類優化操作。考慮這樣一種情況,如果某個函數中存在修改內存0 偏移處數據的代碼,但後續沒有任何地方使用到該數據,那麼實際可以將修改內存0 的代碼直接移除掉,從而節約gas,並且不影響後續的程序邏輯。

這種優化策略本身並沒有任何問題,但在具體的Solidity 編譯器代碼實現中,此類優化只應用於單一的assembly block中。對上述PoC 代碼中的情況,對內存0 的寫入和訪問存在於兩個不同的assembly block中,而編譯器卻只對單獨的assembly block 進行了分析優化,由於第一個assembly block中在寫入內存0 後沒有任何讀取操作,因此判定該寫入指令是冗餘的,會將該指令進行移除,從而產生bug。在漏洞版本中f()函數將返回值0,而實際上上述代碼應該返回正確的值是0x42。

SOL-2022-6 AbiReencodingHeadOverflowWithStaticArrayCleanup[7]

考慮如下代碼:

Solidity編譯器漏洞解析及應對措施

該漏洞影響>= 0.5.8 < 0.8.16版本的編譯器。正常情況下,上述代碼返回a 變量應為"aaaa"。但在漏洞版本中會返回空字符串""。

該漏洞的成因是Solidity 對calldata 類型的數組進行abi.encode操作時,錯誤的對某些數據進行了clean up,導致修改了相鄰的其他數據,造成了編碼解碼後的數據存在不一致。

值得注意的是,Solidity 在進行external call和emit event時,會隱式地對參數進行abi.encode,因此上述漏洞代碼出現的概率會比直觀感覺上更大。

值得一提的是,本漏洞被改編成了在國內知名安全競賽比賽0ctf 2022 中一道區塊鏈題目,題目中展示了真實開發場景下編譯器漏洞對智能合約的影響。前Cobo 實習生同學s3cunda 編寫了相應的題目解析文章[8],對題目感興趣的讀者可以參考。

安全建議

Cobo 區塊鏈安全團隊經過對Solidity 編譯器漏洞威脅模型的分析以及歷史漏洞的梳理,對開發者和安全人員提出以下建議。

對開發者:

  • 使用較新版本的Solidity 編譯器。儘管新版本也可能引入新的安全問題,但已知的安全問題通常較舊版本要少。
  • 完善單元測試用例。大部分編譯器層面的bug 會導致代碼執行結果與預期不一致。這類問題很難通過code review 發現,但這類問題很容易在測試階段暴露出來。因此通過提高代碼覆蓋率,可以最大程度地避免此類問題。
  • 盡量避免使用內聯彙編、針對多維數組和復雜結構體的abi 編解碼等複雜操作,沒有明確需求時避免追求炫技而盲目使用語言新特性和實驗性功能。根據Cobo 安全團隊對Solidity 歷史漏洞的梳理,大部分漏洞與內聯彙編、abi 編碼器等操作有關。編譯器在處理複雜的語言特性時確實更容易出現bug。另一方面開發者在使用新特性時也容易出現使用上的誤區,導致安全問題。

對安全人員:

  • 在對Solidity 代碼進行安全審計時,不要忽略Solidity 編譯器可能引入的安全風險。在Smart Contract Weakness Classification(SWC) 中對應的檢查項為SWC-102: Outdated Compiler Version[9]
  • 在內部SDL 開發流程中,敦促開發團隊升級Solidity 編譯器版本,並可以考慮CI/CD 流程中引入針對編譯器版本的自動檢查。
  • 但對編譯器漏洞無需過度恐慌,大部分編譯器漏洞只在特定的代碼模式下觸發,並非使用有漏洞版本的編譯器編譯的合約就一定存在安全風險,實際的安全影響需要根據項目情況具體評估。

一些實用資源:

  • Solidity Team 定期發布的Security Alerts posts https://blog.soliditylang.org/category/security-alerts/
  • Solidity 官方repo 定期更新的bug list https://github.com/ethereum/solidity/blob/develop/docs/bugs.json
  • 各版本編譯器bug 列表https://github.com/ethereum/solidity/blob/develop/docs/bugs_by_version.json 。據此可在CI/CD 過程中引入自動進行編譯器版本的檢查,提示當前版本中存在的安全漏洞。
  • Etherscan 上Contract -> Code頁面右上角的三角形感嘆號標誌可提示當前版本編譯器所存在的安全漏洞。

小結

本文從編譯器的基本概念講起,介紹了Solidity 編譯器漏洞,並分析了其在實際以太坊開發環境中可能導致的安全風險,最終對開發者和安全人員提供了若干實際的安全建議。

參考資料

[1]編譯器: https://en.wikipedia.org/wiki/Compiler

[2]Javascript 解析引擎的漏洞: https://bugs.chromium.org/p/v8/issues/list

[3]Clang 編譯器漏洞研究: https://i.blackhat.com/eu-20/Wednesday/eu-20-Wu-Finding-Bugs-Compiler-Knows-But-Does-Not-Tell-You-Dissecting -Undefined-Behavior-Optimizations-In-LLVM.pdf

[4]Solidity 開發團隊的安全預警: https://blog.soliditylang.org/category/security-alerts/

[5]SOL-2016-9 HighOrderByteCleanStorage: https://blog.soliditylang.org/2016/11/01/security-alert-solidity-variables-can-overwritten-storage/

[6]SOL-2022-4 InlineAssemblyMemorySideEffects: https://blog.soliditylang.org/2022/06/15/inline-assembly-memory-side-effects-bug/

[7]SOL-2022-6 AbiReencodingHeadOverflowWithStaticArrayCleanup: https://blog.soliditylang.org/2022/08/08/calldata-tuple-reencoding-head-overflow-bug/

[8]題目解析文章: https://s3cunda.github.io/2022/09/19/0ctf-2022-NFT-Market.html

[9]SWC-102: Outdated Compiler Version: https://swcregistry.io/docs/SWC-102