由于win32kfull中的NtUserSetWindowFNID在对窗口对象的fnid进行设置的时候,没有判断该窗口是否已经释放,这样就可以对一个已经释放的窗口进行fnid的设置。而在xxxSBTrackInit和xxxFreeWindow中都存在用户层的回调,通过对函数的劫持可以在回调中释放掉xxxSBTrackInit函数中使用的tagSBTRACK结构体,这样当xxxSBTrackInit释放该结构体的时候就会因为双重释放导致BSOD的产生。通过xxxSBTrackInit函数释放结构体之前会对结构体中的部分成员进行解引用的操作,在相应的内存地址中放置PALETTE的cEntries成员的地址来利用解引用扩大该值,实现越界地址写入相邻PALETTE的pFirstColor来实现任意地址读写,最终实现提权。
操作系统:Win10 x64 1709 专业版
编译器:Visual Studio 2017
调试器:IDA Pro, WinDbg
由于在Win10系统中,很多符号并没有导出,所以一些结构体成员只能通过分析得到。对于窗口对象tagWND,和本次漏洞有关的成员定义如下:
tagWND偏移0x52的fnid表明了窗口的状态,当值包含0x8000的时候,表示窗口被释放:
tagWND偏移0x10的pti指向tagTHREADINFO结构体,改结构体保存了线程信息,定义如下:
tagTHREADINFO偏移0x198的pq指向tagQ结构体,结构体定义如下:
tagTHREADINFO偏移0x2B0指向tagSBTRACK结构体,当鼠标在一个滚动条按下左键的时候,系统会通过该结构体用来标记鼠标的当前状态,结构体定义如下:
内核通过xxxFreeWindow来释放窗口,函数会将要释放的窗口对象的fnid与0x8000进行或运算,表示窗口被释放。接着判断窗口对象是否有扩展内存,即cbwndExtra是否为0,如果不为0,则执行xxxClientFreeWindowClassExtraBytes来释放扩展内存:
xxxClientFreeWindowClassExtraBytes函数会执行KeUserModeCallback来返回用户层:
xxxClientFreeWindowClassExtraBytes函数执行完成之后,函数会判断窗口对象的fnid值的低12位是否在0x2A0到0x2AA之间:
如果fnid在0x2A0到0x2AA之间,则会调用SfnDWORD函数:
SfnDWORD函数也会调用KeUserModeCallback函数返回用户层:
xxxSBTrackInit是用来执行鼠标左键按下滚动条进行拖动的函数,该函数的部分代码如下,函数首先申请一块内存用来保存pSBTrack结构体,对这块内存进行初始化,并对部分成员进行引用;接着函数调用xxxSBTrackLoop用来处理拖动滚动条要处理的消息;最后函数对相关成员进行解引用后,释放掉pSBTrack结构体:
xxxSBTrackLoop会调用xxxTranslateMessage和xxxDispatchMessage来分发处理消息,xxxDispatchMessage函数会调用上面说的SfnWORD函数来返回用户层:
该函数用来设置窗口对象的fnid增加指定的值,但是,这里增加的时候,函数没有判断窗口是否已经被释放,即是否具备0x8000。这就会导致,进行设置的时候很有可能会对一个已经释放的窗口的fnid值进行设置:
这个漏洞的成因比较复杂,要将上面的几个函数都联系起来看,成因如下:
当向滚动条控件发送WM_LBUTTONDOWN(左键按下)的消息时候,xxxSBTrackInit函数就会被调用,xxxSBTrackInit函数会调用xxxDispatchMessage,该函数又会调用SfdDWORD来返回用户层
如果用户HOOK了用户层对应的处理函数,就可以在该函数中调用DestroyWindow来释放拥有该滚动条控件的窗口。这样就会执行xxxFreeWindow,该函数会首先将窗口的fnid标记为删除的窗口,接着在该窗口存在扩展对象的时候,调用xxxClientFreeWindowClassExtraBytes函数返回用户层
如果用户HOOK了用户层对应的处理函数,就可以在处理函数中调用NtUserSetWindowFNID,将窗口的fnid加入0x2A1的标记。这样xxxClientFreeWindowClassExtraBytes函数返回以后,会因为被修改的窗口的fnid值的低12位为0x2A1导致再次调用SfdDWORD返回用户层,此时在对应的处理函数中释放掉xxxSBTrackInit函数申请的pSBTrack结构体,这块内存就会处于释放状态
当xxxFreeWindow函数返回后,就会返回到xxxSBTrackInit继续执行,而xxxSBTrackInit会在最后释放pSBTrack结构体,而这个结构体已经被释放,此时如果在释放就会产生BSOD错误
要成功触发这个漏洞,就需要在SfdDWORD在用户层的处理函数中释放pSBTrack结构体,此时只需要通过向滚动条发送WM_CANCELMODE消息,该函数会导致xxxEndScroll函数来释放内存,该函数的主要代码如下:
其中第二处的限制需要窗口一个新得滚动条对象,并对其调用SetCapture。整个触发漏洞的流程如下:
创建一个带有八字节额外内存的窗口对象,并将该对象的句柄赋值到额外内存中供之后使用。同时,在该窗口中在创建一个滚动条对象用来触发漏洞
HOOK SfdDWORD和xxxCreateFreeWindowClassExtraBytes返回到用户层会执行的函数
向创建的窗口发送WM_LBUTTIONDWORD函数来调用xxxSBTrackInit函数
在用户层定义的xxxClientFreeWindowClassExtraBytes会根据要释放的内存中保存的是否是第一步中窗口的窗口句柄,来判断是否要修改fnid和调用SetCapture
在用户层定义的SfdDWORD的处理函数中,会判断如果是第一次调用就会通过DestroyWindow来调用xxxFreeWindow。如果是第二处调用,则发送WM_CANCELMODE来是否pSBTrackInit函数
当xxxSBTrackInit函数最后释放pSBTrack结构体的时候就会因为双重释放导致BSOD
相应的POC代码如下:
编译运行POC就会产生BSOD错误,以下是部分错误信息:
可以看到BSOD产生的原因就是因为重复释放pSBTrack结构体:
想要不产生BSOD,就需要在第一次释放pSBTrack结构体之后,申请一块共占用0x80大小的内存来占用释放掉的内存,这样在xxxSBTrackInit中第二次释放的时候不会因为释放掉已释放的漏洞产生双重释放。而在xxxSBTrackInit释放内存之前,函数会对其中几个成员通过调用HMAssignmentUnlock来解引用,该函数的实现如下,可以看出就是将传入参数的指针所指向地址偏移为8的地址的值-1,如果可以传入合适的参数来扩大特定的值就可以实现任意地址读写。
之前往往选择BitMap对象,但因为从Win10 1709开始,BitMap对象的data不在对象头之后,pvScan0指向了不同的内存,所以这里就改为选用Palette对象来实现任意地址读写。
创建Palette对象的创建通过CreatePalette函数来实现,该函数的定义如下:
其中参数的定义如下,成员palNumEntries指定了数组palPalEntry的个数:
数组palPalEntry的类型为PALETTENTRY,可以看出每个元素占用4字节:
CreatePalette调用成功之后,就会创建一个PALETTE对象,该对象定义如下,其中偏移0x1C的cEntries等于LOGPALETTE的palNumEntries,偏移0x88的apalColor数组即LOGPALETTE中的palPalEntry数组。
而偏移0x78的pFirstColor指向了apalColors,当通过SetPaletteEntries和GetPaletteEntries进行读写的时候,读写的地址就是由pFirstColor指向的地址:
当创建带有MenuName的窗口时,可以通过tagWND偏移0xA8的pcls来获取其在内存中的地址。因此,可以通过创建一个这样的窗口,然后释放掉,在马上创建一个PALETTE,那么它的pcls就指向了这个PALETTE对象(大概率),获取MenuName地址的代码如下,这里需要注意的是创建的内存如果打到一定程度,这块内存的头就不会有0x10的POOL_HEADER,所以使用的时候要根据具体情况来决定。
此时可以通过创建一个0x1000字节的MenuName,在释放掉它并马上申请两个占用0x800的PALETTE,这样这两个PALETTE就会占用释放掉的MenuName,它们在内存中是相邻的,且可以获取到第一个PALETTE对象的地址,此时记录第一个PALETTE对象cEntries对象的地址供之后使用,相应的代码如下:
接下来在第一次释放的时候,就要创建大量的MenuName来占用释放的内存,由于可以直接修改MenuName的值,所以可以对应tagSBTRACK结构体的成员,将其设为上面记录的cEntries成员的地址,这样之后xxxSBTrackInit函数在最后调用HMAssignmentUnlock进行-1操作的时候就会修改cEntries,相应的代码如下:
在触发漏洞之前可以看到,创建的两个0x800大小的PALETTE对象的第一个PALETTE对象的cEntries为原来的大小:
触发漏洞,进行两次-1操作以后,就被扩大成一个很大的值:
此时就可以通过函数越界读写相邻的那个PALETTE对象的pFirstColor指针来实现任意地址读写了,相应代码如下:
可以进行任意地址读写,就可以通过修改Token来实现提权:
由于触发漏洞的时候,xxxSBTrackInit会释放掉MenuName所指的内存,为了防止在进程退出,释放资源的时候再次对其进行释放导致双重释放,就需要通过前面在g_ulMenuName中保存的地址修改为NULL来解决该问题,对应代码如下:
完整的代码保存在:https://github.com/LegendSaber/exp_x64/blob/master/exp_x64/CVE-2018-8453.cpp。运行程序就可以成功提权,且退出程序的时候不会产生BSOD错误:
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2022-8-11 20:53
被1900编辑
,原因: