6
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
![862439_G57AGPG3VC4APVZ.png][1]
BSOD分析
首先,我们将poc放入虚拟机中并运行,触发崩溃之后转到windbg中。先查看漏洞成因
![CVE-2018-8453 (1).png][2]
程序试图释放一块已经释放了的pool,说明这是一个经典的Double Free漏洞。看一下这个pool的属性
![CVE-2018-8453 (2).png][3]
这是一个0x80大小的session pool,划重点,这里后面要用到的。接着看一下调用关系
![CVE-2018-8453 (3).png][4]
静态分析可知,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
![CVE-2018-8453 (4).png][5]
断了几次都在申请内存的时候,最终,我们可以断在nt!ExFreePoolWithTag函数,该函数正打算释放pSTBrack,看起来和第二次释放没什么区别,但看一下堆栈就发现问题所在了。
![CVE-2018-8453 (5).png][6]
这次释放发生在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: