CVE-2016-5195 (Dirty COW), Linux内核竞争条件漏洞,可导致非授权用户向任意可读文件写入任意内容,从而进行本地权限提升。影响范围为2.6.22-4.8.3,可以用于至今为止Android任意版本的root。

这个漏洞涉及不少内存管理的具体实现,所以分析学习起来很多东西对于我来说还是很不清晰,看来之后要把关键的内核代码多看一看。

漏洞分析

漏洞的根源在于对Copy-on-write的page强制写入的相关处理中,比如POC中通过/proc/self/mem对使用MAP_PRIVATE和PROT_READ方式映射的只读内存进行写入操作(或者是通过ptrace+PTRACE_POKEDATA来完成),这样的写入是没有问题的,因为写入的是COW的内存副本,并不会同步回原文件中去(而如果是使用MAP_SHARED方式映射,那么首先就需要对文件的写入权限才能映射成功,这样即便是将改动写回文件也不是越权操作)。

具体地,kernel会首先通过get_user_pages()获取目标page,然后再对目标page进行无视权限的写入操作。get_user_pages()的相关代码:

1
2
3
4
5
6
7
8
9
10
569 retry:
... [...]
577 page = follow_page_mask(vma, start, foll_flags, &page_mask);
578 if (!page) {
579 int ret;
580 ret = faultin_page(tsk, vma, start, &foll_flags,
581 nonblocking);
582 switch (ret) {
583 case 0:
584 goto retry;

在这里的时候将会进入一个follow_page_mask()和faultin_page()间的循环处理。如果是正常流程的话,第一次执行进入follow_page_mask(),会由于对应pte为空而直接返回进入到falutin_page()处理缺页,在faultin_page()中经过handle_mm_fault()调用到handle_pte_fault进行处理,相关代码:

1
2
3
4
5
6
7
8
9
10
354 static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma,
355 unsigned long address, unsigned int *flags, int *nonblocking)
356 {
... [...]
381 ret = handle_mm_fault(vma, address, fault_flags);
... [...]
414 if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
415 *flags &= ~FOLL_WRITE;
416 return 0;
417 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
3482 static int handle_pte_fault(struct fault_env *fe)
3483 {
... [...]
3523 if (!fe->pte) {
3524 if (vma_is_anonymous(fe->vma))
3525 return do_anonymous_page(fe);
3526 else
3527 return do_fault(fe);
3528 }
... [...]
3540 if (fe->flags & FAULT_FLAG_WRITE) {
3541 if (!pte_write(entry))
3542 return do_wp_page(fe, entry);
3543 entry = pte_mkdirty(entry);
3544 }
... [...]
3562 }

可以看到这一次调用,由于对应pte不存在,满足第一个if,会使用do_fault处理缺页,在do_fault中,由于对应的vma是个VM_PRIVATE同时要求写权限,会直接使用do_cow_fault来获取一个cow的page:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
3317 static int do_fault(struct fault_env *fe)
3318 {
3319 struct vm_area_struct *vma = fe->vma;
3320 pgoff_t pgoff = linear_page_index(vma, fe->address);
3321
3322 /* The VMA was not fully populated on mmap() or missing VM_DONTEXPAND */
3323 if (!vma->vm_ops->fault)
3324 return VM_FAULT_SIGBUS;
3325 if (!(fe->flags & FAULT_FLAG_WRITE))
3326 return do_read_fault(fe, pgoff);
3327 if (!(vma->vm_flags & VM_SHARED))
3328 return do_cow_fault(fe, pgoff);
3329 return do_shared_fault(fe, pgoff);
3330 }

之后结束第一轮处理流程,进入retry第二次进入follow_page_mask()来获取pte,这次pte已经存在但是由于不具备写权限又一次直接返回NULL进入falutin_page(),这一次falutin_page()会使用do_wp_page()来处理对不具备写权限的page的cow操作。do_wp_page()会首先检查是否真的需要cow,由于之前已经进行过cow操作,所以目标page满足PageAnon()且引用计数为1,可以直接reuse这个page而避免一次cow,所以do_wp_page()直接进行reuse同时返回VM_FAULT_WRITE。在前面的faultin_page()的流程中可以看到,如果返回VM_FAULT_WRITE并且对应vma没有写权限,那么它会去除请求标志位中的FOLL_WRITE然后返回。这意味着同一个请页请求不再需要对应的page具有写权限。这样再一次retry通过follow_page_mask()就可以获取对应的page。

但是如果再第二次使用falutin_page()处理然后去掉了FOLL_WRITE请求标志位以后,在这个时候产生了一次madvise()的调用来将目标pte置空,就会出现问题。在这样的情况下,follow_page_mask()会发现pte为空而再次进入falutin_page()处理缺页,同时由于不需要写权限,这一次的缺页处理使用的是do_read_fault(),不会产生一个cow的副本,之后第四次调用follow_page_mask()也就会获得这个page从而对这个page进行强制写入操作,造成越权写。

Exploit

由于这个poc能够对任意可读文件进行写入,基本上root也就拿到了。对于各linux发行版来说,可选择的方法有很多,去写libc让root进程弹个shell或者改写带suid的bin的控制流拿shell,再或者写/etc/passwd也行。一些exploits的清单已经在github上了。

对于Android平台上的利用,像suid的bin和/etc/passwd这样的肯定是不能用了,不过在libc中留后门应该还是没有问题,清单中有一个是用patch VDSO的方式弹shell,这样可以直接让init弹个shell回来,直接就可以有init的context,省去了不少麻烦。可是还是一样的问题,就算有init的context,还是不能为所欲为,依然不能开shell或者是关SELinux,SELinux果然是最麻烦的东西。暂时还没看到什么好的办法,github上的那个exploit也已经明说了”a patch to sepolicy is still needed”。

不过在另一个repo的issue里还看到了另一种思路,如果拿到可以insmod的context可以用插入一个kernel module来关闭SELinux,这样也还是要取决于SEPolicy有没有给这个insmod的权限。

Patch

出于对性能和效率的考虑,对于此漏洞的修复采用额外的FOLL_COW标志来处理。在本来去掉FOLL_WRITE的地方改为了置位FOLL_COW,并把follow_page_pte()中对于如果要写不可写的页则返回NULL,改为了如果要写的页既不可写又不是强制写COW处理过的脏页则返回NULL(我觉得这样更好理解了一些orz),详见漏洞patch:https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=19be0eaffa3ac7d8eb6784ad9bdbc7d67ed8e619