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

在每一个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作者的这篇博客。

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等同审慎对待,绝对不能开放给普通用户。

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

Runc容器运行过程及容器逃逸原理相关推荐

  1. 容器运行过程中异常处理

    容器使用过程中异常处理 最近使用容器时,系统中的容器总是在重启后出现异常,要么处于CREAT状态,要么处于EXIT状态,导致容器中的应用无法运行. 问题 异常状态1: 容器在启动阶段被终端,导致容器一 ...

  2. oracle 容器运行_Oracle应用容器云的自由

    oracle 容器运行 在这篇博客文章中,我将介绍如何部署CloudEE封装在杜克大学应用自由尤伯杯罐子Oracle应用集装箱云端 . 在Oracle Application Container Cl ...

  3. 课时 30:理解 RuntimeClass 与使用多容器运行时(贾之光)

    本文将主要分享以下三方面的内容: RuntimeClass 需求来源 RuntimeClass 功能介绍 多容器运行时示例 RuntimeClass 需求来源 容器运行时的演进过程 我们首先了解一下容 ...

  4. 1.Containerd容器运行时初识与尝试

    0x00 前言简述 1.基础介绍 2.专业术语 3.架构简述 0x01 安装配置 1.Ubuntu安装Containerd.io流程 0x02 简单使用 1.镜像拉取与运行 2.创建和使用网络 3.与 ...

  5. docker配置阿里云镜像加速、镜像和容器常用命令、docker镜像原理

    6. Docker 配置阿里镜像加速服务 6.1 docker 运行流程 6.2 docker配置阿里云镜像加速 查看自己的镜像加速地址(链接直达):https://cr.console.aliyun ...

  6. Docker容器运行GUI程序的方法(直接进入Docker容器运行或通过SSH连接Docker容器运行)

    以下两种方法都需要先在主机执行 xhost + 命令,若无该命令,先apt安装 x11-xserver-utils 后再执行,否则会报 No protocol specified 这个错 sudo a ...

  7. 容器学习Day09-理解容器镜像

    目录 前言 一.理解容器镜像 1.什么是容器镜像? 2.容器镜像结构 3.容器镜像技术实现 二.镜像仓库 1.Repository 2.Registry 三.镜像的拉取和删除 1.查找镜像 2.拉取镜 ...

  8. 【容器运行时-转载】RunC 是什么?

    转载自:RunC 简介 RunC 是什么? RunC 是一个轻量级的工具,它是用来运行容器的,只用来做这一件事,并且这一件事要做好.我们可以认为它就是个命令行小工具,可以不用通过 docker 引擎, ...

  9. RunC 轻量级 容器运行工具 简介

    RunC 是什么? RunC 是一个轻量级的工具,它是用来运行容器的,只用来做这一件事,并且这一件事要做好.我们可以认为它就是个命令行小工具,可以不用通过 docker 引擎,直接运行容器.事实上,r ...

最新文章

  1. 苹果被曝重大系统漏洞:新款MacBook、iPhone 12、iPad Pro统统波及!root权限秒获取,隐私文件随意看...
  2. 阿里云发布ECS磁盘加密,一键加密,业务0改动
  3. Java中的10颗语法糖
  4. jqGrid使用整理
  5. UVA 473——Raucous Rockers
  6. puppet变量、数据类型及类(03)
  7. python eval 字符串替换_Python中eval妙用,字符串转字典和列表
  8. 【MySQL】【备份】mydumper安装与使用细节
  9. angularjs config_AngularJS依赖注入
  10. 自组织特征映射网络1
  11. 破解校园网锐捷无法开热点问题
  12. CSS opacity - 实现图片半透明效果
  13. 改进YOLOv5系列:首发结合最新CSPNeXt主干结构,高性能,低延时的单阶段目标检测器主干,通过COCO数据集验证高效涨点
  14. 饥饿游戏[The Hunger Games]
  15. 【全民免费wifi上网权威软件】wifi共享精灵谈恋爱的4大境界
  16. 周总结2022.1.10-2022.1.16
  17. 《剑魂之刃》游戏破解
  18. uva10635Prince and Princess(LIS)
  19. 每任务-苹果应用市场隐私政策
  20. echarts引入省份地图 失败原因

热门文章

  1. 格林纳达常驻WTO大使孙宇晨发布声明祝贺北京冬奥会顺利召开
  2. 毕业两年,一年工作经验,一个月拿下腾讯T4 offer
  3. 机器视觉光源选型总结---颜色选择
  4. LeetCode225. Implement Stack using Queues
  5. 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度
  6. 排查和判断常见的服务器故障
  7. Lisp:AutoLisp入门、操作编程实例详细攻略
  8. [GYCTF2020]Blacklist
  9. 期刊论文和会议论文的区分与识别
  10. 半导体精密划片机行业介绍及市场分析