訂閱以接收新文章的通知:

CVE-2022-47929:流量控制 noqueue 沒問題?

2023-01-31

閱讀時間:2 分鐘
本貼文還提供以下語言版本:English简体中文

USER 命名空間為我們最喜歡的工具(例如 docker 和 podman)的功能提供了技術支援。我們早在去年 6 月就撰寫了有關 Linux 命名空間的文章,並像這樣解釋它們:

CVE-2022-47929: traffic control noqueue no problem?

大多數命名空間毫無爭議,比如 UTS 命名空間,它可讓主機系統隱藏主機名稱和時間。另一些則既複雜又簡單——眾所週知,NET 和 NS (mount) 命名空間就令人很難理解。最後,還有這個非常特殊、非常奇怪的 USER 命名空間。USER 命名空間非常特殊,因為它允許通常無權限的擁有者在其內部以「root」身分進行操作。正是因為有它作為基礎,才會有 Docker 這樣不以真正的 root 身分操作的工具,以及無根容器這樣的物件。

由於其性質允許無權限使用者存取 USER 命名空間,總是會帶來很大的安全風險。在它的幫助下,無權限使用者可以實際執行通常需要 root 的程式碼。這種程式碼通常未經充分測試,且錯誤很多。今天,我們將研究一個這樣的案例,即利用 USER 命名空間惡意探索核心錯誤,從而導致無權限阻斷服務攻擊。

進入 Linux 流量控制佇列原則

2019 年,我們探索了利用 Linux 流量控制的佇列原則 (qdisc),透過 Hierarchy Token Bucket (HTB) classful qdisc 策略為我們的一個服務排定封包。Linux 流量控制是一個使用者設定的系統,用於排定和篩選網路封包。佇列原則是用於排定封包的策略。特別是,我們希望從介面篩選和排定某些封包,並將其他封包放入 noqueue qdisc 中。

noqueue 是一種特殊情況的 qdisc,排定到其中的封包應捨棄。在實踐中,情況並非如此。Linux 會處理 noqueue,讓封包通過且不被捨棄(大多數情況下)。文檔也是這樣說明的。它還指出「無法將 noqueue 佇列原則指派給實體裝置或類別。」那麼,當我們將 noqueue 指派給一個類別時會發生什麼呢?

我們來撰寫一些殼層命令,實際展示一下這個問題:

  1. 首先,我們需要以 root 身分登入(它會為我們提供 CAP_NET_ADMIN),以便能夠設定流量控制。

  2. 然後,我們將網路介面指派給一個變數。您可使用 ip a 找到它們。虛擬介面可以透過呼叫 ls /sys/devices/virtual/net 找到。這些將與 ip a 的輸出相符。

  3. 我們的介面目前已指派給 pfifo_fast qdisc,因此我們將它取代為 HTB classful qdisc,並為其指派控制代碼 1:。我們可以將其視為樹狀結構中的根節點。「預設值 1」會對此進行設定,以便直接透過此 qdisc 路由傳送未分類的流量,而該 qdisc 會回復到 pfifo_fast 佇列。(稍後會對此進行詳細介紹)

  4. 下面我們將一個類別添加到 root qdisc 1:,將其指派給根 1:1:1 的第一個分葉節點 1,並為它提供一些合理的設定預設值。

  5. 最後,我們將 noqueue qdisc 添加到我們在階層中的第一個分葉節點:1:1。這實際上意味著路由到這裡的流量將被排定至 noqueue

1. $ sudo -i
2. # dev=enp0s5
3. # tc qdisc replace dev $dev root handle 1: htb default 1
4. # tc class add dev $dev parent 1: classid 1:1 htb rate 10mbit
5. # tc qdisc add dev $dev parent 1:1 handle 10: noqueue

假設我們的設定順利執行,我們將收到類似於此核心恐慌的內容:

我們知道 root 使用者負責在介面上設定 qdisc,那麼,如果 root 會損毀核心,那又如何?我們只是不將 noqueue qdisc 套用至 HTB qdisc 的類別 id:

BUG: kernel NULL pointer dereference, address: 0000000000000000
#PF: supervisor instruction fetch in kernel mode
...
Call Trace:
<TASK>
htb_enqueue+0x1c8/0x370
dev_qdisc_enqueue+0x15/0x90
__dev_queue_xmit+0x798/0xd00
...
</TASK>

在這裡,我們利用了 HTB 的預設案例,即指派類別 id 1:2 來限速 (A),並隱含地未將 qdisc 設為另一個類別,例如 id 1:1 (B)。排入到 (A) 佇列的封包會被篩選至 HTB_DIRECT,而排入到 (B) 佇列的封包則會被篩選至 pfifo_fast 中。

# dev=enp0s5
# tc qdisc replace dev $dev root handle 1: htb default 1
# tc class add dev $dev parent 1: classid 1:2 htb rate 10mbit // A
// B is missing, so anything not filtered into 1:2 will be pfifio_fast

因為對程式碼庫的這一部分並不熟悉,所以我們通知了郵件清單並建立了票證。當時這個錯誤對我們似乎並不是那麼重要。

快進到 2022 年,我們正在推動 USER 命名空間建立強化。我們使用新的 LSM 勾點 (userns_create) 擴充了 Linux LSM 架構,以利用 eBPF LSM 為我們提供保護,並鼓勵其他人也這樣做。最近,在梳理票證待處理項目時,我們重新考慮了這個錯誤。我們問自己,「我們可以利用 USER 命名空間來觸發錯誤嗎?」簡短回答就是,是的!

錯誤示範

惡意探索可以使用任何假定 struct Qdisc.enqueue 函數不是 NULL 的 classful qdisc 來執行(稍後會對此進行詳細介紹),但在本案例中,我們只是用 HTB 進行示範。

我們使用「lo」界面來示範這個錯誤可以透過虛擬界面觸發。這對於容器而言非常重要,因為在大多數情況下都是為其饋送虛擬介面,而不是實體介面。正因為如此,我們可以使用容器以無權限使用者身分損毀主機,進而執行阻斷服務攻擊。

$ unshare -rU –net
$ dev=lo
$ tc qdisc replace dev $dev root handle 1: htb default 1
$ tc class add dev $dev parent 1: classid 1:1 htb rate 10mbit
$ tc qdisc add dev $dev parent 1:1 handle 10: noqueue
$ ping -I $dev -w 1 -c 1 1.1.1.1

這種做法為什麼會奏效呢?

為了更好地理解這個問題,我們需要回顧一下原來的修補程式系列,但具體而言,是這個引入了錯誤的 commit。在本系列之前,在介面上實現 noqueue 依賴於一種駭客攻擊,如果裝置具有 tx_queue_len = 0,則該駭客攻擊會將裝置 qdisc 設為 noqueue。而 commit d66d6c3152e8 (“net: sched: register noqueue qdisc”) 明確允許使用 tc 命令添加 noqueue 而不需要繞過該限制,從而規避了這種情況。

核心檢查我們是否處於 noqueue 情況下的方法是檢查 qdisc 是否具有 NULL enqueue() 函數。回想一下,noqueue 在實踐中不一定會捨棄封包?在該檢查失敗的情況下,下列邏輯會處理 noqueue 功能。為了讓檢查失敗,作者不得不_欺騙_從 noop_enqueue ()NULL 的重新指派,方法是在初始化中使 enqueue = NULL 並在執行階段中呼叫(遠離前面的 register_qdisc())。

這就是 classful qdiscs 發揮作用的地方了。對加入佇列函數的檢查不再是 NULL。在此呼叫路徑中,現在將其設為 HTB(在我們的範例中),因此允許將 struct skb 加入佇列,方法是呼叫函數 htb_enqueue()。進入那裡後,HTB 會執行查詢來提取指派給分葉節點的 qdisc,最後嘗試將 struct skb 排入到所選的 qdisc,該 qdisc 最終會到達此函數:

include/net/sch_generic.h

我們可以看到,加入佇列流程與實體/虛擬介面完全無關。將佇列加入介面時,會執行權限和驗證檢查,這就是 classful qdics 假定佇列不是 NULL 的原因。瞭解這些資訊後,我們有幾種解決方案可以考慮。

static inline int qdisc_enqueue(struct sk_buff *skb, struct Qdisc *sch,
				struct sk_buff **to_free)
{
	qdisc_calculate_pkt_len(skb, sch);
	return sch->enqueue(skb, sch, to_free); // sch->enqueue == NULL
}

解決方案

我們有幾種解決方案,從我們認為最好的到最差的都有:

  1. 遵循 tc-noqueue 文件,不允許將 noqueue 指派給 classful qdisc

  2. 不檢查 NULL,而是檢查 struct noqueue_qdisc_ops,並將 noqueue 重設為 noop_enqueue

  3. 對於每個 classful qdisc,檢查 NULL 和後援

雖然我們最終選擇了第一個選項:「不允許 qdisc 類別為 noqueue」,但第三個選項在程式碼中建立了大量變換,並且不能完全解決問題。未來的 qdiscs 實作可能會忘記這項重要的檢查以及維護人員。但是,放棄第二個選項的原因似乎更有趣。

我們未遵循該方法的原因是,我們需要先回答以下問題:

為什麼允許 classful qdiscs 為 noqueue?

這與文件互相矛盾。該文件確實包含一些在實踐中沒有完全遵循的先例,但我們需要對其進行更新來反映目前的狀態。這樣做是很好,但除了移除 NULL 取值錯誤,並沒有解決行為變更問題。

如果我們允許 qdiscs 為 noqueue,會發生什麼行為變更?

這很難回答,因為我們需要確定該行為應該是什麼。目前,當針對介面套用 noqueue 作為 root qdisc 時,採取的做法就是基本上允許處理封包。宣告類別的後援則是另外一個問題。它們可能都有各自的後援規則,那我們如何知道什麼是正確的後援呢?有時在 HTB 中,後援是使用 HTB_DIRECT 通過,有時是 pfifo_fast。那其他類別呢?也許相反,我們應該回復到預設的 noqueue 行為,因為它是用於 root qdiscs 的?

我們認為,走這條路只會讓佇列變得更混亂、更複雜。我們還可以提出一個論點:將此類變更視為功能新增,而不一定是錯誤修正。無需贅言,現在遵循目前的文件來防止漏洞似乎是更吸引人的方法,而其他問題可以稍後解決。

重點

首先,盡快套用此修補程式。然後考慮透過下列設定強化系統上的 USER 命名空間:設定 sysctl -w kernel.unprivileged_userns_clone=0(該設定僅允許 root在 Debian 核心中建立 USER 命名空間)、sysctl -w user.max_user_namespaces=[number](針對流程階層),也可以考慮將這兩個修補程式向後移植:[security_create_user_ns()](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=7cd4c5c2101cb092db00f61f69d24380cf7a0ee8)SELinux 實作(現在位於 Linux 6.1.x 中),讓您可以使用 eBPF 或 SELinux 來保護系統。如果您確定不使用 USER 命名空間,在極端情況下,則可以考慮使用 CONFIG_USERNS=n 關閉該功能。這只是利用命名空間來執行攻擊的一個例子,將來肯定會出現更多不同嚴重程度的此類攻擊。

特別感謝 Ignat Korchagin 和 Jakub Sitnicki 幫忙檢閱程式碼,並協助在實踐中示範錯誤。

我們保護整個企業網路,協助客戶有效地建置網際網路規模的應用程式,加速任何網站或網際網路應用程式抵禦 DDoS 攻擊,阻止駭客入侵,並且可以協助您實現 Zero Trust

從任何裝置造訪 1.1.1.1,即可開始使用我們的免費應用程式,讓您的網際網路更快速、更安全。

若要進一步瞭解我們協助打造更好的網際網路的使命,請從這裡開始。如果您正在尋找新的職業方向,請查看我們的職缺
Linux安全性VulnerabilitiesCVE

在 X 上進行關注

Cloudflare|@cloudflare

相關貼文

2024年7月09日 下午12:00

RADIUS/UDP vulnerable to improved MD5 collision attack

The RADIUS protocol is commonly used to control administrative access to networking gear. Despite its importance, RADIUS hasn’t changed much in decades. We discuss an attack on RADIUS as a case study for why it’s important for legacy protocols to keep up with advancements in cryptography...