UAF学习记录

为什么80%的码农都做不了架构师?>>>   hot3.png

[责任声明]

考虑到安全风险,不提供代码——任何与本文有关的代码或程序,均与本人无关。

[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也是不难的事情。

转载于:https://my.oschina.net/ejoyc/blog/657637

你可能感兴趣的:(UAF学习记录)