高防服务器

Microsoft Windows被在野利用的提权漏洞的分析报告是怎样的


Microsoft Windows被在野利用的提权漏洞的分析报告是怎样的

发布时间:2021-12-29 19:06:09 来源:高防服务器网 阅读:96 作者:柒染 栏目:安全技术

今天就跟大家聊聊有关Microsoft Windows被在野利用的提权漏洞的分析报告是怎样的,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。

背景

2021年03月10日,奇安信威胁情报中心监测发现CVE-2021-1732漏洞利用细节及POC已经被公开,故发布该漏洞分析报告。

2021年2月,微软在例行补丁日中修复了一枚在野的Windows 内核提权漏洞:CVE-2021-1732。与此同时,奇安信红雨滴团队在高级威胁狩猎中通过自研的红雨滴沙箱也自动捕获到该在野攻击样本,样本在红雨滴云沙箱上通过选择Windows 10 x64分析环境进行检测,可以准确识别到样本在运行过程中进行了提权攻击,从而最终进入了我们的分析视野。

https://sandbox.ti.qianxin.com/sandbox/page

漏洞分析

捕获到的攻击样本包含以下的编译pdb

C:UsersWin10sourcereposKSP_EPLx64ReleaseConsoleApplication13.pdb

在win10 1909上运行之后可以看到提权程序被提升到system权限,这里需要注意该程序直接运行完毕后就退出了,因此需要通过调试器附加运行才能看到具体的提权效果。

该漏洞由CreateWindowExW函数产生,CreateWindowExW函数最终会调用到在win32kfull.sys中xxxCreateWindowEx函数,当通过该函数创建一个带扩展内存的窗口时,xxxCreateWindowEx会调用xxxClientAllocWindowClassExtraBytes以分配对应的扩展内存,并将返回值保存到poi(tagWND+0x28)+0x128的位置,而这里poi(tagWND+0x28)+0x128实际是保存了对应窗口的扩展内存的位置,但是这个位置的值有两种情况,内存指针或该内存的内核offset,而具体为哪一种方式则依赖于(tagWND+0x28)+0xE8的位置来确定,默认情况下为内存指针。

而xxxClientAllocWindowClassExtraBytes实际分配内存的操作是通过KeUserModeCallback函数进行的内核回调生成的。

KeUserModeCallback函数的参数原型如下所示,其中比较重要的是第一个ApiNumber,该参数指定了返回到应用层时对应的回调函数在KernelCallbackTable中的序号,并通过该序号获取对应的回调函数的地址,之后的两组参数分别为对应的回调调用的输入参数/参数长度,及回调返回时的输出参数/参数长度。

KeUserModeCallback最终返回到应用层的KiUserCallbackDispatcher,该函数通过PEB获取KernelCallbackTable,这个表中记录了一系列内核返回应用层的回调函数,通过KeUserModeCallback传递的第一个参数ApiNumber寻址到目标回调函数,回调调用完毕后通过int2B 触发KiCallbackReturn返回内核。

KernelCallbackTable这张表的位置默认会保存在当前的peb中,KiUserCallbackDispatcher就是通过PEB获取该表的地址,这里需要注意这张表实际是在use32.dll中,如果当前进程没有加载该dll,peb中对应的KernelCallbackTable位置则是null,因此exp中有专门引入use32.dll的操作。

如下所示为KiUserCallbackDispatcher的一个伪代码,通过peb获取对应的回调并调用后,通过函数NtCallbackReturn返回内核。

NtCallbackReturn的原型如下,其第一个参数中保存了返回的数据,最终会赋值给KeUserModeCallback的outputstring。

由于这里KernelCallbackTable的位置保存在PEB中,因此可以通过代码的方式获取到该表的地址,并对表中的回调函数的地址进行替换,从而hook任意的内核到应用层的回调函数,这里如果在hook的回调函数中调用函数NtUserConsoleControl,并传入对应的创建的窗口的句柄,将会导致对应窗口的扩展内存被设置为分配内存的offset,且对应的flag标记被设置,如下所示,NtUserConsoleControl 最终在win32kfull中会调用xxxConsoleControl。

xxxConsoleControl中如下所示可以看到poi(tagWND +0x28)+0x128处最终被设置为到DesktopAlloc分配的内存空间的offset,同时对应poi(tagWND+0x28)+0xE8处的flag标记被设置为offset寻址。

而这里如果我们在回调hook函数中手动调用NtCallbackReturn,由于漏洞将可以再次设置该窗口的扩展内存,由于之前NtUserConsoleControl已经将该窗口的flag设置为offset,因此这里实际上通过NtCallbackReturn二次设置该offset为任意攻击者指定的地址。

因此该漏洞的实质就是,在创建一个带扩展内存的窗口时,内核中xxxClientAllocWindowClassExtraBytes的应用层回调对来自应用层返回的数据校验不严导致,通过hook xxxClientAllocWindowClassExtraBytes,并在hook函数中调用NtUserConsoleControl/NtCallbackReturn可以将目标窗口的poi(tagWND+0x28)+0x128位置设置为任意offset,从而导致越界写入。

漏洞利用原理分析

具体的利用主函数如下所示,实际的利用代码在函数fun_Eprivlcore中。

fun_Eprivlcore中首先遍历系统进程,以确认是否有卡巴斯基的杀软安装。

之后判断运行环境是否为64位系统,该代码目前只支持64位系统,并初始化一系列偏移常量。

依次动态获取/RtlGetNtVerisonNumbers/NtUserConsoleControl/NtCallbackReturn函数的地址

通过函数RtlGetNtVerisonNumbers判断具体的win10系统版本,这里会判断具体的win10版本是否大于1709,其支持的目标涉及1709-1909,当系统版本大于1903,则会修改其中部分偏移常量的数值,并进入到fun_EOP中进行具体的提权操作。

fun_EOP中调用函数fun_Init/fun_Inithwnd完成前期的初始化工作

fun_Init中首先通过IsMenu函数搜索未导出的函数pHmValidateHandle,该函数可用于获取一个应用层窗口句柄对应的内核句柄对象在应用层的内存映射。

之后通过PEB获取kernelCallbackTable,并修改该表中xxxClientAllocWindowClassExtraBytes函数的地址,将其指向fun_HOOKClientAllocWindowClassExtraBytes,完成对应xxxClientAllocWindowClassExtraBytes函数的hook

之后创建两个窗口类normalClass/magicClass,注意这里magicClass窗口类在申明的时候对应cbWndExtra扩展内存字段的大小设置为var_magicClasscbWndExtra,该值是一个随机生成的长度,而normalClass的cbWndExtra则设置为32,通过var_magicClasscbWndExtra就可以在hook函数中判断具体的回调是来自于哪一个窗口的内核回调

而这里normalClass用于生成辅助的利用窗口

而magicClass则是具体触发漏洞的窗口。

fun_Inithwnd中通过normalClass创建10个窗口0-9,并将10个窗口句柄保存在hWnd这个数组中,同时调用var_pHmValidateHandle获取这10个窗口句柄对应的内核窗口对象在应用层的映射,并保存到v23这个数组中。

之后通过LocalAlloc分配内存,并构造对应的fakespmenu,fakespmenu会在之后用于实现任意地址读的功能。

构造出的fakespmenu相关内存结构如下所示,最终的目标读取地址会写入到var_OffsetrcBarleft

依次获取第0和第1个应用层的窗口句柄,及对应的内核窗口对象的应用层映射地址,并通过映射地址获取该内核对象在内核地址中的偏移(位置在内核对象偏移0x8的位置保存),之后依次将2-0个窗口对象通过函数DestroyWindow释放,并通过函数var_NtUserConsoleControl将第0个窗口对象的扩展内存寻址设置为offset类型。

如下所示r14和rsi中依次保存了0,1的窗口对象的应用层映射内存,其偏移0x8的位置为对应的内核对象在内核地址中的偏移。

通过magicClass类创建窗口,这里简称为窗口2,由于magicClass窗口类中对应的cbwndExtra字段和hook函数中的一致,因此窗口2创建的过程中会触发漏洞函数xxxClientAllocWindowClassExtraBytes并最终通过内核回调进入到我们设置的hook函数中。

内核中函数xxxClientAllocWindowClassExtraBytes调用前,可以看到此时窗口2对应的cbwndExtra为12a5。

win10中tagWND对象的符号已经被删除,其内容也发生了相当的变化,以下为通过分析之后还原出的tagWND的重要结构,上图中的ffffff0681244870即为对应的pwnd,位于tagWND偏移0x28,其中比较重要的是

0x98 spmenu          窗口对应的menu菜单对象

0xc8 cbwndExtra       窗口对应扩展内存的大小

0xe8 Extra flag        窗口对象扩展内存的寻址标记,支持指针寻址,和偏移寻址,默认为指针寻址

0x128 pExtraBytes     保存扩展内存对应的位置,根据Extra flag为指针或offset偏移

tagWND偏移0x18+0x80的位置为对应的内核基地址,配合上对应的内核偏移就可以计算出对应的tagWND对象pwnd在内核中的偏移。

具体的hook函数如下,由于窗口2在创建时使用的是maginClass类,其对应的cbWndExtra符合hook函数中的过滤条件,因此触发对应的var_xxxClientAllocWindowClassExtraByteshook,进入具体的hook流程中,hook中通过函数fun_FindcurrenHwnd获取到当前窗口的hwnd,并以此调用NtUserConsoleControl,将其对应内核对象poi(tagWND2+0x28)+0xE8的处flag标记设置为offset类型寻址,之后将var_normalClassoffset0作为var_NtCallbackReturn参数返回,这将导致var_normalClassoffset0写入到窗口2poi(tagWND2+0x28)+0x128处,此时通过对var_magicClasshwnd2调用SetWindowLongW(该函数用来改变指定窗口的属性,函数也将指定的一个32位值设置在窗口的扩展内存空间的指定偏移位置)来修改扩展空间内存内容时,实际上修改的是var_normalClasshwnd0句柄对应内核对象pwnd的内容。

NtUserConsoleControl对应的内核函数为xxxConsoleControl,如下所示会设置pwnd偏移0xE8处的扩展内存寻址flag为offset类型,并初始化对应的扩展内存pExtraBytes为对应内存的内核偏移。

xxxConsoleControl调用之后的2窗口如下所示,注意此时的pExtraBytes还没有被设置指向0窗口内核对象的pwnd内核偏移。

直到内核回调返回之后2窗口内核对象的pwnd.pExtraBytes字段此时才被设置为0窗口内核对象的pwnd的内核偏移,之后通过xxxSetWindowLong设置窗口2的扩展内存时,实际的操作地址将是0窗口内核对象的pwnd。至此可以通过窗口2调用xxxSetWindowLong来将窗口0的cbwndExtra字段设置为0xfffffff,以此获取越界写入的能力。

漏洞触发后的2窗口内核对象的pwnd,此时扩展内存寻址flag已经设置为offset寻址,对应的pExtraBytes已经被修改为窗口0的内核对象pwnd的内核偏移。

此时通过对var_magicClasshwnd窗口2调用SetWindowLongW,将var_normalClasshwnd0窗口0对应的tagWND0.cbWndExtra设置为0xffffff,操作结束后var_normalClasshwnd0窗口0对应的cbWndExtra将设置为0xffffff,此时var_normalClasshwnd0窗口0具备了越界写入的能力。

通过var_normalClasshwnd0窗口0调用SetWindowLongPtrA设置var_normalClasshwnd1窗口1的dwStely字段,并通过var_normalClasshwnd1窗口1调用SetWindowLongPtrA将spmenu设置为fakespmenu伪造的menu对象,用于实现任意地址读,这里将窗口1的dwStely修改为WS_CHILD将确保spmenu能进行设置,当fakespmenu设置成功之后,还会将该dwStely的类型还原,因为只有在原dwStely中,var_normalClasshwnd1才能通过调用GetMenuBarInfo依赖fakespmenu进入到特定的错误代码的代码分支,以实现具体的读取操作,总结一句话就是通过窗口0的越界写能力将窗口0设置为错误类型,并附加错误的菜单对象,以此来实现任意地址读取。

整个利用过程中关键窗口的设置操作如下所示:

xxxSetWindowLong对应的内核代码如下,其中if中对应扩展内存的offse寻址方式。else为指针寻址方式

通过窗口2调用SetWindowLong,将扩展内存c8的位置设置为fffffff,由于窗口2内核对象的pwnd. pExtraBytes已经被设置为窗口0内核对象的pwnd内核偏移,此时将直接把fffffff写入到窗口0内核对象的pwnd+0xc8处,即0窗口内核对象的pwnd. cbwndExtra。

如下所示获取pExtraBytes(窗口0内核对象的pwnd内核偏移)+内核基址+c8

可以看到此时计算出的目标地址就是窗口0内核对象的pwnd. cbwndExtra,其原本的大小为0x20,ffffffff写入后窗口0对象将具备越界写能力。

写入成功。

之后通过窗口0调用SetWindowLongPtrA以修改窗口1的dwStely属性

SetWindowLongPtrA和SetWindowLong的实现大同小异,如下红框所示为对应的扩展内存offset寻址的实现方式。

进入内核xxxSetWindowLongPtr函数

如下所示为窗口1内核对象的pwnd,需要修改的dwStely位置在其偏移0x18处(right红框才是正确位置)

检测判断对应的寻址flag

进入offset寻址流程,如下获取对应的内核基地址,此时的偏移15308为窗口0/1内核对象的pwnd内核偏移的差值+0x18(dwStely的pwnd偏移)。

最终计算出窗口1内核的pwnd.dwStely地址,写入将其设置为WS_CHILD类型(right红框才是正确位置)。

写入结果如下所示

之后通过窗口1调用SetWindowLongPtrA将窗口1的spmenu设置指向前面构造的fakespmenu

这里可以看到,实际上此时SetWindowLongPtrA设置的参数为-12(0xFFFFFFF4),即为设置子窗口的新标识符。但是需要注意这里注明了该窗口不能是顶级窗口

而当函数xxxSetWindowLongPtr对应的第二个参数不为偏移,而是特殊的负数标记id时,将会进入到函数xxxSetWindowData函数中处理,也就是我们这里的情况

如下所示为对应的-12时的处理逻辑,可以看到这里会检测对应窗口的类型是否为WS_CHILD,如果是则将value(这里指向了我们构造的fakespmenu)设置到对应窗口内核对象0xa和窗口内核对象的pwnd偏移0x98位置

如下所示内核调试器进入xxxSetWindowLongPtr

经过判断之后进入xxxSetWindowData函数中。

进入对应的-12分支处理逻辑。

首先判断其是否WS_CHILD类型的窗口

判断通过直接将伪造的fakespmenu赋值

窗口1内核对象及窗口1内核对象pwnd对应赋值后如下所示

之后窗口0调用SetWindowLongPtrA将窗口1的dwStely还原

进入内核xxxSetWindowLongPtr

如下所示窗口1的dwstely已经恢复,因此这里的操作实际上是通过窗口0的越界写入能力修改了窗口1的dwStely为WS_CHILD,这将导致可以通过窗口1以GWLP_ID调用xxxSetWindowLongPtr将窗口1内核对象的pwnd.spmenu设置指向我们的fakespmenu,之后恢复窗口1的dwStely,这就导致窗口1具备了原本dwStely=WS_CHILD时才有的spmenu属性

设置spmenu之后,由于xxxSetWindowLongPtr的特性会将写入前的内容返回,此时返回的内容为一个标准的内核结构,根据该内核结构的地址配合上后续的任意地址读取,将方便的获取ERPROCESS

借助var_normalClasshwnd1窗口1的fakespmenu,配合GetMenuBarInfo实现任意地址读取,这里首先第一次调用GetMenuBarInfo以获取对应var_OffsetrcBarleft偏移(后文会详述),之后第二次调用GetMenuBarInfo才是用于读取对应地址的数据

GetMenuBarInfo的函数原型如下所示

具体参数如下所示,这里漏洞利用时idObject为0xFFFFFFFD,对应的item为1,最终结果则通过第四个参数返回

第四个参数为MENUBARINFO,如下所示

其中的rcBar为一个矩形结构

这里通过idObject为0xFFFFFFFD可以看到在内核中对应函数为xxxGetMenuBarInfo,其核心为-3这个逻辑代码块。

该逻辑实际上是通过窗口内核对象0xa8处获取对应的spmenu,这里窗口1对应的spmenu指向了我们恶意构造的fakespmenu,并将该段内存映射到内核,之后通过fakespmenu.fakerect.0x28处var_OffsetrcBarleft字段中的数据和窗口内核对象的pwnd指定偏移0x58/0x5c处的数据做计算,并将结果返回对应的pmbi.rcBar这个rect矩形结构,而这个过程中由于fakespmenu中的数值是攻击者完全可控的,而pwnd指定偏移0x58/0x5c默认为0,这就导致通过这个操作可以进行任意地址读取操作,这里需要注意的是实际上dwstelye=WS_CHILD,才能设置对应的spmenu,而设置了spmenu,dwstelye=WS_CHILD的窗口正常情况下并不会进入到以下的代码分支,但是因为利用中在设置了窗口1的spmenu之后,又通过窗口0的越界写恢复了窗口1的dwStely,从而可以进入以下的错误代码分支,如下所示依次计算rect的四个坐标,并保存到变量v7中。

完成最后一个bottom写入

如下所示第一次会读取之前原spmenu处返回的内核结构+0x50处的数据,即下图中fffff0680825000+50处的数据。

读取函数中会尝试两次调用GetMenuBarInfo

第一次调用GetMenuBarInfo以获取计算时var_OffsetrcBarleft的偏移。

之后第二次完成真正意义上的读取,首先第一次读取如下所示,此时传入的pmbi如下所示

xxxGetMenuBarInfo中进入-3的代码逻辑

这里会首先判断对应dwStely,如果之前不将窗口0的dwStely恢复(恢复前为4c),则不会进入之后的代码逻辑

之后获取窗口1内核对象+0xa8处的fakespmenu对象

通过该fakespmenu,调用SmartObjStackRefBase<tagMENU>::operator将其映射到内核中。

如下所示可以看到对应的返回的fffffe0dd36df9e0指向了var_pmenu,之后就是后续构造的var_fakerect

判断GetMenuBarInfo第三个参数idItem是否大于poi(poi(var_fakerect+028)+0x2c),这里idItem为1,poi(poi(var_fakerect+028)+0x2c)在fakespmenu构造的时候将其设置为0x10,检测通过

之后获取var_fakeract偏移0的内容,并保存到返回的pmbi+0x18

依次检测var_fakeract偏移0x40,0x44处是否为0,这里利用代码在构造fakespmenu时也依次将这两个值域进行了设置。

之后再此判断窗口偏移0x1a处的数据,并进入最终的坐标计算逻辑。

获取poi(poi(var_fakerect+0x58))处的var_OffsetrcBarleft,即下图中000002804f910150

可以看到此时第一次的var_OffsetrcBarleft保存指针指向的数据是通过攻击者手动构造的

之后依次计算返回pmbi.rcBar的left; top; right; bottom;

Left=poi(var_OffsetrcBarleft+0x40)+ poi(poi(tagWND1+0x28)+0x58),即0x40+0=0

写入到left字段

Right=left+ poi(var_OffsetrcBarleft+0x48) = 0x40+0x44 = 0x88

0x88写入right字段

top=poi(var_OffsetrcBarleft+0x44)+ poi(poi(tagWND1+0x28)+0x5c),即0x44+0=0x44

写入到top字段

Bottom = top + poi(var_OffsetrcBarleft+0x4c) =0x44+0x4c =0x90。

写入button字段

可以看到如下所示中,pmbi.rcBar返回的矩形实际上是通过poi(var_OffsetrcBarleft)+0x40处0x10长度的内存数据(0x40的偏移由idItem=1决定,如果等于2应该为0x40+0x60=0xa0,所以利用中默认都通过idItem=1调用GetMenuBarInfo)配合窗口内核对象的pwnd+0x58/0x5c两个字段生成,由于这里pwnd+0x58/0x5c默认为0,因此直接依赖于poi(var_OffsetrcBarleft)+0x40处的0x10内存数据,而根据公式可以发现,pwnd+0x58/0x5c为0的情况下,left,top数据实际上就是poi(var_OffsetrcBarleft)+0x40/ poi(var_OffsetrcBarleft)+0x44这连续8个字节的内容,由于poi(var_OffsetrcBarleft)指向内容为攻击者可控,只需将其值设置为des-0x40(0x40的偏移根据idItem而不同),即可实现对des地址数据的读取。

内核返回的pmbi

第一次读取返回的pmbi值

第一次读取测试成功后返回的left正好就是var_OffsetrcBarleft的偏移

这也就是为什么第一次调用时var_OffsetrcBarleft指向内存如此构造的原因,实际上每一个4字节内存都是一个偏移,Left=poi(var_OffsetrcBarleft+0x40)+ poi(poi(tagWND1+0x28)+0x58), poi(poi(tagWND1+0x28)+0x58)=0,因此left中一定返回的是对应的偏移。

进入第二次调用将目标读取地址减去pmbi.rcBar.left中返回的偏移值ffffff0680828050-0x40

= ffffff0680828010

此时更新过var_OffsetrcBarleft值后整体的fakespmenu内存如下所示var_OffsetrcBarleft指向了目标读取地址-0x40的位置。

内核进入xxxGetMenuBarInfo

检验对应窗口的dwStely

映射对应的fakespmenu内存

如下所示left读取此时获取了目标地址指向内容的低四位。

Left=poi(var_OffsetrcBarleft+0x40)+ poi(poi(tagWND1+0x28)+0x58)= 0x8385a690+0=0x8385a690

将目标地址保存的第四位内容保存到left中

获取对应的right,Right=left+ poi(var_OffsetrcBarleft+0x48) = 0x8385a690+0= 0x8385a690

写入到right字段

top=poi(var_OffsetrcBarleft+0x44)+ poi(poi(tagWND1+0x28)+0x5c) = 0xffffff06+0 = 0xffffff06,正好是目标读取地址的高四字节。

写入到top字段

Bottom = top + poi(var_OffsetrcBarleft+0x4c) =0xffffff06+0 =0xffffff06。

写入到bottom字段

此时返回后对应pmbi.rcBar

通过left+top即可获取对应的目标地址内容。

之后如下所示依次读取对应的EPROCESS及内核基址

如下方式读取对应的内核基址

如下方式读取EPROCESS

通过EPROCESS获取系统token及当前进程token地址,并通过函数fun_Write进行写入

fun_write借助窗口0 var_normalClasshwnd0,窗口1 var_normalClasshwnd1实现任意地址写入,其本质是通过窗口0 var_normalClasshwnd0的越界写入能力,调用SetWindowLongPtrA修改窗口1 var_normalClasshwnd1的扩展内存地址pExtraBytes来实现任意地址写。

窗口0通过越界写 调用SetWindowLongPtrA修改窗口1的pExtraBytes为目标写入地址

首先计算对应的窗口0内核对象到窗口1内核对象的pwnd. pExtraBytes的内核偏移。

内核进入xxxSetWindowLongPtr

函数返回后窗口1内核对象的pwnd. pExtraBytes,已经被设置为当前进程的token地址。

此时通过窗口1直接调用xxxSetWindowLongPtr,并将参数index设置为0,将直接完成对当前pExtraBytes(当前进程token)地址的写入操作

进入内核

这里不小心步过了,重新调了下,如下所示为fun_write

窗口1调用xxxSetWindowLongPtr完成对当前进程token的写入替换

进入内核xxxSetWindowLongPtr

窗口1内核对象的pwnd. pExtraBytes指向了当前进程的token地址,这里是pointer的扩展内存寻址方式。

判断具体的拓展内存寻址flag

计算目标地址,由于index为0,因此目标地址直接是pExtraBytes指向的当前进程token的地址。

完成写入,提权完毕。

看完上述内容,你们对Microsoft Windows被在野利用的提权漏洞的分析报告是怎样的有进一步的了解吗?如果还想了解更多知识或者相关内容,请关注高防服务器网行业资讯频道,感谢大家的支持。

[微信提示:高防服务器能助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。

[图文来源于网络,不代表本站立场,如有侵权,请联系高防服务器网删除]
[