所有語言
分享
8月9日,Solana驗證者和客戶端團隊齊心協力解決了一個嚴重的安全漏洞。Solana驗證者Laine表示,這一過程始於8月7日,當時Solana基金會通過私人渠道聯繫了知名網絡運營商。此次聯繫是秘密修補漏洞策略的一部分,旨在防止漏洞被以任何方式利用。補丁通過Anza工程師的GitHub存儲庫提供,使運營商能夠獨立驗證和應用更改。
這次秘密修復的詳情可以在GitHub存儲庫最近一次發布的Mainnet-beta(https://github.com/anza-xyz/agave/compare/v1.18.21...v1.18.22)中找到,唯一的改變是rbpf SVM虛擬機,從8月9日的rbpf SVM虛擬機唯一pull(https://github.com/solana-labs/rbpf/pull/583)可以定位到漏洞所在,雖然這一過程是秘密進行的,但是依然是通過開源存儲庫,Solana順利地過渡了這次安全性危機。這個漏洞究竟有多大危害,以至於讓Solana團隊如此重視?
CertiK團隊對這一漏洞進行了深入分析。漏洞存在於rbpf SVM虛擬機中,SVM(Solana Virtual Machine)是Solana區塊鏈生態系統的核心組件之一,負責執行智能合約和去中心化應用程序。其核心原理是利用即時編譯技術實現高性能的智能合約執行。由於Solana的高吞吐量和低延遲特性,SVM在Solana中扮演着至關重要的角色,為開發者提供了一個高效的去中心化應用開發環境,並且對Solana的安全性起着重要作用。
本文將會詳細分析漏洞的核心原理與影響。
SVM是Solana區塊鏈平台的關鍵組成部分,用於提供高效、安全的執行環境,用於運行智能合約和分佈式應用程序。SVM的設計採用了rbpf字節碼解釋器(interpreter)和即時編譯器(JIT),通過全局狀態和智能合約接口實現與區塊鏈網絡的交互。
關於SVM虛擬機如何加載運行elf智能合約可以參考上一次CertiK對rbpf的漏洞分析章節中關於SVM運行模式介紹。
這次漏洞的核心補丁是commit(https://github.com/solana-labs/rbpf/pull/583)對rbpf SVM虛擬機的修復。漏洞的根源在於精心構造的`callx regs`指令會導致rbpf SVM虛擬機崩潰。接下來,我們將分析`callx regs`指令如何引發如此嚴重的影響。
首先,我們需要了解SVM虛擬機中`SBF`指令`callx regs`的運行模式和基礎信息:
`SBF`指令的基本結構如下圖所示,其中`program_vm_addr`是SVM虛擬機中指令的起始地址。對於SBFV1版本的智能合約,`program_vm_addr`計算公式為`text_section.sh_addr.saturating_add(ebpf::MM_PROGRAM_START)`。`text_section.sh_addr`是ELF頭部的`text address`。在SVM虛擬機中,每條`SBF`指令的大小為`ebpf::INSN_SIZE`,即8字節。下圖中的`program.len`表示n+1條`SBF`指令的總大小。
在SVM虛擬機中,`callx regs`指令的運行模式如下:`target_pc`是傳入`callx`指令的寄存器值,並作為SVM虛擬機中的程序計數器(PC)偏移量。在執行`callx regs`時,兩個關鍵檢查用於確保寄存器值不越界。
檢查程序起始地址:確保`target_pc`不小於程序的起始地址。`program_vm_addr`代表`SBF`程序的起始地址。檢查條件是`program_vm_addr <= target_pc`,確保`target_pc`不低於程序的起始地址,從而避免程序跳轉到非法地址。
檢查程序結束地址:確保`target_pc`不超過程序的最大地址。`program.MaxAddr`代表`SBF`指令在程序中的起始地址加上整個程序的指令大小。檢查條件是`target_pc < program.MaxAddr`,確保`target_pc`在程序的有效範圍內,避免越界訪問。
如果這兩個條件都滿足,則程序會安全地跳轉到指定的PC地址。
通過前文對`SBF`指令基本尋址和`callx regs`運行模式的了解,我們可以分析 JIT模式下`callx regs`存在漏洞的關鍵原因。
首先先分析下在JIT模式中`SBF`指令尋址映射到x86機器碼的過程,`JitProgram`結構體包含了兩個重要成員:
即使在執行x86機器碼時,也需要根據`SBF`指令尋址到相應的機器碼。例如,`callx target_pc`指令中,`target_pc`可以通過索引`pc_section`數組尋址到相應的x86機器碼偏移。如果`target_pc`偏移的換算過程出現問題,導致從`pc_section`取得的偏移不正確,可能會導致獲取的執行的x86機器碼不一致。
在`JitProgram`中初始化`pc_section`和`text_section`的流程如下:
確定頁面大小:通過`get_system_page_size()`獲取系統的頁面大小,這通常是內存管理的基本單位。
`JitProgram`的每一次`compile``SBF`指令時候都會將偏移的`text_section`地址存儲到`pc_section`中,而`text_section`保存了x86機器碼的偏移地址:
在`callx regs`指令中,通過傳入的`target_pc`計算出相對地址后跳轉到存儲在`pc_section`中的x86機器碼地址。在JIT模式中,通過計算`target_pc - program_vm_addr`獲取相對地址。JIT模式下通過獲取的相對地址和`self.result.pc_section.as_ptr() as i64`數組指針地址相加可以獲取`pc_section`數組中存儲的`text_section`地址。其中`self.result.pc_section.as_ptr() as i64`獲取的是`pc_section`裸指針的數組基地址,`pc_section`是一個`&[usize]`類型的切片,想要正確索引`pc_section`數組的值,獲取的裸指針地址索引偏移必須是8字節的整數倍。
在了解完callx regs的尋址方式,接着分析造成漏洞root cause的地方。
漏洞的根本原因在於獲取相對地址的過程。`callx regs`指令的處理流程如下:
1. 獲取`target_pc`的值作為絕對地址。
2. 絕對地址按照8字節對齊。
3. 判斷絕對地址是否越界。
4. 獲取相對地址。
5. 通過相對地址和`pc_section`數組指針地址計算最終跳轉的x86機器碼地址。
漏洞的關鍵點在於第4步,合約中`program_vm_addr`和`target_pc`的值傳入可控,`target_pc`的值為`callx regs`的值,而`program_vm_addr`的值需要根據ELF格式經過精心構造並且繞過SVM虛擬機對ELF格式的安全檢查,就可以控制`program_vm_addr`的值。
這裏起始地址`program_vm_addr`值的構造需要注意SVM虛擬機中的主要幾個檢查:
1. 這個檢查代碼的目的是計算ELF文件中入口點(`Entrypoint`)相對於文本段(`text section`)的偏移量,並檢查這個偏移量是否是指令大小`ebpf::INSN_SIZE`的整數倍,目的是確保入口點(`Entrypoint`)在ELF文件的文本段(`text section`)中對齊到正確的指令邊界,由於`text_section.sh_addr`用作`program_vm_addr`的偏移,所以這裏得和入口點(`Entrypoint`)的偏移對齊:
2. 檢查入口點`header.e_entry`是否在`.text`節的虛擬地址範圍內。如果入口點不在該範圍內,返回`ElfError::EntrypointOutOfBounds`錯誤。
`target_pc`作為絕對地址在第二步中按照8字節對齊,是8的整數倍,`target_pc`個位數只要小於8,執行對齊操作后將為0,大於等於8將為8,傳入正常的`program_vm_addr`與8字節對齊的值將不會造成越界,只要獲取到的`program_vm_addr`為並不與8字節對齊且小於8,`target_pc`減去`program_vm_addr`,可以獲取到不與8字節對齊的相對地址,這裏獲取到的可控的相對地址範圍為(`relative address < number_of_instructions * INSN_SIZE`),相對地址將會用作索引`pc_section`數組,這裏計算方式是直接獲取`self.result.pc_section.as_ptr() as i64`裸指針進行切片地址索引,未與8字節對齊的相對地址將會導致`pc_section`數組基指針引用錯誤,將會獲取到一個越界地址,而越界的範圍需要小於`number_of_instructions * INSN_SIZE`,這個非法地址將會導致後續call跳轉到一個不一致的地址,假如訪問到非法地址程序系統將會拋出段錯誤`Segmentation fault`,這將導致SVM虛擬機直接崩潰,如果通過精心構造的內存數據,可能會獲取到一個能控制的任意跳轉地址,後續甚至執行任意命令!
漏洞修復后的補丁對比如下:
1.絕對地址:獲取`target_pc`的值作為絕對地址。
2.計算相對地址:首先通過減去`program_vm_addr`來獲取相對地址。這一步確保了後續操作能夠正確處理內存對齊問題。
3.內存對齊:將相對地址按照8字節進行內存對齊。
4.越界檢查:判斷對齊后的相對地址是否越界。
5.獲取跳轉地址:最終計算出`PC`跳轉的地址。
修復漏洞的關鍵在於第一步,通過首先獲取相對地址並確保其正確對齊,從而避免了之前未對齊帶來的問題。
在這一章節,我們將通過分析代碼和漏洞調試來複現問題。存在漏洞的合約POC構造如下:
假設`rax = target_pc`且`target_pc = 0x100000129`,以下是相關指令的構造,這裏的r1在SVM中為rax:
這些包含的`SBF`指令被編譯成ELF合約,版本為SBFV1。`text_section.sh_addr`通過以下計算得出:
通過`readelf`工具,可以查看編譯出的包含上述`SBF`指令的執行合約ELF文件的頭部信息,其中`.text`段的地址為`0x121`,這裏通過正常的合約編譯出來的ELF結構並不能完全控制`.text`部分,需要精心修改`.text`段的`address`和`Entrypoint`的偏移,然後修復相應的ELF結構,才能得到能正確執行的合約。
最終的`program_vm_addr`計算如下:
在上述代碼中,`program_vm_addr`的最終值為`0x100000121`。
在JIT模式下,將`SBF`指令翻譯為x86_64彙編指令如下:
在調試器中,`rsi`計算出的`target_pc`值為`0x100000129`,這裏的`target_pc`只需要小於`number_of_instructions * INSN_SIZE`。
獲取`target_pc`后,進入`call`地址檢查流程,最終得到`call_address`:
在調試器中獲取到相對地址,`relative address = absolute address - program_vm_addr`如下:
獲取`pc_section`數組的基地址:`0x7ffff7e9a000`
`pc_section`數組的基地址`0x7ffff7e9a000`中連續保存了3個`SBF`指令映射到x86機器碼的地址分別是:`0x7ffff7e9b6d0`、`0x7ffff7e9b6d4`、`0x7ffff7e9b6e5`,但是引用地址`0x7ffff7e9a007`獲取的值是`0x7ffff7e9b6d400`,這是個無效的非法地址。
最後直接`call`越界的非法內存地址,造成段錯誤`Segmentation fault`
存在漏洞的`commit`補丁如下:
Callx指令在智能合約中至關重要。內存越界常常成為底層漏洞的根源,而在SVM虛擬機中,尤其是在Solana鏈上,這種漏洞可能導致SVM崩潰,使運行惡意合約的Solana節點無法正常使用,如果通過惡意攻擊者進行精心構造的內存布局甚至會導致任意代碼執行,篡改合約執行數據。此外,這個漏洞的生命周期可能長達2年以上。Solana對這一漏洞的秘密處理非常有效,成功保護了鏈上資產和用戶利益。隨着類似SVM虛擬機漏洞的減少,Solana也將變得更加穩定。