一 、基于产生原因1.1 Callback机制Win32k组件最初的内核设计和编写是完全建立的用户层上的,但是提权微软在 Windows NT 4.0 的改变中将 Win32k.sys 作为改变的一部分而引入 ,用以提升图形绘制性能并减少 Windows 应用程序的漏洞内存需求。窗口管理器(User)和图形设备接口(GDI)在极大程度上被移出客户端/服务端运行时子系统(CSRSS)并被落实在它自身的防对一个内核模块中。
这样的基于设计无疑是为内核增添了一部分压力 ,服务器租用win32k.sys需要处理大量的内核用户层回调 ,在这之后国外安全研究人员Tarjei Mandt公开了他对Win32k User-Mode Callback机制的提权研究成果 ,从此User-Mode Callback的漏洞攻击面得到广泛关注,UAF的防对漏洞也不断的涌现 。 下图代码为一个经典UAF漏洞,基于用户层执行的内核某个函数通过syscall传入到内核层 ,当内核代码执行到xxxSomeCallback这一句时 ,提权用户层可以在用户定义的漏洞callback函数中获得代码执行的机会 ,香港云服务器如果用户在callback函数调用了DestroyWindow函数销毁窗口p,防对内核层的相应销毁代码将会被执行 ,p的相应内存被释放,回调执行完毕 ,NtUserSysCall函数继续执行 ,当执行到xxxSetWindowStyle(p)一句时 ,由于p的内存已经被释放从而导致UAF漏洞的产生。
1.2 GDI对象在TypeIsolation机制的引入之前 ,windows内核中以bitmap为代表的GDI对象成为内核漏洞利用时的首选,免费模板对于WWW漏洞来说,GDI对象就是它的“左膀右臂”,借助GDI对象可以很容易构造出稳定内核内存的任意地址读写原语 ,以此来绕过Windows的安全机制(KALSR、SMEP等)。 二 、利用框架漏洞成功利用漏洞触发、漏洞利用这两大环节,而漏洞利用又有以下三个阶段 ,分别是堆喷射阶段 、信息泄露阶段 、源码下载代码执行阶段。下面将结合CVE-2019-0808、CVE-2021-1732进行详细阐述 。 2.1 漏洞触发漏洞的类型虽然五花八门,但基于win32k的内核漏洞是存在者一些共性的 ,也就是它的用户层回调 ,近年来所爆出的win32k的漏洞,其触发点大多都在用户层回调被hook。源码库下面将通过两个案例来进行介绍。 案例1:在CVE-2021-1732中 ,攻击者正是hook xxxClientAllocWindowClassExtraBytes的用户层回调函数,强行改变窗口对象tagWND的扩展数据的保存位置以及寻址方式 ,从而触发一个任意地址覆盖写漏洞。
针对这种触发方式 ,我们可以直接对用户层的usertable进行检测 ,usertable存在于PEB+0x58偏移处: 案例2:CVE-2021-40449 是Win32k 的 NtGdiResetDC 函数中的一个释放后使用漏洞 ,建站模板在执行其自己的回调期间针对同一句柄第二次执行函数 ResetDC 时触发UAF漏洞。
针对这种触发方式 ,可以通过开启Driver Virifier进行检测,UAF的漏洞再此环境下会触发蓝屏。 2.2 漏洞利用漏洞成功被触发仅仅是一个好的开端,更重要的是漏洞利用点的寻找过程 。笔者将此过程大致划分为信息泄露 、堆喷射 、原语构造 、代码执行这四个阶段。 1.信息泄露阶段在win32k漏洞利用过程中 ,内核对象的泄漏是至关重要的 。可以说成功利用一个漏洞的大前提就是获取win32k内核对象的地址。
上图是对Windows内核地址泄漏的总结(来自GitHub),表格中包含截止目前泄漏内核对象的各种技巧。下面也是拿两个demo来阐述内核对象泄露的细节 : Demo1 ,通过GdiSharedHandleTable去泄露bitmap对象:在R3通过CreateBitmap创建位图成功后会得到一个hBitmap,如果要在R3去泄露该对象在内核中的地址可以通过GdiSharedHandleTable获取。GdiSharedHandleTable位于PEB+0x94的位置。
GdiSharedHandleTable的本质是一个指向GDICELL结构体数组的指针 。
同 CreateFile 类似 ,其实Windows 都用句柄(Handle)来标识用户态对内核对象的引用 。这个句柄低 16 位其实是数组索引。
通过上文,就可以计算出bitmap在内核中的地址。
HMValidateHandle是微软未公开的一个函数,凭借此函数可以通过传入句柄获取对于内核对象的地址(win32k对象)。HMAllocObject创建了桌面堆类型句柄后 ,会把tagWND对象放入到内核模式到用户模式内存映射地址里 。 为了验证句柄的有效性,窗口管理器会调用User32!HMValidateHandle函数读取这个表。函数将句柄和句柄类型作为参数 ,并在句柄表中查找对应的项 。如果查找到对象, 会返回tagWND只读映射的对象指针,通过tagWND这个对象我们可以获取到句柄等一系列窗口信息。 该函数地址是通过R3的user32.dll!IsMenu函数获取到的 。
具体获取方式如下:
堆(池)喷射是进行内存布局的常用手段, 通过在分配关键的内核对象之前,首先分配和释放特定长度和数量的其他对象 ,使内核内存首先处于一个确定的状态,来确保在分配关键的内核对象时 ,能够被系统内存管模块分配到我们所希望其分配到的某些位置 ,例如接近某些可控对象的位置。攻击者可利用此种方法构造完美的内存布局从而达到自己的目的。此技术没有固定的方法(“因地制宜”),但是所要达成的目的比较一致--执行shellcode以及信息泄露 。 案例1: Bitmap对象的地址在RS1中是通过AcceleratorTable获取到的。先申请大量的AcceleratorTable对象然后释放其中一个,接着申请大小相等的bitmap对象。通过泄漏AcceleratorTable对象的地址,即可等到bitmap的内核地址 。(内存大小计算方式将在下一章节进行详细阐述)
代码框架如下
在win32k内核漏洞利用中 ,RW原语同样扮演着重要角色 。它可以对所分配的关键内核对象后面的内存区域进行操作,以控制原本不能控制的相邻对象的成员数据,这些数据将作为后续利用操作的重要节点。 下面将对win32k漏洞常用到的读写原语进行介绍: Bitmap系列 :SetBitmapBits/GetBitmapBits可对内核对象bitmap的pvScan0指向的像素数据内存进行修改。
此系列一直到RS3微软把Bitmap header与Bitmap data分离后,彻底失效 。 Palette系列:GetPaletteEntries/SetPaletteEntries可对内核对象Palette的成员pFirstColor(指向4个bytes的数组PALETTENTRY)修改构造RW原语 。
应用场景1 : Wnd->StrName 字段是指向窗⼝标题名的指针,通过修改此变量 ,再借助⽤户态下 的 InternalGetWindowText 和 NtUserDefSetText 函数则可实现任意内核地址读写
应用场景2: a)申请两个连续的Wnd对象(Spray) ,Wnd0与Wnd1 b)通过漏洞能力将Wnd0的pExtraBytes字段变为可越界读写的。 c)通过Wnd0.的越界写入能力 ,修改tagWND1.pExtraBytes到指定地址 d)借助SetWindowLongPtr的修改能力 ,最后使Wnd1获得任意地址写入能力。
一般仅需tagMenuBarInfo.rcBar.left 和 tagMenuBarInfo.rcBar.top读取指定地址的8个字节的数据。
另外,通过GetMenuBarInfo构造Read Primitive时 ,需要提前构造Fake Menu(用户层) ,再通过SetWindowLongPtr对Target Wnd的Menu(内核层)进行替换,以达到读取地址可控的目的。
对于win32k内核漏洞 ,其最终利用方式就是本地提权,而提升权限的主要手法就是进行Token替换,共以下两种方式: Token指针替换(_EX_FAST_REF替换)
将当前进程 EPROCESS 中存储的 Token 指针替换为 System 进程的 Token 指针 。
将当前进程 EPROCESS 的成员 Token 指针指向的 Token 块中的数据替换成 System 进程拥有的 Token 块的数据
将Present和Enabled的值更改为SYSTEM进程令牌的所有权限。
三 、内核对象3.1 Bitmap简介GDI(Windows Graphics Device Interface),是windows为应用程序提供图形、文字显示的 API 接口 。 Bitmap是GDI中的图形对象,用于创建 、操作(缩放 、滚动、旋转和绘制)并将图像作为文件存储在磁盘上 ,其实际上为一个二元数组 ,去存储像素 、颜色、大小等信息 。 Bitmap关键结构体及对象SURFACE对象(Bitmap在内核中的结构)
BaseObject ,内核 GDI 对象类的基类都是一个称作 _BASEOBJECT 的结构 。对内核对象进行标记 ,用于描述最基础的对象信息
SURFOBJ(Bitmap核心结构),用于控制位图的大小 ,像素等信息。
创建具有指定宽度,高度和颜色格式(颜色平面和位每像素)的位图,而前文中也提过bitmap在内核中关联的对象SURACE,由SURACE通过CreateBitamp的前4个参数去精确控制分配的内核内存块的大小。其调用链如下 :
在GreCreateBitmap函数中 ,首先根据传入的 cPlanes 和 cBitsPerPel参数确定位图的像素位类型,然后创建一个DEVBITMAPINFO对象 ,通过CreateBitmap前四个参数去内存块中申请一片内存并且设置位图数据扫描线的长度。接着lpBits如果不为0,则通过GreSetBitmapBits去设置像素数据。
DEVBITMAPINFO的结构:
在 Windows 内核中处理位图像素数据时,通常是以一行作为单位进行的,像素的一行被称为扫描线,而扫描线的长度就表示的是在位图数据中向下移动一行所需的字节数。 位图数据扫描线的长度被存储在SURFACE+0x34 字节偏移的成员(即 SURFACE->so.lDelta 成员)中 。这样一来,成员 pvScan0 将指向当前位图 SURFACE 对象的像素点数据缓冲区的起始位置。在后续对位图像素点进行读写访问时 ,系统位图解析模块将以该对象的 pvScan0 成员存储的地址作为像素点数据区域起始地址 。
3.2 Palette简介调色板是一个数组,其中包含标识当前可以在输出设备上显示或绘制的颜色的颜色值。调色板由能够生成多种颜色但在任何给定时间只能显示或绘制这些颜色的子集的设备使用 。 Palette关键结构体及对象PALETTE对象
PALETTE结构中 ,有三个成员是值得我们关注的分别是cEntries、pFirstColor、apalColors。cEntries指定当前调色板的项数 ,pFirstColor指向调色板列表起始表项(apalColors)的地址,apalColors是一个结构体数组存储调色板列表数据 。 PALETTEENTRY (调色板列表) 结构体 PALETTEENTRY 大小为 4 字节 ,其各个成员用于定义调色板表项对应的 24 位 RGB 颜色值等信息
Createpalette函数用来创建调色板对象,其只有一个参数lplgpl是指向LOFGPALETTE类型结构体对象的指针 。定义如下:
这里我们仅需要关注第二个和第三个参数 ,palPalEntry是可变长度的 PALETTEENTRY 结构体类型数组,而palNumEntries来决定PALETTEENTRY 的个数。也就是说palette对象的大小是由palNumEntries控制的。
显然,我们可以得出palette对象大小的计算方式:4 * cEntries + 0x90/PALETTE/ 3.3Wnd简介Windows是对象,它们同时具有代码和数据,但它们不是C++ 类。 相反,程序通过使用名为句柄 的值来引用窗口 。句柄是不透明类型。实质上它只是操作系统用来标识对象的数字 。可以想象Windows创建的所有窗口都有一个大表 。它使用此表按其句柄查找窗口。 (它内部的工作方式是否完全相同都很重要。) 窗口句柄的数据类型是 HWND , 这通常发音为"aitch-wind" 。 窗口句柄由创建窗口的函数返回 :CreateWindow 和CreateWindowEx。 关键结构体及对象这里只介绍tagWnd常用的字段,分别是tagwnd.cbwndextra和tagWND.ExtraBytes。当使用CreateWindowEx创建窗口时,可以在注册窗口类时通过WNDCLASSEXA结构体中的cbWndExtra字段直接在内存中的tagWND对象之后请求额外的内存字节。
CreateWindowEx是用来创建窗口 ,在漏洞利用的世界中 ,Wnd地位是非常高的,可被用来进行堆spray,越界写的容器等等。此次我们仅介绍Wnd的pExtraByte字段 ,分析它在内核中生成的过程。
上图时CreateWindowEx到内核真正调用xxxCreateWindowEx函数的过程 ,xxxCreateWindowEx中回到tagWND的cbWndExtra字段进行判断。
如果cbWndExtra不为0,则将其作为参数传入xxxClientAllocWindowClassExtraBytes ,在xxxClientAllocWindowClassExtraBytes中通过KeUserModeCallback进入到R3 ,最后调用RtlAllocateHeap为其在用户层的桌面堆上申请一块内存。
对于SetWindowLongPtr函数 ,最重要的就是其对Wnd的修改能力,下图xxxSetWindowLongPtr函数的部分反汇编代码 ,可以看到当dwExtraFlag被设置为800后,可以直接对位于内核桌面堆的pExtraBytes+Index处的数据进行修改。
xxxSetWindowData函数可以对Wnd的spMenu进行替换,这是我们能够利用Menu构造Read Primitive的先决条件 。
相邻的窗口中相差的只是一个tagWNDk结构体的大小 3.4Menu简介菜单是为应用程序指定选项或选项组(子菜单)的项目列表。单击菜单项打开子菜单或使应用程序执行命令。菜单管理也是win32k中最复杂得组件之一,其整体依赖多种十分复杂的函数和结构体。 Menu关键结构体及对象tagMENU,
tagPOPUPMENU ,
通过spMenu泄漏EPROCESS内核地址 ,用于寻找SYSTEM进程以及Token替换。
四 、攻防进化史漏洞只有能够被成功利用 ,才可以体现出它的价值 。而对于win32k内核漏洞,最经典也是利用率最高的类型就是“任意地址任意写(WWW)”漏洞。本章节将围绕“ArbitraryOverwrite” ,对win32k提权利用方式的进化史以及微软相应的缓解措施进行详细阐述 。 4.1 Win7下的利用模式缓解措施个人认为Win7是Win32k内核漏洞利用最理想的环境 ,因其具有以下几点特征 : Win32k.sys未分离信息泄露的方式最多(目前已知技术均兼容win7)0页内存机制未被缓解Win32k 内核对象公开(成员偏移清晰)综上,可以更加确认一个win32k内核漏洞在win7的利用率是极高的。 利用思路我们以“Bitmap”为例,介绍内核提权漏洞在win7上的利用方式 : 前提:Arbitrary Memory Write漏洞 创建两个bitmap对象分别为hManager 、hWorker,通过GdiSharedHandleTable泄露内核地址。利用任意地址覆盖写漏洞,将hManager的pvScan0修改为指向hWorker成员pvScan0的地址 。
操作第一个 hManager ,可以替换第二个 hWorker->pvScan0地址 ,再通过第二个 hWorker来将system进程的token写入当前进程 。
4.2 RS1下的利用模式缓解措施GdiSharedHandleTable泄露bitmap的方式被缓解 ,GdiSharedHandleTable的pKernelAddress 指向一块无意义的地址 。
结合网上资料 ,Windows 中存在着 3 种类型的对象 ,分别为 User object 、GDI object 、Kernel object,一共有 40 多种对象。
而bitmap属于GDI object其存在于换页会话池中,由于GDI object泄露地址方式在RS1版本中被缓解,因此我们需要从另外两种类型对象中去寻找替代方案。此次我们使用的是Accelerator table其属于User object并且也存在于分页会话池中。
Tips:非分页池的虚拟地址被物理地址分配,而分页池对应的虚拟地址和物理地址不存在一一映射,只保证在当前执行会话有效,其余内核操作时,并不要求这些对象必须在内存中。 User Object地址获取在user32.dll中有一个全局变量gSharedInfo,其成员aheList为一个USER_HANDLE_ENTRY的句柄表。
该句柄表的具体结构如下所示 ,其第一个成员为pKernel作用GdiSharedHandleTable 中的 pKernel 一致 ,均指向object在内核中的位置(KernelAddress)。
因此可通过USER_HANDLE_ENTRY去泄露Accelerator table的内核对象地址。 KernelAddress= SHAREDINFO->USER_HANDLE_ENTRY->pKernel + handle&0xffff Accelerator table 对象创建函数 CreateAcceleratorTable 用来在内核中创建快捷键对应表 。
该函数存在 LPACCEL lpaccl 和 int cAccel 两个参数。参数 lpaccl 作为指向 ACCEL 结构体类型数组的指针,cAccel 表示数组的元素个数 。结构体 ACCEL (szie=6)的定义如下:
通过1)、2)我们肯定可以想到通过内存分水技术去泄露bitmap内核对象的地址,经笔者分析得出可以控制CreateAcceleratorTable 的参数 ,来控制其到内核所申请内存的大小。
现在,bitmap和AcceleratorTable内核对象的大小均可控,可以进行内存布局 。过程如下:
至此,我们就可以成功泄露bitmap内核对象的地址啦!
后续提权过程与win7一致。 4.3 RS2 下的利用模式缓解措施在RS2中pKernel指向异常地址, AcceleratorTable + 池风水泄露pvScan0的方式失效 利用思路新的替代方案—桌面堆,更准确一点就在桌面堆上的lpszMenuName 。lpszMenuName就是我们创建窗口时wndclass的成员 ,同bitmap一样它也存于换页内存池 。 另外 ,我们还需知道桌面堆是有两份分别存于用户态和内核态。
上图 ,在网上流传的比较火,应该是国外某位大神逆出来,再此稍稍瞻仰一下。有了这个我们就可以通过桌面堆来泄露bitmap的内核地址啦 。 获取lpszMenuName的地址.首先,我们要得到内核态桌面堆到用户态桌面堆的偏移值ulClientDelta 。
通过HMValidateHandle泄露Wnd的内核地址 ,然后通过用户态的tagCLS获取lpszMenuName(0x98)的地址 。
泄露结果如下所示:
到这,相信大家都知道可以通过桌面堆对象进行占坑 ,并控制lpszMenuName的大小与bitmap大小一致 。即可成功泄露bitmap的内核地址 。lpszMenuName申请内存过程堆栈如下,其大小为实际申请大小 。
另外,BitMap的SurFace结构 在RS2上比RS1增大了10。 内存布局如下,申请等大小的lpszMenuName和bitmap。利用UAF技术确保泄露lpszMenuName的地址稳定之后使用bitmap占坑 。
下图来自fuzzySecurity的blog,精确的展现出通过lpszMenuName进行UAF后泄露bitmap的过程。
4.4 RS3 下的利用模式缓解措施微软引入了 TypeIsolation 功能将Bitmap header与Bitmap data分离 ,无法通过Bitmap header取得pvscan0指针的内核地址 。 利用思路在第三章节介绍过另一个用于WWW漏洞利用的内核对象—Palette ,它和bitmap有着异曲同工之妙 ,palette对象的0x90处是一个4字节的数组apalColors ,相当于BitMap里的pixel data,pFirstColor是一个指针,指向apalColors,相当于BitMap里的pvScan0。
因此 ,我们完全可以按照在RS2中的利用思路 ,完成在RS3中的提权。 创建两个palette对象分别为hManager 、hWorker,并通过桌面堆泄露内核地址。
泄露的方法还是采用池分水技术,利用lpszMenuName为palette占坑。 Palette大小的计算方式 :LOGPALETTE->palNumEntries进行控制的,具体如下: 利用任意地址覆盖写漏洞,将hManager的pFirstColor修改为指向hWorker成员pFirstColor的地址。
4.5 攻防现状缓解措施RS4: HMValidateHandle泄露内核方法失效 Palette同样被TypeIsolation进行header和body分离 RS5: 微软修改大量API,桌面堆泄露的方法被缓解 。 2.新的思路到这,已有的所有泄露GDI对象的方法均以被缓解 。但车到山前必有路 ,这也正体现了攻防对抗的魅力 ,直到2021年CVE-2021-1732的漏洞被爆出,不同于传统的内核漏洞,需借助“溢出”来完成漏洞利用 ,该漏洞是由于窗口类型混淆而导致的 ,通过spMenu的信息泄漏能力和借助GetMenuBarInfo/SetWindowLong函数构造读写原语(RW Primitive),最后通过纯数据攻击(DataOnlyAttack ,特点不需要执行原语,只操纵操作系统使用的数据结构以实现提权)实现内核提权 。
五、附录5.1参考链接https://www.anquanke.com/post/id/235716#h3-4 https://mp.weixi2n.qq.com/s/6mT0O9eur5-VEs0rbV0-mg https://github.com/gdabah/win32k-bugs/ http://fuzzysecurity.com/tutorials/expDev/21.html https://www.wangan.com/p/7fygf309c52e2678 5.2环境介绍靶场HEVD的一个Windows Kernel Exploition训练项目--HackSysExtremeVulnerableDriver https://github.com/hacksysteam/HackSysExtremeVulnerableDriver
|