Dirty COW, Dirty Pipe, CopyFail: trzy sposoby na roota przez page cache
Trzy podatności, trzy różne podsystemy kernela, ten sam efekt: nieuprzywilejowany użytkownik zapisuje dane do page cache pliku tylko do odczytu i zostaje rootem. Dirty COW potrzebował race condition. Dirty Pipe był deterministyczny. CopyFail robi to samo w 732 bajtach Pythona.
Dirty COW (CVE-2016-5195)
Kiedy: w kernelu od 2007, odkryty w 2016. Siedział 9 lat.
Gdzie: mm/gup.c, obsługa copy-on-write w get_user_pages().
Jak działa:
Kernel przy zapisie do prywatnego mapowania pliku (MAP_PRIVATE) powinien najpierw utworzyć kopię COW, a potem zapisać. Problem w tym, że te dwie operacje nie były atomowe.
Exploit uruchamia dwa wątki:
- Pierwszy pisze do mapowania przez
/proc/self/mem, wymuszając COW break - Drugi wywołuje
madvise(MADV_DONTNEED), każąc kernelowi wyrzucić właśnie utworzoną kopię COW
Jeśli madvise trafi w okno między wyszukaniem strony a zapisem, kernel zapisze dane bezpośrednio do oryginalnej strony page cache. Po milionach prób w końcu się udaje.
Niezawodność: niska, race condition wymaga wielu iteracji.
Odkrywca: Phil Oester. Znalazł exploit w ruchu sieciowym, czyli podatność była wykorzystywana zanim ją ujawniono.
Fix: commit 19be0eaffa3a, zablokowanie możliwości przerwania operacji COW przez madvise.
Dirty Pipe (CVE-2022-0847)
Kiedy: w kernelu od 5.8 (2020), odkryty w 2022. Siedział 2 lata.
Gdzie: lib/iov_iter.c, niezainicjalizowane pole flags w struct pipe_buffer.
Jak działa:
W 2020 dodano flagę PIPE_BUF_FLAG_CAN_MERGE, która mówi kernelowi, że może dopisywać dane do istniejącego bufora pipe zamiast alokować nowy. Problem: po opróżnieniu pipe bufory wracają do puli, ale flaga CAN_MERGE nie jest czyszczona.
Exploit:
- Utwórz pipe i zapełnij go danymi, każdy bufor dostaje flagę
CAN_MERGE - Opróżnij pipe, bufory zwolnione, ale flaga zostaje
splice()jeden bajt z pliku docelowego do pipe, page cache trafia do bufora z zastaną flagąCAN_MERGE- Zapis do pipe: kernel widzi
CAN_MERGEi dopisuje dane bezpośrednio do strony page cache
Deterministyczny, bez race condition. Sekwencja kilku syscalli.
Ograniczenia: nie da się pisać na offset 0 strony, nie da się przekraczać granic stron, nie da się powiększać pliku.
Odkrywca: Max Kellermann (CM4all). Trafił na problem, diagnozując uszkodzone logi.
Fix: commit 9d2231c5d74e, inicjalizacja flags na zero przy tworzeniu nowych buforów.
CopyFail (CVE-2026-31431)
Kiedy: w kernelu od 4.14 (2017), odkryty w 2026. Siedział 9 lat.
Gdzie: algif_aead.c, optymalizacja in-place w podsystemie kryptograficznym kernela (AF_ALG).
Jak działa:
Trzy niezależne zmiany utworzyły podatność:
- 2011: algorytm
authencesn(IPsec ESN) zapisuje 4 bajty scratch data za obszarem tagu - 2015: AF_ALG dostał AEAD ze
splice(), więc page cache trafia do scatterlists - 2017: optymalizacja
req->src = req->dstsprawia, że strony page cache lądują w zapisywalnej liście docelowej
Exploit:
- Otwórz socket AF_ALG, binduj do
authencesn(hmac(sha256),cbc(aes)) splice()dane z pliku docelowego (np./usr/bin/su), page cache ląduje w scatterliściesendmsg()z kontrolowanym AAD, gdzie bajty 4–7 to wartość zapisurecv()uruchamia deszyfrowanie,authencesnzapisuje 4 bajty do page cache- Powtórz dla każdego fragmentu shellcode’u
execve("/usr/bin/su")odpala zmodyfikowaną binarkę z page cache, shellcode jako root
Cały exploit: 732 bajty Pythona, standardowa biblioteka, zero zależności.
Odkrywca: Theori / Xint Code.
Fix: commit a664bf3d603d, powrót do operacji out-of-place, rozdzielenie req->src i req->dst.
Porównanie
| Dirty COW | Dirty Pipe | CopyFail | |
|---|---|---|---|
| CVE | CVE-2016-5195 | CVE-2022-0847 | CVE-2026-31431 |
| CVSS | 7.0 | 7.8 | 7.8 |
| Podsystem | mm (COW) | pipe | crypto (AF_ALG) |
| Mechanizm | race condition w madvise/write | stała flaga CAN_MERGE po splice | scratch write authencesn do page cache |
| Deterministyczny | nie | tak | tak |
| Czas w kernelu | 9 lat (2007–2016) | 2 lata (2020–2022) | 9 lat (2017–2026) |
| Exploit | C, dwa wątki, miliony prób | C, sekwencja syscalli | Python, 732 bajty |
| Modyfikuje dysk | tak | nie (tylko page cache) | nie (tylko page cache) |
| Ucieczka z kontenera | nie dotyczy | tak | tak |
| Używany in the wild | tak (przed ujawnieniem) | tak (CISA KEV) | niepotwierdzony |
Ewolucja
Trzy podatności pokazują ten sam prymityw (zapis do page cache pliku tylko do odczytu), ale z rosnącą elegancją:
Dirty COW wymagał wygrania wyścigu między dwoma wątkami. Niestabilny, ale działał na kernelach od 2.6.22, czyli praktycznie na wszystkim.
Dirty Pipe wyeliminował race condition. Jedna niezainicjalizowana flaga wystarczyła, żeby splice + write trafiały bezpośrednio do page cache. Nie modyfikował pliku na dysku, więc był trudniejszy do wykrycia.
CopyFail poszedł dalej: exploit mieści się w jednym krótkim skrypcie Pythona, działa identycznie na Ubuntu, RHEL, Amazon Linux i SUSE. Też nie dotyka dysku. Kontrolowany 4-bajtowy zapis pod dowolnym offsetem, powtarzalny bez limitu.
Wspólny mianownik: page cache jako współdzielony zasób między userspace a kernelem to potężny mechanizm, ale każdy błąd, który pozwala na nieautoryzowany zapis do niego, natychmiast daje eskalację uprawnień.
Więcej o CopyFail: CopyFail: 9 lat ukrytej eskalacji uprawnień | Sprawdź czy Twój kernel jest podatny
Źródła: dirtycow.ninja, dirtypipe.cm4all.com, copy.fail, NVD