ufs_create,发现 0 个漏洞struct dirent 对象中的填充字节泄露信息的提交msdosfs_readdir 的补丁不完整。他们修补了一个实例,但没有修补第二个。mqueuefs、autofs、smbfs 和 tmpfs 中找到变种,这些变种允许我泄露完整的 8 字节指针。编写 PoC 确认。如上所述,我最初发现的漏洞是在分析上述提交的补丁时,在 msdosfs_readdir 中发现的。
在 FreeBSD 上调用 readdir 的基本流程如下:
#include <dirent.h>
int main(void) {
struct dirent *dp;
DIR *dirp;
dirp = opendir("./somedir");
dp = readdir(dirp);
}
根据 somedir 所在的具体文件系统,FreeBSD 内核中可能会调用许多 *_readdir 函数中的某一个。
上面的补丁添加了一个名为 dirent_terminate 的函数,旨在将 struct dirent 对象返回给用户空间(通常使用 uiomove 函数)之前调用它。该函数将清空结构体中 d_name 字段的填充字节以及剩余字节。此处是 struct dirent 的定义。
查看补丁,在第 1562 行,可以看到 dirent_terminate 被调用,参数为 dirbuf 变量。随后,uiomove 被调用来将 dirbuf 的内容复制回用户空间。然而,请注意这些代码行位于这个 if 语句的块内。此 if 语句上方的注释说明,只有在 MSDOS 文件系统的根目录上调用 readdir 时才会走这个分支,因此我们只需在文件系统的根目录之外的任何子目录中调用 readdir,即可跳过这个 if 语句。
继续往下看,我们在第 1691 行看到另一个对 uiomove 的调用。然而,仔细阅读代码会发现,在这种情况下并没有调用 dirent_terminate,这意味着填充字节将保持未初始化状态。不幸的是,d_name 字段在该函数开头已被清空(此处),所以我们无法获得更大的泄露。
首先,我没有 USB 驱动器,所以不得不想办法挂载一个 MSDOS 文件系统。以下操作可行:
$ dd if=/dev/zero of=test.img bs=512 count=256000
$ sudo mdconfig -a -t vnode -f test.img
$ sudo newfs_msdos -s 131072000 /dev/md1 # 我的 mdconfig 返回了 md1
$ mkdir ./temp
$ sudo mount -t msdosfs /dev/md1 ./temp
$ mkdir ./temp/test_dir
PoC 可以在 original_poc.c 中找到。只需用 clang 编译,并在与上述命令相同的目录中运行,你将看到打印出的泄露字节。
我开始寻找这个漏洞的变种。我大致是用 uiomove\(&.*, 进行 grep,返回了大约 15-20 个结果,然后我手动检查了所有结果。不幸的是,这些变种在 FreeBSD 中默认都不存在(这些文件系统需要手动启用/编译到内核中)。存在变种的函数如下:
mqfs_readdirtmpfs_dir_getdotdenttmpfs_dir_getdotdotdentsmbfs_readvdirautofs_readdir_one所有这些函数中的漏洞完全相同,因此我只介绍 mqfs_readdir。
struct dirent entrydirent_terminate 清空结构体的填充字节和 d_name 字段vfs_read_dirent。该函数会调用 uiomove 将结构体复制到用户空间到目前为止一切看起来都正常,对吧?不一定。我们必须确保结构体的所有字段都已初始化。如果仔细看代码,你会发现 d_off 字段保持未初始化。该字段的类型是 off_t,本质上是一个 int64_t。当结构体被复制到用户空间时,我们会得到未初始化的数据。
同样的 PoC 适用于所有变种,你只需在不同的文件系统上运行它。对于 mqueuefs,执行以下操作(需要先启用/将 mqueuefs 编译到内核中):
$ mkdir ./temp
$ sudo mount -t mqueuefs null ./temp
PoC 本身可以在 variants_poc.c 中找到。只需用 clang 编译,并在与上述命令相同的目录中运行。你将看到打印出的内核指针(大概一个栈指针和一个代码段/堆指针,我没有确认)。