
Exploit de preuve de concept et analyse détaillée de la vulnérabilité d'élévation de privilèges du noyau Linux CVE-2022-0847 (Dirty Pipe), incluant un environnement Docker pour les tests et le débogage.
[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.