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)+0x98tagWnd+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://ti.dbappsecurity.com.cn/blog/articles/2021/02/10/windows-kernel-zero-day-exploit-is-used-by-bitter-apt-in-targeted-attack-cn/

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/

 

posted @ 2021-11-22 23:17  Bl0od  阅读(772)  评论(0编辑  收藏  举报