USER命名空間為我們最喜歡的工具(例如docker和podman)驅(qū)動(dòng)各種功能。我們在去年6月份寫過關(guān)于Linux命名空間的文章并這樣解釋:
“許多命名空間都不存在爭議的,例如UTS命名空間,其允許主機(jī)系統(tǒng)隱藏其主機(jī)名和時(shí)間。其他一些命名空間復(fù)雜但直接明了,例如,NET和NS(mount)命名空間令人難以理解。最后,還有一個(gè)非常特殊、很不尋常的USER命名空間。USER名稱空間之所以特殊,是因?yàn)樗试S通常沒有特權(quán)的所有者在其中作為根運(yùn)行。這樣我們才能擁有非作為真正根運(yùn)行的工具,例如Docker,以及無根容器之類的東西?!?/p>
由于其性質(zhì),允許非特權(quán)用戶訪問USER命名空間總是存在巨大的安全風(fēng)險(xiǎn)。在它的幫助下,非特權(quán)用戶事實(shí)上可以運(yùn)行通常需要根權(quán)限的代碼。這種代碼往往未經(jīng)充分測試和存在bug。今天我們將研究一個(gè)此類案例,即通過USER命名空間來利用一個(gè)內(nèi)核缺陷,從而導(dǎo)致非特權(quán)的拒絕服務(wù)攻擊。
Linux流量控制隊(duì)列規(guī)則
2019年,我們正在探索利用Linux流量控制的隊(duì)列規(guī)則(qdisc),使用Hierarchy Token Bucket(HTB)classful qdisc策略為我們的服務(wù)之一調(diào)度數(shù)據(jù)包。Linux流量控制是一個(gè)用戶配置的系統(tǒng),用于調(diào)度和過濾網(wǎng)絡(luò)數(shù)據(jù)包。隊(duì)列規(guī)則是調(diào)度數(shù)據(jù)包的策略。具體而言,我們想從一個(gè)接口過濾和調(diào)度某些數(shù)據(jù)包,并將其他數(shù)據(jù)包丟入noqueue qdisc。
noqueue是qdisc的一個(gè)特例,調(diào)度到其中的數(shù)據(jù)包應(yīng)該被丟棄。實(shí)踐中情況并非如此。Linux對noqueue處理方式導(dǎo)致數(shù)據(jù)包被通過而不是被丟棄(大部分情況下)。文檔也很能說明問題。它還指出,“不可能將noqueue排隊(duì)規(guī)則分配給物理設(shè)備或類”。那么,當(dāng)我們把noqueue分配給一個(gè)類時(shí)會(huì)發(fā)生什么呢?
讓我們寫一些shell命令來顯示這個(gè)問題的實(shí)際情況。
1.$sudo-i2.#dev=enp0s53.#tc qdisc replace dev$dev root handle 1:htb default 14.#tc class add dev$dev parent 1:classid 1:1 htb rate 10mbit5.#tc qdisc add dev$dev parent 1:1 handle 10:noqueue
首先我們需要以根身份登錄,這樣我們將獲得CAP_NET_ADMIN權(quán)限,從而能夠配置流量控制。
然后我們將一個(gè)網(wǎng)絡(luò)接口分配給一個(gè)變量。這些可以通過ip a找到。虛擬接口可以通過調(diào)用ls/sys/devices/virtual/net定位。這些將匹配ip a的輸出。
我們的接口目前被分配給pfifo_fast qdisc,所以我們用HTB classful qdisc來代替它,并把它分配給1:的句柄。我們可以把它看作是樹中的根節(jié)點(diǎn)。“默認(rèn)1”配置的結(jié)果是,未分類的流量被路由直接通過這個(gè)qdisc,后者會(huì)回退到pfifo_fast排隊(duì)。(下文將進(jìn)一步說明)
接下來我們給我們的根qdisc添加一個(gè)類1:,把它分配給根1:的第一個(gè)葉節(jié)點(diǎn)1:1,并給它一些合理的配置默認(rèn)值。
最后,我們將noqueue qdisc添加到層級(jí)結(jié)構(gòu)中的第一個(gè)葉節(jié)點(diǎn):1:1。這實(shí)際意味著,在這里路由的流量將被調(diào)度到noqueue。
假設(shè)我們的設(shè)置順利執(zhí)行,我們將收到類似于這個(gè)內(nèi)核錯(cuò)誤的提示:
BUG:kernel NULL pointer dereference,address:0000000000000000#PF:supervisor instruction fetch in kernel mode...Call Trace:htb_enqueue+0x1c8/0x370dev_qdisc_enqueue+0x15/0x90__dev_queue_xmit+0x798/0xd00...
我們知道該根用戶負(fù)責(zé)在接口設(shè)置qdisc,如果根用戶可以導(dǎo)致內(nèi)核崩潰,那怎么辦呢?我們不要將HTB qdisc應(yīng)用到一個(gè)HTB qdisc的noqueue qdisc就是了。
#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
在這里,我們利用HTB的默認(rèn)情況,其中分配一個(gè)類id 1:2以被限速(A),并隱含地沒有將qdisc設(shè)置另一個(gè)類(例如id 1:1)(B)。排隊(duì)到(A)的數(shù)據(jù)包將被過濾到HTB_DIRECT,排隊(duì)到(B)的數(shù)據(jù)包將被過濾到pfifo_fast。
因?yàn)槲覀儾皇煜ごa庫的這一部分,我們通知了郵件列表并創(chuàng)建了一個(gè)工單。當(dāng)時(shí)這個(gè)bug對我們來說似乎并不那么重要。
快進(jìn)到2022年,我們正在推進(jìn)USER命名空間創(chuàng)建強(qiáng)化。我們用一個(gè)新的LSM鉤子擴(kuò)展了Linux LSM框架:userns_create,以利用eBPF LSM提供保護(hù),并鼓勵(lì)其他人也這樣做。最近,在梳理我們的積壓工單時(shí),我們重新考慮了這個(gè)bug。我們自問:"我們能不能利用USER命名空間來觸發(fā)這個(gè)bug?”簡短的答案是肯定的!
演示這個(gè)bug
這個(gè)漏洞可以用任何一個(gè)假設(shè)struct Qdisc.enqueue函數(shù)不為空的classful qdisc來執(zhí)行(后面會(huì)詳細(xì)介紹),但在本例中,我們只用HTB來演示。
$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
我們用“l(fā)o”接口來證明這個(gè)bug可以用虛擬接口觸發(fā)。這對容器來說很重要,因?yàn)樗鼈冊诖蠖鄶?shù)時(shí)候都是被提供虛擬接口,而不是物理接口。正因?yàn)槿绱耍覀兛梢允褂靡粋€(gè)容器以非特權(quán)用戶的身份使主機(jī)崩潰,從而執(zhí)行拒絕服務(wù)攻擊。
為什么有這樣的結(jié)果?
為了更好地理解這個(gè)問題,我們需要回顧一下最初的補(bǔ)丁系列,但特別是引入了這個(gè)bug的提交。這一系列之前,在接口上實(shí)現(xiàn)noqueue依賴于一種hack:如果設(shè)備有tx_queue_len=0,則將設(shè)備qdisc設(shè)為noqueue。提交d66d6c3152e8("net:sched:register noqueue qdisc")通過顯式允許用tc命令添加noqueue來解決這個(gè)問題,無需繞過以上限制。
內(nèi)核檢查我們是否處于noqueue情況的方式,就是簡單地檢查qdisc是否有NULL enqueue()函數(shù)。記得前面說過,noqueue在實(shí)踐中不一定會(huì)丟棄數(shù)據(jù)包?在以上檢查失敗后,下面的邏輯處理noqueue的功能。為了不通過檢查,作者不得不以欺騙方式將noop_enqueue()重新賦值為NULL,方式是在init中使enqueue=NULL,后者將在運(yùn)行時(shí)在register_qdisc()后調(diào)用。
這就是classful qdiscs發(fā)揮作用的地方了。對入隊(duì)函數(shù)的檢查不再為NULL。在此調(diào)用路徑中,它現(xiàn)在設(shè)置為HTB(在我們的示例中),因此允許通過調(diào)用htb_enqueue()函數(shù),將struct skb排入隊(duì)列。進(jìn)入那里后,HTB會(huì)進(jìn)行查找以提取分配給葉節(jié)點(diǎn)的qdisc,并最終嘗試將struct skb排入選定的qdisc,最終到達(dá)這個(gè)函數(shù):
include/net/sch_generic.h
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}
我們可以看到,排隊(duì)過程對物理/虛擬接口是相當(dāng)無關(guān)的。權(quán)限和驗(yàn)證檢查是在向接口添加隊(duì)列時(shí)進(jìn)行的,這就是為什么classful qdics假定隊(duì)列不是NULL。了解這一點(diǎn)使我們找到了一些可以考慮的解決方案。
解決方案
我們有幾個(gè)解決方案,介于從我們認(rèn)為最好到最壞的。
1.遵循tc-noqueue文檔,不允許將noqueue分配給一個(gè)classful qdisc
2.不檢查NULL,而是檢查struct noqueue_qdisc_ops,并將noqueue重設(shè)為noop_enqueue
3.對于每個(gè)classful qdisc,檢查是否有NULL和回退
我們最終選擇了第一個(gè)選項(xiàng):“disallow noqueue for qdisc classes(不允許對qdisc類的noqueue)”,而第三個(gè)選項(xiàng)在代碼中造成了大量的混亂,且沒有徹底解決問題。未來的qdiscs實(shí)現(xiàn)可能會(huì)忘記這個(gè)重要的檢查以及維護(hù)者。然而,不采用第二個(gè)選項(xiàng)的原因更為有意思。
我們之所以沒有采用這種方法,是因?yàn)槲覀冃枰紫然卮疬@些問題:
為什么不允許將noqueue分配給classful qdisc?
這背離了文檔。文檔確實(shí)有一些在實(shí)踐中不被完全遵循的先例,但我們需要對其更新,以反映目前的狀況。這樣做很好,但是除了刪除NULL解引用錯(cuò)誤之外,并不能解決行為變化問題。
如果我們允許將noqueue分配給qdisc,會(huì)發(fā)生什么行為變化?
這個(gè)問題更難回答,因?yàn)槲覀冃枰_定這種行為應(yīng)該是什么。目前,當(dāng)noqueue被作為根qdisc為一個(gè)接口應(yīng)用時(shí),其路徑基本上是允許數(shù)據(jù)包被處理。聲明了回退的類則是另一回事。它們可能每個(gè)都有自己的回退,我們?nèi)绾沃朗裁词钦_的回退?在HTB中,有時(shí)回退是通過HTB_DIRECT,有時(shí)是pfifo_fast。其他類又如何呢?也許我們應(yīng)該回到默認(rèn)的noqueue行為,就像對根qdiscos那樣?
我們覺得走這條路只會(huì)給排隊(duì)增加混亂和額外的復(fù)雜性。我們還可以提出一個(gè)觀點(diǎn),即這樣的更改可以被視為特性的添加,而不一定是bug的修復(fù)。可以說,對于目前防止漏洞而言,遵守當(dāng)前文檔似乎是更有吸引力的方法,其他事情可以日后再解決。
要點(diǎn)
首先,也是最重要的,盡快應(yīng)用這個(gè)補(bǔ)丁。同時(shí),考慮通過設(shè)置setting sysctl-w kernel.unprivileged_userns_clone=0,只允許根在Debian內(nèi)核中創(chuàng)建USER命名空間,sysctl-w user.max_user_namespaces=【number】用于進(jìn)程層級(jí),或者考慮回退到這兩個(gè)補(bǔ)?。簊ecurity_create_user_ns()和SELinux實(shí)現(xiàn)(現(xiàn)在為Linux 6.1.x),允許您用eBPF或者SELinux保護(hù)自己的系統(tǒng)。如果確定沒有使用USER命名空間和處于極端情況下,您可以考慮用CONFIG_USERNS=n關(guān)閉該功能。這只是利用命名空間進(jìn)行攻擊的眾多例子之一,未來肯定會(huì)有更多嚴(yán)重程度不同的例子出現(xiàn)。
特別感謝Ignat Korchagin和Jakub Sitnicki的代碼審查工作,并幫助在實(shí)踐中演示這個(gè)bug。