基于Windows 32k内核提权漏洞的攻防对抗
一、基于产生原因
1.1 Callback机制
Win32k组件最初的内核设计和编写是完全建立的用户层上的 ,但是提权微软在 Windows NT 4.0 的改变中将 Win32k.sys 作为改变的一部分而引入 ,用以提升图形绘制性能并减少 Windows 应用程序的漏洞内存需求 。窗口管理器(User)和图形设备接口(GDI)在极大程度上被移出客户端/服务端运行时子系统(CSRSS)并被落实在它自身的防对一个内核模块中。
wKg0C2Q1UZKAI9jFAAA8HhChBCU732.png
这样的基于设计无疑是为内核增添了一部分压力 ,服务器租用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漏洞的产生。
wKg0C2Q1UdGAG1l0AAB8AXXrrIM630.png
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的扩展数据的保存位置以及寻址方式 ,从而触发一个任意地址覆盖写漏洞。
wKg0C2Q1UrqAce6UAACLexflaGQ178.png
针对这种触发方式,我们可以直接对用户层的usertable进行检测,usertable存在于PEB+0x58偏移处:
案例2 :CVE-2021-40449 是Win32k 的 NtGdiResetDC 函数中的一个释放后使用漏洞,源码库在执行其自己的回调期间针对同一句柄第二次执行函数 ResetDC 时触发UAF漏洞 。
wKg0C2Q1UuOAZdTrAACPaTgCZog116.png
针对这种触发方式,可以通过开启Driver Virifier进行检测,UAF的漏洞再此环境下会触发蓝屏 。
2.2 漏洞利用
漏洞成功被触发仅仅是一个好的开端 ,更重要的是漏洞利用点的寻找过程。笔者将此过程大致划分为信息泄露、堆喷射 、原语构造、代码执行这四个阶段 。
1.信息泄露阶段在win32k漏洞利用过程中,内核对象的泄漏是至关重要的。可以说成功利用一个漏洞的大前提就是获取win32k内核对象的地址。
wKg0C2Q1U3aAYy0iAAEH1E9EANQ917.png
上图是对Windows内核地址泄漏的总结(来自GitHub) ,表格中包含截止目前泄漏内核对象的各种技巧 。下面也是拿两个demo来阐述内核对象泄露的细节:
Demo1 ,通过GdiSharedHandleTable去泄露bitmap对象 :在R3通过CreateBitmap创建位图成功后会得到一个hBitmap,如果要在R3去泄露该对象在内核中的地址可以通过GdiSharedHandleTable获取。GdiSharedHandleTable位于PEB+0x94的位置。
wKg0C2Q1U5CAaWhkAAAepg2jbsk966.png
GdiSharedHandleTable的本质是一个指向GDICELL结构体数组的指针 。
wKg0C2Q1U6KAT7J1AAA0SxX9GE617.png
同 CreateFile 类似,其实Windows 都用句柄(Handle)来标识用户态对内核对象的引用 。这个句柄低 16 位其实是数组索引。
wKg0C2Q1VAWAfZUXAAANNlwfltw725.png
通过上文 ,就可以计算出bitmap在内核中的地址。
wKg0C2Q1VBGAQck3AAAUsLBZxA641.png
HMValidateHandle是微软未公开的一个函数 ,凭借此函数可以通过传入句柄获取对于内核对象的地址(win32k对象) 。HMAllocObject创建了桌面堆类型句柄后 ,会把tagWND对象放入到内核模式到用户模式内存映射地址里。 为了验证句柄的有效性 ,窗口管理器会调用User32!HMValidateHandle函数读取这个表 。函数将句柄和句柄类型作为参数 ,并在句柄表中查找对应的项 。如果查找到对象, 会返回tagWND只读映射的对象指针,通过tagWND这个对象我们可以获取到句柄等一系列窗口信息 。
该函数地址是通过R3的user32.dll!IsMenu函数获取到的。
wKg0C2Q1VDaAOEifAABK7i13EFg646.png
具体获取方式如下:
wKg0C2Q1VEOAQ8crAADZGjJrmGw662.png
堆(池)喷射是进行内存布局的常用手段, 通过在分配关键的内核对象之前,首先分配和释放特定长度和数量的其他对象,使内核内存首先处于一个确定的状态,来确保在分配关键的内核对象时,能够被系统内存管模块分配到我们所希望其分配到的某些位置,例如接近某些可控对象的位置 。攻击者可利用此种方法构造完美的内存布局从而达到自己的目的 。此技术没有固定的方法(“因地制宜”) ,但是所要达成的目的比较一致--执行shellcode以及信息泄露。
案例1:
Bitmap对象的地址在RS1中是通过AcceleratorTable获取到的 。先申请大量的AcceleratorTable对象然后释放其中一个 ,接着申请大小相等的bitmap对象。通过泄漏AcceleratorTable对象的地址,即可等到bitmap的内核地址。(内存大小计算方式将在下一章节进行详细阐述)
wKg0C2Q1VFiAatRuAABZb8ZW9wQ135.png
代码框架如下
wKg0C2Q1VGGAC6cqAACw29G8S0A729.png
在win32k内核漏洞利用中 ,RW原语同样扮演着重要角色。它可以对所分配的关键内核对象后面的内存区域进行操作,以控制原本不能控制的相邻对象的成员数据 ,这些数据将作为后续利用操作的重要节点。
下面将对win32k漏洞常用到的读写原语进行介绍 :
Bitmap系列 :SetBitmapBits/GetBitmapBits可对内核对象bitmap的pvScan0指向的像素数据内存进行修改 。
wKg0C2Q1VJ2AUPU9AAE36tbfPzk258.png
此系列一直到RS3微软把Bitmap header与Bitmap data分离后,彻底失效 。
Palette系列:GetPaletteEntries/SetPaletteEntries可对内核对象Palette的成员pFirstColor(指向4个bytes的数组PALETTENTRY)修改构造RW原语。
wKg0C2Q1VNWAAeetAAB4GMR5X8A958.png
wKg0C2Q1VNqAACqeAACfThupnU170.png
应用场景1:
Wnd->StrName 字段是指向窗⼝标题名的指针 ,通过修改此变量,再借助⽤户态下 的 InternalGetWindowText 和 NtUserDefSetText 函数则可实现任意内核地址读写
wKg0C2Q1VPWAP7o8AABRuCzwQr0810.png
应用场景2:
a)申请两个连续的Wnd对象(Spray) ,Wnd0与Wnd1
b)通过漏洞能力将Wnd0的pExtraBytes字段变为可越界读写的 。
c)通过Wnd0.的越界写入能力 ,修改tagWND1.pExtraBytes到指定地址
d)借助SetWindowLongPtr的修改能力,最后使Wnd1获得任意地址写入能力。
wKg0C2Q1VSeAbZPuAAAwYN3HNgQ077.png
wKg0C2Q1VTWATycRAACFCy5ktlM232.png
一般仅需tagMenuBarInfo.rcBar.left 和 tagMenuBarInfo.rcBar.top读取指定地址的8个字节的数据。
wKg0C2Q1VUCAPhNuAACdn1vLEPQ467.png
另外,通过GetMenuBarInfo构造Read Primitive时,需要提前构造Fake Menu(用户层),再通过SetWindowLongPtr对Target Wnd的Menu(内核层)进行替换,以达到读取地址可控的目的。
wKg0C2Q1VUuAd8KAACqSW0mBns734.png
对于win32k内核漏洞,其最终利用方式就是本地提权,而提升权限的主要手法就是进行Token替换,共以下两种方式:
Token指针替换(_EX_FAST_REF替换)
wKg0C2Q1VWKASYr9AAA52Kc5NjM556.png
将当前进程 EPROCESS 中存储的 Token 指针替换为 System 进程的 Token 指针。
wKg0C2Q1VW2AA6MzAACv5Wa8u5Q094.png
wKg0C2Q1VXKALdlUAAEJvY13O8519.png
将当前进程 EPROCESS 的成员 Token 指针指向的 Token 块中的数据替换成 System 进程拥有的 Token 块的数据
wKg0C2Q1VYSAWQaSAABAUD69sOQ973.png
将Present和Enabled的值更改为SYSTEM进程令牌的所有权限 。
wKg0C2Q1VY2AK7OpAAA1U2FkTU378.png
三、内核对象
3.1 Bitmap
简介GDI(Windows Graphics Device Interface),是windows为应用程序提供图形 、文字显示的 API 接口。
Bitmap是GDI中的图形对象 ,用于创建 、操作(缩放、滚动 、旋转和绘制)并将图像作为文件存储在磁盘上 ,其实际上为一个二元数组,去存储像素 、颜色、大小等信息。
Bitmap关键结构体及对象SURFACE对象(Bitmap在内核中的结构)
wKg0C2Q1VcKAYJpSAAAsz7rKY4c312.png
BaseObject,内核 GDI 对象类的基类都是一个称作 _BASEOBJECT 的结构 。对内核对象进行标记,用于描述最基础的对象信息
wKg0C2Q1VdCASXVAAAlRzvaNM671.png
SURFOBJ(Bitmap核心结构),用于控制位图的大小,像素等信息 。
wKg0C2Q1VdABlnSAABTSUqvvrg712.png
wKg0C2Q1VgCAbkXoAAApoINYtcc373.png
创建具有指定宽度,高度和颜色格式(颜色平面和位每像素)的位图 ,而前文中也提过bitmap在内核中关联的对象SURACE,由SURACE通过CreateBitamp的前4个参数去精确控制分配的内核内存块的大小。其调用链如下 :
wKg0C2Q1VhSAUxAQAAB75owvyTw709.png
在GreCreateBitmap函数中,首先根据传入的 cPlanes 和 cBitsPerPel参数确定位图的像素位类型 ,然后创建一个DEVBITMAPINFO对象 ,通过CreateBitmap前四个参数去内存块中申请一片内存并且设置位图数据扫描线的长度。接着lpBits如果不为0 ,则通过GreSetBitmapBits去设置像素数据。
wKg0C2Q1Vh6AEDA3AABVoGSJSBo274.png
DEVBITMAPINFO的结构:
wKg0C2Q1VieADLBBAABGYsmlzbI562.png
在 Windows 内核中处理位图像素数据时 ,通常是以一行作为单位进行的 ,像素的一行被称为扫描线 ,而扫描线的长度就表示的是在位图数据中向下移动一行所需的字节数。
位图数据扫描线的长度被存储在SURFACE+0x34 字节偏移的成员(即 SURFACE->so.lDelta 成员)中。这样一来,成员 pvScan0 将指向当前位图 SURFACE 对象的像素点数据缓冲区的起始位置 。在后续对位图像素点进行读写访问时,系统位图解析模块将以该对象的 pvScan0 成员存储的地址作为像素点数据区域起始地址。
wKg0C2Q1VkiADXVeAABsg3IFe0151.png
3.2 Palette
简介调色板是一个数组,其中包含标识当前可以在输出设备上显示或绘制的颜色的颜色值。调色板由能够生成多种颜色但在任何给定时间只能显示或绘制这些颜色的子集的设备使用 。
Palette关键结构体及对象PALETTE对象
wKg0C2Q1VuCAO8nAAE5zXPcDuQ249.png
PALETTE结构中 ,有三个成员是值得我们关注的分别是cEntries 、pFirstColor 、apalColors。cEntries指定当前调色板的项数,pFirstColor指向调色板列表起始表项(apalColors)的地址,apalColors是一个结构体数组存储调色板列表数据 。
PALETTEENTRY (调色板列表)
结构体 PALETTEENTRY 大小为 4 字节,其各个成员用于定义调色板表项对应的 24 位 RGB 颜色值等信息
wKg0C2Q1Vu6AZ8bAAApmXRjvG0967.png
wKg0C2Q1Vx2AOGL7AAAZ8CsH2YM553.png
Createpalette函数用来创建调色板对象,其只有一个参数lplgpl是指向LOFGPALETTE类型结构体对象的指针。定义如下 :
wKg0C2Q1V4SACU02AABEui0D474872.png
这里我们仅需要关注第二个和第三个参数,palPalEntry是可变长度的 PALETTEENTRY 结构体类型数组 ,而palNumEntries来决定PALETTEENTRY 的个数。也就是说palette对象的大小是由palNumEntries控制的。
wKg0C2Q1V4ABNCAAAD39Rl4gy4172.png
显然,我们可以得出palette对象大小的计算方式 :4 * cEntries + 0x90/PALETTE/
3.3Wnd
简介Windows是对象,它们同时具有代码和数据,但它们不是C++ 类。 相反 ,程序通过使用名为句柄 的值来引用窗口。句柄是不透明类型 。实质上它只是操作系统用来标识对象的数字。可以想象Windows创建的所有窗口都有一个大表 。它使用此表按其句柄查找窗口。 (它内部的工作方式是否完全相同都很重要 。) 窗口句柄的数据类型是 HWND, 这通常发音为"aitch-wind" 。 窗口句柄由创建窗口的函数返回