
Exploit proof-of-concept e analisi dettagliata della vulnerabilità CVE-2022-0847 (Dirty Pipe) di escalation dei privilegi del kernel Linux, incluso un ambiente Docker per test e debug.
[toc]
Questo articolo è stato pubblicato per la prima volta sul account pubblico WeChat di Huawei Security, questa è la versione blog (più completa)
Link originale: https://mp.weixin.qq.com/s/6VhWBOzJ7uu80nzFxe5jpg
ID vulnerabilità: CVE-2022-0847 (alias: Dirty Pipe)
Prodotto affetto: kernel Linux - syscall splice
Versioni interessate: Linux 5.8 patch f6dd975583bd introdotta ~ riparata in 5.16.11, 5.15.25, 5.10.102
Danno: possibilità di scrivere fino a una pagina di contenuto (sufficiente) su qualsiasi file leggibile, consentendo l'elevazione locale dei privilegi.
Docker per analisi vulnerabilità: chenaotian/cve-2022-0847 (se non ancora accessibile significa che non l'ho ancora caricato)
Fornisce:
Avvio:
cd ~/cve-2022-0847
gcc exp.c -o exp --static && cp exp ./rootfs && cd rootfs
find . | cpio -o --format=newc > ../rootfs.img
cd ../
./boot.sh
Debug:
gdb ./vmlinux
target remote :10086
directory /root/linux-5.13
b do_splice
b copy_page_to_iter_pipe
b pipe_write
ignore 3 15
...
p *(struct pipe_inode_info *) pipe
p (struct pipe_buffer)pipe->bufs[0]
Il principio sintetico della vulnerabilità è che chiamando la funzione
splice, è possibile inviare un file a unapipein modalità "zero-copy". A livello di codice, la zero-copy consiste nell'utilizzare direttamente la cache di pagina del file (page cache) come paginabufdellapipe. Tuttavia, qui è stata introdotta una vulnerabilità di variabile non inizializzata, che consente di modificare successivamente la pagina della cache del file attraverso il canalepipetrattandola come una normale paginapipe, permettendo così la sovrascrittura. In questa situazione, il kernel non considera questa pagina cache come "sporca" e non la scrive su disco per un breve periodo (fino al prossimo riavvio). Durante questo intervallo, tutti gli accessi a quel file utilizzeranno la pagina cache alterata, consentendo un'operazione di "scrittura arbitraria su qualsiasi file leggibile per un breve periodo". Ciò può portare all'elevazione locale dei privilegi.
Secondo la patch, il punto di occorrenza è nella funzione copy_page_to_iter_pipe, con l'aggiunta dell'inizializzazione di buf->flags. Quindi si tratta di una vulnerabilità di variabile non inizializzata.

Il punto di chiamata di copy_page_to_iter_pipe si trova all'interno della chiamata di sistema splice. La funzione splice (chiamata di sistema) utilizza un metodo "zero-copy" per inviare il contenuto del file a una pipe, con prestazioni migliori rispetto al tradizionale invio diretto del contenuto del file. I dettagli verranno spiegati più avanti.
Innanzitutto, l'alias della vulnerabilità è "dirty pipe", quindi è utile comprendere la pipe. La pipe è un canale di comunicazione fornito dal kernel, creato tramite la funzione pipe/pipe2, che restituisce due descrittori di file: uno per inviare dati e l'altro per riceverli, simile a un tubo con due estremità. I dettagli d'uso non verranno approfonditi.

Spiegazione breve dell'implementazione nel kernel: di solito, lo spazio totale della cache pipe è di 65536 byte, gestito in pagine, per un totale di 16 pagine (4096 byte ciascuna). Le pagine non sono contigue ma gestite tramite un array, formando una lista circolare. Vengono mantenuti due puntatori di lista: uno per scrivere (pipe->head) e uno per leggere (pipe->tail). Analizziamo principalmente la funzione pipe_write:
linux-5.13\fs\pipe.c : 400 : pipe_write
static ssize_t
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
struct file *filp = iocb->ki_filp;
struct pipe_inode_info *pipe = filp->private_data;
unsigned int head;
ssize_t ret = 0;
size_t total_len = iov_iter_count(from);
ssize_t chars;
bool was_empty = false;
bool wake_next_writer = false;
··· ···
··· ···
head = pipe->head;
was_empty = pipe_empty(head, pipe->tail);
chars = total_len & (PAGE_SIZE-1);
if (chars && !was_empty) {
//[1] Se la cache pipe non è vuota, prova a scrivere "continuando" dall'ultima pagina corrente
unsigned int mask = pipe->ring_size - 1;
struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
int offset = buf->offset + buf->len;
if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
offset + chars <= PAGE_SIZE) {
/*[2] Chiave: se il flag PIPE_BUF_FLAG_CAN_MERGE è presente, la pagina consente la scrittura continua
*Se la lunghezza da scrivere non supera la pagina, scrive in coda; altrimenti inizia una nuova pagina */
ret = pipe_buf_confirm(pipe, buf);
···
ret = copy_page_from_iter(buf->page, offset, chars, from);
···
}
buf->len += ret;
···
}
}
for (;;) {//[3] Se non si può continuare dall'ultima pagina, inizia una nuova pagina
··· ···
head = pipe->head;
if (!pipe_full(head, pipe->tail, pipe->max_usage)) {
unsigned int mask = pipe->ring_size - 1;
struct pipe_buffer *buf = &pipe->bufs[head & mask];
struct page *page = pipe->tmp_page;
int copied;
if (!page) {//[4] Alloca una nuova pagina
page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);
if (unlikely(!page)) {
ret = ret ? : -ENOMEM;
break;
}
pipe->tmp_page = page;
}
spin_lock_irq(&pipe->rd_wait.lock);
head = pipe->head;
··· ···
pipe->head = head + 1;
spin_unlock_irq(&pipe->rd_wait.lock);
/* Inserisce nell'array dei buffer */
buf = &pipe->bufs[head & mask];
buf->page = page;//[5] Inserisce la nuova pagina nell'array di pagine
buf->ops = &anon_pipe_buf_ops;
buf->offset = 0;
buf->len = 0;
if (is_packetized(filp))
buf->flags = PIPE_BUF_FLAG_PACKET;
else
buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
//[6] Imposta il flag, per default PIPE_BUF_FLAG_CAN_MERGE
pipe->tmp_page = NULL;
copied = copy_page_from_iter(page, 0, PAGE_SIZE, from);
//[7] Operazione di copia
··· ···
ret += copied;
buf->offset = 0;
buf->len = copied;
··· ···
}
··· ···
}
··· ···
return ret;
}
head==tail indica pipe vuota), significa che ci sono dati non ancora letti. Ottiene il puntatore head, cioè l'ultima pagina usata per scrivere, ne esamina len e offset (per trovare la fine dei dati). Quindi tenta di continuare a scrivere nella pagina corrente.PIPE_BUF_FLAG_CAN_MERGE: se non presente, non consente la scrittura continua. Inoltre, se la concatenazione dei dati eccede una pagina (operazione di scrittura oltre i limiti di pagina), non può continuare.alloc_page alloca una nuova pagina.buf->flag viene inizializzato per default a PIPE_BUF_FLAG_CAN_MERGE, perché per default la pagina consente la scrittura continua.