CVE-2016-5195 Dirtycow分析
前言
(Linux kernel>2.6.22 (released in 2007))
写时拷贝
COW(copy on write)技术即写时拷贝技术是linux程序中用的一个技术,在程序fork进程时,内核只为子进程创建虚拟空间结构,虚拟空间拷贝父进程的对应段内容,也就是说子进程对应段和父进程指向同一块物理内存,直到父进程/子进程中有改变段内容的操作再为子进程相应段分配物理空间(如exec)。
基本原理是将内存页标为只读,一旦父子进程要改变内存页内容,就会触发页异常中断,将触发的异常的内存页复制一份(其余页还是同父进程共享)。
dirty bit,这个标志位是Linux中的概念,当处理器写入或修改内存的页,该页就被标记为脏页。这个标志的作用是提醒CPU内存的内容已经被修改了但是还没有被写入到磁盘保存。
漏洞涉及的相关函数和全局变量:
FAULT_FLAG_WRITE,表示这是写异常
#define FAULT_FLAG_WRITE 0x01 /* Fault was a write access */
FOLL_WRITE,表示在寻页的时候需不需要页具有写权限
#define FOLL_WRITE 0x01 /* check pte is writable */
VM_WRITE,表示该页可不可写
#define VM_WRITE 0x00000002
__get_user_page中的retry:
flags变量是一开始传入的FOLL_FORCE,代表强制读写, pte_write(pte)检测页表里的可写位 因为此块内存是只读映射,所以pte_write返回false。进入if ((flags & FOLL_WRITE) && !pte_write(pte)) 条件分支。
漏洞成因在于:进程的写请求标志移除后,要对COW进行写入时,由于条件竞争,调用madvise,清空页表项,触发pagefault,此时可以対页进行写入,所以不再进行COW操作,对原文件进行写入。
代码流程梳理
第一次会检测是否获得page,如果page为空,就会触发页错误,调用fault_page,第一次是通不过if(!pte_present(pte))条件,因为页表不存在,进入faultin_page,进行了COW操作:
__get_user_pages
follow_page_mask
follow_page_pte
goto no_page <- if(!pte_present(pte))
faultin_page <- if(!pte_present(pte))
handle_mm_fault
__handle_mm_fault
handle_pte_fault
do_fault <- pte is not present if (!pte_present(entry)) { if (pte_none(entry)) {
do_cow_fault <- FAULT_FLAG_WRITE=1, if(!(vma->vm_flags&VM_SHARED))
alloc_set_pte
maybe_mkwrite(pte_mkdirty(entry), vma) <- mark the page dirty
/*but keep it RO , VMA的vm_flags属性不具有可写属性,因此这里不会设置pte_entry为可写,只是设置为可读和dirty*/
第二次触发faultin_page,此时页表不为空,FOLL_WRITE为1,pte不可写,返回NULL,再次触发一个pagefault,faultin_page会再次调用handle_mm_fault进行处理:
__get_user_pages(retry)
-> follow_page_mask
follow_page_pte <- if(flags&FOLL_WRITE) && !pte_write(pte)
return NULL;
faultin_page <- if(flags&FOLL_WRITE) && !pte_write(pte) ,此时FOLL_WRITE为1
handle_mm_fault
__handle_mm_fault
handle_pte_fault
do_wp_page <- if (flags & FAULT_FLAG_WRITE) {if (!pte_write(entry))
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
一直return 到faultin_page
if((ret&VM_FAULT_WRITE)&&!(vma->vm_flags&VM_WRITE))
*flags &= ~FOLL_WRITE;
return 0;
此时ret= VM_FAULT_WRITE, VM_WRITE为0(表示该页不可写),满足条件,所以flags中的FOLL_WRITE被置0,即第一个bit被置0,返回到follow_page_pte。
第三次:
__get_user_pages(retry)-> cond_resched -> different thread will now unmap via madvise
->follow_page_mask
follow_page_pte
goto no_page; <-!pte_present(pte) & pte_none(pte)
faultin_page <-!pte_present(pte) & pte_none(pte)
handle_mm_fault
__handle_mm_fault
handle_pte_fault
do_fault <- pte is not present if (!pte_present(entry)) { if (pte_none(entry)) {
do_read_fault <- if(!(fe->flags)&FAULT_FLAG_WRITE)
(this is a read fault and we will get pagecache page!)
因为FOLL_WRITE被置为0,在寻页的时候不再需要页具有写权限,但是madvise(Mappedmem,DONT_NEED)系统调用,通知内核Mappedmem在接下来不会被使用。内核会将Mappedmem所在页的页表项置为空,再次导致了pagefault。此时不再进入COW流程,所以此时没有写时复制操作,直接写入原文件。正常只是在映射的内存上进行写操作,不会影响原文件。
如果接下来get_user_pages第四次调用follow_page_mask进行寻页的话,会成功返回对应的页表项,接下来的写入操作会被同步到只读的文件中。从而造成了越权写。
unsigned int foll_flags = gup_flags;
page = follow_page_mask(vma, start, foll_flags, &page_mask);
ret = faultin_page(tsk, vma, start, &foll_flags, nonblocking);
faultin_page:
if (*flags & FOLL_WRITE)
fault_flags |= FAULT_FLAG_WRITE;
ret = handle_mm_fault(vma, address, fault_flags);//处理缺页的函数
static int __handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
unsigned int flags)
{
struct fault_env fe = {
.vma = vma,
.address = address,
.flags = flags,
};
从上面可以看出if(!(fe->flags)&FAULT_FLAG_WRITE)中的flags值和*flags &= ~FOLL_WRITE;中的相同,所以flag的第一个bit(FOLL_WRITE)为0,导致进入do_read_fault,而不是do_cow_fault。
补丁:
(1)补丁中增加了FOLL_COW标志用以标识所映射页面是否已进行过COW,即不用移除写请求标志来标识COW,而是或上新的标识位,这样进程的写请求标志属性依然存在,避免了后面错误地转换成读请求处理。
(2)在判断进程请求标识与页面属性是否匹配时(决定是否进入新的缺页异常),会综合考虑FOLL_FORCE标志位与COW是否匹配,即只有COW后的页面才能强行读写。(保证COW过的页面不会再进入异常,可以正常对COW页面进行写)
参考链接
分析文章:
https://xz.aliyun.com/t/7561 有完整的相关函数代码
https://github.com/dirtycow/dirtycow.github.io/wiki/VulnerabilityDetails https://www.youtube.com/watch?v=kEsshExn7aE
Exp代码:https://github.com/FireFart/dirtycow/blob/master/dirty.c 测试可用
https://github.com/gbonacini/CVE-2016-5195/blob/master/dcow.cpp 可稳定提权
漏洞补丁:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=19be0eaffa3ac7d8eb6784ad9bdbc7d67ed8e619
补丁commit:19be0eaffa3ac7d8eb6784ad9bdbc7d67ed8e619