为什么80%的码农都做不了架构师?>>>
[责任声明]
考虑到安全风险,不提供代码——任何与本文有关的代码或程序,均与本人无关。
[STEP0:问题背景]
"CVE 2015-0057"是一个关于win32k!xxxEnableWndSBArrows的UAF可利用漏洞 : win32k!xxxEnableWndSBArrows没有校验ScrollBar对应内核数据结构的状态, 直接修改既定地址的数据, 触发UAF.
在一个相对较老的Win7sp1x86系统上, 笔者触发了这个漏洞, 并简单验证了其利用。 相关的背景及说明, 可以参考PDF链接https://www.nccgroup.trust/globalassets/newsroom/uk/blog/documents/2015/07/exploiting-cve-2015.pdf
[STEP1:问题根源]
借助WINDBG/NT4, 可以确定代码路径: user32!EnableScrollBar -> win32k!NtUserEnableScrollBar -> win32k!xxxEnableScrollBar -> win32k!xxxEnableWndSBArrows .
在IDA/NT4的帮助下, 可知win32k!NtUserEnableScrollBar ->..-> win32k!xxxEnableWndSBArrows的路径是:
//BOOL NtUserEnableScrollBar(HWND hwnd, UINT wSBflags, UINT wArrows)
if ( wSBflags <= SB_BOTH )// SB_BOTH : 3 ; SB_CTL : 2; SB_VERT : 1; SB_HORZ : 0
{
if ( wSBflags != SB_CTL || pwnd->fnid == FNID_SCROLLBAR )
{
retval = xxxEnableScrollBar(pwnd, wSBflags, wArrows);//pwnd = ValidateHwnd(hwnd);
}
}
//BOOL xxxEnableScrollBar(PWND pwnd, UINT wSBflags, UINT wArrows)
if ( wSBflags != SB_CTL )
{
return xxxEnableWndSBArrows(pwnd, wSBflags, wArrows);
}
对于EnableScrollBar, 若hwnd为一个Scrollbar窗体, wSBflags为{0,1,3}之一, wArrows为任意值 那么xxxEnableScrollBar就会被执行到。现在是时候看看问题的根源函数了:
BOOL xxxEnableWndSBArrows(PWND pwnd, UINT wSBflags, UINT wArrows)
{
UINT wOldFlags = 0;
BOOL bRetValue = FALSE;
HDC hdc;
PWND pw = pWnd->pSBInfo;
if (pw != NULL)
{
wOldFlags = (UINT)pw->WSBflags; //(1)
}
else
{
if (!wArrows)
return FALSE;
if((pw = _InitPwSB(pwnd)) == NULL)
return FALSE;
}
if ((hdc = _GetWindowDC(pwnd)) == NULL) //(1)
return FALSE;
if((wSBflags == SB_HORZ) || (wSBflags == SB_BOTH)) //(2)
{
if(wArrows == ESB_ENABLE_BOTH) // ESB_ENABLE_BOTH : 0
pw->WSBflags &= ~SB_DISABLE_MASK; // SB_DISABLE_MASK : 3
else
pw->WSBflags |= wArrows; //(2)
if (pSBINFO->WSBflags != (int)WSBflags) //(3)
{
bRetValue = TRUE;
WSBflags = (UINT)pSBINFO->WSBflags;
if(TestWF(pwnd, WFHPRESENT) && !TestWF(pwnd, WFMINIMIZED) && IsVisible(pwnd))
xxxDrawScrollBar(pwnd, hdc, FALSE); //(3)
}
//EVENT_OBJECT_STATECHANGE : 0x0000800A
if ( ((BYTE)wOldFlags ^ (BYTE)pSBINFO->WSBflags) & 1 )
xxxWindowEvent(EVENT_OBJECT_STATECHANGE, pwnd, -6, 1, 1);
if ( ((BYTE)wOldFlags ^ (BYTE)pSBINFO->WSBflags) & 2 )
xxxWindowEvent(EVENT_OBJECT_STATECHANGE, pwnd, -6, 5, 1);
}
if((wSBflags == SB_VERT) || (wSBflags == SB_BOTH))
{
if(wArrows == ESB_ENABLE_BOTH) //ESB_DISABLE_BOTH : 3
pw->WSBflags &= ~(SB_DISABLE_MASK << 2);
else
pw->WSBflags |= (wArrows << 2);
if(pw->WSBflags != (int)wOldFlags)
{
bRetValue = TRUE;
if (TestWF(pwnd, WFVPRESENT) && !TestWF(pwnd, WFMINIMIZED) && IsVisible(pwnd))
xxxDrawScrollBar(pwnd, hdc, TRUE);
if ( ((BYTE)wOldFlags ^ (BYTE)pSBINFO->WSBflags) & 4 )
xxxWindowEvent(EVENT_OBJECT_STATECHANGE, pwnd, -5, 1, 1);
if ( (((BYTE)wOldFlags ^ (BYTE)pSBINFO->WSBflags) & 8 )
xxxWindowEvent(EVENT_OBJECT_STATECHANGE, pwnd, -5, 5, 1);
}
}
_ReleaseDC(hdc);
return bRetValue;
}
[STEP2:利用分析]
对于上面的函数, 结合IDA + NT4, 可知 :
(1) 若传入的pwnd对应一个有效的Scrollbar窗口对象, 则pw和hdc不会为NULL;
(2) 若传入的wSBflags值为SB_BOTH,
如果wArrows为ESB_ENABLE_BOTH时, 对pw->WSBflags的影响有限;
考虑wArrows取值非0, 比如 0x20|ESB_DISABLE_BOTH, pw->WSBflags会被调整;
(3) 要想win32k!xxxDrawScrollBar被执行, 需要保证pwnd对应的窗体是可见的、非最小化;
特别的, win32k!xxxDrawScrollBar很有可能会被执行两次.
(4) win32k!xxxDrawScrollBar(win32k!xxxDrawSB2)在完成之前, 会调用nt!KeUserModeCallback切换到UserMode执行一些必要的逻辑,
其中一个逻辑由Win32k!xxxGetColorObjects触发, 最终执行的是USER32!__fnDWORD;
(5) Win32k!xxxGetColorObjects通过Win32k!xxxGetControlBrush获取HBRUSH句柄;
Win32k!xxxGetControlBrush通过Win32k!xxxSendMessage(xxxSendMessageTimeout)切换到UserMode;
(6) Win32k.sys通过nt!KeUserModeCallback回调到USER32!__fnDWORD, 借助的是user32.dll内部维护的分发表apfnDispatch;
而apfnDispatch的地址是被记录在PEB的KernelCallbackTable成员中, PEB的基址由多种方法获取, 比如*(PVOID*)(__readfsdword(0x18) + 0x30);
在Win7sp1x86上, USER32!__fnDWORD对应apfnDispatch[2];
回到上面的函数xxxEnableWndSBArrows, 若在执行win32k!xxxDrawScrollBar过程中, 恰好在USER32!__fnDWORD被调用之前, apfnDispatch[2]被篡改为ProxyfnDWORD。
在ProxyfnDWORD中释放掉pWnd->pSBInfo对应的内存, 并设法把这个刚释放的内存申请到。那么在执行流回到win32k!xxxEnableWndSBArrows后即可触发UAF.
桌面窗口的GUI资源, 一般都是用RtlAllocateHeap/RtlFreeHeap在一个特殊的堆结构(桌面堆)上申请/回收内存资源: 窗口对象使用tagWND结构来描述, 一个Scrollbar属于特殊的窗口对象, 每CreateWindow一个Scrollbar, 系统会通过RtlAllocateHeap先创建一个tagWND, 再创建一个对应的tagSBInfo, 二者协同描述一个Scrollbar窗口对象; 这个tagSBInfo基址可能在tagWND基址之前, 也可能在其之后。因此要想利用这个UAF, 还需要做一些额外的工作。
(1) 通过CreateWindow创建大量的Scrollbar窗口对象, 迫使Scrollbar窗口对象的tagSBInfo位于tagWND之后;
(2) 需要至少三个tagSBInfo位于tagWND之后Scrollbar窗口对象;
基于WINDBG分析的结果,
对于这样的Scrollbar窗口对象: Base(tagSBInfo) == Base(tagWND) + 0x100;
对于这样的相邻接的Scrollbar窗口对象, 二者的基址差值为 0x148;
tagWND中有一个特殊的结构tagPROPLIST, 对应PropList对象;
对于这样的Scrollbar窗口对象: Base(tagSBInfo) == Base(tagWND) + 0x130;
(3) 对于三个邻接的Scrollbar窗口对象, 利用中间对象的UAF, 诱使低地址对象的PropList数据能淹没到高地址对象上.
[STEP3:利用完成]
现在, 到了利用上述UAF的时候了。
(1) 分配大量的Scrollbar窗口, 保证它们都是可见的(ShowWindow(hSBCtrl, SW_SHOW)即可),
得到多个3个tagSBInfo位于tagWND之后Scrollbar窗口对象, 设分别为A、B、C, 其中A地址最小、C地址最大。
(2) Hook当前进程的apfnDispatch[2], 设代理函数为ProxyfnDWORD;
(3) 调用EnableScrollBar(B.hWnd, SB_BOTH, ESB_DISABLE_BOTH | 0x40), 在后续的ProxyfnDWORD调用中, 销毁B.hWnd:
此时, 由于B的tagWND还在被使用, 不能立即free, 而对应的tagSBInfo则立即free了;
立即调用user32!NtUserSetProp为A申请资源, 设置三组即可, 然后调用原始USER32!__fnDWORD返回到win32k中;
(4) win32k!xxxEnableWndSBArrows后续会调整B.tagWND->pSBInfo->WSBflags, 实际上调整的是A.tagWND->ppropList.cEntries:
最初为1, 接着给其新建了3个(此时为4), 这就导致A.tagWND->ppropList申请到了原B.tagWND->pSBInfo的内存资源;
由于win32k!xxxEnableWndSBArrows调整, 导致A.tagWND->ppropList.cEntries变为0x10C;
这样一来, A.tagWND->ppropList(原B.tagWND->pSBInfo), 可以再容纳0x108个tagPROP数据;
(5) EnableScrollBar返回后, 可以自由的给A执行user32!NtUserSetProp来覆盖B和C的内存了:
比如可以覆盖C.tagWND->lpfnWndProc、C.tagWND->strName, 可选的余地很大;
一种投机方式(不考虑SMEP)是, 在Win7sp1x86上可以选择覆盖C.tagWND->pSBInfo为nt!HalDispatchTable, 通过NtUserSetScrollInfo完成攻击.
[STEP4:后话]
原作者提到通过调整tagWND->strName或者或者堆指针完成攻击, 这是很高端的利用手法。
笔者选择了攻击tagWND->pSBInfo, 比较直接地实现任意地址写(可控内容至少2个ULONG)的最终目的, 但副作用是BSOD. BSOD的原因是在漏洞利用过程中破坏了太多的关键数据, 比如free的堆块和高地址的Scrollbar窗口对象的数据, 好在可以事前备份数据+事后修复数据, 规避BSOD也是不难的事情。