CVE-2018-8453分析及利用EXP编写 (上篇)
在2018年11月份的时候,向论坛里各位请教了这个漏洞
https://bbs.pediy.com/thread-247444.htm
现在这个漏洞已经彻底搞完啦!来这里跟大家说一下,算是一个收尾。
上篇:从补丁diff到EXP--CVE-2018-8453漏洞分析与利用
下载该漏洞的单独补丁用bindiff与历史补丁进行查看,可发现主要变化如下(新旧变化主要用上一个补丁日的win32k.sys来比较,下同):
在底部,看到颜色差异比较大的就是一个叫NtUserSetWindowFNID的函数,比较一下:
可见判断流程中,多了一个IsWindowBeingDestroyed函数调用:
也就是说,主要在设置改变窗口的一个成员时,多了一个检查。那么就意味着,本漏洞的原因是设置成员时,没有判断某成员造成,从名字上看,这个成员为FNID。
那么,问题在于,FNID这个成员没有检查又会造成什么影响呢?我们查看一下这个成员的作用,在win2000的部分源代码中,我们可以搜索FNID来探明FNID是什么意义。
这个FNID成员是用来标识本窗口是一个什么样的窗口,比如是一个按钮还是一个编辑框,这一点从文章里也可以印证。而从补丁修改后新加的函数名IsWindowBeingDestroyed来看,这里是要判断本窗口是否已经准备删除了。从文章中说的查看ReactOS代码,可知道准备删除标记就是添加上FNID_FREE(0x8000)的标记。关键在于,不检查FREE之后的窗口是如何触发漏洞呢?
通过卡巴的文章,我们整理出来大致利用思路:代码先HOOKKernelCallbackTable->产生一个主窗口->在USER32!__fnINLPCREATESTRUCT回调中去查找并取消掉sysShadow窗口->以主窗口作为父窗口产生一个滚动条窗口SrollBar->发送WM_LBUTTONDOWN消息->系统处理消息时会发生USER32!__fnDWORD回调,在USER32!__fnDWORD回调中销毁主窗口->这将导致主窗口销毁从而产生USER32!__fnNCDESTROY回调->USER32!__fnNCDESTROY回调中调用NtUserSetWindowFNID更改掉FNID->至此文章中开始语焉不详,文章中说重用了sysShadow,但我们根本理解不了如何发生得重用。所以需要我们自己来动手实现。
首先我们来实现漏洞函数得调用,仔细观察:
可以看到,要想成功更改FNID,需要满足几个条件,我们不可能只设置为0x4000(这个只是打个标记,不产生实际作用)。至于新FNID得值,我们可以按照文章中说的直接设置为0x2a1即可。而对于后面的条件,要求我们要设置的窗口原来不能有FNID(除0x4000和0x8000外,但这两个标记我们打了没用)。
这里经过多次测试,发现三种情况会时FNID为空:一种是在任意类型窗口刚建立时,这时系统在用户态主动调用NtUserSetWindowFNID来设置FNID(user32.dll中自动实现),而此时,如果没有设置完FNID,则窗口还没有设置消息处理函数,也就没有处理消息的能力。而文章中提到了WM_LBUTTONDOWN消息,则可以肯定是在Scrollbar窗口完全创建之后。故此种情况不行。
二种是用户注册的窗口类所产生的窗口,此窗口一直到销毁,都没有设置FNID。第三种就是文章中所说的sysShadow窗口,此窗口的作用只是产生阴影效果,但是确实FNID为空。也正是由于这个特性,本人被文章误导很长时间。后来请教leeqwind才知道,根本不是重用的sysShadow,而是SBTrack结构。另外也可以看文章截图:
由于本人注意力全放在了文章触发中,还未关注利用,没注意后面的内容,其实这里已经泄露了真相(深刻检讨反思!)。从截图的红框中可看到,标记是Usst,分配者又是win32k!xxxSBTrackInit。所以很明显可知要被重用的是SBTrack。
文章中说明了需要在FNID设置为0x8000之后,再调用漏洞函数更改FNID。我们知道,一个窗口销毁的用户态接受到的最后消息是WM_NCDESTORY,在win32k中,这是在xxxFreeWindow函数中发送给窗口的:
可以看到在106行发送了0x82(WM_NCDESTORY)号消息,所以我们需要在106行之后想办法回到用户态。但同时有另外一个问题,就是注意第134行,这行把FNID打上0x4000的标记,而文章中完全是0x8000直接变成了0x82a1,没有0x4000的标记,所以我们如果再WM_NCDESTORY消息中去更改FNID,那么确实可以马上更改掉FNID,但是这时窗口还并没有打上0x8000的标记(到136行中才被标记),这与文章明显不符。所以文中所说的在USER32!__fnNCDESTROY中去调用NtUserSetWindowFNID更改FNID的做法为故意错误。
经过本人用pykd动态测试发现,窗口在426行的调用后,窗口句柄将不存在,NtUserSetWindowFNID函数的ValidateHwnd函数将返回0从而直接跳过FNID设置。也就是我们想要实现文章中直接将0x8000设置成0x82a1的效果,需要我们在134行到426行之间,找到一个回到用户态的调用。
这里插播一点题外话。本人一开始是在WINDOWS7的win32k.sys做分析,结果搜索很长时间未能成功找到,直到某天twitter上有人提到该漏洞在win8.1之后可利用,在看了win10的win32kfull.sys后恍然大悟,教训深刻!
回到正题,之后我们可以看到这里:
上面这张截图为win10下的win32kfull.sys的IDA分析结果。我们可以看到,在256行改为了0x8000后,在266行有一个xxxClientFreeWindowClassExtraBytes函数调用:
该函数中很直接的调用了KeUserModeCallback!毫无条件的直接回到了用户态。所以我们只要符合进入到xxxClientFreeWindowClassExtraBytes函数的条件即可。
仔细查看代码,发现258行的判断,主要是判断窗口是否具有扩展字节(正如xxxClientFreeWindowClassExtraBytes函数名字所暗示的那样),有的话则调用xxxClientFreeWindowClassExtraBytes函数释放掉,由于扩展字节是分配在用户空间中的,所以该函数返回到用户态让用户态代码去释放掉(至少要通知)。
所以只要我们在注册窗口类的时候,cbWndExtra成员不为0即可。在窗口销毁时,就会在设置了0x8000之后,又回到了用户态。当窗口以0x8000回到用户态后,我们更改FNID为0x82a1,返回到内核态后,xxxFreeWindow继续往后执行。
回到xxxFreeWindow函数:
其中这里,可以看到代码判断了FNID的值,从而决定要不要调用USER32!__fnDWORD。我们知道,当一个窗口被多个其他窗口、结构引用时,即时这个窗口已经被用户调用DestoryWindow销毁掉了,窗口对象也要在内存中继续存在,以等待所有引用它的地方不再引用它才真正释放本对象内存。那么,如果我们在销毁了一个窗口后,它的最后一个引用也释放的时候,调用xxxFreeWindow时,我们就可以用FNID来控制流程是否要回到用户态的USER32!__fnDWORD调用。所以攻击链也就此完整。
结合上面提到的,文章中提到了使用xxxSBTrackInit。该函数主要用来实现滚动条按钮的跟随鼠标滚动,当用户在一个滚动条上按下左键,表示用户想要拖动滚动条,此时需要开始处理鼠标的移动,让滚动条也跟着相应动起来,在系统中,产生SBTrack结构来标记用户鼠标的当前位置,最后当用户放开鼠标左键时,表示用户已经拖动完成,需要释放相应SBTrack结构。
在windows 2000的源代码中,xxxSBTrackInit部分代码如下:
大致流程就是在调用UserAllocPoolWithQuota申请了内存后,初始化SBtrack,会将滚动条窗口以及通知窗口的指针放在本结构中,然后在2425行将当前窗口设置为捕获窗口。之后就调用xxxSBTrackLoop开始循环来处理用户的鼠标消息:
可以看到,xxxSBTrackLoop循环获取消息、判断消息、分发消息。当用户放开鼠标时,应当停止跟踪处理消息,退出xxxSBTrackLoop后回到xxxSBTrackInit之后,释放SBTrack占用的内存:
而往上两行,可以看到在释放SBTrack之前,会解除一次spwndSBNotify窗口的引用。结合上面的分析,我们可以让这次解除引用时,回到用户态。如果在用户态释放掉SBTrack,则流程再次回到内核时,紧接着后面的UserFreePool即造成重复释放的问题。
那么我们在用户态如何释放SBTrack呢?分析发现,导致释放SBTrack一种是用户正常放开了鼠标左键,还有一种就是xxxEndScroll函数:
void xxxEndScroll(
PWND pwnd,
BOOL fCancel)
{
UINT oldcmd;
PSBTRACK pSBTrack;
CheckLock(pwnd);
UserAssert(!IsWinEventNotifyDeferred());
<br>
pSBTrack = PWNDTOPSBTRACK(pwnd);
if (pSBTrack && PtiCurrent()->pq->spwndCapture == pwnd && pSBTrack->xxxpfnSB != NULL) {
……..省略部分代码…….
<br>
pSBTrack->xxxpfnSB = NULL;
<br>
/*
* 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;
}
}
xxxEndScroll函数判断了主要根据窗口的线程信息中存放的SBTrack和pq->sqpwndCapture()。
而我们的程序是单线程,由于每个线程信息是属于线程的,所以线程创建的所有窗口也都指向同一线程信息结构。所以,即使SBTrack所属于的Scrollbar窗口已经释放了,只要还是同一线程创建的新窗口,pSBTrack也还是原来的。而qp->spwndCapture==pwnd如何绕过呢?我们如果创建新的窗口,给这个新窗口发送的消息和操作,pwnd则为新窗口,这显然不会等于在xxxSBTrackInit中设置的捕获窗口----旧窗口。
通过测试发现,这个Capture窗口的设置,只要简单的在用户态调用SetCapture API即可直接设置。所以我们只要直接调用API即可让xxxEndScroll中的判断完全通过。
在搜索之后,发现可以通过如下路径调用xxxEndScroll函数:
向一个窗口发送WM_CANCELMODE-> xxxDefWindowProc判断消息->调用xxxDWP_DoCancelMode-> xxxDWP_DoCancelMode判断当前线程信息中pSBTrack-> xxxEndScroll。而上面我们知道,所有的窗口都在同一线程中创建,所以这里的判断也可以通过!
整理一下流程:
HOOK KernelCallbackTable->注册窗口类,
WNDCLASSEXW.cbWndExtra设置为4->产生主窗口->以主窗口作为父窗口产生一个滚动条窗口SrollBar->发送WM_LBUTTONDOWN消息->系统处理消息初始化SBTrack结构并开始循环->发生fnDWORD回调,
fnDWORD回调中销毁主窗口->销毁主窗口,释放扩展字节xxxClientFreeWindowClassExtraBytes->xxxClientFreeWindowClassExtraBytes系统调用回调fnClientFreeWindowClassExtraBytesCallBack->fnClientFreeWindowClassExtraBytesCallBack
HOOK中调用NtUserSetWindowFNID更改掉窗口FNID->创建新窗口并调用SetCapture设置新窗口为捕获窗口->xxxSBLoop返回后解除主窗口引用->由于这是主窗口唯一的一个引用,这次解除导致彻底释放主窗口对象,
xxxFreeWindow函数执行->由于主窗口对象的FNID已经被更改,xxxFreeWindow函数执行过程中将再一次回到用户态->用户态向新窗口发送WM_CANCELMODE消息->系统处理WM_CANCELMODE消息,释放了SBTrack->流程返回到内核继续执行xxxSBTrackInit函数最后的释放SBTrack->重复释放SBTrack!
值得说明的一点是:在上面这个流程中,完全跟sysShadow窗口没有关系,自然也跟本不需要HOOK __fnINLPCREATESTRUCT回调。
下面看一下具体代码实现。
首先,我们设置一下回调HOOK,这里就直接用fs来获取PEB了:
创建主窗口及ScrollBar:
WNDCLASSEXW wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = DefWindowProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 4;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_CVE8453));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = L"WNDCLASSMAIN";
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
RegisterClassExW(&wcex);
hMainWND = CreateWindowW(L"WNDCLASSMAIN", L"CVE", WS_DISABLED , 2, 2, 4, 3,nullptr, nullptr, hInstance, nullptr);
hSBWND = CreateWindowEx(0, L"ScrollBar", L"SB", WS_CHILD | WS_VISIBLE | SBS_HORZ, 0, 0, 3, 3, hMainWND, NULL, hInstance, NULL);
之后发送WM_LBUTTONDOWN消息:
bMSGSENT = TRUE;
SendMessage(hSBWND, WM_LBUTTONDOWN, 0, 0x00020002);
这将导致系统初始化SBTrack并开始循环。这导致系统回调fnDWORD:
void fnDWORDCallBack(PDWORD msg) {
if (*msg) {
HWND hCurrentDestroyWND = (HWND)*((DWORD*)(*msg));
memset(ClassName, 0, 0x10);
GetClassNameA(hCurrentDestroyWND, ClassName, 0xF);
if (!strcmp(ClassName, "ScrollBar")) {
if (bMSGSENT) {
bMSGSENT = FALSE;
DestroyWindow(hMainWND);
}
}
}
fnDWORD(msg);
}
由于在运行过程中,DWORD回调会执行很多次,所以我们加一个全局变量bMSGSENT来控制。在系统执行DestroyWindow时,由于已经预留了扩展字节,所以会回调到用户HOOK:
void fnClientFreeWindowClassExtraBytesCallBack(PDWORD msg) {
if ((HWND)*(msg + 3) == hMainWND) {
hSBWNDnew = CreateWindowEx(0, L"ScrollBar", L"SB", SB_HORZ, 0,0, 0, 0, NULL, NULL, NULL, NULL);
SetWindowFNID(hMainWND, 0x2A1);
SetCapture(hSBWNDnew);
}
fnClientFreeWindowClassExtraBytes(msg);
}
我们在fnClientFreeWindowClassExtraBytes回调中,直接设置FNID。由于后面还有捕获窗口的检查,所以我们一并创建窗口并且设置为捕获窗口。当流程回到系统后,发现捕获窗口已经改变,退出了xxxSBTrackLoop函数并开始释放SBTrack内存空间,在解除对主窗口的引用时,会导致调用xxxFreeWindow释放主窗口内存对象,由于我们已经改变了FNID,所以再次回到用户态。此时消息为0x70:
所以在fnDWORD中,判断消息:
if ((*(msg + 1) == 0x70) && (hCurrentDestroyWND == hMainWND)) {
SendMessage(hSBWNDnew, WM_CANCELMODE, 0, 0);
}
}
WM_CANCELMODE将导致SBTrack被释放,从用户态返回后,xxxSBTrack继续释放SBTrack将导致重复释放!
最后:非常感谢leeqwind的帮助!在分析过程中给了很大的帮助!再次感谢!极力推荐他的博客:https://xiaodaozhi.com/
下篇《从补丁diff到EXP--CVE-2018-8453漏洞分析与利用》,择日更新。
- End -
看雪ID:bksaro
https://bbs.pediy.com/user-370883.htm
本文由看雪学院 bksaro 原创
转载请注明来自看雪学院
热门图书推荐:
(点击图片即可进入)
热门技术文章:
公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com