雲原生之容器安全實踐

概述

雲原生(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結合創建及運行容器。更多的相關內容,請參考文末鏈接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/重新打開二進制文件,並且用單獨的一個進程不停地寫入。當寫入成功時,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內核安全及開源軟件安全。

參考文獻

美團-信息安全部招聘雲原生安全工程師/專家

崗位職責:

  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](備註:雲安全)

閱讀更多技術文章,請關注「美團技術團隊」微信公衆號。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章