用VBoxDbg除錯並理解單執行緒版髒牛(CVE-2016-5195)

Editor發表於2018-08-10


1、序言


髒牛是前幾年比較流行的一個漏洞,網上關於該漏洞的分析也比較多。但是對於像UP主一樣的新手而言,始終未能看到漏洞觸發的第一現場,比如像OOB/UAF等漏洞可以直接通過偵錯程式觀察記憶體看到漏洞確實可以觸發成功。但髒牛卻不是這樣,主要因為雙機除錯Linux的時候,KGDB無法對頁表(實體記憶體)進行讀寫。再加上多執行緒Race也提升了除錯的難度,所以一直沒能感覺很好的看到髒牛觸發的第一現場到底發生了什麼,直到我遇到了VBoxDbg這個偵錯程式。

2、VboxDbg說明


在Windows平臺下,使用Windbg可以對實體記憶體和一些特殊暫存器進行讀寫。但是在Linux核心除錯的時候,就不那麼方便,因為(嚴格來說)KGDB+Linux核心沒有實現這樣的功能。即便可以通過編寫proc偽檔案系統模組來對實體記憶體進行讀寫,但還是沒有Windbg那種方式感覺方便。於是UP主找了各種資料,最後終於發現一款比較好用的偵錯程式VboxDbg。

VBoxDbg是VirtualBox內建的一款偵錯程式,主要用於除錯在VirtualBox內執行的Guest主機。VBoxDbg也可以檢視Guest機器的實體記憶體和GDTR/IDTR等資訊。


3、單執行緒版DirtyCow


髒牛能觸發的一個必要原因是:雙執行緒競爭執行、對第四級頁表中的PTE進行讀寫操作。

為了更好的凸顯髒牛的Race的問題,我把網上的公開POC修改為一個單執行緒版本的髒牛poc,具體如下:


#include <stdio.h>

#include <sys/mman.h>

#include <fcntl.h>

#include <unistd.h>

#include <sys/stat.h>

#include <string.h>

#include <stdint.h>

void *map;

int f;

struct stat st;

char *name;

void *worker_write(void *arg)

{

char *str;

str=(char*)arg;

int f=open("/proc/self/mem",O_RDWR);

int i,c=0;

lseek(f,(uintptr_t) map,SEEK_SET);

write(f,str,strlen(str));

printf("procselfmem %d\n\n", c);

}

int main(int argc,char *argv[])

{

if (argc<3)

{

(void)fprintf(stderr, "%s\n","usage: dirtycowtest target_file new_content");

return 1;

}

f=open(argv[1],O_RDONLY);        //這裡由於開啟的是root許可權檔案,所以理論上來說使用普通使用者許可權是無法對root檔案進行修改的

fstat(f,&st);

name=argv[1];

map=mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);

printf("mmap %zx\n\n",(uintptr_t) map);

worker_write(argv[2]);        //這裡嘗試進行了寫操作  

return 0;

}

可以看到,程式碼中只有一個執行緒,且該執行緒僅僅執行了一個write動作。我們等下會用VBoxDbg來模擬第二個執行緒原本的動作(madvise)


4、除錯單執行緒版DirtyCow


除錯的總體思路為:

使用GDB+KGDB在follow_page_mask函式呼叫返回後的第一條核心指令、以及faultin_page函式內部呼叫handle_mm_fault函式返回後的第一條核心指令處分別下記憶體訪問斷點;

使用GDB+KGDB監控使用者態呼叫mmap後返回地址處的記憶體資訊;

在第二次呼叫handle_mm_fault後藉助VBoxDbg手動修改PTE資訊並讓程式執行起來,觀察結果。

由於Dirty Cow與ASLR/KASLR無關,所以在本實驗中為了方便除錯已經手動關閉了ASLR

首先直接執行修改過的dirtycowtest(執行結果如圖1),可以發現,執行後sensitive.txt檔案資訊並未發生改變,同時記錄下mmap返回地址:0x7ffff7ff7000

用VBoxDbg除錯並理解單執行緒版髒牛(CVE-2016-5195)

圖1 直接執行dirtycowtest發現root許可權檔案未被修改


然後再次執行dirtycowtest程式,同時使用GDB+KGDB觀察在__get_user_pages函式內部的執行情況,情況如下(如圖2):

用VBoxDbg除錯並理解單執行緒版髒牛(CVE-2016-5195)

圖2 第一次呼叫follow_page_mask後斷下


可以看到,當前處於剛剛執行完follow_page_mask的時刻(圖2),並且此時由於頁面未被調入實體記憶體中,所以使用者態地址0x7ffff7ff7000處於無法訪問的狀態。且此時執行follow_page_mask的返回結果也為0(表示返回錯誤,未獲取到struct page資訊)。接下來繼續執行,進入到faultin_page函式內部,在handle_mm_fault函式執行完畢後斷下:

用VBoxDbg除錯並理解單執行緒版髒牛(CVE-2016-5195)

圖3 第一次呼叫handle_mm_fault後斷下


此時(圖3)可以看到,地址0x7ffff7ff7000可以被訪問到了(頁表中PTE狀態為present),並且結果為0x000a363534333231(sensitive.txt的檔案內容的二進位制形式表示)。然後繼續執行:

用VBoxDbg除錯並理解單執行緒版髒牛(CVE-2016-5195)

圖4 第二次呼叫follow_page_mask後斷下


當再次執行完follow_page_mask函式時(圖4),發現雖然地址0x7ffff7ff7000可以被訪問了,但follow_page_mask返回值page依然為0。這是由於foll_flags與頁表PTE記錄的許可權資訊不符,因此觸發了一個寫異常。繼續執行:

用VBoxDbg除錯並理解單執行緒版髒牛(CVE-2016-5195)

圖5 第二次呼叫handle_mm_fault後斷下


此時(圖5)是第二次執行完handle_mm_fault函式,也就是此時核心完成了COW頁面的生成過程(但由於是同一程式訪問自身記憶體,所以實際上只有一個物理頁面)。然後就需要使用VBoxDbg進行後續工作了:

首先通過已知的使用者態地址0x7ffff7ff7000,可以計算出在x86_64環境中,其四級頁表的每級偏移分別為:0x7F8、0xFF8、0xDF8、0xFB8。這裡我們要使用到VBoxDbg的四個命令,分別為:


用VBoxDbg除錯並理解單執行緒版髒牛(CVE-2016-5195)


結合cr3暫存器資訊,和上述已知四個頁表的偏移資訊,可以計算出位於第四級頁表中的PTE資訊。然後手動對該PTE清零,即可模擬原本Dirty Cow漏洞exploit中的第二個madvise執行緒工作,具體操作記錄如圖6:

用VBoxDbg除錯並理解單執行緒版髒牛(CVE-2016-5195)

圖6 使用VBoxDbg修改PTE


隨後禁用GDB+KGDB環境中的所有斷點繼續執行,即可從虛擬機器中看到執行結果。對比圖1中的結果可以發現,雖然dirtycowtest程式沒有發生變化,但是確實完成了對sensitive.txt檔案的修改(已修改為111111),結果如圖7:

用VBoxDbg除錯並理解單執行緒版髒牛(CVE-2016-5195)

圖7 利用dirtycowtest成功使用低許可權使用者修改root許可權檔案內容


至此,完成了DirtyCow單執行緒版本的除錯。在進行頁表或實體記憶體除錯等工作時,VBoxDbg有著GDB/KGDB無法比擬的優勢。VBoxDbg還有很多功能,本文無法一一介紹。總體來說,VBoxDbg是一款可以與GDB/KGDB互補的簡單好用的除錯工具。


5、參考文件

Dirty Cow(CVE-2016-5195)

VirtualBox User Manual

相關文章