深入分析CVE-2016-5195 Dirty Cow


  前面一段時間,這個編號為CVE-2016-5195的漏洞刷爆了各個安全相關的博客和網站,這個漏洞可以對任意可讀文件進行寫操作從而導致提權,通殺了包括Android在內的絕大多數linux版本,,影響不可為不大,試着分析一下。

一:漏洞分析

  這個漏洞邏輯並不復雜,分析的最大難點是流程復雜,容易繞暈在代碼迷宮里,所以先梳理一下流程,流程如下(初略掃過即可),根據下面分析來查看上面流程):

//進行寫操作
mem_write
    mem_rw  
     access_remote_vm
            __access_remote_vm        
                //用於獲取頁
                get_user_pages
                    __get_user_pages
                        retry:
                            follow_page_mask(...,flag,...);
                                //通過內存地址來找到內存頁
                                follow_page_pte(...,flag,...);
                                     //如果獲取頁表項時要求頁表項所指向的內存映射具有寫權限,但是頁表項所指向的內存並沒有寫權限。則會返回空
                                    if ((flags & FOLL_WRITE) && !pte_write(pte)) 
                                        return NULL
                                    ////獲取頁表項的請求不要求內存映射具有寫權限的話會返回頁表項
                                    return page
                            if (foll_flags & FOLL_WRITE)//要求頁表項要具有寫權限,所以FOLL_WRITE為1
                                fault_flags |= FAULT_FLAG_WRITE;
                            //獲取頁表項
                            if (!page) {
                                faultin_page(vma,...); //獲取失敗時會調用這個函數
                                     handle_mm_fault();
                                        __handle_mm_fault()
                                            handle_pte_fault()
                                                if (!fe->pte) 
                                                    do_fault(fe);
                                                        ////如果不要求目標內存具有寫權限時導致缺頁,內核不會執行COW操作產生副本,ers
                                                        if (!(fe->flags & FAULT_FLAG_WRITE))
                                                            do_read_fault(fe, pgoff);
                                                                __do_fault(fe, pgoff, NULL, &fault_page, NULL);
                                                                ret |= alloc_set_pte(fe, NULL, fault_page)
                                                                    //如果執行了COW,設置頁表時會將頁面標記為臟,但是不會標記為可寫。
                                                                    if (fe->flags & FAULT_FLAG_WRITE)
                                                                        entry = maybe_mkwrite(pte_mkdirty(entry), vma);
                                                        //如果要求目標內存具有寫權限時導致缺頁,目標內存映射是一個VM_PRIVATE的映射,內核會執行COW操作產生副本
                                                        if (!(vma->vm_flags & VM_SHARED))
                                                            do_cow_fault(fe, pgoff);
                                                                new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, fe->address);
                                                                ret = __do_fault(fe, pgoff, new_page, &fault_page, &fault_entry);
                                                                copy_user_highpage(new_page, fault_page, fe->address, vma);
                                                                ret |= alloc_set_pte(fe, memcg, new_page);
                                                if (fe->flags & FAULT_FLAG_WRITE)
                                                    if (!pte_write(entry))
                                                        do_wp_page(fe, entry)//VM_FAULT_WRITE置1
                                                            if ((flags & FAULT_FLAG_WRITE) && reuse_swap_page(page))
                                                                maybe_mkwrite(pte_mkdirty(entry), vma);
                                                                if (likely(vma->vm_flags & VM_WRITE))
                                                                    pte_mkwrite(pte);
                                                                flags &= ~FAULT_FLAG_WRITE;
                                                                ret |= VM_FAULT_WRITE;
                                if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
                                    *flags &= ~FOLL_WRITE;
                          ,==0 goto retry        
        if (write)
          copy_to_user_page

  進行逐步分析,並在分析中參考上文代碼流程。

  漏洞發生在調用write函數時,經過一系列調用(write->mem_write->mem_rw->access_remote_vm->__access_remote_vm),通過在__access_remote_vm的get_user_pages來獲得頁,copy_to_user_page來講內容復制進頁中。而漏洞就發生在get_user_pages中。下面來分析get_user_pages的具體流程。

  get_user_pages中主要部分是一個循環,直到正確找到頁,其中有兩個函數極為重要,follow_page_mask和faultin_page,其中follow_page來找到頁,dofault_page在尋頁失敗的時候建立映射為下次調用follow_page_mask來做准備。

  第一次執行,由於mmap對第一次對映射內存進行操作,所以並不能直接從頁表中找到。get_user_page,因為我們要求頁表項要具有寫權限,所以FOLL_WRITE為1,設置FAULT_FLAG_WRITE然后調用了faultin_page,之后依次調用了handle_mm_fault->__handle_mm_fault->handle_pte_fault->do_fault->do_cow_fault,此時利用COW來生成了頁表,建立了映射。

  第二次執行,follow_page_mask會通過flag參數的FOLL_WRITE位是否為1判斷要是否需要該頁具有寫權限,以及通過頁表項的VM_WRITE位是否為1來判斷該頁是否可寫。由於Mappedmem是以PROT_READ和MAP_PRIVATE的的形式進行映射的。所以VM_WRITE為0,而FOLL_WRITE為1,返回null,進而調用faultin_page函數,此時由於已經找到了頁表,不再調用_do_fault,而是調用了do_wp_page,在do_wp_page中,將FAULT_FLAG_WRITE置0,同時,將ret的VM_FAULT_WRITE置1,表示已經執行過COW,在faultin_page之后的判斷中,由於ret中VM_FAULT_WRITE置1,則flag的FOLL_WRITE置0,而FOLL_WRITE置0代表着也頁表項不需要寫權限。

  第三次執行,此時調用follow_page_mask時可以正確找到頁了。由於進行了COW,所以寫操作並不會涉及到原始內存。

  但是正如POC,如果madvice發生在get_user_page第二次執行之后,madvice即取消內存的映射關系,那么第三次執行會follow_page_mask函數會失敗,進入dofault_page函數,此時的調用流程會和第一次有一定的區別,由於FAULT_FLAG_WRITE置0,所以直接執行do_read_fault。而do_read_fault函數調用了__do_fault,由於標志位的改變,此時直接與文件內存進行映射。

__do_fault部分代碼如下:

if ((flags & FAULT_FLAG_WRITE) && !(vma->vm_flags & VM_SHARED)) {
                 if (unlikely(anon_vma_prepare(vma)))
                       return VM_FAULT_OOM;
                 cow_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, address);
                if (!cow_page)
                         return VM_FAULT_OOM;
                 if (mem_cgroup_newpage_charge(cow_page, mm, GFP_KERNEL)) {
                         page_cache_release(cow_page);
                         return VM_FAULT_OOM;
                 }
         } else
                cow_page = NULL;

if (flags & FAULT_FLAG_WRITE) {
     if (!(vma->vm_flags & VM_SHARED)) { page = cow_page; anon = 1; copy_user_highpage(page, vmf.page, address, vma); __SetPageUptodate(page); } else 
            ...
}

 

    

  在第四次執行的時候,follow_page_mask直接獲得文件內存頁從而對其進行讀寫。

二:補丁分析

  這個漏洞是Linux的創始人Linus親自修復,簡略補丁如下:

+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)); +} +
 static struct page *follow_page_pte(struct vm_area_struct *vma, unsigned long address, pmd_t *pmd, unsigned int flags) { @@ -95,7 +105,7 @@ retry: } if ((flags & FOLL_NUMA) && pte_protnone(pte)) goto no_page; -    if ((flags & FOLL_WRITE) && !pte_write(pte)) { +    if ((flags & FOLL_WRITE) && !can_follow_write_pte(pte, flags)) { pte_unmap_unlock(ptep, ptl); return NULL; } @@ -412,7 +422,7 @@ static int faultin_page(struct task_struct *tsk, struct vm_area_struct *vma, * reCOWed by userspace write). */
     if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE)) -        *flags &= ~FOLL_WRITE; +            *flags |= FOLL_COW; return 0; }

 

  Linus在這里新添加了一個FOLL_COW的標志位,來表明已經進行了COW。同時在get_follow_mask判定權限的時候同時利用dirty位來判定FOLL_COW是否有效。

 

參考文獻:

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

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

https://bugzilla.suse.com/show_bug.cgi?id=1004418#c14

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM