LWN:非特權(quán)容器中的系統(tǒng)調(diào)用攔截機(jī)制!
關(guān)注了就能看到更多這么棒的文章哦~
System call interception for unprivileged containers
By Jake Edge
June 29, 2022
LSSNA
DeepL assisted translation
https://lwn.net/Articles/899281/
在德克薩斯州奧斯汀舉行的 2022 年北美 Linux 安全峰會(huì)(LSSNA)的第一天,Stéphane Graber 和 Christian Brauner 發(fā)表了關(guān)于使用系統(tǒng)調(diào)用攔截來(lái)實(shí)現(xiàn)安全容器(container security)的演講。這是為了讓沒(méi)有特權(quán)的容器,即那些在 host 上沒(méi)有提升過(guò)權(quán)限的容器,如果需要執(zhí)行一些需要特權(quán)的任務(wù)的話,也仍然能夠完成。目前已經(jīng)做了相當(dāng)多的工作來(lái)實(shí)現(xiàn)可行性,但仍有更多工作需要繼續(xù)完成。
Graber 首先說(shuō),他在為 Canonical 工作,負(fù)責(zé) LXD container manager 項(xiàng)目,而 Brauner 為微軟工作,負(fù)責(zé) Linux 安全的各個(gè)領(lǐng)域。Graber 說(shuō),現(xiàn)在有兩種類型的容器,帶有特權(quán)的和非特權(quán)的,"一種是無(wú)法接受的,另一種可以"。他指出,特權(quán)容器就是 Docker 容器、Kubernetes 等等, "很不幸的是大家都在使用的東西"。
Unpriviledged containers
LXD 默認(rèn)使用非特權(quán)容器(unprivileged container);用戶命名空間就是這些容器中 "主要的安全屏障了"。特權(quán)容器一直在進(jìn)行一個(gè)持續(xù)的打地鼠式的工作方式,也就是使用 Linux 安全模塊(LSM)、seccomp()過(guò)濾器以及其他一些機(jī)制來(lái)逐個(gè)關(guān)閉那些導(dǎo)致容器內(nèi)的進(jìn)程在 host 上獲得權(quán)限的漏洞。他以及其他一些開(kāi)發(fā)者希望能實(shí)現(xiàn)一個(gè)人人都使用無(wú)特權(quán)容器的世界;"有特權(quán)的容器不應(yīng)該存在",他說(shuō)。

[Stéphane Graber]
但是有很多東西在非特權(quán)容器中是無(wú)法正常工作的。因?yàn)楣ぷ魅萜鲗?shí)際上是作為 host 系統(tǒng)上的很普通的用戶在運(yùn)行;"我們不會(huì)允許我們系統(tǒng)上的隨便一個(gè)用戶能做很多越界的事情"。使用其他類型的命名空間和添加新的命名空間就可以讓非特權(quán)容器繞過(guò)其中的一些限制,但這種做法是有限制的,不確定能走多遠(yuǎn)。人們也并不喜歡在內(nèi)核中增加更多的命名空間類型。
所以 LXD 項(xiàng)目開(kāi)始研究 seccomp()過(guò)濾器,尤其是想看看用戶空間的系統(tǒng)調(diào)用攔截是否可以利用。它可以提供一種方法,來(lái)允許容器做那些需要特殊權(quán)限的事情,但以一種受控的方式進(jìn)行,由容器管理器(container manager)來(lái)管束。
Brauner 說(shuō),seccomp()在系統(tǒng)調(diào)用的特定代碼被調(diào)用之前就已經(jīng)在系統(tǒng)調(diào)用入口路徑上了。在一些系統(tǒng)調(diào)用中,即使容器沒(méi)有必需的權(quán)限,也應(yīng)該能夠成功地進(jìn)行調(diào)用。例如,mknod()應(yīng)該被允許用于某些類型的設(shè)備節(jié)點(diǎn),如/dev/zero、/dev/null、/dev/console 等等。這些是 "沒(méi)有什么大問(wèn)題的設(shè)備節(jié)點(diǎn)",但是內(nèi)核的權(quán)限模型要么允許創(chuàng)建任意的設(shè)備節(jié)點(diǎn),要么就任何一個(gè)都不可以創(chuàng)建。
例如,沒(méi)有特權(quán)的進(jìn)程(或容器)不應(yīng)該能夠創(chuàng)建/dev/kmem 或一些隨便什么 block 設(shè)備,因?yàn)檫@可能導(dǎo)致主機(jī)被攻破。但是,有幾個(gè)簡(jiǎn)單的設(shè)備節(jié)點(diǎn)是容器中所需要的,它們目前是由 host 來(lái) bind-mount 過(guò)來(lái)的。并沒(méi)有什么理由說(shuō)不可以直接在容器中創(chuàng)建。
Brauner 說(shuō),我們可以設(shè)想在內(nèi)核中設(shè)置某種允許列表(allowlist),指定哪些設(shè)備節(jié)點(diǎn)是不需要權(quán)限來(lái)創(chuàng)建的。這種做法 "有點(diǎn) hack",所以他嘗試了其他解決方案。在這一過(guò)程中,他發(fā)現(xiàn)已經(jīng)有一個(gè)功能比較有限制的 allowlist,那就是 Overlay 文件系統(tǒng)中使用的 whiteout 機(jī)制,用來(lái)標(biāo)記在上層被刪除的文件的 "留白 ",這實(shí)際上是具有特殊設(shè)備號(hào)(0/0)的字符設(shè)備節(jié)點(diǎn)。這些節(jié)點(diǎn)不需要額外的權(quán)限就可以被創(chuàng)建。他說(shuō),這削弱了那些反對(duì)在內(nèi)核中為 mknod() 設(shè)置允許列表的論點(diǎn),但這一方案并沒(méi)有被采納。

[Christian Brauner]
還有一些嘗試是允許無(wú)特權(quán)的進(jìn)程來(lái)創(chuàng)建設(shè)備節(jié)點(diǎn),但不可以 open 這些節(jié)點(diǎn)。Brauner 說(shuō),這幾乎破壞了所有的容器運(yùn)行機(jī)制。有一個(gè)根深蒂固的假設(shè)是,如果一個(gè)進(jìn)程可以創(chuàng)建一個(gè)設(shè)備節(jié)點(diǎn),它就可以打開(kāi)它。所以事實(shí)證明,允許創(chuàng)建無(wú)法打開(kāi)的設(shè)備節(jié)點(diǎn) "并不是一個(gè)好主意"。
但所有這些都集中在了一個(gè)唯一的系統(tǒng)調(diào)用上,事實(shí)上有必要支持系統(tǒng)調(diào)用的其他 "safe" 使用的情況。因此,系統(tǒng)調(diào)用攔截的想法在 2017 年的 Linux Plumbers 大會(huì)(LPC)上就誕生了,Brauner 認(rèn)為。一個(gè)能夠檢查系統(tǒng)調(diào)用參數(shù)的機(jī)制就可以做一些工作了,例如拒絕對(duì) block 設(shè)備和不在批準(zhǔn)列表中的字符設(shè)備進(jìn)行 mknod()調(diào)用。與其在內(nèi)核中制定一些關(guān)于允許或拒絕的靜態(tài)規(guī)則,不如將決定權(quán)下放給用戶空間進(jìn)程。
他說(shuō),因此 seccomp() 就被進(jìn)行了擴(kuò)展來(lái)支持這種用法。其中增加了一種新的 filter,以便在進(jìn)行系統(tǒng)調(diào)用時(shí)就收到用戶空間的通知;然后容器管理器進(jìn)程可以取得一個(gè)文件描述符,它可以輪詢系統(tǒng)調(diào)用 event。當(dāng)容器管理器收到系統(tǒng)調(diào)用的通知時(shí),可以使用 ioctl()命令來(lái)查詢調(diào)用的參數(shù),這些參數(shù)可以用來(lái)做決定。然后會(huì)把決定通過(guò)寫(xiě)入文件描述符的方式來(lái)返回給內(nèi)核。
seccomp() filter 只能告訴內(nèi)核繼續(xù)調(diào)用,還是讓調(diào)用失敗并返回一個(gè)特定的錯(cuò)誤代碼給調(diào)用者,或者直接返回成功。如果容器管理器認(rèn)為一個(gè)沒(méi)有特權(quán)的容器應(yīng)該可以成功進(jìn)行系統(tǒng)調(diào)用,那么它不能直接通知內(nèi)核來(lái)繼續(xù)執(zhí)行該系統(tǒng)調(diào)用,因?yàn)檫@個(gè)調(diào)用的發(fā)起者其實(shí)并沒(méi)有相應(yīng)的權(quán)限。因此,容器管理器必須執(zhí)行該動(dòng)作來(lái)模擬系統(tǒng)調(diào)用的發(fā)生,就好像該任務(wù)擁有相應(yīng)的權(quán)限一樣。在它完成這些工作并將結(jié)果提供給容器之后,它就可以告訴內(nèi)核來(lái)直接返回成功。
他問(wèn)與會(huì)者,是否能看到這個(gè)方案中有沒(méi)有什么安全問(wèn)題;有人很快提到了檢查時(shí)間到使用時(shí)間的問(wèn)題(TOCTTOU,time-of-check-to-time-of-use)的問(wèn)題。Brauner 說(shuō),mknod()是一個(gè) "相當(dāng)無(wú)聊的系統(tǒng)調(diào)用,因?yàn)樗挥姓麛?shù)參數(shù)"。其他的系統(tǒng)調(diào)用可能帶有指針參數(shù),也許會(huì)欺騙容器管理器在判定其安全之后就修改了這個(gè)地址上的參數(shù)。seccomp()過(guò)濾器是用 classic BPF 編寫(xiě)的,而不是擴(kuò)展 BPF(eBPF),這意味著它們不能對(duì)指針進(jìn)行解析和檢查。因此,為了檢查一個(gè)以指針引用方式傳遞的參數(shù),管理器需要直接從進(jìn)程的內(nèi)存中讀取數(shù)據(jù)(使用該地址作為/proc/PID/mem 的 offset)。這種做法 "可行",但它受到 TOCTTOU 的競(jìng)態(tài)沖突問(wèn)題的影響。
在 seccomp()通知機(jī)制被加入之后,人們立即開(kāi)始思考如何創(chuàng)建一個(gè)安全框架(security framework),例如,查看 open()系統(tǒng)調(diào)用的路徑名參數(shù)來(lái)決定是否允許或拒絕訪問(wèn)某一個(gè)特定的文件。然后,如果文件名沒(méi)有問(wèn)題,它可以告訴內(nèi)核繼續(xù)進(jìn)行系統(tǒng)調(diào)用。被過(guò)濾的進(jìn)程可能已經(jīng)擁有打開(kāi)文件所需的權(quán)限,但如果過(guò)濾進(jìn)程決定它不應(yīng)該訪問(wèn)該文件的話就可能會(huì)被拒絕。不過(guò),在檢查完成后,該進(jìn)程可以簡(jiǎn)單地修改這個(gè)參數(shù),而內(nèi)核也會(huì)很高興地直接打開(kāi)該文件。
這就限制了能夠從 filter 來(lái)繼續(xù)進(jìn)行系統(tǒng)調(diào)用這個(gè)方案的用處。只有在最終的安全邊界(也就是內(nèi)核本身)無(wú)論如何都會(huì)拒絕這個(gè)動(dòng)作的情況下,才能做到這一點(diǎn),就像來(lái)自非特權(quán)容器的 mknod() 的處理那樣。這意味著 seccomp() 通知機(jī)制不能用于給特權(quán)容器實(shí)現(xiàn)安全策略的場(chǎng)景。為了警告人們不要這樣做,Brauner 說(shuō),他們?cè)?seccomp.h 中加了注釋來(lái)描述這些問(wèn)題。
一般來(lái)說(shuō),seccomp()系統(tǒng)調(diào)用攔截需要在 host 上有一個(gè)受信任的、有特權(quán)的進(jìn)程來(lái)監(jiān)督這個(gè)調(diào)用。例如,在嵌套運(yùn)行非特權(quán)容器的情況下,讓外部容器的容器管理器監(jiān)督來(lái)自內(nèi)部容器的調(diào)用是沒(méi)有什么用的,他說(shuō)。在規(guī)劃這一安全設(shè)施的用途時(shí)需要記住這一點(diǎn)。
Target system calls
Graber 在這時(shí)接手描述了他們一直在努力攔截的系統(tǒng)調(diào)用,這與他們?cè)诼迳即?LPC 開(kāi)始時(shí)的 list 完全不同。這并不奇怪,因?yàn)榧词乖谀莻€(gè)時(shí)候,他們也早就知道 list 上的一些東西很難或不可能處理好。目前的列表是 mknod(),如前所述,setxattr(),bpf(),sched_setscheduler(),mount(),和 sysinfo()。這些都是為 LXD 實(shí)現(xiàn)的。其他項(xiàng)目一直在使用 LXD 所做的工作,并且可能正在努力攔截其他系統(tǒng)調(diào)用。
攔截 mknod()/mknodat() 允許 LXD 在非特權(quán)容器中運(yùn)行 debootstrap 等工具。這意味著可以在這些容器中構(gòu)建發(fā)行版鏡像了。這些調(diào)用需要被攔截的另一個(gè)原因是允許容器為 overlayfs 創(chuàng)建 whiteouts。例如,這允許 Docker 將其 layer 解壓到一個(gè)無(wú)特權(quán)的容器中。Graber 說(shuō),他認(rèn)為在 LXD 的限制下攔截 mknod() 是 "相對(duì)安全"的。他沒(méi)有發(fā)現(xiàn)任何問(wèn)題,但 LXD 容器中默認(rèn)不啟用該功能。不過(guò)項(xiàng)目組認(rèn)為大多數(shù)容器都可以啟用該功能。
setxattr()在 overlayfs 中提供了一種標(biāo)記已刪除目錄的方法,所以 LXD 也需要支持它。有一個(gè)擴(kuò)展屬性(xattrs)的允許列表,可以在無(wú)特權(quán)的容器中設(shè)置。顯然,只有一些屬性是可以 allow 的,因?yàn)樵谀承┟臻g設(shè)置這些屬性,如 "security.*" 這些 xattrs "會(huì)是非常糟糕的",Graber 說(shuō)。
Brauner 隨后描述了 mount() 調(diào)用的情況。他說(shuō),在 mknod()的情況下,沒(méi)有必要在 supervisor/manager 中 "對(duì)權(quán)限級(jí)別或安全級(jí)別進(jìn)行任何調(diào)整"。它可以直接訪問(wèn)容器的 mount 命名空間并在其中創(chuàng)建設(shè)備節(jié)點(diǎn)。對(duì)于 mount()來(lái)說(shuō),事情并不那么簡(jiǎn)單。
在代表容器來(lái)執(zhí)行 mount()時(shí),有許多安全屬性需要處理,如 Linux capability、LSM profile、UID 和 GID 等用戶 ID、各種命名空間(如 mount、PID 或 user 命名空間)等等。管理器中模擬的調(diào)用需要對(duì)容器中請(qǐng)求進(jìn)程的身份進(jìn)行假設(shè),這樣在執(zhí)行 mount 時(shí)就不會(huì)出現(xiàn)額外的權(quán)限。他說(shuō):"要做到這一點(diǎn)真的很棘手"。
鑒于此,他問(wèn)道:"為什么要攔截 mount()系統(tǒng)調(diào)用?" 在有些情況下,host 為容器提供了一個(gè)文件系統(tǒng),而容器管理器可以證明這一點(diǎn)。在這些很有限的情況下,允許文件系統(tǒng)被 mount 是確實(shí)有用的。然而,你不能允許在容器內(nèi)任意進(jìn)行 mount,因?yàn)橛锌赡艹霈F(xiàn)惡意的文件系統(tǒng)映像(malicious filesystem image)。
容器管理器可以模擬 mount()調(diào)用,所以它可以避免可能發(fā)生的 TOCTTOU 競(jìng)爭(zhēng)問(wèn)題,因?yàn)榇蠖鄶?shù)參數(shù)都是指針。mount()系統(tǒng)調(diào)用也有問(wèn)題,因?yàn)樗且粋€(gè) "可怕的多路復(fù)用的功能",除了 mount 一個(gè) block 設(shè)備上的文件系統(tǒng)外,還可以執(zhí)行各種各樣的操作:bind-mount、mount 一個(gè)偽文件系統(tǒng)、改變 mount 或改變 superblock 屬性等等。攔截系統(tǒng)調(diào)用目前是有價(jià)值的,盡管如果能在虛擬文件系統(tǒng)(VFS)層實(shí)現(xiàn) "delegated mounting" 功能在未來(lái)可能是一個(gè)更好的解決方案。
Graber 說(shuō),LXD 允許容器內(nèi)的 mount 自動(dòng)實(shí)現(xiàn)用戶和組 ID 的重新映射。它也有一種模式可以攔截 mount,并將其變成使用用戶空間的文件系統(tǒng)(FUSE)的等效 mount。這使得它 "相當(dāng)安全",因?yàn)槲募到y(tǒng)實(shí)際上沒(méi)有直接通過(guò)內(nèi)核 mount,而是由容器內(nèi)的一個(gè)用戶空間進(jìn)程來(lái)處理的。
Brauner 說(shuō),他已經(jīng)實(shí)現(xiàn)了一個(gè) bpf()攔截的原型驗(yàn)證,其中使用了他在過(guò)去幾年中完成的 pidfd 工作。要模擬那些返回文件描述符的系統(tǒng)調(diào)用(如 open()和 bpf())會(huì)有一個(gè)困難,因?yàn)槲募枋龇枰c請(qǐng)求進(jìn)程共用。pidfd API 就可以將描述符安全地提供給另一個(gè) task 了。LXD 限制了容器中可以運(yùn)行的程序;它允許的一個(gè)程序可以讓容器進(jìn)一步限制對(duì)其中設(shè)備的訪問(wèn)。
Graber 說(shuō),sched_setscheduler() 攔截在 LXD 看來(lái)并不安全;"我覺(jué)得它很可疑",Brauner 說(shuō)。但是,Graber 說(shuō),Android 經(jīng)常使用這個(gè)調(diào)用,所以當(dāng)在非特權(quán)容器中運(yùn)行 Android 時(shí),它就可以被啟用。然而,這可能會(huì)導(dǎo)致各種問(wèn)題,所以應(yīng)該謹(jǐn)慎使用,當(dāng)然盡量別用。
最近添加了 sysinfo()攔截,從而可以進(jìn)一步支持 LXCFS 的一個(gè)功能,它可以根據(jù)容器的 cgroup 限制,而不是根據(jù)系統(tǒng)范圍內(nèi)的相應(yīng)數(shù)值來(lái)報(bào)告有多少內(nèi)存可用等信息。這很好,但有很多工具還在使用 sysinfo()來(lái)獲取報(bào)告值,所以它們?nèi)匀粫?huì)顯示 host 范圍的全局?jǐn)?shù)值。通過(guò)攔截這個(gè)調(diào)用,就可以在容器內(nèi)報(bào)告出來(lái)通常的運(yùn)行時(shí)間、內(nèi)存數(shù)量等信息。
Graber 隨后演示了 LXD 容器中的各種攔截。作為一個(gè)例子,他展示了 sysinfo()的攔截。他以 256MB 的內(nèi)存限制啟動(dòng)了容器,在容器內(nèi),free 命令確實(shí)正確顯示了這個(gè)信息。這是因?yàn)?LXCFS 被掛載在/proc/meminfo 上,所以它可以攔截對(duì)該文件的讀取。但是,運(yùn)行一個(gè)查詢 sysinfo() 的二進(jìn)制文件,報(bào)出來(lái)的卻是他的筆記本上的 16GB。重新啟動(dòng)帶有攔截功能的容器之后,就可以解決這個(gè)小問(wèn)題了。
Brauner 說(shuō),sysinfo() 攔截所使用的所有信息都來(lái)自 LXCFS 已經(jīng)收集的信息卻不通過(guò)系統(tǒng)調(diào)用報(bào)告,這導(dǎo)致了多個(gè) bug report。例如,Java 通過(guò) sysinfo()查看可用的內(nèi)存,并會(huì)在此基礎(chǔ)上進(jìn)行內(nèi)存預(yù)分配。此外,Graber 說(shuō), Alpine Linux 中的 free 使用(或曾經(jīng)使用過(guò))sysinfo(),從而導(dǎo)致了 LXD cgroup limit 相關(guān)的 bug report。
最后,他們對(duì)未來(lái)的工作提出了一些想法。Brauner 說(shuō),他想探索在 seccomp() filter 中至少添加一些有限的對(duì) eBPF 的支持。長(zhǎng)期以來(lái),帶有指針參數(shù)的新系統(tǒng)調(diào)用都被拒絕了,因?yàn)?seccomp()不能解析指針。這種情況已經(jīng)改變了,所以一些多功能的 API,如 io_uring 以及新的可擴(kuò)展系統(tǒng)調(diào)用方案(extensible system call scheme)并沒(méi)有被阻止。但這導(dǎo)致了另一個(gè)問(wèn)題。
GNU C 庫(kù)(glibc)想轉(zhuǎn)而使用 clone3()系統(tǒng)調(diào)用,但觸犯了許多容器中安裝的 seccomp() filter 的限制。這些 filter 根本不允許 clone3(),因?yàn)樗械膮?shù)都在指針之后,不能被檢查到。舊的 clone()系統(tǒng)調(diào)用有一個(gè)直接傳遞的 flags 參數(shù),因此可以用來(lái)決定系統(tǒng)調(diào)用是否應(yīng)該繼續(xù)。所以 Brauner 希望看到一些機(jī)制來(lái)檢查指針后的那些參數(shù),而如果能有限地支持 eBPF 就可以符合這一要求。在過(guò)去,seccomp() 的維護(hù)者 Kees Cook 一般都反對(duì)這樣做,但 Cook 今年沒(méi)有出席 LSSNA 會(huì)議。
除此之外,Graber 說(shuō),可能會(huì)對(duì)內(nèi)核 module 加載來(lái)提供某些有限的支持。這個(gè)想法讓很多人感到害怕,卻是也應(yīng)該害怕,但它將會(huì)是嚴(yán)格限制在對(duì) init_module()/finit_module() 的攔截。并不會(huì)允許容器實(shí)際加載 module;相反,容器將傳入它想加載的內(nèi)容,如果該 module 通過(guò)了一些檢查,容器管理器將加載該 module 的 host 版本。這方面的一個(gè)應(yīng)用是容器中需要各種網(wǎng)絡(luò)模塊的防火墻。他說(shuō),現(xiàn)在,有一個(gè) module 列表會(huì)在容器啟動(dòng)時(shí)被加載,但如果能按需加載 module 就更好了。
關(guān)于 seccomp() filter 的一個(gè)有趣的事情是,攔截甚至在查詢系統(tǒng)調(diào)用表之前就已經(jīng)完成了,這意味著新的系統(tǒng)調(diào)用可以完全在用戶空間創(chuàng)建。新的系統(tǒng)調(diào)用將簡(jiǎn)單地被定義為一個(gè)未被使用的系統(tǒng)調(diào)用編號(hào),它將被 filter 攔截從而可以調(diào)用新的代碼。這可以用來(lái)作為新系統(tǒng)調(diào)用的實(shí)現(xiàn)原型方案了。他還沒(méi)有看到有人真的這樣做,但這是一種可能性。
[作者要感謝 LWN 的用戶支持才能去奧斯汀參加 LSSNA。]
全文完
LWN 文章遵循 CC BY-SA 4.0 許可協(xié)議。
長(zhǎng)按下面二維碼關(guān)注,關(guān)注 LWN 深度文章以及開(kāi)源社區(qū)的各種新近言論~
