@云原生之容器安全实践( 三 )


利用步骤如下:

  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. 不安全部署(配置)
在实际中 , 我们经常会遇到这种状况:不同的业务会根据自身业务需求提供一套自己的配置 , 而这套配置并未得到有效的管控审计 , 使得内部环境变得复杂多样 , 无形之中又增加了很多风险点 。 最常见的包括:


推荐阅读