返回 TI 主页

背景

2018年10月10日,卡巴斯基公开披露了其在同年8月份捕获到的一个Windows内核提权0day漏洞的相关信息[1],漏洞编号为CVE-2018-8453。

CVE-2018-8453是卡巴斯基实验室于2018年8月份在一系列针对中东地区进行APT攻击的活动中捕获到的Windows提权0day漏洞,该漏洞与Windows窗口管理和图形设备接口相关(win32kfull.sys)。漏洞可以被利用于将Windows下较低级别的用户权限提升为系统权限(users->system),也可以用于穿透应用程序的沙盒保护(PDF、Office、IE等),以及轻易突破杀毒软件的防护,具有极高的利用价值。截止本文完成时,已有POC/EXP或者利用技术被公开。

奇安信威胁情报中心通过对该漏洞进行详细分析,编写了可以针对大部分受影响Windows系统(Windows10 32位及64位版本)从users到system进行权限提升的Exploit程序,重现了整个攻击过程,确认了该漏洞的危害性。由于该漏洞已经被用于真实的APT攻击,因此极有可能被利用来执行大规模的攻击并构成现实的威胁,奇安信威胁情报中心披露漏洞利用的相关技术细节,以便安全厂商可以增加相应的防护措施。

分析环境

本文涉及的所有漏洞分析及Exploit代码均在 Windows 10 64位1709版本下进行(镜像下载链接参考[7]),并且打上了修复CVE-2018-8453前的所有系统补丁:

漏洞成因

该漏洞产生的原因是win32kfull!NtUserSetWindowFNID函数存在缺陷:在对窗口对象设置FNID时没有检查窗口对象是否已经被释放,导致可以对一个已经被释放了的窗口(FNID_FREED:0x8000)设置一个新的FNID。通过利用win32kfull!NtUserSetWindowFNID的这一缺陷,我们可以控制窗口对象销毁时在xxxFreeWindow函数中回调fnDWORD的hook函数,从而可以在win32kfull!xxxSBTrackInit中实现对pSBTrack的Use After Free。

关于FNID:通过查看WIN2000泄露的源代码代码以及ReactOs的文档,可以知道FNID是标记一个窗口是一个什么样的窗口的数据,比如是一个按钮还是一个编辑框。也可以标记窗口的状态,比如FNID_FREED(0x8000) 标记了该窗口已经被释放。

POC - 如何触发漏洞

可以通过以下流程来触发漏洞:

下面来简要介绍如何通过上图描述的执行流程来触发该漏洞。

第一步:首先我们需要Hook KernelCallbackTable中的两个回调

第二步:创建主窗口和ScrollBar

第三步:接着向滚动条发送WM_LBUTTONDOWN消息触发调用xxxSBTraackInit函数

提示:对一个滚动条进行鼠标左击时,会触发调用win32kfull!xxxSBTrackInit函数,其中会调用xxxSBTrackLoop循环获取鼠标消息,直到释放鼠标左键或者收到其它消息,才会退出xxxSBTrackLoop函数。

第四步:当xxxSBTrackInit中调用xxxSBTrackLoop回调fnDWORD_hook时,调用DestoryWindow(g_hMAINWND)

这样会导致调用win32kfull!xxxFreeWindow,因为我们在注册主窗口类的时候设置了主窗口类的cbWndExtra不为0,那么在win32kfull!xxxFreeWindow中会调用xxxClientFreeWindowClassExtraBytes函数来释放主窗口类的额外数据。

而上图这个函数会回调KernelCallbackTable[126],就会进入我们的第二个hook函数中去。

第五步:在进入我们的第二个hook函数fnClientFreeWindowClassExtraBytesCallBack_hook后我们必须手动调用NtUserSetWindowFNID(g_hMAINWND,spec_fnid)设置主窗口的FNID(spec_fnid为0x2A1至0x2AA中的一个值,这里我们设spec_find为0x2A2即可)。同时申请一个新的滚动条:g_hSBWNDNew,并调用SetCapture(g_hSBWNDNew)设置当前线程捕获鼠标消息的窗口是g_hSBWNDNew。

第六步:由于主窗口被Destory掉了,那么xxxSBTrackLoop会返回,继续执行HMAssignmentUnLock(&pSBTrack->spwndNotify)解除对主窗口的引用。从而导致主窗口被彻底释放,这会导致再一次的调用xxxFreeWindow,而xxxFreeWindow中有如下图代码段:

从上图中我们可以知道,一旦调用了xxxFreeWindow之后,窗口的FNID就会打上0x8000标记。

由于在第5步的时候我们就设置了主窗口的FNID为0x2A2,此时LOWORD(FNID)就会为0x82A2(因之前我们在第4步调用DestoryWindow的时候就已经调用了xxxFreeWindow给主窗口打上0x8000标记了)。所以上图中的判断会通过并调用SfnDWORD,然后回调fnDWORD 进入我们的hook函数中。

当再次进入fnDWORD_hook函数时就是我们最后一个回到R3的时机了,这个时候如果调用SendMessage(g_hSBWNDNew,WM_CANCLEMODE) 就会调用xxxEndScroll(查看win2k的代码如下图)来释放pSBTrack。

最终,由于POC程序是单线程,而每个线程信息是属于线程的,所以线程创建的所有窗口也都指向同一线程信息结构。即使SBTrack所属于的Scrollbar窗口已经释放了,只要还是同一线程创建的新窗口,pSBTrack也还是原来的。由于我们是向新创建滚动条g_hSBWNDNew发送的WM_CANCLEMODE消息,且之前我们就调用了SetCaputure(g_hSBWNDNew)设置当前线程捕获鼠标消息的窗口为g_hSBTWNDNew,所以qp->spwndCapture==pwnd也会满足。所以上图中的if判断会通过,并最终会执行UserFreePool(pSBTrack)将pSBTrack给释放掉,从而造成返回执行HMAssignmentUnLock(&pSBTrack->spwndSB)时,pSBTrack就已经被释放掉了。从而造成了对pSBTrack的Use After Free。

Exploit - Windows 10 64位环境上实现漏洞利用

由于我们可以通过hook KernelCallbackTable中的回调函数来控制win32kfull!xxxSBTrackInit中的pSBTrack被提前释放,造成对pSBTrack的Use After Free。之后则可以通过池风水喷射技术占用被提前释放掉的pSBTrack实现有限次的任意内存减1或减2。这个有限次的任意内存减1或减2配合桌面堆 [2]泄露技术和GDI Palette Abuse技术就可以实现任意内存读写。最后使用强大的任意内存读写实现权限提升!

如何实现任意内存减1或减2

通过以上分析我们可以知道在调用HMAssignmentUnlock(&pSBTrack->spwndSBNotify)后,pSBTrack所指向内存就已经被释放掉了:

继续执行下一个HMAssignmentUnlock(&pSBTrack->spwndSB)。查看一下HMAssignmentUnlock的反汇编,可以发现一个非常有意思的地方:

可以发现,执行 lock xadd dword ptr [rdx+8],eax 会将 rdx+8 处内存内的值减去1,通过跟踪可以发现rdx的值其实就是pSBTrack->spwndSB的值!

所以,如果我们可以控制pSBTrack->spwndSB的值,那么我们就可以实现任意内存减1了。而pSBTrack在我们调用SendMessage(g_SBWNDNew,WM_CANCELMODE)后就被释放了,所以如果我们在pSBTrack释放后,再立即分配一个大小与SBTrack相同的且能够控制分配数据的对象(比如Bitmap)。根据Windows的一些特性,有极大概率将被Free掉的pool重新分配给Bitmap。

测试结果:

同理继续调用HMAssignmentUnlock(&pSBTrack->spwndSBTrack),也会有一个任意内存减1,只不过减的是pSBTrack->spwndSBTrack+8处的内存的值。

所以我们可以通过控制Bitmap中的数据喷射占用pSBTrack释放的空间,来实现任意内存减1,或者减2。减1只需要pSBTrack->spwndSB、pSBTrack->spwndSBTrack中的一个为0,另一个为要减一的地址-sizeof(PVOID)即可。

那么只要我们重复触发这个流程,就可以对一个内存减很多次的1或者2了。接下来我们来将这个任意内存减1或减2,将一个内存内的值改为指定数据:

result = target - control_sub_times

很明显我们必须提前知道target的值,才能控制target减为我们想要的值result。所以这个利用并不是直接性的改一个内存为指定的值,利用存在一定的局限性。

思考:假如我们需要将0x10000000变为0x00000000,我们需要0x01000000次减2吗?答案是不需要,仅仅只需要1次减2即可。因为我们是任意内存的减2或减1,把"0x10"变为一个DWORD的低位,那么就变成将0x00000010改为0x00000000的问题了。所以利用这个思路,完全不用特别担心次数和时间问题。

使用GDI Palette实现任意内存读写

通过查阅文档,可以了解到PALETTE的结构:

typedef struct _PALETTE64
{
    BASEOBJECT64      BaseObject;    
    ...
    ULONG64       pRGBXlate;    
    PALETTEENTRY    *pFirstColor; 
    struct _PALETTE *ppalThis;     
    PALETTEENTRY    apalColors[3]; 
}

PALETTE的成员apalColors,是一个成员个数可以指定的数组,数组中的每一个成员大小为4字节,且内容可以指定。pFirstColor是一个指向该数组的指针,就是利用该指针来构造R/W primitive,相当于Bitmap里的pvScan0指针 ,而且他们之间满足如下的关系(利用这个关系我们就可以知道pFirstColor所指向内存的初始值):

PALETTEENTRY的地址=pFirstColor的地址+sizeof(PVOID)*2

而跟Bitmap使用GetBitmapBits和SetBitmapBits来操作Pixel区的数据一样。PALETTE会使用GetPaletteEntries和SetPaletteEntries来操作pFirstColor指针指向的数据。

所以我们可以构造两个Palette。分别叫做hManager和hWorker:

如果我们可以得到hManager的pFirstColor的值和hWorker的pFirstColor的值,那么我们就可以利用任意内存减1或减2将hManager->pFirstColor的值减为hWorker的pFirstColor值了。之后就可以使用hManager调用SetPaletteEntries来控制hWorker->pFirstColor,再使用hWorker调用SetPaletteEntries和GetPaletteEntries来实现任意内存读写了。

幸运的是我们可以利用下面的技术来稳定得到hManager的pFirstColor的值和hWorker的pFirstColor的值,并且可以使hManager的pFirstColor的值比hWorker的pFirstColor的值大,且两者差值不会很大。

使用桌面堆泄露GDI Palette的地址

由于窗口菜单名的大小不受特别大的限制(名字可以特别特别长)、窗口菜单名lpszMenuName和Palette分配的块位于同一个内存池,并且我们可以通过HmValidateHandle返回的tagWND对象指针来拿到lpszMenuName的内核地址。所以,我们可以使用桌面堆[2]来帮助我们预测pFirstColor指针的内核地址,通过合理构造,准确率可以达到100%:

首先我们需要重复分配释放一个窗口对象来申请坑、释放坑。待多次申请所得到的地址不变时,说明下一次构造一个大小等于lpszMenuName的Palette对象时,Palette对象一定被分配在上次lpszMenuName申请的地址处:

然后我们就可以根据pFirstColor在_PALETTE64的偏移来得到pFirstColor的内核地址了:

之后通过任意内存减1或减2,将hManager->pFirstColor改为hWorker的pFirstColor值即可实现任意内存读写。

使用任意内存读写进行提权

现在有了强大的任意内存读写,那么我们就可以通过任意读遍历内核空间中的EPROCESS链来找到当前进程的Token地址,和system进程的Token值。然后将system进程的Token值写到当前进程的Token地址内就实现权限提升了。

如何在用户层获取到system的EPROCESS呢?可以通过去查找ntoskrnl.exe中的PsInitialSystemProcess[3]来得到:

遍历获取当前进程的_EPROCESS结构地址:

然后使用任意内存读写复制Token:

总结利用流程

奇安信威胁情报中心总结了该漏洞的整个利用过程如下:

  • 使用桌面堆泄露技术,获取hManager和hWorker的pFirstColor值

  • 多次触发漏洞流程使hManager->pFirstColor的值变为hWorker的pFirstColor值。

  • 使用任意内存读写进行权限提升

  • 使用任意内存读写欺骗操作系统无需清理Bitmap对象。(不做这步,关闭程序时系统会清理Bitmap对象,导致我们用来占坑的Bitmap对象被DoubleFree,触发蓝屏)。

Exploit Demo提权截图:

补丁分析

通过bindiff可以发现,打了补丁的win32kfull!NtUserSetWIndowFNID在对窗口对象设置新的FNID之前会调用IsWindowBeingDestroyed检查窗口是否已经被释放掉了。如果窗口对象已经被释放那么就直接返回,不会允许设置新的FNID值。所以当我们调用DestoryWindow后,再想调用NtSetUserWindowFNID设置FNID时就会失败,从而无法在调用HMAssignmentUnLock(&pSBTrack->spwndNotify)解除对主窗口的引用的时候,触发fnDWORD回调回到R3来调用SendMessage()提前释放pSBTrack。

总结

奇安信威胁情报中心的安全研究人员编写了Windows 10 pro v1709 x32/x64 截止2018年10月补丁日之前版本的POC,并进行了利用测试,都能提权成功。实现其它版本的利用仅仅需要更改相应结构的偏移即可,比如_EPROCESS的Token等等。

参考链接

[1].https://securelist.com/cve-2018-8453-used-in-targeted-attacks/88151/

[2].https://blogs.msdn.microsoft.com/ntdebugging/2007/01/04/desktop-heap-overview/

[3].https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/mm64bitphysicaladdress

[4].https://mp.weixin.qq.com/s/ogKCo-Jp8vc7otXyu6fTig

[5].https://www.anquanke.com/post/id/168572#h2-1

[6].https://www.anquanke.com/post/id/168441#h2-0

[7].ed2k://|file|cn_windows_10_multi-edition_vl_version_1709_updated_sept_2017_x64_dvd_100090774.iso|4630972416|8867C5E54405FF9452225B66EFEE690A|/

CVE-2018-8453 VULNERABILITY 0DAY