漏洞概述

CVE-2015-0057是影响Windows XP到Windows 10(预览版)的Windows内核漏洞,而造成该漏洞的函数是win32k!xxxEnableWndSBArrows 函数。win32k!xxxEnableWndSBArrows 函数在触发 user-mode callback 后,执行完相应操作后从用户层返回到内核层,对接下来操作的对象未能验证其是否已经释放(更改),而继续对其进行操作,从而导致UAF。

漏洞分析

在win32k!xxxEnableWndSBArrows函数中通过xxxDrawScrollBar函数层层的函数调用最后调用KeUserModeCallback函数返回到用户层执行,从用户层返回到内核层执行时导致UAF。这个漏洞会导致write操作,可以修改相邻的对象。这个漏洞利用scrollbar对象,然后通过一个新的对象(大小相同)替换释放的对象,新对象的header可以被后面的修改。通过对代码跟踪看出在xxxDrawScrollBar中进入用户模式,如果在用户模式将tagWND对象中的pSBInfo结构释放,在返回内核模式后没有对pSBInfo进行判断,会继续执行后面的代码。pSBInfo是tagWND对象中偏移0xb0的数据。在psbInfo存储滚动条的相关信息,定义的结构如下:漏洞的利用思路就是在 win32k!xxxEnableWndSBArrows 函数执行到关键代码之前,触发某个函数回调,用户可以控制这个函数回调,假设这个函数回调为fakeCallBack,在fakeCallBack函数里面使用DestoryWindow(hwndVulA), 这样就可以使psbInfo内存块为free状态,使用堆喷技术可以修改psbInfo的值,后面会继续对该值(修改后的值)进行操作。

漏洞利用

所以首先要实现控制回调函数,对回调函数进行HOOK。先要找到一个对应的回调函数,并HOOK该回调函数使其指向我们自己写的回调函数,在自己的回调函数里面就可以通过DestoryWindow(hwndVulA)函数来进行释放。
关于怎么去找该HOOK的函数,可以想到一个函数nt!KeUserModeCallback,任何的user-mode callback流程最终内核到用户层的入口点都会是 nt!KeUserModeCallback。而函数名带有"xxx"和"zzz"前缀的一般都可以触发该函数。nt!KeUserModeCallback函数定义如下。这里的ApiNumber是表示函数指针表(USER32!apfnDispatch)项的索引,在指定的进程中初始化 USER32.dll期间该表的地址被拷贝到进程环境变量块(PEB.KernelCallbackTable)中。 KernelCallbackTable是回调函数数组指针表,可以通过peb来索引。从上面的分析知道xxxEnableWndSBArrows函数会通过xxxDrawScrollBar函数进入用户空间执行代码,而跳转到用户空间必然会调用函数nt!KeUserModeCallback。可以在调用xxxDrawScrollBar函数的地址FFFFF9600012745A和下条指令地址FFFFF9600012745F下断。
再在nt!KeUserModeCallback函数下断,通过观测ApiNumber的值来判断会调用哪些回调函数。
可以看到上面是索引为2的函数,也就是函数fnDWORD。
在这儿选取的回调函数为USER32!_ClientLoadLibrary,可以看到在win7 sp1x64的系统上USER32!_ClientLoadLibrary的索引为0x41。
可以使用如下的汇编代码来获取该USER32!_ClientLoadLibrary的地址。
通过如下代码把USER32!_ClientLoadLibrary的地址换成HOOK后函数的地址。
在HOOK函数中要做的就是通过DestroyWindow函数来释放psbInfo结构,在通过堆喷来覆盖释放的空间。其中的hookedFlag和hookCount主要用来控制流程,在hook函数之后回调函数很可能被系统的其他部分使用,但想控制的只是由xxxDrawScrollBar触发的时候, 所以要确定哪一次才是由xxxDrawScrollBar触发的。在EnableScrollBar函数执行前在修改hookedflag为TRUE。同时介绍一个很好用的函数HmValidateHandle,给HmValidateHandle函数提供一个Window句柄,它会返回桌面堆上用户映射的tagWND对象,但是该函数HmValidateHandle并未被user32导出。从一大堆公开的解释中发现HmValidateHandle与导出的User32::IsMenu函数最近,可以通过User32::IsMenu来查找HmValidateHandle函数的地址。

从上面的代码可以看出需要做的就是获取User32::IsMenu运行时地址,寻找第一个0xE8字节(call xxx)并攫取出HmValidateHandle的指针,这个函数获取的是用户态的tagWND对象。同时用户态的tagWND结构中tagWND->THRDESKHEAD->pSelf是一个指向内核态的tagWND的指针,这里就可以通过内核tagWND的地址减去用户tagWND的地址来计算出ulClientDelta。
现在可以在xxxEnableWndSBArrows函数下断点来查看tagWND对象,但是在进行调试发现xxxEnableWndSBArrows函数要调用多次,不容易区分是否是自己函数在调用xxxEnableWndSBArrows函数,所以最好在调用xxxDrawScrollBar函数处下断。通过阅读汇编代码发现,在调用xxxDrawScrollBar函数处,rcx保存的是tagWND指针,rbx中保存的为pSBInfo指针。
可以看到执行到用户态之前pSBInfo结构的信息,但是当xxxDrawScrollBar函数执行完,从用户态返回到内核态时,在调用xxxDrawScrollBar函数的下句代码下断,也就是hook函数执行完后在查看该地址的值。
可以看到在hook函数中该处的值已经被释放,为了正确的控制填充的值,要知道结构的大小,pSBInfo结构大小大小为0x24,因为在x64的系统会按8字节对齐,所以该结构实际占用大小0x28,再加上_HEAP_ENTRY,总共大小为0x30。堆头是0x10字节大小,前8字节如果有对齐的需要就存放上一个堆块的数据,因为前8字节存放的上一个堆块的数据所以总共大小为0x30。
为了精确的填充数据这里选用了函数SetPropA,该函数的定于如下。第一次调用SetPropA的时候,首先会分配一个堆用于存储生成的tagPropLIST结构体。其后的调用时如果lpString在以前没有声明过,那么会添加一个tagPROP结构体(0x10),所以在前面定义的0x7和0x8。
这两个结构体加起来刚好0x28,再加上其_HEAP_ENTRY,总共大小也为0x30正好可以覆盖释放的pSBInfo空间。
当把释放后的pSBInfo覆盖为tagPropLIST结构后,由于UAF漏洞程序会继续使用pSBInfo的WSBflags字段(实际使用的字段为填充的tagPropLIST结构的cEntries字段),代码会把cEntries字段的值修改为0xe。
从上面的分析知道可以通过UAF漏洞修改tagPropLIST结构的cEntries字段,继续看tagPropLIST结构中各项字段代表的值和SetPropA函数对应的关系。
知道cEntries字段表示tagPROP结构的数量,利用漏洞增加了tagPROPLIST.cEntries 大小,内核会认为一共有0xe个tagPROP。可以在后面继续调用setProp()覆盖后面的数据,可以利用 SetProp() 对tagPROPLIST 相邻内存进行越界写,可写的范围是(0xe-0x2)*0x10

堆利用布局

tagPROLIST 有两个成员属性,cEntries和iFirstFree分别表示tagPROP的数量和指向当前正在添加的tagPROP的位置。当插入新的tagPROP时会先对已有的tagPROP条目进行扫描直到到达iFirstFree指向的位置,这里并没有检查iFirstFree是否超过了 cEntries,但如果扫描中发现了相应的atomKey则会实施检查确保iFirstFree不和cEntries相等,然后新的tagPROP才会添加到iFirstFree索引的位置,如果iFirstFree和cEntries相等的话表明空间不够用了,就分配一个新的能容纳所插入条目的属性列表,同时原有的项被复制过来并插入新的项。
而tagPROP结构和SetProp()函数相关联,hData 成员对应SetProp的HANDLE hData参数,atomkey对应lpString参数,且属于我们可控的范畴,根据文档的说明,我们可以用这个参数传递一个字符串指针或者16位的atom值,当传递字符串指针时会自动转化为atom值,这样我们可以传递任何atom值来控制两个字节的数据。不过还是有一些限制,当我们添加新的条目到列表中时,atomKey不能重复,否则新的条目会把旧的给替换掉。另外还有一点值得注意的是tagPROP只有0xc字节大小,不过系统分配的是0x10字节用来对齐。这里在对相邻块进行覆写时要注意保持其它结构的完整性,如果只是覆盖相邻块开头的8字节就能产生效果就还行,但若是继续往后覆盖后面的字段才能产生效果就会不可避免的破坏一些原本的值,可能造成蓝屏。好在这有个不错的结构对象,就是 tagWND 结构体的 strName 成员,该成员的结构类型是_LARGE_UNICODE_STRING如果能够覆盖到 Buffer 字段就可以通过窗体字符串指针任意读写 MaximumLength 大小字节的数据。现在知道了如何用 tagPROPLIST 来修改数据,也知道哪些部分能控制,以及有哪些限制,接下来我们要做的就是想办法用这部分修改数据的能力获得任意地址读写的能力。
但是写的数据范围是(0xe-0x2)*0x10,而在tagWND中strName.Buffer的偏移是0xd8,而且在strName之前tagWND还有很多其它的结构,所以不能直接进行覆盖 。在这里只有想其它的办法,在前面知道在tagPropLIST结构之前有一个_HEAP_ENTRY结构,主要用来堆内存的管理,标识堆块的大小与是否空闲的状态。堆头是0x10字节大小,前8字节如果有对齐的需要就存放上一个堆块的数据,size域和prevsize域存放的是本来数值除以0x10,Flags域用来表示当前堆块是空闲状态还是使用状态,SmallTagIndex域则是用来做安全检查的,存放一个异或加密过的值就像stack cookie那样检测是否有溢出。
虽然不能直接覆盖strName.Buffer,但是可以考虑修改_HEAP_ENTRY,而且SetPROP刚好可以完全控制下一个堆块_HEAP_ENTRY关键的数据结构,通过修改_HEAP_ENTRY的大小让其包含tagWND结构,然后释放掉它再分配一个tagPROP + tagWND大小的堆块,这样就可以控制堆块的内容来修改tagWND。现在调整一下风水布局,用window text结构可以任意分配内存大小,新的堆布局如下:主要的实现代码如下触发uaf漏洞后tagSBINFO位置处会被替换成tagPROPLIST结构,然后调用setPROP修改相邻window text的_HEAP_ENTRY将其size域覆盖成sizeof(overlay1) + sizeof(tagWND) + sizeof(_HEAP_ENTRY),然后释放掉,接着分配一个新的window text来操作里面的数据。主要代码如下:现在能用任何想要的数据覆盖strName.Buffer的指针,但是tagWND的其他数据需要修复,这可以从用户空间读取,桌面堆会映射到用户空间,准备好tagWND的全部数据,把Buffer指针的值修改成目的地址,然后申请这部分内存。在能够控制strName.Buffer指针后,在利用tagWnd.strNam相关的函数InternalGetWindowText()和NtUserDefSetText()可以分别获得任意地址读和任意地址写的能力。现在就是考虑要覆盖哪个地址,常用的地址就是HalDispatchTable+0x8(32位+4,64位+8)的地址。因为NtQueryIntervalProfile最终会调用一个在内核服务函数指针表HalDispatchTable+0x8处的指针。如果可以覆盖该指针使其指向shellcode,那么当调用NtQueryIntervalProfile时shellcode就可以在内核层运行。所以利用是通过把strName.Buffer的指针修改成HalDispatchTable +8的地址,再通过strName.Buffer指针来修改HalDispatchTable +8地址的值,最后通过调用NtQueryIntervalProfile函数来实现调用shellcode。最后在0xf0000地址上布置提权的shellcode,执行后实现提权。

点击收藏 | 0 关注 | 1 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖