前面一段時間,這個編號為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://bugzilla.suse.com/show_bug.cgi?id=1004418#c14
