从 Docker 安全机制探究 Docker 逃逸

Posted by RTL on August 31, 2019

本文首发于先知社区

背景

自2013年Docker发行开始,一直受到人们广泛关注,被认为成可能改变行业的一款软件。Docker的使用相信大家已经很熟悉,在这我也不一一介绍,今天我们探究的是 Docker 的实现问题。

Docker -> 容器

Docker 是一种容器,容器的官方定义是:容器就是将软件打包成标准化单元,以用于开发、交付和部署。容器的英文为 container,container 有一种意思为集装箱,集装箱的特点,在于其格式划一,并可以层层重叠。容器也是这样,我们平常在使用 Docker 过程中,其实就相当于将每一层的 Layer 拼接起来,去实现功能。在这说一个小问题,在构建Dockerfile 时,尽量把命令写到同一个 RUN 中,使用 && 拼接起来,因为 Docker 会将 Dockerfile 中不同的 RUN 封装到不同层中,导致整个 Docker 体积过大。 0.png

Docker vs 虚拟机

Docker 自出现后便经常与虚拟机做比较,有些人甚至认为 Docker 就是一种虚拟机。虚拟机总的来说是利用 Hypervisor 虚拟出内存、CPU等等。正如我上面所说,Docker 是一种容器,那么它跟虚拟机的区别在哪? 我们来看一张图:我们把图中的矩形看作一个计算机,内部的圆圈看作一个又一个的进程,它们使用着相同的计算机资源,并且相互之间可以看到。 1.png

Docker 做了什么事呢?Docker 给它们加了一层壳,使它们隔离开来,此时它们之间无法相互看到,但是它们仍然运行在刚刚的环境上,使用着与刚刚一样的资源。我们可以理解为,它们与加壳之前的区别就是无法相互交流了。需要说一句的是,这个壳我们可以将它看作一个单向的门,外部可以往内走,但是内部却不能往外走。这在计算机中的意思就是,外部进程可以看到内部进程,但是内部进程却不能看到外部进程。 2.png

这种机制是由 Linux namesapce 来实现的 namespace 是Linux 内核用来隔离内核资源的方式。其主要有Cgroup、IPC、Network、Mount、PID、User、UTS 七种,我刚刚所举的例子便是将进程进行了隔离。我们可以将命名空间看作一个集合,集合便存在父集以及子集,这便可以理解为我刚刚所说的单向门,父命名空间可以访问子命名空间,但子命名空间却不能访问父命名空间。Docker的隔离便是基于Linux namespace来实现的,其默认开启的隔离有ipc、network、mount、pid、uts。进程所属的命名空间可以在 /proc/$$/ns 里面查看。 3.png

由以上可知,Docker 并没有使用任何虚拟化技术,其就是一种隔离技术。如果你对 Linux 命令比较熟悉,甚至可以理解为 Docker 是一种高级的 chroot。

Docker安全机制

因为Docker所使用的是隔离技术,使用的仍然是宿主机的内核、cpu、内存,那会不会带来一些安全问题?答案是肯定的,那 Docker 是怎么防护的? Docker的安全机制有很多种:Linux Capability、AppArmor、SELinux、Seccomp等等,本文主要讲述一下 Linux Capability 因为Docker默认对User Namespace未进行隔离,在Docker内部查看 /etc/passwd 可以看到 uid 为 0,也就是说,Docker内部的root就是宿主机的root。但是如果你使用一些命令,类似 iptables -L,会提示你权限不足。 4.png

这是由 Linux Capability 机制所实现的。 自Linux 内核 2.1 版本后,引入了 Capability 的概念,它打破了操作系统中超级用户/普通用户的概念,由普通用户也可以做只有超级用户可以完成的操作。 Linux Capability 一共有 38 种,分别对应着一些系统调用,Docker 默认只开启了 14种。 这样就避免了很多安全的问题。熟悉 Docker操作的人应该可以意识到,在开启 Docker 的时候可以加一个参数是 –privileged=true,这样就相当于开启了所有的 Capability。 使用 docker inspect {container.id} 在 CapAadd 项里可以看到添加的 capability 5.png

具体每个Capability所对应的功能可去Linux官方手册查询 Capability定义

Docker 逃逸

Docker 实现原理已知,下面我们就来探讨一下 Docker 逃逸的原理。 Docker 逃逸的目的是获取宿主机权限,目前的 Docker 逃逸的原因可以划分为三种:

  • 由内核漏洞引起
  • 由 Docker 软件设计引起
  • 由配置不当引起

对于由内核漏洞引起的漏洞,其实主要流程如下:

  1. 使用内核漏洞进入内核上下文
  2. 获取当前进程的task struct
  3. 回溯 task list 获取 pid = 1 的 task struct,复制其相关数据
  4. 切换当前 namespace
  5. 打开 root shell,完成逃逸

对于 Docker 软件设计引起的逃逸 爆出过的 runc (cve-2019-5736),就是因为 Docker 挂载了宿主机的 /proc 目录而引起的。

对于配置不当引起的逃逸 在这我举一个例子。比如我们开启了特权容器或者cap-add=SYS_ADMIN,此时 Docker 容器具有 mount 权限,查看 fdisk -l 将宿主机存储硬盘挂载到 Docker 内部目录上,这样就引起了逃逸。 6.png