CVE-2021-1732 LPE漏洞分析
概述
CVE-2021-1732是一个发生在windows内核win32kfull模块的LPE漏洞,并且由于创建窗口时调用win32kfull!xxxCreateWindowEx过程中会进行用户模式回调(KeUserModeCallback),从而给了用户态进程利用的机会。
该漏洞由安恒信息在2020年12月在野外攻击样本中发现,在2021年2月份公开披露。相关样本在2020年APT组织蔓灵花针对国内的一次攻击中作为提权组件被发现。
分析
Windows中创建窗口时,会调用API CreateWindowEx,最终在内核会调用至win32kfull!xxxCreateWindowEx。在win10 1909上调试时调用堆栈回溯如下:
... ... win32kfull!xxxCreateWindowEx+0x1259
... ... win32kfull!NtUserCreateWindowEx+0x6a0
... ... nt!KiSystemServiceCopyEnd+0x25
... ... win32u!NtUserCreateWindowEx+0x14
... ... USER32!VerNtUserCreateWindowEx+0x211
... ... USER32!CreateWindowInternal+0x1b4
... ... USER32!CreateWindowExW+0x82
win32kfull模块的xxxCreateWindowEx函数为最终负责窗口对象创建的过程。CVE-2021-1732主要是在win32kfull!xxxCreateWindowEx调用win32kfull! xxxClientAllocWindowClassExtraBytes进行窗口扩展内存时触发。xxxClientAllocWindowClassExtraBytes函数中会调用KeUserModeCallback进行用户模式回调,以在用户模式执行回调。该函数中指定的回调ApiNumber为0x7B,即为user32! _xxxClientAllocWindowClassExtraBytes。相关回调函数表可在PEB->KernelCallBackTable中查看。
查看user32! _xxxClientAllocWindowClassExtraBytes,只是在用户模式当前进程堆中分配了指定大小的空间,并将分配的堆地址通过NtCallbackReturn传回内核。
由于用户模式回调函数的执行是在用户态进行,因此用户可以直接从进程中对该函数进行Hook,改变执行流程。
分析时使用POC为https://github.com/KaLendsi/CVE-2021-1732-Exploit,经过和原始样本的比对,可以发现该POC是对原始样本的完全还原,仅是在部分变量名含义上不正确。
漏洞首先对user32! _xxxClientAllocWindowClassExtraBytes进行Hook,在之后进程每次调用CreateWindowExW创建窗口时将会走到Hook函数处。替换后的KernelCallBackTable如下所示:
接着创建多个普通窗口,后续都会经过Hook函数。对于普通窗口,Hook函数仍旧按照旧流程,为其调用user32! _xxxClientAllocWindowClassExtraBytes。判断依据是传入的参数值,即tagWnd. cbwndExtra,相关细节在创建利用窗口时再说。
不过虽然普通窗口的创建仍是走的正常流程,但是会记录每个创建窗口的对象地址。窗口对象地址利用HMValidateHandle进行泄露。该函数未导出,不过可以通过调用了该函数的其他API进行搜寻,比如IsMenu。
调用方式为HMValidateHandle(HANDLE h, int type),传入窗口句柄和type值,如果句柄类型和参数type一致,返回句柄对应的对象在用户态内存的地址,值得注意的是,该调用成功返回值实际为poi(tagWnd+0x28)。窗口传入type为1。
1:TYPE_WINDOW
如此连续创建多个窗口,查询(VirtualQuery)每个窗口对象所在内存块的基址,记录其中最小的基址。接着除了窗口0和1,调用DestroyWindow销毁其余窗口。保留下的窗口0和1将结合后续将创建的magicWnd进行漏洞利用,而记录的最小基址将用于搜寻magicWnd。
对比窗口0和1分别相对于桌面堆的偏移,较小者和较大者分别记为WndMin、WndMax。偏移值位于窗口对象tagWnd对象偏移0x08处。
tagWnd对象结构部分偏移如下:
+0x00 Handle
+0x08 cLockObj
+0x10 unk
++0x00 ETHREAD
... ...
+++0x220 EPROCESS
... ...
++++0x2e8 UniqueProcessId
++++0x2f0 ActiveProcessLinks
++++0x360 Token
++++0x3e8 InheritedFromUniqueProcessId
... ...
... ...
+0x18
++0x80 桌面堆基址
... ...
+0x20 pSelf
+0x28
++0x00 Handle
++0x08 *(tagWnd+0x28)相对于桌面堆基址的偏移
++0x18 exStyle
++0x1c dwStyle
++0x98 spMenu
... ...
+++0x50 tagWnd
... ...
++0xc8 cbwndExtra,指定Extrabytes字节数
++0xe8 不明flag,flag|=0x800可指定pExtrabytes属性为偏移
++0x128 pExtrabytes,指向分配的Extrabytes内存
... ...
+0xa8 spMenu
... ...
++0x50 tagWnd
... ...
窗口销毁后调用NtUserConsoleControl,指定参数ConsoleControl为6,ConsoleCtrlInfoLength为0x10,将窗口WndMin对象pExtrabytes(0x128)字段属性设置为偏移,设置成功后pExtrabytes字段值为相对于桌面堆的偏移值,而0xe8处的flag将|=0x800。重新申请后的Extrabytes内存大小由poi(poi(tagWnd+0x28)+0xc8)指定。
(由于中间反复调试过几次,截图之间的数据可能有些对不上)
然后创建一个magic窗口WndMagic,同之前一样,会执行到xxxClientAllocWindowClassExtraBytes的Hook函数处。此时将进入另一分支,触发Hook函数真正作用流程。判断方式是传入的参数值,之前创建的普通窗口和现在的magic窗口指定的cbWndExtra值是不同的,普通窗口固定为32字节,magic窗口为一个随机值。
而wndClass.cbWndExtra值将被赋值到窗口对象poi(tagWnd+0x28)+0xc8处,并作为ExtraBytes内存分配时的大小指定值,然后进行用户模式回调。用户态回调函数执行结束后返回内存地址到内核,赋值到poi(tagWnd+0x28)+0x128处。而Hook函数的目的就是为了返回一个虚假偏移,指向其他地址,实现可任意地址写的功能。
窗口创建过程中,执行到Hook函数中,通过比对传入的参数值和随机值,可确定此次创建是WndMagic。不过此时win32kfull! xxxCreateWindowEx尚未执行完毕,所以HWND句柄值还未返回,尚不可知。然而在进行额外内存进行创建时,窗口对象部分属性已经完成初始化,比如句柄值、窗口属性、扩展属性等。
所以通过匹配cbWndExtra值,再比对窗口扩展属性值exStyle(此次利用中所有窗口属性值都设置为了WS_EX_NOACTIVATE [0x8000000]),一致的情况下可以大概率确认WndMagic位置,自然可通过偏移获取到相应属性值。
获取WndMagic窗口句柄后,调用NtUserConsoleControl设置magic窗口pExtrabytes属性为相对于桌面堆的偏移。接着再借助NtCallbackReturn将普通窗口WndMin对象poi(tagWnd+0x28)+0x08处的值传回内核,从而结束回调。而poi(tagWnd+0x28)+0x08的值为poi(tagWnd+0x28)基于桌面堆基址的内存偏移,因此这里将导致WndMagic对象pExtrabytes值实际是指向WndMin窗口对象的偏移。
之后调用SetWindowLongW,指定参数为(WndMagic句柄、Index=0x128、WndMin对象在内存中的偏移),返回数据应为原偏移处的旧数据,所以此处返回值为Hook函数中返回的WndMin虚假偏移。
LONG SetWindowLongW(
[in] HWND hWnd,
[in] int nIndex,
[in] LONG dwNewLong
);
调用API SetWindowLongW最终执行到win32kfull! xxxSetWindowLong。Index大于等于0的情况下会执行到下图所示的位置。而此次利用中wndClass.cbClsExtra指定为0 ,poi(tagWnd+0x28)+0xfc也持续为0,可以忽略。因为poi(tagWnd+0x28)+0xe8已被设置0x800属性,所以poi(poi(tagWnd+0x28)+0x128)+DesktopHeapBaseAddr+Index=tagWnd_WndMin+0x128。也就是说虽是对WndMagic进行的操作,实际上实对WndMin对象pExtrabytes字段的写入,值为自身WndMin在桌面堆中的偏移。
然后执行SetWindowLongW(hWndMagic, offset_0xc8, 0xFFFFFFF),设置WndMin对象poi(tagWnd+0x28)+0xc8处cbwndExtra值设为0xFFFFFFF,扩大可以写入的范围,在xxxSetWindowLong和xxxSetWindowLongPtr中都存在对该值和Index的大小比较判定。
现在WndMagic可控制WndMin,而WndMax对象偏移已知,因此也可控制,可以实现任意位置写。接着就是对任意位置数据读,这里采用的的是API GetMenuBarInfo,对Menu Bat信息的获取,这种利用一次可以读取8字节内容。
BOOL GetMenuBarInfo(
[in] HWND hwnd,
[in] LONG idObject,
[in] LONG idItem,
[in, out] PMENUBARINFO pmbi
);
利用中构造了一个fakeMenu,将复制给WndMax,SetWindowLongPtr指定Index为-12,且窗口dwStyle为WS_CHILDWINDOW(0x40000000L),那么窗口spMenu字段可以被设置为指定的值。spMenu字段有两处位置,poi(tagWnd+0x28)+0x98和tagWnd+0xa8。而SetWindowLongPtr成功调用后返回的值为窗口的原spMenu,记录该值。
但是此时窗口并不是子窗口类型,所以在这之前需要对该字段手动进行设置。调用SetWindowLongPtrA,参数为(hWndMin, offset_0x18+WndMax_offset-WndMin_offset, poi(poi(tagWnd+0x28)+0x18)^0x4000000000000000),可以将WndMax窗口类型添加上WS_CHILDWINDOW属性,从而通过检测。
为WndMax设置WS_CHILDWINDOW属性,并添加spMenu后,再次调用SetWindowLongPtrA恢复其dwStyle,去除WS_CHILDWINDOW属性,原因是后续在使用GetMenuBarInfo读取指定地址数据时,窗口不能为子窗口类型。
WndMax的fake spMenu设置完成,且已获取了旧spMenu,记为old_spMenu。而在spMenu结构的0x50偏移处是spMenu所属窗口对象地址,即poi(spMenu+0x50)==tagWnd。
了解以上信息后,需要对指定地址进行读,该漏洞利用对GetMenuBarInfo进行了封装,传入地址,封装函数返回该地址下的内容。
对GetMenuBarInfo的利用核心主要是指定idObject为-3,idItem为1,pmbi接收数据。API最终会走到win32kfull! xxxGetMenuBarInfo函数,传参数据同GetMenuBarInfo。对该函数分析,可以看到需要对一些特殊的位置进行伪造,从而进入目的代码处。其中poi(tagWnd+0x28)+0x58和poi(tagWnd+0x28)+0x5C处的值常为0,忽略。
最终读取时,可以看到pmbi->left读取值为poi(poi(poi(poi(menu)+0x58))+0x40),pmbi->top为poi(poi(poi(poi(menu)+0x58))+0x44),其中poi(poi(poi(menu)+0x58))值可由用户进行控制,令其为X,也就意味着我们通过控制X值,可以读取X+0x40处的8字节内容,即pmbi.rcBar.left+(pmbi.rcBar.top<<32)。那么只需要控制X为欲要读取的目的地址减去0x40,即可获取相应数据。
回到漏洞利用时封装的读取函数中,函数中首先向X指向的内存中每4个字节填写一个相对于X基址的偏移值,这样GetMenuBarInfo读取回的pmbi.rcBar.left即为目标读取地址应减去的差值。这么做的目的可能是为了防止系统版本的不同导致的差值不同,比如此次调试时win10 1909就为0x40。
然后第二次调用GetMenuBarInfo,传入(目的读取地址- pmbi.rcBar.left),即可获取目的地址8字节内容。
这么一步步通过读取,可以获取到EPROCESS,然后通过ActiveProcessLinks,遍历找到当前进程和system进程EPROCESS位置。
再次两次调用SetWindowLongPtrA,替换当前进程Token为system进程,获取system权限。第一次将当前进程Token地址写入WndMax对象pExtrabytes处,第二次将system进程Token写入当前进程Token中。完成提权。
参考
https://www.freebuf.com/vuls/271177.html
https://github.com/KaLendsi/CVE-2021-1732-Exploit
https://xiaodaozhi.com/exploit/29.html
https://theevilbit.github.io/posts/a_simple_protection_against_hmvalidatehandle_technique/
本文来自博客园,作者:Bl0od
转载请注明原文链接:https://www.cnblogs.com/zUotTe0/p/15591015.html