概述

  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/

CVE-2021-1732 LPE漏洞分析的更多相关文章

  1. 漏洞分析:CVE 2021-3156

    漏洞分析:CVE 2021-3156 漏洞简述 漏洞名称:sudo堆溢出本地提权 漏洞编号:CVE-2021-3156 漏洞类型:堆溢出 漏洞影响:本地提权 利用难度:较高 基础权限:需要普通用户权限 ...

  2. Java反序列化漏洞分析

    相关学习资料 http://www.freebuf.com/vuls/90840.html https://security.tencent.com/index.php/blog/msg/97 htt ...

  3. FFmpeg任意文件读取漏洞分析

    这次的漏洞实际上与之前曝出的一个 CVE 非常之类似,可以说是旧瓶装新酒,老树开新花. 之前漏洞的一篇分析文章: SSRF 和本地文件泄露(CVE-2016-1897/8)http://static. ...

  4. CVE-2016-10190 FFmpeg Http协议 heap buffer overflow漏洞分析及利用

    作者:栈长@蚂蚁金服巴斯光年安全实验室 -------- 1. 背景 FFmpeg是一个著名的处理音视频的开源项目,非常多的播放器.转码器以及视频网站都用到了FFmpeg作为内核或者是处理流媒体的工具 ...

  5. Elasticsearch 核心插件Kibana 本地文件包含漏洞分析(CVE-2018-17246)

    不久前Elasticsearch发布了最新安全公告, Elasticsearch Kibana 6.4.3之前版本和5.6.13之前版本中的Console插件存在严重的本地文件包含漏洞可导致拒绝服务攻 ...

  6. ThinkCMF X2.2.2多处SQL注入漏洞分析

       1.     漏洞描述 ThinkCMF是一款基于ThinkPHP+MySQL开发的中文内容管理框架,其中X系列基于ThinkPHP 3.2.3开发,最后更新到2.2.2版本.最近刚好在渗透测试 ...

  7. 看个AV也中招之cve-2010-2553漏洞分析

    试想:某一天,你的基友给你了一个视频文件,号称是陈老师拍的苍老师的老师题材的最新电影.avi,你满心欢喜,在确定文件格式确实为avi格式后,愉快的脱下裤子准备欣赏,打开后却发现什么也没有,而随后你的基 ...

  8. CVE-2010-3971 CSS内存破坏漏洞分析

    看了仙果版主的议题演讲,其中提到cve-2010-3971是一个浏览器漏洞利用中的里程碑.于是找来POC,尝试分析一下. 1.漏洞重现 XP SP3+ie6.0环境 poc如下: poc.htm &l ...

  9. jackjson学习2+CVE-2019-14379漏洞分析

    最近想着分析jackson,jackson和fastjson有点相似,浅蓝大神的文章很好,个人受益匪浅 昨天简单说了下jackson的用法,现在继续拓扑,补充前置知识,前置知识补充的足够多,那么漏洞分 ...

随机推荐

  1. IDEA开启热部署

    双击shift,查找Registry

  2. 关于 Intel CPU 和Iris Xe Graphics的报告问题

    关于 Intel CPU 和Iris Xe Graphics的报告问题 有些用户报告了一些技术问题,这里有更多的信息和如何解决. Intel 11th CPU & Iris Xe Graphi ...

  3. 【代码优化】Bean映射之MapStruct

    [代码优化]Bean映射之MapStruct 一.背景 领域模型相互转换就只能靠手工的 get()/set()? 普遍的做法有以下几种: 手工 get()/set(): 构造器: BeanUtils ...

  4. Windows 和 Ubuntu 的网络能互相 ping 通之后,linux无法上网原因:①路由没设置好,②DNS 没设置好

    确保 Windows 和 Ubuntu 的网络能互相 ping 通之后,如果 Ubuntu 无法上网,原因通常有 2 个:路由没设置好,DNS 没设置好. 如果执行以下命令不成功,表示路由没设置好: ...

  5. 以太 ip tcp udp 三次握手的理解

    以太帧: 1.前导码(7字节):使接收器建立比特同步. 2.起始定界符SFD(1字节):指示一帧的开始. 3.目的地址DA(6字节):指出要接收该帧的工作站. 4.源地址SA(6字节):指示发送该帧的 ...

  6. 《剑指offer》面试题55 - II. 平衡二叉树

    问题描述 输入一棵二叉树的根节点,判断该树是不是平衡二叉树.如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树. 示例 1: 给定二叉树 [3,9,20,null,null, ...

  7. RabbitMQ 中的分布式,普通 cluster 模式的构建

    RabbitMQ 如何做分布式 前言 集群配置方案 cluster 普通模式 镜像模式 federation shovel 节点类型 RAM node Disk node 集群的搭建 1.局域网配置 ...

  8. Golang单元测试框架整理

    目录 一.单元测试是什么 二.单元测试的意义 三.Golang单元测试框架 3.1 Golang内置testing包 3.1.1 简单的测试 3.1.2 Benchmark 基准测试 3.1.3 运行 ...

  9. gin中的路由参数

    package main import ( "fmt" "github.com/gin-gonic/gin" ) func main() { router := ...

  10. Gulp自动化任务及nvm、npm常用命令

    项目环境配置 nvm:   node版本管理工具,安装和环境变量         cmd常用命令: · nvm use [version]: 切换至指定版本的node · nvm install no ...