任你博CVE-2018-8453内核漏洞分析_HUC惠仲娱乐

任你博

漏洞概述

CVE-2018-8453是卡巴斯基实验室于2018年8月份在一系列针对中东地区进行APT攻击的活动中捕获到的Windows提权0day漏洞,该漏洞与Windows窗口管理和图形设备接口相关(win32kfull.sys)。漏洞可以被利用于将Windows下较低级别的用户权限提升为系统权限(users->system)。
该漏洞产生的原因是win32kfull!NtUserSetWindowFNID函数存在缺陷:在对窗口对象设置FNID时没有检查窗口对象是否已经被释放,导致可以对一个已经被释放了的窗口(FNID_FREED:0x8000)设置一个新的FNID。通过利用win32kfull!NtUserSetWindowFNID的这一缺陷,可以控制窗口对象销毁时在xxxFreeWindow函数中回调fnDWORD的hook函数,从而可以在win32kfull!xxxSBTrackInit中实现对pSBTrack的Use AfterFree。

漏洞细节

漏洞分析
从卡巴的报告来看该漏洞位于win32kfull!xxxDestroyWindow中。但是通过对补丁进行对比可以发现在NtUserSetWindowFNID函数中有较大差异,可以看到在判断流程中,多了一个对IsWindowBeingDestroyed函数的调用。
漏洞版本:
补丁版本:通过对IsWindowBeingDestroyed函数名可以推测该函数主要用来判断窗口被销毁,从NtUserSetWindowFNID函数名可以推测主要用来判断的成员应该是FNID。可以从网上泄露的源码来查找FNID的含义,可以看到FNID用来标识窗口,比如是一个菜单还是一个编辑框。
通过对NtUserSetWindowFNID函数中修改窗口FNID的分析知道要修改窗口的FNID需满足条件。从代码可以看出如果要修改FNID为0x4000的时候可以,但0x4000表示FNID_DESTROY,只是打标记不产生实际作用,而且如果是0x4000后面加的补丁函数毫无意义。第二个条件就是FNID的值小于等于0x2A1并且要修改窗口的FNID的值必须大于0x3FFF或者为0,大于0x3FFF的有0x4000和0x8000,而这两个都是销毁窗口打了没用。
所以只能考虑为空的情况,发现三种情况会时FNID为空:一种是在任意类型窗口刚建立时,这时系统在用户态主动调用NtUserSetWindowFNID来设置FNID(user32.dll中自动实现),而此时,如果没有设置完FNID,则窗口还没有设置消息处理函数,也就没有处理消息的能力。而文章中提到了WM_LBUTTONDOWN消息,则可以肯定是在Scrollbar窗口完全创建之后。故此种情况不行。二种是用户注册的窗口类所产生的窗口,此窗口一直到销毁,都没有设置FNID。第三种就是文章中所说的sysShadow窗口,此窗口的作用只是产生阴影效果,FNID为空。通过根据卡巴报告的截图可以看到重用的SBTrack结构,标记是Usst,分配者是win32k!xxxSBTrackInit。xxxSBTrackInit主要用来实现滚动条按钮的跟随鼠标滚动,当用户在一个滚动条上按下左键,表示用户想要拖动滚动条,此时需要开始处理鼠标的移动,让滚动条也跟着相应动起来,在系统中,产生SBTrack结构来标记用户鼠标的当前位置,最后当用户放开鼠标左键时,表示用户已经拖动完成,需要释放相应SBTrack结构。函数流程就是在调用UserAllocPoolWithQuota申请了内存后初始化SBtrack,会将滚动条窗口以及通知窗口的指针放在本结构中,然后将当前窗口设置为捕获窗口。之后就调用xxxSBTrackLoop开始循环来处理用户的鼠标消息。xxxSBTrackLoop循环获取消息、判断消息、分发消息。当用户放开鼠标时,停止跟踪处理消息。通过卡巴的报告还可以知道主要变化的FNID是滚动条的FNID,最初滚动条被创建时,它的值为FNID_SCROLLBAR(0x029A)。在执行了NtUserSetWindowFNID函数前后的FNID值从0x8000变成了0x82A1。通过查询可知0x8000代表FNID_FREED,0x2A1表示FNID_BUTTON。
一个窗口销毁的用户态接受到的最后消息是WM_NCDESTROY,在win32k中是在xxxFreeWindow函数中发送给窗口。WM_NCDESTROY 消息号是0x0082,可以在xxxFreeWindow函数中找到。并且可以看到在后面的代码中把FNID打上先打上了0x4000的标记,0x4000标记表示FNID_DESTROY ,后面在打上了0x8000的标记,而卡巴的报告中提到FNID值从0x8000变成了0x82A1,所以被hook的函数应该在打上0x8000标记之后执行。
现在需要在打上0x8000标记之后找到一个可以返回应用层的HOOK函数,通过查看打上0x8000标记之后的代码可以在离打上0x8000标记很近的代码位置看到函数xxxClientFreeWindowClassExtraBytes,函数名带有"xxx"和"zzz"前缀的一般都会通过nt!KeUserModeCallback返回用户层。现在就要看调用函数xxxClientFreeWindowClassExtraBytes的条件,通过代码不好直接判断满足的条件,可以通过函数名来初步判断是释放窗口的扩展字节,由于扩展字节是分配在用户空间中所以该函数返回到用户态让用户态代码去释放。WNDCLASSEX结构体中cbWndExtra成员表示“为每个窗体预留的空间大小”, cbWndExtra的值可以在0~40之间,单位是字节。要使用cbWndExtra成员指定的空间,则必须在注册窗体类时预先预留好指定的大小,否则无法使用。
所以需要在注册窗口类的时候设置cbWndExtra成员不为0。在窗口销毁时,就会在设置了0x8000之后通过xxxClientFreeWindowClassExtraBytes函数回到用户态。当窗口以0x8000回到用户态后就可以通过HOOK后的xxxClientFreeWindowClassExtraBytes函数来更改FNID为0x82a1,从而满足卡巴的条件。

在卡巴的文章里还提到了当处理WM_LBUTTONDOWN消息时,fnDWORD钩子会在父节点上执行DestroyWindow函数,导致窗口被标记为空闲,并且随后被垃圾收集器释放。所以需要HOOK __fnDWORD函数,在HOOK函数中去执行DestroyWindow函数释放窗口。释放窗口会调用xxxFreeWindow函数与上面的流程组成完整的过程。整理整个攻击过程如下:
a)先需要HookKernelCallbackTable中的两个回调user32!__fnDWORDuser32!__xxxClientFreeWindowClassExtraBytesb)注册窗口类,设置WNDCLASSEXW.cbWndExtra为4产生一个主窗口,以主窗口作为父窗口产生一个滚动条窗口SrollBar。c)发送WM_LBUTTONDOWN消息系统处理消息触发调用xxxSBTrackInit函数,初始化SBTrack结构并开始循环。对一个滚动条进行鼠标左击时,会触发调用win32kfull!xxxSBTrackInit函数,其中会调用xxxSBTrackLoop循环获取鼠标消息,直到释放鼠标左键或者收到其它消息,才会退出xxxSBTrackLoop函数。
d)当xxxSBTrackInit中调用xxxSBTrackLoop回调fnDWORD_hook时,调用DestoryWindow销毁主窗口,这样会导致调用win32kfull!xxxFreeWindow。e)销毁主窗口会调用释放扩展字节的函数xxxClientFreeWindowClassExtraBytes从而进入设置的HOOK函数, 在HOOK函数中调用NtUserSetWindowFNID更改掉窗口FNID(spec_fnid为0x2A1至0x2AA中的一个值)。
f)创建新窗口并调用SetCapture设置新窗口为捕获窗口,由于这是主窗口唯一的一个引用,那么xxxSBTrackLoop会返回,解除对主窗口的引用这次解除导致彻底释放主窗口对象,win32kfull!xxxFreeWindow函数再次被执行。
g)通过前面的分析知道调用xxxFreeWindow函数后会打上0x8000的标记,在e中FNID被修改0x2A2,所以再次进入win32kfull!xxxFreeWindow函数执行后,FNID的值已经变成了0x82A2,从而会通过下面的判断调用SfnDWORD回到用户态进入HOOK函数。h)当再次进入fnDWORD_hook函数时就是最后一个回到R3的时机,这个时候如果调用SendMessage(g_hSBWNDNew,WM_CANCLEMODE)就会调用xxxEndScroll来释放pSBTrack。i)流程返回到内核继续执行xxxSBTrackInit函数最后会再次释放SBTrack,造成重复释放SBTrack。