gcc -Wall exp.c `pkg-config fuse --cflags --libs` -o exp
./exp /tmp

本文的理论知识(命名空间、overlay文件系统、fuse文件系统等)均来自chatGPT。
漏洞编号: CVE-2023-0386
漏洞产品: linux kernel - overlay文件系统
影响范围: 5.11 ~ 5.19
利用条件: 可以unshar 或可以创建overlay文件系统
利用效果: 本地提权
自己编译内核:
准备漏洞版本范围内的,5.15版本之外的(5.15貌似有坑),开启overlay 和fuse 两个fs:
CONFIG_SLUB_DEBUGOVERLAY_FS
CONFIG_FUSE_FS
ubuntu 21.10 内核版本5.13.0-16-generic实测可以完成:

在漏洞分析之前,我们先让chatGPT cosplay一下linux内核专家:
(询问chatGPT:下面你扮演一个linux内核专家,帮助我解答一些问题)
漏洞的公开信息比较少,比较直接的就是漏洞的补丁信息,补丁链接如下:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4f11ada10d0a

可以看到是在ovl_copy_up_one函数中增加了一个判断,我们先问一下chatGPT,这个函数是做什么的:

所以这个函数是发生在,overlay文件系统的下层文件向上层拷贝的动作中,然后我们结合上下文来看这个补丁新加的判断:
static int ovl_copy_up_one(struct dentry *parent, struct dentry *dentry,
int flags)
{
int err;
DEFINE_DELAYED_CALL(done);
struct path parentpath;
struct ovl_copy_up_ctx ctx = {
.parent = parent,
.dentry = dentry,
.workdir = ovl_workdir(dentry),
};
if (WARN_ON(!ctx.workdir))
return -EROFS;
ovl_path_lower(dentry, &ctx.lowerpath);
err = vfs_getattr(&ctx.lowerpath, &ctx.stat,//[1] 获取底层文件系统的stat
STATX_BASIC_STATS, AT_STATX_SYNC_AS_STAT);
if (err)
return err;
//[2]补丁新加判断文件的stat属性中的用户id和用户组id是否在当前命名空间有映射
if (!kuid_has_mapping(current_user_ns(), ctx.stat.uid) ||
!kgid_has_mapping(current_user_ns(), ctx.stat.gid))
return -EOVERFLOW;
[1] 首先通过vfs_getattr函数获取底层文件系统目标文件的属性。vfs_getattr函数获通过传入一个文件的struct path结构来获取这个文件对应的struct stat结构
[1.1] ctx.lowerpath为overlay文件系统中的下层文件系统的某文件路径,overlay文件系统会在后文介绍。
[1.2] struct stat结构存放文件的元数据信息,包括文件的属主属组等。而获取到的文件属主信息就会在下面补丁新加的判断中进行判断。
[2] 然后调用kuid_has_mapping函数对上面刚刚获取到的文件的属主信息和属组信息进行判断。判断目标文件属主和属组是否在当前用户命名空间中有映射。
[2.1] kuid_has_mapping函数传入两个参数,一个struct user_namespace用户命名空间结构体和一个struct kuid内核用户结构体,该函数会判断给定的用户信息是否在给定的用户命名空间中有映射,关于命名空间中用户的映射会在下文详细介绍。
所以我们知道,现在在进行该含有漏洞函数(ovl_copy_up_one)操作的时候,如果目标下层文件的属主用户或属组用户在当前命名空间中没有映射的话,就会失败。
那么补丁原理便清楚了,但即便如此,我们还需要解决下面问题,才可完成这个漏洞的复现:
ovl_copy_up_one所在的逻辑,即overlay文件系统中下层文件向上层拷贝?lowerpath究竟在上面逻辑链中扮演什么角色?那么在求解这两个疑问之前,我们需要搞清楚一些基本知识:
(询问chatGPT:请介绍一下linux内核中的命名空间)
在Linux中,命名空间(namespaces)是一种内核特性,用于实现资源隔离。通过命名空间,可以使一组进程看起来好像它们在独立的系统环境中运行,从而提高系统的安全性和可管理性。命名空间在容器技术(如Docker)中起着关键作用,它们使得容器能够在隔离的环境中运行,而不会影响其他容器或主系统。
Linux内核支持7种命名空间(mount、pid、net、ipc、user、time、cgroup),每种命名空间都隔离了一类特定的系统资源。命名空间通过一系列系统调用(如clone、**unshare**和setns)进行创建、修改和管理。容器运行时(如Docker)和其他虚拟化工具会利用这些命名空间特性,为容器提供独立、隔离的运行环境。
其中漏洞补丁新增的判断函数kuid_has_mapping涉及的是上述7个命名空间中的用户命名空间(user namespace)。
(询问chatGPT:请介绍一下这其中的用户命名空间)
用户命名空间(User Namespace)用于隔离用户ID(UID)和组ID(GID)。通过用户命名空间,可以在不同命名空间中使用独立的用户和组ID集合。这意味着,在一个用户命名空间中的用户和组可能在另一个命名空间中具有不同的ID或权限。用户命名空间可以提高系统的安全性和可管理性,尤其在容器化环境中。
用户命名空间的关键特性就是ID映射:用户命名空间允许将一个命名空间中的UID和GID映射到另一个命名空间中的UID和GID。这意味着,在不同的用户命名空间中,相同的UID和GID可能代表不同的用户和组。例如,一个容器中的root用户(UID 0)可能在主系统中被映射为一个非特权用户。
我们只需要记住以下几点:
如,我使用breeze用户创建一个新的用户命名空间,然后我再该用户命名空间中查看root属主的文件,显示属组为nobody:

这是因为在新命名空间中,root用户是创建该命名空间的breeze用户,而初始命名空间中的root并没有被我手动映射到新命名空间中,所以在新命名空间中被识别为nobody。
所以到这里我们就知道这个补丁的意义了:**对于拷贝的目标overlay 下层文件系统的文件,必须其属主(组)用户(组)在当前命名空间中有映射,才会继续下面的拷贝动作,否则返回错误。**也就是说这种被识别为nobody的情况就会造成拷贝失败。
(询问chatGPT:请介绍一下linux中的overlay文件系统)
Overlay 文件系统(又称为 OverlayFS)是一个 Linux 内核的虚拟文件系统。它允许将两个或多个已存在的目录层次结构(称为“lower”和“upper”层)合并成一个统一的视图。Overlay 文件系统在只读文件系统(如镜像)上实现写入操作的能力时非常有用,因为它可以将写操作重定向到一个叠加的可写层。这种方法在容器技术(如 Docker)中得到广泛应用,因为它提供了一种轻量级、高性能的文件系统虚拟化方案。
可以用下图理解某个overlay文件系统目录的实际上下层文件对应merge层文件的效果:

由于上层文件系统是可写的,所以用户修改来自上层的文件时则直接修改。但如果用户想要修改下层文件系统中的文件,如上图中的file D,由于下层文件系统是只读的,则会将file D拷贝(copy up)到上层变成file D‘然后再进行修改操作,实际修改的是拷贝到上层的file D’,而下层文件系统中的file D本身不会被改变,这也是overlay文件系统中的COW(copy on write 写时复制):

(询问chatGPT:请给我一个创建一个简单overlay文件系统的实际操作的例子)
我们通过如下方法简单演示一下如何创建一个overlay文件系统:
首先,我们需要创建 lower1、lower2、upper 和 work 目录。这些目录将用于 Overlay 文件系统。同时,我们还需要创建一个挂载点(例如,merged)来访问合并后的视图。并向 lower1 和 lower2 目录中添加一些内容:
mkdir lower1 lower2 upper work merged
echo "This is a file in lower1." > lower1/file1.txt
echo "This is a file in lower2." > lower2/file2.txt
使用 mount 命令和 -t overlay 选项来挂载 Overlay 文件系统。您需要指定 lowerdir、upperdir 和 workdir 参数,如下所示:
mount -t overlay overlay -o lowerdir=lower1:lower2,upperdir=upper,workdir=work merged
可以在merge目录中看到来自上下层文件系统的文件:

我们在这个目录中无论是创建新文件、删除文件、修改文件,都只会改变上层文件系统,对下层不影响,如创建一个新文件(实际创建在了upper中):

修改现有文件(将文件从lower1中拷贝到upper然后修改):

总结一下,跟漏洞相关的逻辑就是,当我们修改一个overlay文件系统中的来自下层的文件的时候,会先将这个文件拷贝到上层文件系统,然后进行修改动作。
经过上面的分析,我们基本可以复原出漏洞的全貌,如果一个overlay文件系统发生了copy up操作(尝试修改下层文件,触发下层文件向上层拷贝)的时候:
那么问题就是,为什么拷贝没有映射的用户属主的文件就会造成问题呢?
其实上面问题的答案很简单,拷贝文件并不只是拷贝文件的内容,包括文件的元数据,也就是文件的属主信息、时间戳、权限信息、还有扩展信息如capbilities等都会一起拷贝过来。引发的风险就是,如果下层文件系统是一个用户文件系统(如fuse),用户高度可控,可以自定义任何文件,但该文件系统存在限制(如nosuid),那么本漏洞就允许将下层用户自定义的suid文件从一个nosuid 文件系统拷贝到一个正常文件系统中,导致非法的suid文件获得suid特权。进而造成提权。
(询问chatGPT:请介绍一下fuse文件系统)
FUSE(Filesystem in Userspace)是一种文件系统接口,允许用户在用户空间(而非内核空间)实现和运行自定义的文件系统。FUSE 设计的目的是简化文件系统的开发和部署,同时提供良好的性能和安全性。FUSE 在 Linux 和其他类 Unix 系统(如 macOS 和 FreeBSD)上广泛使用。
其实简单的来说就是,fuse文件系统允许我们自己在用户层定义文件系统的一些回调函数(如open、write、readdir、甚至是getattr等文件元数据信息)。
下面的fuse文件系统代码(by chatGPT)既可以作为一个例子来学习,也可以用于后续的漏洞利用:
(询问chatGPT:请给我一个fuse文件系统的简单代码示例,这个文件系统中有一个hello文件,文件内容是一个"helloworld"字符串,并且这个文件是一个root属主的setuid文件)
经过简单修改(修改文件内容为后门二进制数据,修改一些文件权限设置,文件大小等):
#define FUSE_USE_VERSION 30