更多奇技淫巧欢迎订阅博客:https://fuckcloudnative.io

前言

在每一个 Kubernetes 节点中,运行着 kubelet,负责为 Pod 创建销毁容器,kubelet 预定义了 API 接口,通过 GRPC 从指定的位置调用特定的 API 进行相关操作。而这些 CRI 的实现者,如 cri-o, containerd 等,通过调用 runc 创建出容器。runc 功能相对单一,即针对特定的配置,构建出容器运行指定进程,它不能直接用来构建镜像,kubernetes 依赖的如 cri-o 这类 CRI,在 runc 基础上增加了通过 API 管理镜像,容器等功能。

Kubelet,Cri-O,runc,Linux 大致层级示意图如下:

在 Kubernetes 源码中,可以在pkg/kubelet/cri目录下找到相关代码,其中remote目录包含了常见的如镜像拉取,容器创建等操作,streaming目录中包含了一些需要 TCP 流的操作,如 attach,port-forward 等。

构建并使用 runc 运行一个容器

构建

runc 的源码可以下载并通过 make 命令构建:

git clone https://github.com/opencontainers/runc.git
cd runc
make

runc 是一个 Go 程序,使用 CGO 调用了一些外部库。构建除了需要安装 go 之外,可能需要额外安装如 pkgconfig, libseccomp-dev, libseccomp 等包,视具体错误排查。

运行容器

runc 并不负责从镜像等上下文直接创建容器,因此需要从 docker 等更高级的运行时直接导出 CRI,会更容易一些。

mkdir /mycontainer
cd /mycontainer# create the rootfs directory
mkdir rootfs# export busybox via Docker into the rootfs directory
docker export $(docker create busybox) | tar -C rootfs -xvf -

然后使用构建好的 runc 创建出容器运行的具体配置 config.json:

runc spec

切换到 root,然后运行:

runc run <container-id>

或者将run命令拆解,分成多步运行:

runc create <container-id>
runc list # 列出创建状态的容器
runc start <container-id>
runc list
runc delete <container-id>

运行分析

通过ps axef命令,打印出所有进程及其层级关系,发现我们之前运行的容器进程关系如图:

表面上看,在通过runc run <container-id>之后,进程创建了一个子进程sh,也就是我们进入容器后指定运行的第一个程序。

容器隔离机制

Linux 通过 namespace 将不同的资源隔离,就像一个沙箱一样。被隔离到某个 namespace 中的内容,无法访问到其它 namespace 的内容。可以通过unshareclone设置标志位来将进程放入新的命名空间。

  • PID Namespace

标志位: CLONE_NEWPID

文档: man pid_namespaces

即进程命名空间,在一个隔离的空间中,PID 从 1 开始,相同 PID 与主机 PID 不构成冲突。类似主机的 init 进程,PID 为 1 的进程被终止时,该命名空间下的所有进程都会收到SIGKILL信号从而被终止。正因如此,一个容器的初始化进程只能是一个,而且终止后容器也就被停止了。

在不同的 PID 命名空间,进程互相看不到对方,不能通过 PID 找到对方,/proc目录下也只能看到自己命名空间中的进程。但是一个父进程fork出的子进程可以通过set_ns放入子命名空间,在父进程的命名空间,仍然可以看到这个子进程,只是 PID 不一样。进程可以被挪到子命名空间,但不能被反向挪回更高级的命名空间。

  • User Namespace

标志位: CLONE_NEWUSER

文档: man user_namespaces

用户命名空间,主要隔离的是安全相关的 id 和属性,尤其是用户 id 和用户组 id,root 目录,密钥,以及各种进程的能力(capabilities)。

  • Network Namespace

标志位: CLONE_NEWNET

文档: man network_namespaces

网络命名空间,隔离出全新的网络设备、IPv4、IPv6 协议栈,路由表,iptables 规则,以及 sockets,端口号。隔离后,命名空间下的/proc/net以及/sys/net也会不同于上级命名空间。

一对veth网络设备可以实现跨命名空间的通信,也可以桥接到主机物理设备上。

  • UTS Namespace

标志位: CLONE_NEWUTS

隔离 hostname 以及 NIS domain name,两个都是主机对自己的网络标识,在容器中可以重新定义。

  • Cgroup Namespace

标志位: CLONE_NEWCGROUP

文档: man cgroup_namespaces

Cgroup Namespace 隔离出了新的 cgroup 分组,可以通过/proc/[PID]/cgroup获得进程的 cgroup 相关情况。Cgroup 是Control group的缩写,不同的 cgroup 通过树状结构组织在一起,每个节点上挂载着不同的控制器(controllers),可以通过他们控制 cpu, memory, bulkio 等资源的使用,也可以获得 cpu 等资源的使用情况,或者通过 freezer 将进程暂时冻结或恢复。

  • IPC Namespace

标志位: CLONE_NEWIPC

隔离了 System V 常用的 IPC 通信手段,如信号量(semaphore),共享内存,消息队列等。

  • Mount Namespace

标志位: CLONE_NEWNS

挂载文件系统的隔离,但是一部分文件系统也可以通过共享跨命名空间共享。通过cat /proc/self/mountinfo可以获得挂载信息,带有shared标志的就是共享出来的部分。

runc 容器初始化流程

runc 目前初始化大致流程如下图所示,其中一些步骤经过了简化:

通过在 init.go 中隐式的导入包: import _ "github.com/opencontainers/runc/libcontainer/nsenter", runc 在初始化阶段就完成了clone/unshare过程,创建出子进程,并将其通过setns放入新的命名空间:

    /*libcontainer/nsenter/nsexec.c:join_namespaces(char*)*/for (i = 0; i < num; i++) {struct namespace_t ns = namespaces[i];if (setns(ns.fd, ns.ns) < 0)bail("failed to setns to %s", ns.path);close(ns.fd);}

一般来说,只要通过 clone/share+setns+execve 就可以完成容器的基本运行过程,在发现漏洞[CVE-2019-5736]之后,runc 加入了一段重要代码ensure_cloned_binary,确保当前 runc 是通过 memfd 在内存中克隆出来并重新运行的。

容器逃逸

"特权"容器

"特权"在 runc 及基于 containerd 的 docker 中, 对应选项是--privileged,在 K8S 中对应的是pod.Spec.privileged: true,但它的特权实际指的是 User Namespace 中的 Capabilities,即启动容器时用户的 Capabilities 将会全部被保持,不会为了构建沙箱而扔掉权限,这样容器就可以执行各种特权操作,例如挂载文件系统,改写主机 iptables,改写主机 Ipvs 等。因此对于像 kube-proxy 这类需要改写主机网络的组件,一些容器,可能还会需要访问特定的蓝牙设备或 GPU 等,它们要正常工作,就必须拥有特权。

但这种特权实际已经是“超级特权”了,必须经过谨慎的权衡使用,一般不会被普通用户滥用。容器面临的权限安全问题,更多的是来自 UID/GID 映射。

通过 User Namespace,我们可以将主机/上级 namespace 中的一个普通用户映射成子 namespace 中的一个特权用户 root 或者其它,反之则不行。但是 Linux 的权限系统是通过 UID/GID 来辨认用户的,当一个容器中的 UID 0 用户在主机中被映射成 UID 0 时,那么容器中的进程如果能够访问主机上的文件,它实际等同于 root(UID=0),这时候就有了逃逸的机会。所谓容器逃逸,就是容器中的进程通过某种方式改写主机环境,从容器这个平行世界中“逃脱”,改变主世界。在容器中它可能只是个“村长”,但由于它的 UID 与外面的“国王”相等,一旦逃逸发生,它就等同于拥有“国王”权限,可以对外发布更高权限的命令。

对此,我们可以在主世界创建一个“村长”(UID=65535),然后将有限的领土“村级行政区”划分给他,然后映射到子命名空间中做“国王”(root,UID=65535),这样即使容器中的国王逃出来,它依然只能治理之前划分给它的那一小块“村级行政区“。

更多关于“特权”容器的讨论可以参考 LXC 作者的这篇博客[1]

CVE-2019-5736: 改写 runc 容器逃逸

在 2019 年初,爆发了一个容器严重漏洞,运行 docker 的容器环境,普通用户可以通过特殊构建的镜像,运行后改写主机上的 runc,从而进一步进行入侵操作。

当一个进程运行时,它自己可以通过/proc/self/exe得到指向自己的链接,也可以进一步在/proc目录下找到自己的 fd。一个恶意构建的镜像可以将自己的入口改成/proc/self/exe,由于容器入口需要通过 runc 来 clone+execve 启动,这样就使得一个普通的用户容器,访问并执行了主机上的 runc。

之前编译 runc 的步骤中,我们也已经知道了,runc 使用了 CGO 来调用 libc/libseccomp 的代码,通过ldd命令可以看到 runc 的外部依赖库:

在之前的 runc 容器初始化流程中,我们直到当容器开始执行我们的程序时,已经进入了新的 namespace,这时程序如果需要外部依赖什么文件,一定会从容器内寻找,这时我们可以通过修改容器的LD_LIBRARY环境变量,迫使 runc 优先使用改造过的.so文件,而这个.so的作用,就是改写/proc/self/exe指向的文件,即主机上的 runc。

在这个漏洞中,我们可以看到它需要满足几个条件:

  1. 容器能够通过入口/proc/self/exe指向主机中的 runc

  2. 容器允许用户自行任意指定,将其中的恶意代码伪装成普通文件

  3. 容器中的用户 UID 在主机中的映射 UID 同样具有较高权限,否则即使 runc 被暴露,也会因为容器中用户权限不足而无法访问

runc 最终的漏洞修复手段: 增加了一个ensure_cloned_binary阶段,通过在内存中只读的复制自己并clone,避免了/proc/self/exe指向主机 runc 的问题。

CVE-2019-14271: 通过 docker-cp 容器逃逸

这个漏洞是指当运行 docker 的环境中调用docker cp时,如果访问的是一个恶意容器,容器中的用户就可以在主机中运行任意代码。

docker cp 是通过 chroot 的方式,切换到容器所在主机文件目录,然后从那里复制文件。这个 chroot 是 docker 自己实现的,需要依赖 nsswitch 相关动态库,这时可以通过在容器中替换这些动态库,从而实现借docker cp的高级权限,运行恶意代码的目的。

官方的修复是让 chroot 在切换成容器目录之前就提前执行一次 dns lookup,从而调用 cgo,总体看上去还是稍微有点魔幻的: https://github.com/moby/moby/pull/39612/files

func init() {// initialize nss libraries in Glibc so that the dynamic libraries are loaded in the host// environment not in the chroot from untrusted files._, _ = user.Lookup("docker")_, _ = net.LookupHost("localhost")
}

小结

从上面两个逃逸漏洞来看,仍然没有摆脱“特权用户运行恶意代码”的范畴。一些 CRI 如 Cri-O,可以通过修改/etc/crio/crio.conf中的uid_mappingsgid_mappings修改映射,从而避免容器逃逸后容器中的进程获取主机上的文件访问权限。这样做会有一些额外负担,就是如 HostPath 这类挂载,需要确保主机上这部分文件也能够被指定 UID 访问。

另外通过扫描镜像,避免恶意镜像也可以起到一定作用。对于镜像准入需要采取一定的手段。

K8S 和 docker/crio 的特权模式一定慎用,可以把它跟 root 等同审慎对待,绝对不能开放给普通用户。

关注容器生态安全漏洞,及时发现预警,避免修复不及时造成损失。

参考资料

[1]

这篇博客: https://brauner.github.io/2019/02/12/privileged-containers.html

原文链接:https://ethantang.top/posts/runc-container/

你可能还喜欢

点击下方图片即可阅读

我就要在容器里写文件!?

云原生是一种信仰 ????

码关注公众号

后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具,只需一条命令,连 ssh 都不需要!

点击 "阅读原文" 获取更好的阅读体验!

❤️给个「在看」,是对我最大的支持❤️

Runc 容器初始化和容器逃逸相关推荐

  1. SpringMVC容器初始化篇----ContextLoaderListener

    学习学习容器初始化,若有不对的地方,请指出更正,大家共同学习学习. 此篇幅主要围绕着 ContextLoaderListener加载容器,理解其中的原理. ContextLoaderListener的 ...

  2. 【C++ 语言】vector 容器 ( 容器分类 | vector 声明 | vector 初始化 | vector 容器元素增删查改 )

    文章目录 序列式容器 vector 简介 vector ( 向量 ) 头文件 vector ( 向量 ) 声明及初始化 vector ( 向量 ) 添加元素 vector ( 向量 ) 查询元素 ve ...

  3. spring源码 — 一、IoC容器初始化

    IoC容器初始化 注意:本次的spring源码是基于3.1.1.release版本 容器:具有获取Bean功能--这是最基本功能,也是BeanFactory接口定义的主要行为,在添加了对于资源的支持之 ...

  4. Spring IOC源代码具体解释之容器初始化

    Spring IOC源代码具体解释之容器初始化 上篇介绍了Spring IOC的大致体系类图,先来看一段简短的代码,使用IOC比較典型的代码 ClassPathResource res = new C ...

  5. Spring容器初始化和bean创建过程

    文章目录 Spring容器初始化过程(注解) 1.this() 初始化bean读取器和扫描器 2. register(annotatedClasses) 3 refresh()刷新上下文 前言:一直想 ...

  6. web.xml初始化spring容器

    初始化spring容器

  7. 【spring源码分析】IOC容器初始化(二)

    前言:在[spring源码分析]IOC容器初始化(一)文末中已经提出loadBeanDefinitions(DefaultListableBeanFactory)的重要性,本文将以此为切入点继续分析. ...

  8. IOC 容器初始化小结

    总结一下IOC 容器初始化的基本步骤: 1.初始化的入口在容器实现中的refresh()调用来完成. 2.对Bean 定义载入IOC 容器使用的方法是loadBeanDefinition(),其中的大 ...

  9. AnnotationConfigApplicationContext容器初始化

    AnnotationConfigApplicationContext容器初始化目录 (Spring源码分析)AnnotationConfigApplicationContext容器初始化 this() ...

最新文章

  1. 好久没更新了,马上回来,精彩继续
  2. 核心交换机的链路聚合、冗余、堆叠、热备份
  3. 读书笔记_CLR.via.c#第五章_基元类型_引用类型_值类型
  4. Spark Streaming 实战案例(二) Transformation操作
  5. POJ 计算几何(3)
  6. LeetCode 170. 两数之和 III - 数据结构设计(哈希map)
  7. 计数排序和桶排序 java代码实现
  8. c语言修改字符串c2133,通过create_string_buffer、create_unicode_buffer让C语言具备修改字符串的能力...
  9. 预测一下web前端未来的6个趋势
  10. Prometheus 轻松实现集群监控
  11. java ews appointment_EWS API 2.0读取日历信息-读取内容注意事项
  12. 【P5850】calc 加强版(生成函数)(多项式)
  13. 天猫精灵开发技能【3】
  14. CPU中运算器的功能
  15. linux下下载基因组程序,从 NCBI 批量下载基因组的方法
  16. 百度地图-设置地图最小、最大级别
  17. 软件工程学习笔记(一)
  18. 浅谈计通银行机房集中监控系统功能
  19. Mac配置VScode
  20. Skynet日志服务

热门文章

  1. HTML5前期学习准备(一)
  2. 忙里偷闲第三弹:开发成绩查询微信公众号
  3. JAVA中接口存在的意义
  4. 如何将NOAA官网下载的气象雷达原始数据转化为NC文件
  5. 华为Ascend昇腾CANN详细教程(二)
  6. 迅搜(xunsearch)的安装使用以及操作类分享
  7. 测距仪控制c语言程序,激光测距仪系统设计(机械图电路图c语言程序)
  8. pta 构造哈夫曼树-有序输入 优先队列做法
  9. CA-MKD:置信多教师知识蒸馏
  10. CPLD个人学习笔记