剖析臟牛4_madvise()與漏洞成因
測試程序
int fd;struct stat st;void *mem;
void processMem(void){ int f = open("/proc/self/mem", O_RDWR); lseek(f, mem, SEEK_SET); write(f, "BBB", 3);
printf("%s", (char*)mem);
madvise(mem ,100 ,MADV_DONTNEED);}
int main(void){ fd = open("./test", O_RDONLY); fstat(fd, &st); mem = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
processMem();}
sys_madvise()
sys_madvise()首先進行簡單的參數處理

如果需要的話獲取mmap_sem信號量, 然后遍歷[start, start+len)內所有的VMA, 對于每個VMA調用madvise_vma()進行處理. 這里我們只關注behavior = MADV_DONTNEED

madvise_vma()
madvice_vma()根據behavior把請求分配到對應處理函數, 對于MADV_DONTNEED會調用madvise_dontneed()處理

madvise_dontneed()
排除掉一些無法丟棄的情況后, 會調用zap_page_range()處理

zap_page_range()
zap_page_range()會遍歷給定范圍內所有的VMA, 對每一個VMA調用unmap_single_vma(…)

后續會沿著unmap_single_vma() => unmap_page_range() => zap_pud_range() => zap_pmd_range() => zap_pte_range()的路徑遍歷各級頁表項, 最后調用zap_pte_range()遍歷每一個PTE
zap_pte_range()
zap_pte_range()會釋放范圍內所有的頁, 函數頭如下

然后遍歷范圍內所有頁, 清空頁表中對應的PTE, 并減少對應頁的引用計數, 當頁的引用計數為0時會被內核回收

dirty_COW漏洞
回想一下利用/proc/self/mem寫入進程只讀內存區的過程: access_remote_vm()會先調用get_user_pages()鎖定要寫入的頁, get_user_pages()會通過while( !follow_page_mask(foll_flag) ){ faultin_page(foll_flag); } 這個循環分配滿足foll_flag要求的頁
__get_user_pages()第一次循環
faultin_page()判斷屬于寫入只讀區域的情況, 因此會調用do_cow_fault()
do_cow_fault()會復制原始的文件緩存頁到一個新頁中, 并設置PTE映射到這個新頁, 但由于VMA不可寫入, 因此這個新頁的PTE頁沒有設置RW標志
__get_user_pages()第二次循環
由于foll_flags中有FOLL_WRITE標志, 但是頁對應的PTE沒有RW標志, 因此follow_page_mask()判斷權限有問題, 再次進入faultin_page()
faultin_page()判斷, 屬于寫入只讀的已存在的頁造成的問題, 因此會調用do_wp_page()處理
do_wp_page()發現對應頁是只有一個引用的匿名頁,因此會調用wp_page_reuse()直接重用這個頁
wp_page_reuse()由于對應VMA只讀, 因此只會給PTE設置一個Dirty標志, 而不會設置RW標志, 然后返回一個VM_FAULT_WRITE表示內核可以寫入這個頁
返回到faultin_page()中, 由于handle_mm_fault()返回了VM_FAULT_WRITE, 因此會去掉FOLL_WRITE標志, 含義為: 雖然此頁對應PTE不可寫入, 但是已經COW過了, 內核是可以寫入的, 后續follow_page_mask()就不要檢查能不能寫入了
如果說在清除FOLL_WRITE標志之后, 第三次調用follow_page_mask()之前, 我們通過madivse()設置此頁對應PTE為空會發生什么?
首先follow_page_mask()會因為對應PTE為NULL而再次失敗, 進入faultin_page(), 但是注意, 這次進入的時候沒有FOLL_WRITE標志
faultin_page()因此設置fault_flags時是沒有FAULT_FALG_WRITE標志的, 也就是說faultin_page()對handle_mm_fault()承諾不會寫入這個頁
handle_mm_fault()由于pte為none, 并且不要求寫入, 因此最終會分派給do_read_fault()處理
do_read_fault()會查找這片VMA映射的地址空間中, address對應的原始緩存頁, 然后返回這個原始緩存頁

如果是用戶映射一片只讀內存頁到文件, 返回原始緩存頁是沒有問題的, 因為用戶無權對其進行寫入. 但是在這里access_remote_vm()后續會調用copy_to_user_page() 寫入__get_user_pages()鎖定的頁, 由此污染了文件的原始緩存頁.
一段時間后當進行磁盤同步時(sync, kflushd….)內核會把被污染的頁面回寫到磁盤中, 從而寫入特權文件完成攻擊
那么下一個問題這個條件競爭的時間窗口有多大? 由于faultin_page()返回之后會調用cond_resched()切換到別的任務, 因此時間窗口是非常大的

受攻擊時對/proc/self/mem進行寫入時的執行流程:

EXP偽代碼
Main: fd = open(filename, O_RDONLY) //打開一個文件 fstat(fd, &st) map = mmap(NULL, st.st_size , PROT_READ, MAP_PRIVATE, fd, 0) //把文件映射到map指向的內存區域 start Thread1 start Thread2
Thread1: f = open("/proc/self/mem", O_RDWR) //打開mem文件 while (1): lseek(f, map, SEEK_SET) //偏移到map映射的區域 write(f, shellcode, strlen(shellcode)) //寫入
Thread2: while (1): madvise(map, 100, MADV_DONTNEED) //取消映射
反思
對于進程中的只讀內存區域, 如果通過地址進行寫入會得到一個段錯誤, 但是通過mem文件進行寫入, 就會得到一個dirty的COW的只讀頁, 為什么會有這樣的差異?
對于段錯誤, 這個很好理解, 但是通過mem文件寫入一個進程的只讀內存區, 破壞了進程的地址空間. 那么為什么內核還要引入這種外部訪問機制呢? 這是為了方便調試器和一些跟蹤程序而加入的設計
這個漏洞的修復也很簡單, COW之后不去掉FOLL_WRITE標志, 而引入一個新的標志FOLL_COW.
- pte設置了RW標志, 表示頁可寫入
- flags設置了FOLL_COW標志, 表示這是一個COW之后的頁面, 雖然PTE說不可寫入, 但是內核實際可以寫入
- 這樣就算進入follow_page_mask()前這個PTE被設為nonoe ,但foll_flags保留了FOLL_WRITE標志, 仍然會要求faultin_page分配一個要寫入的頁.
- 在follow_page_mask()檢查的時候如果foll_flags中設置了FOLL_WRITE要求寫入, 那么下面兩種情況都會被判斷為頁可寫入
- 修復的diff如下
diff --git a/include/linux/mm.h b/include/linux/mm.hindex e9caec6..ed85879 100644--- a/include/linux/mm.h+++ b/include/linux/mm.h@@ -2232,6 +2232,7 @@ static inline struct page *follow_page(struct vm_area_struct *vma, #define FOLL_TRIED 0x800 /* a retry, previous pass started an IO */ #define FOLL_MLOCK 0x1000 /* lock present pages */ #define FOLL_REMOTE 0x2000 /* we are working on non-current tsk/mm */+#define FOLL_COW 0x4000 /* internal GUP flag */
typedef int (*pte_fn_t)(pte_t *pte, pgtable_t token, unsigned long addr, void *data);diff --git a/mm/gup.c b/mm/gup.cindex 96b2b2f..22cc22e 100644--- a/mm/gup.c+++ b/mm/gup.c@@ -60,6 +60,16 @@ static int follow_pfn_pte(struct vm_area_struct *vma, unsigned long address, return -EEXIST; }
+/*+ * FOLL_FORCE can write to even unwritable pte's, but only+ * after we've gone through a COW cycle and they are 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));+}+ 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; }