buffer overflow在CWE Top 25榜單上盤踞了二十年之久。我們擁有堆疊金絲雀、ASLR、DEP、控制流完整性以及記憶體安全語言,然而它們仍然不斷出現在關鍵程式碼中。原因很簡單:這些緩解措施無一不是在軟體中執行的,而軟體可以被繞過、被錯誤配置,或者根本未被啟用。
倘若硬體本身拒絕讓你讀取陣列末尾之後的資料呢?
為何軟體緩解措施持續失利
buffer overflow是指程式向已分配記憶體區域的邊界之外寫入資料。在C語言中,這幾乎不費吹灰之力,因為指標不過是一個位址。編譯器與CPU都信任程式設計師清楚自己在做什麼。如果你分配了64位元組,卻向偏移量80處寫入,CPU會照做不誤。它根本不知道buffer 的邊界在哪裡。
軟體緩解措施試圖在事後捕獲這一問題。堆疊金絲雀在返回位址前放置一個已知值,並在返回前進行校驗。ASLR隨機化記憶體佈局,使攻擊者更難構造利用程式碼。控制流完整性限制間接跳轉的目標位置。這些方法有所幫助,但要么是概率性的,要么是不完備的。一個意志堅定的攻擊者只要有足夠的時間和資訊洩漏,通常都能找到繞過的辦法。
Rust等記憶體安全語言在語言層面解決了這個問題,但對已經投入生產的數十億行C和C++程式碼無能為力。在本十年內用Rust重寫Linux核心、OpenSSL或你的遺留後端系統,是不現實的。我們需要一種無需重寫就能保護現有程式碼的防禦機制。
CHERI究竟對指標做了什麼
CHERI,全稱Capability Hardware Enhanced RISC Instructions,是由劍橋大學開發、現由Arm以Morello形式支援的ISA擴展。它改變了指標的本質。
在傳統的64位元系統中,指標就是64位元:僅僅是一個位址。而在CHERI系統中,指標變成了128位元或256位元的權能。額外的位元儲存著中繼資料:分配的基底位址、邊界(延伸範圍)以及權限(讀、寫、執行)。該 capability 受 hardware integrity tag 保護,篡改 metadata 將使其失效。
當你為CHERI編譯程式碼時,malloc返回的不再只是一個位址,而是一個邊界恰好等於請求大小的權能。當你遞增該指標時,硬體會對每一次存取進行邊界校驗。若你試圖在邊界之外讀寫,CPU將立即產生同步異常。你無法偽造一個邊界更寬的權能——硬體根本不會允許。
這就是關鍵差異。軟體邊界檢查需要在記憶體操作周圍插入校驗邏輯,而編譯器可能將其最佳化掉,程式設計師可能遺忘,攻擊者可能繞過。硬體邊界檢查則發生在每一次載入與儲存操作中,無條件執行,且不會為你的程式增加任何額外指令。
邊界強制在指令層面如何運作
下面是一個典型的C語言buffer overflow範例:
#include <string.h>
void vulnerable(char *input) {
char buf[64];
strcpy(buf, input); // No bounds check. Classic overflow.
}
在常規架構上,這會破壞堆疊。在CHERI上,buf並非原始位址,而是一個邊界恰好為64位元組的權能。當strcpy試圖寫入第64位元組之後的資料時,硬體會拋出Capability Bounds Violation異常。程式會在引發錯誤的精確指令處立即當機。
同樣的保護也適用於堆積分配:
#include <stdlib.h>
#include <cheri.h> // CHERI intrinsics header
void heap_example(void) {
char *buf = malloc(32);
// buf is a capability with base=buf, length=32
// This works fine
buf[0] = 'A';
buf[31] = 'Z';
// This traps at the hardware level
buf[32] = 'X'; // Capability bounds violation
}
當機是精確的。你能得到確切的指令、確切的權能以及被違反的確切邊界。偵錯它比追蹤三個堆疊幀之後的受損堆疊幀要容易得多。
CHERI還能防止指標偽造。你不能將整數強制轉換為指標,然後開始解參照任意記憶體。硬體只認可由合法指令(如malloc、stack allocation或顯式權能衍生)建立的權能。整數到指標的強制轉換會產生未標記的值,在使用時觸發陷阱。
相容性問題真實存在
既然CHERI如此出色,為何並非每台伺服器都在執行它?因為改變指標的本質,會破壞數十年來C程式碼所依賴的種種假設。
第一個問題是大小。權能比原始指標更大。在Morello上,一個權能是128位元,外加由硬體單獨儲存的1位元標記。這會增大指標密集型資料結構的記憶體佔用。裝滿指標的鏈結串列或樹會變得明顯更大。對許多應用程式而言,開銷僅為個位數百分比,但對指標追蹤型工作負載來說可能更糟糕。
第二個問題是類型轉換。C程式碼常常將指標轉換為uintptr_t,進行算術運算後再轉換回來。在CHERI上,uintptr_t實際上是一種權能類型,而非整數。假設可以對指標值進行任意整數運算的程式碼將無法編譯,或在執行階段觸發陷阱。修復方案通常是使用ptrdiff_t表示偏移量,並透過cheri_address_set應用,但這需要修改原始碼。
第三個問題是生態系統。CHERI硬體十分稀少。Arm Morello開發板雖然存在,但並非商用伺服器。CheriBSD和啟用了CHERI的Linux已足夠成熟,可以執行真實軟體,但大多數Linux發行版並未提供CHERI軟體包。你的container 倉庫、CI執行器和雲端服務商目前都不支援它。
無需訂製晶片,今日即可體驗
你不需要Morello開發板就能實驗CHERI。CHERI專案維護著QEMU支援,因此你可以在任何Linux或macOS主機上執行CHERI使用者空間。
以下是獲取CHERI工具鏈並在QEMU中執行簡單範例的方法:
# Clone the CHERI SDK setup
$ git clone https://github.com/CTSRD-CHERI/cheribuild.git
$ cd cheribuild
# Build the CheriBSD disk image and QEMU
$ ./cheribuild.py cheribsd-riscv64-purecap -d
$ ./cheribuild.py qemu -d
# Boot CheriBSD in QEMU
$ ./cheribuild.py run-cheribsd-riscv64
進入CheriBSD後,編譯器是支援CHERI目標的clang。預設情況下編譯會強制啟用邊界檢查:
$ clang -o test test.c
$ ./test
# Buffer overflows trap immediately with a clear message
如果你想測試現有軟體,cheribuild可以編譯FreeBSD ports中的熱門軟體包並為其新增CHERI支援。CHERI團隊已經移植了OpenSSH、nginx、PostgreSQL以及FreeBSD基礎系統的大部分內容。許多程式無需修改即可執行。出現問題的程式通常是因為不安全的指標類型轉換或對指標大小的錯誤假設。
若想跳過QEMU快速上手,劍橋大學還發布了預裝CHERI LLVM工具鏈的Docker映像。你可以在不啟動完整作業系統的情況下編譯CHERI二進位檔案,並檢查生成的權能感知組合語言程式碼。
硬體記憶體安全對現有程式碼庫意味著什麼
CHERI並非安全程式設計的替代品,而是你已有程式碼的安全網。執行遺留C程式碼的CHERI系統仍然會有邏輯缺陷、race condition和釋放後使用錯誤。但它不會再出現覆蓋返回位址、破壞相鄰堆積中繼資料或跨越分配邊界洩漏敏感資訊的buffer overflow。
這一演進趨勢已經顯現。Arm已將CHERI衍生功能整合到其記憶體標記擴充(MTE)中,而MTE如今已部署在生產級Android裝置上。MTE的粒度比CHERI更粗(它以16位元組顆粒為單位進行標記,而非針對單個分配),但能以更低開銷捕獲大量同類缺陷。隨著生態系統日趨成熟,完整的CHERI方案有望隨之而來。
如果你維護著處理不可信輸入的C或C++程式碼,問題不在於硬體記憶體安全是否會到來,而在於到來時你是否已經做好準備。請開始審查程式碼中的指標到整數轉換、無關物件上的指標運算,以及對sizeof(void*)的假設。這些是在CHERI下會失效的程式設計模式,而修復它們即使對於傳統硬體也能讓程式碼更整潔。
硬體終於準備好說「不」了。我們應當允許它這麼做。