云原生之容器安全實踐
概述:
云原生(Cloud Native)是一套技術體系和方法論。云原生(Cloud Native)由2個詞組成,云(Cloud)和原生(Native)。云(Cloud)表示應用程序位于云中,而不是傳統的數據中心;原生(Native)表示應用程序從設計之初即考慮到云的環境,原生為云而設計,在云上以優質狀態運行,充分利用和發揮云平臺的彈性和分布式優勢。
云原生的代表技術包括容器、服務網格(Service Mesh)、微服務(Micro Service)、不可變基礎設施和聲明式API。
更多對于云原生的介紹請參考文末鏈接1。

云原生安全技術沙盤(Security View)
筆者將“云原生安全”抽象成如上圖所示的技術沙盤。自底向上看,底層從硬件安全(可信環境)到宿主機安全 。將容器編排技術(Kubernetes等)看作云上的“操作系統”,它負責自動化部署、擴縮容、管理應用等。在它之上由微服務、Service Mesh、容器技術(Docker等)、容器鏡像(倉庫)組成。它們之間相輔相成,以這些技術為基礎構建云原生安全。
再對容器安全做一層抽象,又可以看作構建時安全(Build)、部署時安全(Deployment)、運行時安全(Runtime)。
在美團內部鏡像安全由容器鏡像分析平臺保障。它以規則引擎的形式運營監管容器鏡像,默認規則支持對鏡像中dockerfile、可疑文件、敏感權限、敏感端口、基礎軟件漏洞、業務軟件漏洞以及CIS和NIST的最佳實踐做檢查,并提供風險趨勢,同時它確保部分構建時安全。
容器在云原生架構下是由容器編排技術(例如:Kubernetes)負責部署的,部署安全同時也與上文提及的容器編排安全有交集。
運行安全管控交由HIDS負責(可以參考,分布式HIDS集群架構設計,文末鏈接2)。本文所討論的范疇也屬于運行安全之一,主要解決以容器逃逸為模型構建的風險(在本文中,若無特殊說明,容器指代Docker)。
對于安全實施準則,我們將其分為三個階段:
1.攻擊前:裁剪攻擊面,減少對外暴露的攻擊面(本文涉及的場景關鍵詞:隔離)。
2.攻擊時:降低攻擊成功率(本文涉及的場景關鍵詞:加固)。
3.攻擊后:減少攻擊成功后攻擊者所能獲取的有價值的信息、數據以及增加留后門難度等。
近些年,數據中心的基礎架構逐漸從傳統的虛擬化(例如:KVM+Qemu架構)轉向容器化(Kubernetes+Docker架構),但逃逸始終都是企業要在這2種架構下所需要面對的最嚴峻的安全問題,同時它也是容器風險中最具代表性的安全問題。筆者將以容器逃逸為切入點,從攻擊者角度(容器逃逸)到防御者角度(緩解容器逃逸)去闡述容器安全實踐,從而緩解容器風險。
容器風險
容器提供了將應用程序的代碼、配置、依賴項打包到單個對象的標準方法。容器建立在2項關鍵技術之上,Linux Namespace和Linux Cgroups。
Namespace創建一個近乎隔離的用戶空間并為應用程序提供系統資源(文件系統、網絡棧、進程和用戶ID)。Cgroup強制限制硬件資源,如CPU、內存、設備和網絡。
容器和VM不同之處在于,VM模擬硬件系統,每個VM都可以在獨立環境中運行OS。管理程序模擬CPU、內存、存儲、網絡資源等,這些硬件可由多個VM共享多次。

容器攻擊面(Container Attack Surface)
容器一共有7個攻擊面:Linux Kernel、Namespace/Cgroups/Aufs、Seccomp-bpf、Libs、Language VM、User Code、Container(Docker) engine。
筆者以容器逃逸為風險模型,提煉出3個攻擊面:
1. Linux內核漏洞
2. 容器自身
3. 不安全部署(配置)
一、Linux內核漏洞
容器的內核與宿主內核共享,使用Namespace與Cgroups這兩項技術使容器內的資源與宿主機隔離,所以Linux內核產生的漏洞能導致容器逃逸。
內核提權VS容器逃逸——通用Linux內核提權方法論
信息收集
收集一切對寫exploit有幫助的信息。如:內核版本,需要確定攻擊的內核是什么版本?這個內核版本開啟了哪些加固配置?還需知道在寫 shellcode 的時候會調用哪些內核函數?這時候就需要查詢內核符號表,得到函數地址。還可從內核中得到一些對編寫利用有幫助的地址信息、結構信息等等。
觸發階段
觸發相關漏洞,控制RIP,劫持內核代碼路徑,簡而言之獲取在內核中任意執行代碼的能力。
布置shellcode
在編寫內核exploit代碼的時候需要找到一塊內存來存放我們的shellcode 。這塊內存至少得滿足兩個條件:
第一:在觸發漏洞的時候我們要劫持的代碼路徑,必須保證代碼路徑可以到達存放shellcode的內存。
第二:這塊內存是可以被執行的,換句話說,存放 shellcode 的這塊內存具有可執行權限。
執行階段
第一:獲取高于當前用戶的權限,一般我們都是直接獲取root 權限,畢竟它是 Linux 中最高權限,也就是執行我們的shellcode。
第二:保證內核穩定,不能因為我們需要提權而破壞原來內核的代碼路徑、內核結構、內核數據等等,使內核崩潰了,這樣的話,即使得到 root 權限也沒什么太大的意義。
簡而言之,收集對編寫exploit有幫助的信息,然后觸發漏洞去執行特權代碼,達到提權的效果。

容器逃逸簡易模型(Container Escape Model)
容器逃逸和內核提權只有細微的差別,需要突破namespace的限制。將高權限的namespace賦到exploit進程的task_struct中。這部分的詳細技術細節不在本文討論范圍內,筆者會抽空再寫一篇關于容器逃逸的技術文章,詳細介紹相關技術細節。
經典的DirtyCoW
筆者以Dirty CoW漏洞來說明Linux漏洞導致的容器逃逸。漏洞雖老,奈何太過經典。寫到這,筆者不禁想問:多年過去,目前國內外各大廠,Dirty Cow漏洞的存量機器修復率是多少?
在Linux內核的內存子系統處理私有只讀內存映射的寫時復制(Copy-on-Write,CoW)機制的方式中發現了一個競爭沖突。一個沒有特權的本地用戶可能會利用此漏洞獲得對其他情況下只讀內存映射的寫訪問權限,從而增加他們在系統上的特權,這就是知名的Dirty CoW漏洞。
Dirty CoW漏洞的逃逸這里的實現思路和上述的思路不太一樣,采取Overwrite vDSO技術。
vDSO(Virtual Dynamic Shared Object)是內核為了減少內核與用戶空間頻繁切換,提高系統調用效率而設計的機制。它同時映射在內核空間以及每一個進程的虛擬內存中,包括那些以root權限運行的進程。通過調用那些不需要上下文切換(context switching)的系統調用可以加快這一步驟(定位vDSO)。vDSO在用戶空間(userspace)映射為R/X,而在內核空間(kernelspace)則為R/W。這允許我們在內核空間修改它,接著在用戶空間執行。又因為容器與宿主機內核共享,所以可以直接使用這項技術逃逸容器。
利用步驟如下:
1.獲取vDSO地址,在新版的glibc中可以直接調用getauxval()函數獲取。
2.通過vDSO地址找到clock_gettime()函數地址,檢查是否可以hijack。
3.創建監聽socket。
4.觸發漏洞,Dirty CoW是由于內核內存管理系統實現CoW時產生的漏洞。通過條件競爭,把握好在恰當的時機,利用CoW的特性可以將文件的read-only映射為write。子進程不停的檢查是否成功寫入。父進程創建二個線程,ptrace_thread線程向vDSO寫入shellcode。
madvise_thread線程釋放vDSO映射空間,影響ptrace_thread線程CoW的過程,產生條件競爭,當條件觸發就能寫入成功。
5.執行shellcode,等待從宿主機返回root shell,成功后恢復vDSO原始數據。
二、容器自身
我們先簡單的看一下Docker的架構圖:

Docker架構圖(圖片來自網絡如有侵權聯系刪除)
Docker本身由docker(docker client)和dockerd(docker daemon)組成。但從Docker 1.11開始,Docker不再是簡單的通過docker dameon來啟動,而是集成許多組件,包括containerd、runc等等。
Docker client是docker的客戶端程序,用于將用戶請求發送給dockerd。dockerd實際調用的是containerd的api接口,containerd是dockerd和runc之間的一個中間交流組件,主要負責容器運行、鏡像管理等。containerd向上為dockerd提供了gRPC接口,使得dockerd屏蔽下面的結構變化,確保原有接口向下兼容;向下,通過containerd-shim與runc結合創建及運行容器。更多的相關內容,請參考文末鏈接4、5、6。了解清楚這些之后,我們就可以結合自身的安全經驗,從這些組件相互間的通信方式、依賴關系等尋找能導致逃逸的漏洞。
下面我們以docker中的runc組件所產生的漏洞來說明因容器自身的漏洞導致的逃逸。
CVE-2019-5736:runc – container breakout vulnerability
runc在使用文件系統描述符時存在漏洞,該漏洞可導致特權容器被利用,造成容器逃逸以及訪問宿主機文件系統;攻擊者也可以使用惡意鏡像,或修改運行中的容器內的配置來利用此漏洞。
攻擊方式1:(該途徑需要特權容器) 運行中的容器被入侵,系統文件被惡意篡改 ==> 宿主機運行 docker exec命令 在該容器中創建新進程 ==> 宿主機runc被替換為惡意程序 ==> 宿主機執行 docker run/exec 命令時觸發執行惡意程序;
攻擊方式2:(該途徑無需特權容器) docker run 命令啟動了被惡意修改的鏡像 ==> 宿主機 runc 被替換為惡意程序 ==> 宿主機運行 docker run/exec 命令時觸發執行惡意程序;
當runc在容器內執行新的程序時,攻擊者可以欺騙它執行惡意程序。通過使用自定義二進制文件替換容器內的目標二進制文件來實現指回runc二進制文件。
例如,如果目標二進制文件是/bin/bash,這可以用指定解釋器的可執行腳本替換#!/proc/self/exe;因此,在容器內執行/bin/bash,/proc/self/exe的目標將被執行,將目標指向runc二進制文件。
然后攻擊者可以繼續寫入/proc/self/exe目標,嘗試覆蓋主機上的runc二進制文件。這里需要使用O_PATH flag打開/proc/self/exe文件描述符,然后以O_WRONLY flag 通過/proc/self/fd/
三、不安全部署(配置)
在實際中,經常會遇到這種狀況:不同的業務會根據自身業務需求有自己的一套配置,而這套配置并未得到有效的管控審計,使得內部環境變的復雜多樣,無形之中又增加了許多風險點。譬如,最常見的:
1.特權容器或者以root權限運行容器。
2.不合理的Capability配置(權限過大的Capability)。
面對特權容器,在容器內簡單的執行一下命令就可以輕松的在宿主機上留下后門。

在美團內部已經有效的收斂了特權容器問題。
這部分業界已經給出了最佳實踐,從宿主機配置、Dockerd配置、容器鏡像、Dockerfile、容器運行時等方面保障安全,更多細節請參考文末鏈接10,同時Docker官方已經將其實現成自動化工具(見文末鏈接11)。
安全實踐
為解決上述部分所闡述的容器逃逸問題,下文將重點從隔離(安全容器)與加固(安全內核)兩個角度去討論。
一、安全容器
安全容器的技術本質其實就是隔離。gVisor和Kata Container是比較具有代表性的實現方式,當然目前學術界有在探索基于Intel SGX的安全容器。
簡單的說,gVisor是在用戶態和內核態之間抽象出一層,封裝成API,有點像user-mode kernel,以此實現隔離;Kata Container是采用輕量級虛擬機隔離,與傳統的VM比較類似,但是它實現了無縫集成當前的Kubernetes加Docker架構。我們接著來看gVisor與Kata Container的異同。
Case 1: gVisor
gVisor是用Golang編寫的用戶態內核,或者說是沙箱技術,它主要實現了大部分的system call。它運行在應用程序和內核之間,為它們提供隔離。gVisor被使用在Google云計算平臺的App Engine、Cloud Functions和Cloud ML中。gVisor運行時,是由多個沙箱組成,這些沙箱進程共同覆蓋了一個或多個容器。通過攔截從應用程序到主機內核的所有系統調用并使用用戶空間中的Sentry處理它們,gVisor充當guest kernel的角色,且無需通過虛擬化硬件轉換,可以將他看做vmm與guest kernel的**,或是seccomp的增強版。

gVisor架構圖(圖片來自網絡如有侵權聯系刪除)
Case 2: Kata Container
Kata Container的Container Runtime 是用 hypervisor ,是用 hardware virtualization 實現的,如同虛擬機。所以每一個像這樣的 Kata Container 的 Pod,都是一個輕量級虛擬機,它是擁有完整的 Linux 內核。所以 Kata Container 與 VM 一樣能提供強隔離性,但由于它的優化和性能設計,它擁有與容器相媲美的敏捷性。

Kata Container 架構圖(圖片來自網絡如有侵權聯系刪除)
Kata Container在主機上有一個kata-runtime來啟動和配置新容器。對于Kata VM中的每個容器,主機上都有相應的Kata Shim。Kata Shim接收來自客戶端的API請求(例如:docker或kubectl),并通過VSock將請求轉發給Kata VM內的代理。Kata容器進一步優化以減少VM啟動時間。使用QEMU的輕量級版本NEMU,刪除了約80%的設備和包。VM-Templating創建運行Kata VM實例的克隆,并與其他新創建的Kata VM共享,這樣減少了啟動時間和Guest VM內存消耗。Hotplug功能允許VM使用最少的資源(例如:CPU,內存,virtio塊)進行引導,并在以后請求時添加其他資源。
gVisor VS Kata Container

在二者之間筆者更愿選擇gVisor,因為gVisor設計上相比與Kata Container更加“輕”量級,但gVisor的性能問題始終是一道暫時無法逾越的檻。綜合二者的優劣,Kata Container目前來看會更適合企業內部。總體而言,安全容器技術還需做諸多探索,以解決不同企業內部基礎架構上面臨的挑戰。
二、安全內核
眾所周知,Android由于不同廠商都維護著自己的Android版本,又因為Android 內核態代碼來自于Linux kernel upstrem,當一個漏洞產生在upstrem內核,安全補丁推送到Google,再從Google下發到各大廠商,最終到終端用戶。Android 生態的碎片化,補丁周期非常之長,使得終端用戶的安全,在這過程中始終處于“空窗期”。把目光重新聚焦在Linux上,它也同樣存在類似的問題。
1.內核面臨的問題

漏洞生命周期(The Vulnerability Life Cycle)
內核補丁
當一個安全漏洞被披露,通常是由漏洞發現者通過Redhat、OpenSuse、Debian等社區反饋或直接提交至上游相關子系統maintainer。在企業內部面臨多個不同內核大版本、內核定制化,針對不同版本從上游代碼backport相關補丁及制作相關熱補丁,定制內核還需對補丁進行二次開發,再升級生產環境內核或hotfix內核。不僅修復周期過長,而且推進修復過程人員溝通也存在成本,拉長了漏洞危險期。在危險期間對于漏洞是毫無防護能力的。
內核版本碎片化
內核版本碎片化在任意具備一定規模的公司都是無法避免的問題。隨著技術日新月異,不斷迭代,基礎架構上的技術棧需要較新版本的內核功能去支持,久而久之產生內核版本碎片化。碎片化問題的存在,使得在安全補丁的推送方面,遭遇了很大的挑戰。本身補丁還需要做針對性的適配,包括不同版本的內核,并進行測試驗證,碎片化使得維護成本也十分高昂。最重要的是,由于維護工作量大,必然拉長了測試補丁的時間線。也就是說,暴露在攻擊者面前的危險期變得更長,被攻擊的可能性大大增加。
內核版本定制化
同樣,因不同公司的基礎架構不同、需求不同,導致的定制化內核問題。對于定制化內核,無法簡單的通過從上游內核合并補丁,還需對補丁做一些本地化來適配定制化內核。這又拉長了危險期。
解決之道
我們使用安全特性去針對某一類漏洞或是針對某一類利用方式做防御與檢測。比如SLAB_FREELIST_HARDENED,針對double free類型漏洞做實時檢測,且防御overwrite freelist鏈表,性能損耗僅0.07%(參考upstrem內核源碼,commit id: 2482ddec)。
當完成所有全部安全特性,漏洞在被反饋之前和漏洞補丁被及時推送至生產環境前,無需關心漏洞的細節,就能防御。當然,安全補丁該打還是得打的,這里我們主要解決在安全補丁最終落在生產環境過程中“空窗期”對于漏洞與利用毫無防御能力的問題,同時也可以對0day有一定的檢測及防御能力。
實施策略
1. 已經合并進Linux主線版本的安全特性,如果公司的內核支持該特性,選擇開啟配置,對開啟前后內核做性能測試,分析安全特性原理,行業數據,給出real world攻擊案例(自己寫exploit去證明),將報告結論反饋給內核團隊,內核團隊再做評估,結合安全團隊與內核團隊雙方意見,最終評估落地。
2. 已經合并進Linux主線版本但未被合并進Redhat的安全特性,可選擇從Linux內核主線版本中移植,這點上代碼質量上得到了保障,同時社區也做了性能測試,將其合并到公司的內核再做復測。
3. 未被合并進Linux內核主線版本,從Grsecurity/PaX中做移植,在Grsecurity/PaX的諸多安全特性中,評估選擇,選取代碼改動少的,收益高的安全特性優先移植,比如改動較少的內核代碼又能有效解決某一類的漏洞,再打個比方,dirty cow的全量修復可能需要花費1-2年,加了某個安全特性,即使未修復也能防御。
內核后話
最后,分享一下筆者眼中較為理想中的狀況。當然,我們得根據實際情況“因地制宜”,在不同階段做出不同的取舍與選擇。
將內核團隊看成社區,我們向他們提交代碼,如同Linux內核社區有RFC(Request for Comment)、patch review等,無爭議后合并進公司內核。
先挑選實用的安全特性且代碼量少的,去移植,去實現,并落地。代碼量少意味著,對內核代碼改動少,出問題的可能性越小,穩定性越高,性能損耗越低。
一年完成幾個安全特性,不需要多,1~2個即可,對于內核態的加固,慎重慎重再慎重,譬如國外G家公司數據中心的內核發版前大概需要6~7個月時間做性能、穩定性測試。
需要做到加固某個安全特性后,使用0day或Nday去驗證防御效果,且基于該內核跑業務是穩定,性能損耗在可接受范圍之內或者可控。每個安全特性需要技術評審。為保障代碼質量的問題,找實際的高吞吐以及高并發低延遲的服務器小范圍灰度測試,無爭議后,推送給內核團隊。
最后,還可以通過將安全特性的代碼直接提交給Linux內核社區,如果代碼有不足的地方也可以和社區協同解決,合并進Linux內核主線代碼,從而側面推動落地。