此篇文章由 Cobo 區塊鏈安全團隊供稿,團隊成員來自知名區塊鏈安全廠商,具備豐富的智能合約審計經驗,曾在多個 DeFi 項目中發現高危漏洞。團隊目前重點關注智能合約安全、DeFi 安全等方向,研究並分享前沿區塊鏈安全技術。

我們也希望對加密數字貨幣領域有研究精神和科學方法論的終身迭代學習者可以加入我們的行列,向行業輸出思考洞察與研究觀點!

作者: Cobo 安全團隊,Cobo Global

封面: Photo by Leopold Stenger on Unsplash

編譯器漏洞

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

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

Solidity 編譯器也不例外,根據 Solidity 開發團隊的安全預警,在多個不同版本的 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)。

考慮如下代碼

contract Test {
uint32 a = 0xFFFFFFFF;
uint32 b;

function run() returns (uint32) {
var x = 1;
a = uint32(a + x);
return b;
}
}

其 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]

考慮如下代碼:

pragma solidity 0.8.13;

contract Bug {
function f() external pure returns (uint256 x) {
assembly {
mstore(0, 0x42) // will be removed incorrectly
}
assembly {
x := mload(0)
}
}
}

該漏洞存在 >=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]

考慮如下代碼:

pragma solidity 0.8.13;

contract Bug {
function f(uint[2] calldata b)
public
returns (bytes memory)
{

return abi.encode("aaaa", b);
}
}

contract Executor {
function run() external returns(bytes memory, string memory, uint[2] memory){
Bug bug = new Bug();
bytes memory r = bug.f([uint(1), 2]);
(string memory a, uint[2] memory b) = abi.decode(r, (string, uint[2]));
return (r, a, b);
}
}

該漏洞影響 >= 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 編譯器漏洞,並分析了其在實際以太坊開發環境中可能導致的安全風險,最終對開發者和安全人員提供了若干實際的安全建議。

一直以來 Cobo 都十分重視安全,Cobo 的智能合約相關產品及服務均經過內部完整的 SDL 流程,盡最大努力避免存在包括編譯器漏洞在內的各類安全風險。Cobo 安全團隊將持續關注各個層面的前沿區塊鏈安全技術,保障 Cobo 產品及客戶的資產安全。

參考資料

[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:// b log.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

免責聲明:作為區塊鏈信息平台,本站所發布文章僅代表作者及嘉賓個人觀點,與 Web3Caff 立場無關。文章內的信息僅供參考,均不構成任何投資建議及要約,並請您遵守所在國家或地區的相關法律法規。