張新義,武澤慧,賈 瓊,陳志浩
1(數學工程與先進計算國家重點實驗室,鄭州 450001) 2(北京計算機技術及應用研究所,北京 100000)
近年來,云計算技術蓬勃發展,云計算技術依賴于共享計算資源來實現按需擴展,從而節約和降低了運營和維護成本.在云計算中,容器因其開發操作方便和資源利用率高被認為是向云中部署微服務和應用程序的標準,成為了當前的主要趨勢.云原生計算基金會發布(CNCF)2021年的調查報告顯示[1],在企業實際生產部署中,容器的使用率已經增加到93%,相比于2016年第1次調查時的23%,5年的時間里增加了近3倍.
傳統的虛擬機技術使用Hypervisor來創建硬件抽象層,相比之下,容器作為一種操作系統級的虛擬化技術,依賴于由Linux內核提供的NameSpace和Cgroup等內核特性來實現容器之間的隔離.一個容器可以被認為是一組共享專用Linux內核資源的進程,因此在容器中運行的應用程序可以實現接近其宿主機的性能.然而,容器的隔離強度卻遠遠比不上虛擬機,容器與宿主機之間、以及同一主機上運行的容器之間的隔離完全由底層操作系統內核在軟件中強制執行,一個惡意容器可以利用宿主機上的內核漏洞來進行特權升級類的攻擊,進而達到損害主機以及在該主機上運行的所有其他容器的目的.
容器鏡像是容器的基礎,鏡像包含應用程序以及其相關依賴的一個基礎文件系統.容器鏡像通常在其他基礎鏡像上以層的方式構建,在鏡像制作的過程中,通常對一些不會在容器運行中使用的資源進行了過度積累(如文件、程序等),進而導致容器鏡像的體積過度膨脹.此外,Linux內核的代碼庫也一直在擴展,多年來公開的系統調用數量的增加表明了Linux內核代碼的膨脹.Linux內核中系統調用數從1991年第1個版本中的126個增加到5.11.0版本中的334個( x86_64(64-bit))[2].容器體量的增大和Linux系統調用的膨脹導致了容器攻擊面的增大.
作為一種緩解策略,容器攻擊面減少技術最近開始獲得關注,依據安全方面的最佳實踐:最小特權原則和特權分離原則,安全研究人員主要從兩個方面來減少容器的攻擊面:減少容器的資源或限制容器的功能.以前的許多工作都在不同層次上應用了這個概念,如根據應用程序的需求定制內核代碼[3,4]、從共享庫中刪除使用不到的函數[5-9]以及限制容器或應用程序的系統調用[10-15]等.
NIST容器安全指南[16]中的建議是最好通過限制容器可用的系統調用來減少攻擊面.盡管各種方法的形式的多樣的,但是所有這些方法的共同挑戰是如何準確的識別系統調用集.大部分工作依賴于動態分析和訓練[10-12],使用現實的工作負載來對容器進行訓練,之后刪除或者限制所有訓練中沒有覆蓋到的代碼或系統調用.這種方法最大限度地提高了可以限制的系統調用數量,但是動態訓練往往無法獲取全部工作負載所需的系統調用集,尤其是一些很少被執行的部分,如錯誤處理、緩沖處理等,因為在訓練期間沒有覆蓋到的功能無法保證永遠不會使用.一些工作依賴于靜態分析[13,17,18],通過靜態分析容器中的可執行文件和庫來獲取系統調用集合,這種方法雖然可以彌補動態訓練結果的不完備,但是靜態分析的范圍往往受限,而且會得到一個比實際情況更大的系統調用集,達不到理想的減少容器攻擊面的效果.
在本文中,容器可用性的概念主要指以下兩個方面:1)功能的完備性:受限容器需要能夠運行正常容器實現的所有功能;2)性能的穩定性:受限容器的工作負載與正常容器的差異處于可接受的范圍之內.簡單來說,用戶在使用時,感覺不到受限容器與未受限容器之間的差異,受限容器不會因為運行正常的功能需求而報錯.
上面兩種方法都在尋求滿足容器運行的最小系統調用集合,假設前提是在分析階段沒有用到的系統調用在以后的實際運行中也不會用到,這在保護容器安全性的同時,卻忽略了容器的可用性,受限的容器僅僅運行極少的一部分功能,一旦在實際生產環境中部署,應用程序的一個合規操作可能會由于觸發違規策略而被終止,導致嚴重影響.
鑒于以前學者們的工作,本文提出了AutoSec,一種容器限制策略自動生成和靈活配置的方法.AutoSec綜合考慮了容器的安全性與可用性的均衡,采用動靜態結合的方式來提取系統調用集合,并根據容器生命周期的不同階段動態的生成Seccomp BPF策略,在容器部署后可以實現策略的靈活配置.AutoSec主要分為兩個階段:策略提取階段和策略實施階段.策略提取階段由動態訓練引擎和靜態分析引擎組成,給定一個應用程序容器,動態訓練引擎追蹤容器初始化階段的系統調用以及容器運行階段的應用程序集合,靜態分析引擎再對應用程序集合進行靜態分析來獲取應用程序中系統調用的集合進而生成應用程序與系統調用之間的映射.在策略實施階段,容器首先使用初始化時獲取的系統調用集合啟動;在運行時,為了實現靈活性的配置,根據容器的需求動態的生成Seccomp BPF的規則文件以限制容器的系統調用.實驗選取Docker hub上最為流行的4款功能型容器,驗證了AutoSec在減少容器攻擊面的有效性.
本文的主要工作包括以下幾個方面:
1)本文提出了一種動靜態結合的容器系統調用提取方法,可以有效的提取容器初始化階段所需的系統調用集合和生成應用程序與系統調用之間的映射.
2)本文提出了一種需求驅動的系統調用調度方法,根據容器生命周期的不同階段,按需調度容器中可用的系統調用.
3)根據以上工作,本文設計了一種通用的系統AutoSec,并選取Docker hub上最為流行的4款容器,驗證了AutoSec在較少的性能損耗內可以有效的減少70%以上的系統調用數量.
Linux容器是一種操作系統級的虛擬化方法,可在同一宿主機的內核上運行多個用戶群組.Linux內核使用Namespace[19]、Capabilities[20]、和Cgroup[21]來提供不同容器之間的隔離.
Namespace,即命名空間,Linux內核通過實現命名空間機制來實現資源的隔離.命名空間為容器提供了最原始也是最簡單的隔離方式,保證一個容器中運行的進程看不到或者影響不到運行在另一個容器中的進程或者容器主機的進程.目前,Linux內核提供了8種類型的命名空間,即IPC、Network、Mount、PID、Time、User、Cgroup和UTS,分被負責信號量、掛載點、進程號等資源的隔離.
Cgroup(Control group)是Linux內核的另一個重要特性,主要用來實現對于資源的限制和審計.在容器技術的實現中,Cgroup提供了多種度量標準來確保每個容器獲得公平的CPU、內存和I/O等資源,同時,Cgroup限制某個容器的資源使用量,防止其資源耗盡致使系統性能降低.Cgroup目前支持13種資源,包括cpu,cpuacct,cpuset,freezer,perf event,memory,hugetlb,rdma,blkio,pid,device,net_cls和net_prio,每個資源類型都由相應的Cgroup控制器管理.
Linux 還支持將部分root 的特權操作權限細分成 Capabilities,如果將某個權限賦給某進程,即使不是 root用戶也可以執行該權限對應的特權操作.該機制可加強容器內部的權限管控,使容器內外的 root 權限隔離.
系統調用是指運行在用戶空間的程序向操作系統內核請求需要更高權限運行的服務.系統調用提供了用戶程序與操作系統之間的接口,系統調用接口幾乎允許程序與網絡、文件系統和其他敏感系統資源的所有交互.
在容器環境中,盡管使用操作系統所提供的Capabilities、Namespace等嚴格的軟件隔離機制,但惡意容器仍然可以通過利用內核漏洞來繞過這些隔離機制.與此同時,為了支持新的特性、協議和硬件,Linux內核的代碼一直在擴展.盡管不同的應用程序使用不同的系統調用,但幾乎都用不到內核提供的全部系統調用,剩余的系統調用處于程序可用但是用不到的狀態,但是可供攻擊者所利用,進而突破隔離機制達到訪問敏感資源甚至特權升級的效果.而且每個新的系統調用函數都是訪問整個內核代碼的大部分的入口點,從而導致內核的攻擊面增大[22].限制可用的系統調用數量是減少程序攻擊面的一種有效的方法,已經存在大量的相關工作,一些系統調用限制工具,如Janus[23]、Systrace[24]、ETrace[25]以及Seccomp[26]等能有效的執行系統調用限制策略.
Seccomp(Secure Computer)是Linux內核的一個特性,用于限制阻塞進程的敏感系統調用和危險參數.Seccomp支持兩種模式:嚴格模式和過濾模式.當進程應用嚴格模式時的Seccomp策略時,只能執行4種系統調用,即read()、write()、exit()、sigreturn();過濾模式即Seccomp BPF,Seccomp BPF在Linux 3.5內核版本中引入,支持通過使用Berkeley的數據包過濾器做過濾規則匹配,與嚴格模式不同,Seccomp BPF允許用戶指定需要限制的系統調用和參數.
Docker默認支持使用Seccomp BPF來過濾容器可用的系統調用.目前,Docker提供的默認配置禁止了諸如bpf、mount、ptrace 等40多個系統調用[27],可有效防御針對CVE-2014-3519、CVE-2015-8660、CVE-2016-0728 等漏洞的攻擊.但是,最小特權原則[28]規定,程序應該被限制只訪問完成其操作所必需的資源,對于某一個特定容器來說,其實用不到Docker默認允許的大多數系統調用.除了默認的策略外,Docker還允許用戶使用“--security-optseccomp”選項為Docker配置用戶自定義的Seccomp規則文件.
圖1顯示了AutoSec的基本架構,它由兩個主要的階段組成,策略提取階段和策略實施階段.

圖1 AutoSec架構Fig.1 AutoSec architecture
策略提取階段負責提取滿足容器運行所需的系統調用集合,包括兩個主要的組件,動態訓練引擎和靜態分析引擎.首先通過動態訓練引擎獲取容器初始化時所需的系統調用集合以及在運行時使用的應用程序集合;接著靜態分析引擎對應用程序集合進行分析,得到每個程序在運行時所對應的系統調用集合.最終得到容器在初始化階段和程序運行階段兩種不同階段的系統調用集合.策略實施階段負責為容器施加Seccomp BPF策略,包括3個主要的組件,監控器、生成器和調度器.首先使用動態訓練中獲取的容器初始化系統調用集合生成的Json格式的Seccomp配置文件來啟動容器;并在容器運行期間維護一個系統調用池,池中的初始系統調用集合為啟動容器所需的集合,并使用監控器對容器的運行狀態進行監控,根據容器運行不同應用程序的系統調用需求,調度器會動態的改變的系統調用池中的內容,并使用生成器生成相應的Seccomp BPF策略來動態調度容器可用的系統調用.
策略提取階段目的是提取目標容器的系統調用列表.鑒于前人的工作[10,11],動態訓練使用系統可見性工具追蹤程序的執行以發現程序在運行時所使用的系統調用,但是動態分析通常不能發現所有可能的執行路徑;靜態分析[13,17]能夠更全面的發現程序中所使用的系統調用,但是往往得到的結果是一個比實際程序所使用系統調用大的多的集合.為了得到一個相對精確的系統調用集合,本文采用動靜態結合的方式,分階段和方式為容器提取系統調用集合.
2.2.1 動態訓練
動態訓練主要包括兩個目標:在容器啟動時獲得容器初始化所必須的系統調用,以及在容器運行時獲得必要的可執行文件.
動態分析引擎使用Sysdig[29]對容器進行監控.Sysdig是一款原生支持容器的通用性系統可見性工具,它支持多種輸出過濾模式來獲取系統內容.當使用dockerrun命令啟動一個容器時,容器會首先進行初始化,Docker引擎會調用多組系統調用,此時,動態訓練引擎監控捕獲其系統調用集合,該集合用作生成容器啟動時的安全策略基礎.容器初始化完成后,基本服務啟動,開始進入正常運行階段,根據容器最初的設定的腳本開始執行相應的功能,動態分析引擎開始捕獲容器運行了哪些可執行文件,獲取其在容器中使用的可執行文件的集合.
1)獲取初始化系統調用
在Docker的體系架構中,創建和啟動容器由docker daemon負責,當docker daemon收到來自與docker client的啟動容器的請求時,containerd會根據用戶的請求來執行不同的操作,如掛載文件系統、初始化容器網絡等,但是containerd并不會直接去操作容器,而是創建一個containerd-shim的進程來對容器進行操作.在啟動一個新容器時,需要做一些 Namespace 和 Cgroup 的配置,以及掛載 root 文件系統、使Seccomp配置文件生效等,這些操作都有了標準的規范,即OCI(開放容器標準),runc 就是OCI的一個標準實現,containerd-shim會調用runc來執行這一系列操作來啟動容器,此時runc已經被施加Seccomp策略,所以動態分析引擎首先要記錄runc運行的系統調用.
此外,在大部分容器中,通常會有名稱類似于docker-entrypoint.sh的 shell腳本,它主要用于在啟動應用程序之前做一些準備工作,如設置環境變量、設置配置文件等.如果存在這樣的腳本,那么在動態分析階段也需要記錄由該腳本執行的系統調用.
2)獲取運行時應用程序
容器初始化完畢后,容器對應功能啟動,容器開始進入運行階段.容器中的應用程序大體可以分為兩類,一類為容器功能相關的關鍵性應用,如Mysql容器中的mysqld、mysql_conf、mysqlsh等相關程序,另一類為容器功能無關的輔助性應用,如find、bash等.在容器運行的過程中,除了功能必須所需的應用外,還存在一些輔助性工具來滿足容器的可操作性,因此動態訓練需要引擎找出容器在運行時使用的多個應用程序.
不同功能的容器類型、不同的執行環境都會使得容器所使用的系統調用集合以及應用程序存在差異.為了增加獲取應用程序的覆蓋率,本文從兩個方面對容器進行訓練,即容器命令和應用程序命令.容器命令如dockerrun有很多的參數,如--net指定容器的網絡連接類型、--volume指定掛載操作、--env-file指定文件讀入環境變量等功能,在容器運行中,根據用戶的需求進行設置會可能對容器的環境造成影響.本文從Docker hub中爬取對應鏡像的文檔說明中容器命令執行的部分,作為訓練容器的命令之一.對于應用程序命令,本文為不同的容器制定了功能覆蓋腳本以提高覆蓋到的應用程序,確保能夠覆蓋絕大多數的容器功能,并在訓練期間,通過手動輸入命令來覆蓋腳本無法觸及的應用程序.
經過動態訓練,AutoSec得到了兩個集合:容器啟動時的系統調用集合和容器運行時的應用程序集合.
2.2.2 靜態分析
Linux中的應用程序大多為ELF可執行文件,在經過動態訓練獲得容器運行時需要的應用程序后,AutoSec通過靜態分析的方式來獲得應用程序的在運行時所需的系統調用.直接通過動態訓練監控程序運行來獲取系統調用的結果是不健全的,因為無法保證在訓練階段執行路徑的覆蓋率,如Nginx中緩存管理機制,當緩存滿時,Nginx會生成一個單獨的緩存管理進程,該進程使用unlink()系統調用來處理緩存,清除舊的緩存文件.但是,動態訓練往往只是監控到Nginx緩存管理程序初始化,可能無法捕獲緩存文件被刪除的過程,因此無法捕獲到unlink()系統調用的使用.而且,程序在正常運行期間,unlink()系統調用并不會在其他的地方使用,而且單純的延長訓練時間也并不能很好的解決問題.所以,相比于動態捕獲程序在運行時使用的系統調用集合,直接對可執行文件進行靜態逆向分析,往往能覆蓋程序較為全面的系統調用集合.
Linux為用戶程序提供了兩種系統調用的方法,即直接調用和庫函數調用.用戶程序可以調用函數syscall()來直接調用系統調用,而且程序可以通過將嵌入匯編指令將系統調用號傳遞到EAX/RAX寄存器,觸發軟件中斷以切換到內核空間的方式來直接調用系統調用.然而直接調用的方法有兩個主要的缺點:首先,當直接調用系統調用時,程序可能會在用戶空間和內核空間之間引入頻繁的上下文切換.例如,當使用write()系統調用將字符串輸出到文件時,程序必須先切換到內核空間,并為每個字符返回到用戶空間,這種切換可能會給程序帶來巨大的開銷;其次,由于系統調用在不同的操作系統中有所不同,因此直接調用的方式不容易在不同的操作系統中移植.
大多數應用程序更多是通過調用標準庫提供的庫函數來調用系統調用,如GNUC庫(gLibc),它是大多數容器中使用的最流行的Libc實現,gLibc提供了一種通用、高效、安全的方式來調用不同操作系統上的系統調用.例如,當使用printf()輸出一個字符串時,首先程序會緩沖所有字符,然后使用write()系統調用刷新到一個文件中.在這種情況下,只需要一次用戶空間到內核空間的切換,這大大減少了用戶態與內核態來回切換的開銷,由于庫函數的效率和方便性,幾乎所有的程序都使用庫函數來調用系統調用.
1)直接系統調用提取
Linux中進行系統調用最直接的方式就是使用syscall()函數或原生的syscall匯編指令,在調用Libc庫中沒有包裝函數的系統調用時,往往采用這種方式.分析這類調用需要獲得被傳遞給系統調用的參數的值.
靜態分析引擎使用二進制代碼分解來提取syscall指令分配給RAX/EAX寄存器和syscall()函數分配給RDI/EDI寄存器的值來識別系統調用號.系統調用號沒有在指令中編碼,而是在一個寄存器中提供,如x86_64上的RAX寄存器,靜態分析引擎通過推斷寄存器的內容來獲得系統調用號.首先,引擎使用Objdump 將二進制文件反匯編,從找到syscall指令開始,利用反向符號執行技術尋找RAX/EAX寄存器的值,最終返回包含系統調用號的符號值.之后將系統調用號映射到對應的系統調用,得到直接系統調用的集合.
2)庫函數調用提取
使用庫函數進行系統調用是最為常用的一種方式,為了提取程序使用庫函數進行的系統調用,靜態分析引擎首先生成庫函數與系統調用之間的映射,之后通過提取應用程序的函數列表,來生成應用程序與系統調用的映射,具體步驟如下:
·生成庫函數與系統調用之間的映射
對于程序調用的每個庫,靜態分析引擎首先構建一個函數調用樹(FCT)來表示庫中的函數調用關系.然后結合所有文件中的FCT,構建一個有向無環函數調用圖(FCG),FCG表示庫函數之間的所有函數調用關系.此外,由于系統調用不會調用其他庫函數,因此系統調用總是出現在FCG的結束節點上.因此,可以通過遍歷FCG中的所有路徑來構建從庫函數到系統調用的映射表.
·提取二進制文件的函數列表
給定一個ELF可執行文件,函數提取器提取每個程序中所有被調用的庫函數.利用包含所有全局變量和函數信息的ELF符號表來派生出被調用的函數.具體來說,分析器首先使用工具Readelf[30]獲取ELF文件的可讀的ELF符號表,然后根據庫到系統調用的映射表提取庫函數.
·生成二進制文件與系統調用的映射
在獲得程序的所有調用庫函數之后,將標識的函數列表與庫到系統調用的映射表進行交叉檢查,最終生成應用程序與系統調用之間的映射.
策略實施階段負責啟動容器并實時監控容器的執行情況,在不同的執行階段動態地更改一個容器內的所有進程的可用系統調用列表,限制容器在不同的階段中只使用該階段允許的系統調用,并按照容器內運行的程序動態的改變可用系統調用的數量.
2.3.1 Seccomp BPF策略的實現形式
BPF在1992年的Tcpdump[31]程序中首次提出,Tcpdump是一個網絡數據包的監控工具,但是由于數據包的數量很大,而且將內核空間捕獲到的數據包傳輸到用戶空間會帶來很多不必要的性能損耗,所以要對數據包進行過濾,只保留感興趣的那一部分,而在內核中過濾感興趣的數據包比在用戶空間中進行過濾更有效.BPF就是提供了一種進行內核過濾的方法,因此用戶空間只需要處理經過內核過濾的后感興趣的數據包.
Seccomp 過濾模式允許開發人員編寫 BPF 程序來確定是否允許給定的系統調用,基于系統調用號和參數(寄存器)值進行過濾.在Linux內核中,一個進程可以被附加到多個Seccomp過濾器中,并且所有的Seccomp過濾器都被組織在一個單向鏈表中,每個Seccomp過濾器都被實現為一個由Seccomp指令組成的程序代碼,即一個bpf_prog結構,bpf_prog解析sock_filter的指令,如圖2所示.

圖2 Seccomp BPF的內核實現Fig.2 Kernel implementation of Seccomp BPF
在得到策略提取階段對應的系統調用列表之后,生成器根據不同的使用階段生成不同的形式的Seccomp BPF策略,包括兩種形式,一種是作為限制容器啟動時容器引擎所用的安全策略,一種作為容器運行時程序所需的安全策略.
2.3.2 使用Seccomp BPF保護容器安全
通過提取容器運行時的系統調用來生成Seccomp BPF策略來對容器進行限制,雖然在一定程度上可以有效的減少來自于內核系統調用的威脅.但是動態分析得到的系統調用集合往往會使得受限容器的可用性不足;靜態分析得到的系統調用集合過大,達不到減少容器攻擊面的理想效果.綜合考慮前人的工作,AutoSec根據容器運行的不同階段,以容器內應用程序為粒度,動態的來調度容器可用的系統調用.
Docker在啟動時,允許用戶使用自定義的Seccomp策略啟動,如表1所示,該策略表示默認禁止所有系統調用,以白名單的方式允許chdir()系統調用的使用.但是容器一旦啟動,就無法通過Docker 客戶端對策略進行更改.雖然Linux內核提供了兩個系統調用prctl()和seccomp()用來更改某個進程的Seccomp過濾規則,但是在本文中卻不可取.因為它們只能在容器內部的進程上配置Seccomp過濾器,但是本文需要從容器外部更改容器內部的進程的Seccomp過濾器.而且,在添加了一個Seccomp過濾器后,在進程運行時不能刪除或更改,即這兩個系統調用來添加新的Seccomp過濾規則,但不能刪除已添加的Seccomp規則.因此調度器選擇定位Seccomp過濾器指針的內存地址,然后將指針指向每一個對應的應用程序生成的bpf_prog結構體以實現動態的策略部署.具體來說,分為以下4個步驟,如圖3所示.

表1 Docker Seccomp 規則文件實例Table 1 Example of a Docker Seccomp ruleset file

圖3 動態調度容器的系統調用的工作流Fig.3 Workflow of dynamically schedule the container′s system call
1)使用Seccomp策略啟動容器
首先根據動態分析得到的系統調用列表生成表1格式的Seccomp限制策略,策略默認禁止所有系統調用,并使用白名單的形式賦予可用的系統調用.在容器啟動時,使用-security-opt指定該文件.接著容器引擎開始進行一系列的初始化工作,containerd-shim在調用runc時,Seccomp開始生效,并作用于所有容器的所有的進程,此時,容器內所有的進程都被施加了Seccomp策略.
此外,AutoSec開始維護一個系統調用池,此時池中的元素為動態分析所得到的系統調用集合.
2)監控容器運行并動態更新調用池
容器開始運行后,此時系統調用池中可用的系統調用可能不能滿足容器內程序所運行的需求.AutoSec基于Sysdig實現了一個監控器來捕獲容器想要執行的程序,接著,對于捕獲到特定程序,監控器根據靜態分析中得到的二進制文件與系統調用的映射,導出執行該程序所需的系統調用集合,并將其更新到系統調用池中,如算法1所示.
算法的輸入為狀態變動任務Task和系統調用池Syscall_pool;輸出為更新過的Syscall_pool.當監控器檢測到一個任務狀態的變化,會根據應用程序與系統調用的映射圖將程序映射到對應的系統調用Syscalls(1~2行),接著調度器檢查任務的行為,若為啟動狀態,則遍歷Syscalls并將其加入系統調用池中,并將加入池中的系統調用的使用數(Syscall.use_num)加1,代表當前有多少任務在使用該系統調用(3~12行);若任務的狀態為結束,則對于Syscalls中的每一個系統調用,將其系統調用的使用數(Syscall.use_num)減1,當使用數等于0時,則代表該系統調用沒有程序正在使用,調度器將其從池中刪除(13~20行).
3)Seccomp BPF規則構造與更新
系統調用池中更新后,生成器需要根據系統調用池中的集合從新生成結構體.具體來說分為兩個步驟.首先,生成器使用libseccomp[32]庫將一個系統調用轉換為BPF過濾器指令,使用libseccomp庫中的seccomp_rule_add()方法來添加所有可用的系統調用,并使用seccomp_export_bpf()方法來導出生成的BPF過濾程序.接著使用內核函數bpf_prog_create_from_user()將在用戶空間生成的BPF過濾程序傳遞的內核空間,并在內核空間中生成bpf_prog.函數bpf_prog_create_from_user()有兩個參數,一個指向用戶空間的bpf程序指針和一個指向內部內核函數seccomp_check_filter()的函數指針,首先將用戶空間過濾器緩沖區復制到內核緩沖區中,然后調用傳入seccomp_check_filter()函數,將經典的BPF過濾器程序轉換為Seccomp BPF過濾器程序,最后,生成包含帶有新指令集的bpf_prog結構.
算法1.動態調度算法
輸入:Task:任務,Sycall_pool:系統調用池
輸出:Syscall_pool:系統調用池
1.Syscalls←[] //初始化映射

3.ifTask.action==Startthen
4.forSyscallinSyscallsdo
5.ifSyscallinSyscall_poolthen
6.Syscall.ued_num++
7.continue;
8.Syscall_pool.add(syscall)
9.Syscall.ued_num++
10.endif
11.endfor
12.endif
13.ifTask.action==Stopthen
14.forSyscallinSyscallsdo
15.Syscall.ued_num--
16.ifSyscall.use_num==0
17.Syscall_pool.delete(syscall)
18.endif
19.endfor
20.endif
21.returnSycall_pool
4)動態改變Seccomp 過濾器
使用新生成的bpf_prog結構體,調度器可以動態的更改所有容器內進程的Seccomp 過濾器.如圖3所示,當容器使用“dockerrun-itd-security-optseccomp” 命令啟動容器時,容器中的第1個進程(PID=1)被施加了Seccomp過濾器,它是容器內所有進程的父進程,它的子進程都會繼承該過濾器,并包含指向bpf_prog結構的相同指針,當更改容器中一個進程的bpf_prog結構時,所有進程的Seccomp過濾器都會被更改.調度器通過(PID=1)的進程的task_struct來定位bpf_prog在內存中位置,然后修改內存的內容,將seccomp_filter中的filter指針指向新生成的bpf_prog結構.
實驗使用一臺16g內存的x86_64計算機,英特爾酷睿i7-10700 CPU和Ubuntu 20.04主機操作系統來對AutoSec進行驗證.不同操作系統的系統調用數量不同,在本文的實驗環境中,主機操作系統總共有334個系統調用.Docker 在默認情況下禁止44個系統調用,所以在本文的實驗環境中,Docker默認允許的系統調用數量是290個.本文使用的Docker版本是20.10.18,在動態監控階段,使用Sysdig 0.29.3來跟蹤系統調用.實驗選取了Docker hub上下載量超過10億的Linux官方容器進行了系統的統計分析,并選取4款最流行的容器對AutoSec進行測試.
根據容器的用途,可以將Docker hub上的官方鏡像分為基礎環境性容器和特定服務型容器.其中基礎環境性容器可分為操作系統型(如Ubuntu)和編程語言型容器(如Python),特定服務型容器主要包括web服務型和數據庫服務型,還有一些其他特定功能的容器,如表2所示.

表2 Docker hub 熱門鏡像Table 2 Docker hub popular images
操作系統型容器主要為作為構建其他容器的鏡像基礎,其鏡像大小受其功能的影響有較大差距,Busybox僅有1.24MB大小,而Centos的鏡像超過了231MB.編程語言型容器同樣經常作為用戶構建自身容器的基礎容器,Docker hub中使用最多的3個編程語言容器為Python、Golang和Openjdk(Java),其平均鏡像大小是所有類別中最大的,將近800MB.以Nginx為代表的web服務型容器以及Mysql為代表的數據庫服務型容器是Docker在實際生產部署中使用最多的容器類型.
基礎環境型容器由于其目的是作為用戶在構建鏡像或實驗的基礎,在實際運行中會盡量的保持其功能的完整性.在本文的實驗中,選取了具有代表性的4款功能型容器進行測試,分別為Nginx、Httpd、Mysql和Postgres.其中Nginx和Httpd(Apache)是在實際生產部署中使用最多的Web服務器,數據庫服務器選擇Mysql和Postgres.
3.2.1 動態訓練
動態分析引擎監控容器的初始化和運行共30秒,得到容器在初始化階段的系統調用以及運行階段時用到的可執行文件集合.動態分析的結果如表3所示,可以發現在初始化階段,Web服務型容器使用的系統調用數目較多余數據庫服務型,Nginx最多,使用了98個系統調用;但在容器運行的過程中,數據庫服務型容器中用到的可執行文件數目往往多于Web服務型,其中Mysql最多,達到了57個,這也驗證了數據庫型容器鏡像的大小往往大于web服務型容器.

表3 動態分析結果Table 3 Result of dynamic analysis
在容器初始化運行的過程中,系統調用執行的數量會達到成百數千個,圖4展示了每個容器中調用最頻繁的前20個系統調用,可以發現頻率最高的幾個系統調用為rt_sigaction()、read()、opennat()、mmap()等,rt_sigaction()用于更改進程在接收到特定信號時采取的行為,read()用于讀取文件,opennat()用于打開文件,mmap()能夠將文件映射到內存空間,然后可以通過讀寫內存來讀寫文件.可以看出,在容器初始化階段,大多數系統調用用于對文件系統的訪問以及信號的處理上.

圖4 每個容器中使用頻率最多的系統調用使用次數Fig.4 Most frequently used syscall usage per container
3.2.2 靜態分析
靜態分析引擎分析每個容器中提取的可執行文件的系統調用數,部分結果如表4 顯示,在容器運行過程中,除了容器功能相關的程序之外,一些環境相關的程序也會經常用到,如find、env等.其中,對于每一個可執行文件,在運行時用到的系統調用數基本都在70以下,最多的為nginx,用到了62個系統調用,最少是env,僅僅用了15個系統調用.

表4 靜態分析結果Table 4 Static analysis results
3.3.1 系統調用數減少
以前在攻擊面減少的領域的工作主要集中在減少代碼的數量作為改進的主要措施.相比之下,AutoSec不刪除任何代碼,而只是限制惡意容器可以調用的系統調用,它通過減少(潛在惡意)應用程序暴露的系統調用數量來減少來自于主機內核的攻擊面.
實驗通過監控容器運行時系統調用池的數量驗證減少系統調用方面的有效性,如圖5所示,在監控的60s內,容器啟動時的前幾秒,系統調用池的數量會上升,之后逐漸下降到一個穩定的范圍內,大多數容器系統調用池里面的數量維持在60~100之間,與容器默認允許的將近300個系統調用相比,AutoSec可以有效的減少容器中70%以上的系統調用數量.

圖5 系統調用池中的數量波動Fig.5 Number in the syscall pool fluctuates
3.3.2 有效阻止CVE
為了進一步驗證AutoSec在減少容器攻擊面方面的有效性,本文收集了近5年內可以通過系統調用進行漏洞利用的CVE,一共有75個.如CVE-2022-0185使用了fsconfig()系統調用,CVE-2022-0847使用了splice()系統調用,其中大約75%的系統調用只對應于1個CVE,19%的系統調用存在過兩個CVE,7%的系統調用出現在3個CVE中.
Docker默認的Seccomp策略可以減少25個CVE利用威脅,應用AutoSec可以有效的緩解51個CVE對容器的安全威脅.
3.4.1 功能的完備性
為了進一步驗證受限容器功能的完備性,模擬真實場景下一個受限容器與外界的交互的情況,在容器啟動后,AutoSec使用基準測試工具對容器進行測試.CloudSuite[33]的網絡服務基準測試是基于開源的社交網站Elgg[34].Elgg是一個基于PHP的應用程序,它使用MySQL作為數據庫服務器,提供了一個運行在Web服務器上的流媒體服務器,本文使用CloudSuite來對Nginx和Httpd進行功能測試;對于MySQL和Postgres,實驗分別選取了Sysbench[35]和Pgbench[36]對其進行基準測試.
除了基準測試外,本文還同時對容器進行手動的命令調試,以測試容器是否因為合法的命令執行而崩潰.在對容器一周的運行測試中,容器沒有因為意外的命令執行而崩潰.使用AutoSec對容器進行安全加固基本不會影響容器功能的完備性.
3.4.2 性能的穩定性
對容器施加Seccomp策略,會對容器的性能產生一定的負面影響.由于容器默認啟用了Seccomp過濾器,因此過濾規則的更改幾乎不會影響應用程序的性能.此外,一個容器中的所有進程共享同一組Seccomp過濾器,因此在更新系統調用池時,只需要生成一個新的BPF程序結構來記錄BPF指令,并刪除舊的BPF程序結構即可.
本文使用上面提到的基準測試工具來測量容器的每秒服務數量TPS(Transaction per Second),圖6展示了與不施加Seccomp策略相比,施加Docker默認的Seccomp策略與Autosec的策略在TPS上的減少百分比.可以發現,與不施加任何Seccomp策略相比,使用Seccomp策略導致容器的TPS都略有減少,Autosec的策略會略高于Docker默認的策略,原因是Docker默認的Seccomp策略雖然包含更多的Seccomp規則,但是AutoSec在運行中對系統調用的調度以及BPF策略的生成會增加系統的開銷.但是與默認的策略相比,開銷增加都在2%之內,基本不會對容器的正常運行產生負面影響,可以認為是能夠接受的系統開銷.

圖6 施加Seccomp策略導致TPS減少的百分比Fig.6 Percentage reduction in TPS due to application of Seccomp policy
容器的安全性近年來備受關注,學者們做了不少的相關工作.在減少容器攻擊面方面,Cimplifier[37]依賴于動態識別資源,然后根據分析的結果,在滿足用戶自定義約束的前提下將一個復雜容器分離為單一用途的容器,然后通過遠程過程調用(RPC)實現分離容器之間的通信.Lic-Sec[39]結合了LicShield[40]和Docker-Sec[38]的工作,并在其基礎上進行了改進,首先通過SystemTap動態跟蹤內核操作,之后將所有的追蹤結果轉換為AppArmor規則,基于強制訪問控制來增強Docker容器的安全性.
正如前文所描述的,所有必需的系統調用都不能單獨通過動態分析來提取,特別是對于處理異常和錯誤的情況,它們通常不是公共執行路徑的一部分.因此動態分析不能保證完全覆蓋每個應用程序所需的所有系統調用.后來,Confine[13]、Chestnut[15]、RSDS[17]等工作將靜態分析融入到系統調用的提取中,以補充動態分析覆蓋率不足的問題,這在一定程度上緩解了容器運行時的意外崩潰情況.但是靜態分析的結果受限于分析范圍的把控,分析范圍過大,得到的系統調用集合起不到很好的安全保護作用,分析范圍過小,則無法彌補動態分析的短板.此外,無論是動態還是靜態分析,受限容器的可用性都無法得到很好的滿足,調試工具和一些應用程序無法使用,除了滿足基本功能之外,容器內的程序幾乎無法被調試.
AutoSec強調容器的安全性與可用性的均衡,主要靈感來自于Face-change[41]、AutoArmor[42]等按需調度所需功能進而減少攻擊面的方式,相比于Cimplifier、Confine等工作,AutoSec不盲目的尋求最小的系統調用集合,而是使用動靜態結合的方式盡可能的獲取多的容器運行狀態,之后使用按需調度的方式再賦予給容器系統調用,旨在保護安全性的同時,使用戶感覺不到受限容器與普通容器的區別,仍然能夠滿足容器的大部分功能需求.
為了保護容器的安全,減少容器的攻擊面,本文基于Seccomp BPF提出了一種容器系統調用限制策略自動生成和按需調度方法,并實現了該方法實現了系統原型AutoSec,AutoSec通過動態訓練和靜態分析相結合的方式來提取容器的系統調用,進而生成不同的Seccomp BPF規則,在容器生命周期的不同階段,根據容器的功能需求動態的調度可用的系統調用.實驗選取了Docker Hub中最為流行的4款容器,通過安全性和可用性兩個方面對AutoSec進行驗證,結果表明在能夠接受的性能損耗內,并不損傷容器原有的功能下,可以有效的減少暴露在容器中70%以上的系統調用,有效的保護容器安全.