進程被攻破了。緩衝區溢位、封包格式錯誤、一個有著靜默野心的依賴項。攻擊者現在控制了執行。接下來會發生什麼,完全取決於該進程能夠觸及什麼,而在傳統的 Unix 系統上,自 1969 年以來答案都是一樣的:使用者能觸及的一切。每一個檔案。每一個 socket。每一個裝置。被攻破的進程繼承了啟動它的使用者的全部環境權限。

這不是一個 bug。這是原始的 Unix 安全模型,它在「使用者」僅僅是指貝爾實驗室的研究員,他們可以被信任不會執行來自網路的惡意程式碼時,表現得相當出色。可惜的是,網際網路有其他的計畫。

有兩個作業系統決定解決這個問題。它們選擇了相反的哲學。一個移除了房間的門。另一個聘請了一位保鑣,並交給他一份名單。

2010 年,劍橋大學的 Robert Watson 和 Jonathan Anderson 發表了一篇論文,該論文獲得了 USENIX Security 的最佳學生論文獎。其核心洞見非常簡單:與其列出進程不能做什麼,不如移除所有權限,然後只歸還它真正需要的。

結果就是 Capsicum,它在 2014 年預設編譯進了 FreeBSD 10.0。其 API 中只有一個重要的函數:cap_enter()。一個系統調用。不可逆。

沒有 cap_exit()。沒有權限升級路徑,沒有權限恢復,也沒有禮貌的請求表格讓進程可以要回它們的權限。核心會設定一個標誌。這個標誌不會被取消。進程進入一個只包含其開啟的檔案描述符的世界,並且僅限於明確授予的操作。檔案系統、網路、進程表:它們不僅變得無法存取。對於沙盒化的進程來說,它們根本就不存在了。

這個模型是減法。從全部開始,移除全部,然後只歸還真正需要的。進程無法逃脫。不是因為有篩檢程式阻止它,而是因為門根本就不存在了。有人可能會說,這相當難以繞過。

在 Linux 上,答案分兩個階段出現。2005 年,Andrea Arcangeli 加入了 seccomp strict 模式:只允許四個系統調用(read、write、exit、sigreturn),其他任何調用都會終止進程。當然,這很優雅。但對於任何需要實際工作的東西來說,幾乎完全無法使用。

2012 年,Will Drewry 在 Linux 3.5 中引入了 seccomp-bpf:一個在執行時檢查每個系統調用的 BPF 程式,並決定是允許、拒絕還是終止。這確實很有用。但它也是一個根本上不同的哲學。

這個模型是過濾。進程保留全部環境權限。一個過濾器介於進程和核心之間,檢查每個調用是否符合列表。保鑣會在門口檢查你的名字。他不會檢查你進去後做了什麼。

Docker 的預設 seccomp 設定檔封鎖了約 44 個 300 多個系統調用。其餘 256 個則通過。允許列表和封鎖列表之間的區別,就像「你可以進入 3 號和 7 號房間」與「你可以進入任何房間,除了 12 號和 15 號」之間的區別。當建築物增加樓層時,其中一個會變得更加危險。

最富啟發性的比較不是理論上的。而是 tcpdump。

tcpdump 擷取網路封包。它以 root 權限執行,因為它必須開啟 BPF 裝置。它解析來自網路的未受信任的網路資料。這正是攻擊者夢寐以求的被攻破的工具類型:root 權限、面向網路、解析任意輸入。FreeBSD 和 Linux 都對其進行了沙盒化。它們選擇了相反的方法。

在 FreeBSD 上,tcpdump 使用 Capsicum。它開啟 BPF 擷取裝置和輸出檔案,使用 cap_rights_limit() 限制它們的權限,然後呼叫 cap_enter()。從那一刻起,只有先前開啟的 BPF 描述符和輸出檔案仍然存在。檔案系統不存在。網路不存在。無法開啟新的 socket。在 FreeBSD 上被攻破的 tcpdump 可以從一個裝置讀取封包並寫入一個檔案。僅此而已。

在 Linux 上,tcpdump 使用 seccomp-bpf。一個過濾列表決定哪些系統調用可以通過。允許的調用對所有開啟的檔案描述符保留全部權限。一個允許的 read() 可以讀取任何開啟的 FD。過濾器檢查調用,而不是目標。相同的工具。相同的威脅模型。一個移除存取權限。另一個過濾調用。

當核心不斷擴充套件時,結構上的差異就顯得重要了。Capsicum 不關心核心增加了多少系統調用。在呼叫 cap_enter() 後,一個新的開啟檔案的系統調用將無法工作,因為進程處於能力模式。這種限制是結構性的,而不是列舉式的。核心可以增加一千個新的系統調用,沙盒仍然有效,因為沙盒不是一個你不能做的事情的列表。它是無法做這些事情的能力的缺失。

2022 年,CVE-2022-30594 精確地展示了為什麼架構差異很重要。在 Linux 核心 5.17.2 之前的版本中,PTRACE_SEIZE 允許本地攻擊者設定 PT_SUSPEND_SECCOMP,從而完全繞過 seccomp 過濾器。過濾器是正確的。每一個規則都寫得很好。但強制執行它的機制卻不是。

Capsicum 的模型在結構上免疫於這類攻擊。沒有過濾器可以暫停。沒有可以被規避的強制執行層。進程處於能力模式。全域命名空間不存在。你無法繞過一個不存在的門,無論你的開鎖技巧有多麼巧妙。

過濾器是正確的。強制執行它的機制卻不是。這是減法和過濾之間的基本區別。

Capsicum 和 seccomp 之間的差異不是實作上的差異。它是認識論上的差異,值得花點時間思考為什麼這比技術細節更重要。

seccomp 問:「這個進程不應該被允許呼叫什麼?」這要求你預先知道進程可能做的每一件危險的事情。每一個新的核心版本,每一個新的系統調用,每一個新的攻擊向量都必須被預期並添加到過濾器中。Linux 核心在 2012 年有 335 個系統調用。今天有超過 450 個。每一次新增都可能成為每個使用封鎖列表的 seccomp 設定檔中的一個潛在漏洞。

Capsicum 問:「這個進程實際上需要什麼?」然後它移除所有其他東西。你不是列舉威脅。你列舉需求。進程需要的東西的集合是小的、可知的、穩定的。進程可能濫用的東西的集合隨著每一個核心版本的發布而增長。其中一個集合相對更容易管理。

tcpdump 並非孤例。FreeBSD 的基礎系統默默地展示了 Capsicum 在其最安全關鍵的工具中實現了什麼。

dhclient 也遵循相同的模式。開啟 socket,開啟租約檔案,進入能力模式。DHCP 客戶端,它以 root 權限執行並處理來自不受信任來源的網路輸入,被沙盒化到它所需的精確資源。在大多數 Linux 發行版上,被攻破的 dhclient 可以讀取 dhclient 使用者可以讀取的任何內容,而鑑於它通常以 root 權限執行,這意味著一切。

FreeBSD 基礎系統中啟用 Capsicum 的工具完整列表非常具有啟發性:tcpdump、dhclient、hastd、auditdistd、gzip 和 OpenSSH。這些正是攻擊者首先瞄準的工具:面向網路、解析不受信任的輸入、通常以 root 權限執行。在 FreeBSD 上,它們恰恰是攻破後能提供的最少東西的工具。

這兩種方法都提高了安全性。而且是顯著的。seccomp-bpf 每天保護著數百萬個容器、手機和瀏覽器。忽視它將是無知的。但問題,一如既往,是架構上的:你寧願修補過濾器,還是移除需要過濾的東西?

Capsicum 消除了環境權限。seccomp 限制了它。一個鎖上了門並將其從鉸鏈上拆下。另一個聘請了保鑣,並希望賓客名單是完整的。

不存在的門無法被打開。這相當令人安心。

數字:Capsicum 在能力模式下允許約 190 個系統調用,約佔總數的 567 個。Docker 預設設定檔透過 seccomp 封鎖約 44 個 300 多個系統調用。Capsicum 的開銷:每個進程一個核心標誌,可忽略不計。seccomp-bpf:執行時的每個系統調用過濾器評估。CVE-2022-30594 在 5.17.2 之前的 Linux 版本上完全繞過了 seccomp。Capsicum 的模型不受此類攻擊的影響,因為沒有過濾器可以暫停。Linux 並未停滯不前:Landlock(已合併到 5.13,2021 年)增加了檔案系統沙盒化,這更接近 Capsicum 的能力模型。原始的 Capsicum 論文(Watson, Anderson 等人,USENIX Security 2010)仍然是能力模型的權威解釋。