0x00引号
本文讨论了导致Windows内核池(Kernel Pool)损坏的整数溢出问题,并根据Bitmap和Palette这两个GDI对象研究了内核漏洞的利益过程。当然,中提出的观点只是代表作者如何理解和解决这些问题。
注:仅翻译了所述词典知识点部分。
0x01 WinDbg上与内核池相关的命令
!Poolused:使用此命令可以查看特定id或类型的内核池状态。
!Poolfind:此命令查找具有特定id的内核池分配对象。
!Pool:此命令查看特定地址所在的内核池信息。
0x02内核池
内核池的类型
内核池除了是在内核状态下创建外,可能与主状态下的堆内存相似。这里有很多类型[1],最常见的类型如下:
表堆:主要用于表对象(如窗口、类、菜单等)的分配函数为RtlAllocateHeap()和DesktopAlloc();释放函数为RtlFreeHeap()。
非分页会话池:分配给相应虚拟地址和物理地址映射到的类池的对象。这些对象是系统对象,如信号量、事件对象等。
分池(Paged)
会话池。
):这种类型是这种类型的主要关注点。分配给此类型池的对象的相应虚拟地址和物理地址没有映射关系。只需确保对象在当前执行会话中有效。剩下的内核操作不需要这些对象在内存中。例如:
GDI对象和一些用户对象等
分区池和分区池的分配函数如下
ExAllocatePoolWithTag()。其中,将第一个参数分为类型,如果为0x21,则分配给池
如果为0x29,则分配给分区池。人的释放函数为ExFreePoolWithTag()和ExFreePool()。内核池分配
通过查看Win32AllocPool()函数,您可以知道内核如何分配池对象(类型参数0x21)。
关于内核池,需要知道的另一点是,内存空间以0x1000字节分隔,对于每个池,最初分配的chunk块位于开头,下一个chunk块有时在底部分配。
NgDylbzB%2BBhdJANKu1Xf%2BzcqC8%3D&index=5" width="174" height="157"/>
此外,在 64 位系统中,内核 Pool Header 结构的⼤⼩为 0x10 字节,相应的 32 位系统中的⼤⼩则为 0x8 字节。
Pool Feng shui(池喷射)
Pool Feng shui 背后依据的原理是通过适当操作可将池内存置于⼀种可预测的状态。即通过⼀系列的分配和释放操作来构造与漏洞对象⼤⼩相同的内存空洞(holes),以便将存在漏洞的对象分配到我们可控对象的相邻处,从⽽完成利⽤对象的内存布局。
如果该漏洞对象在执⾏过程中没有被释放,那么需要构造的空洞可位于池⻚⾯的任何地⽅,但如果该对象最终被释放掉了,那么就需要确保漏洞对象被置于池⻚⾯的最后,即下⼀ chunk 头将不再有效,这样对象被释放后不会由于 BAD POOL HEADER ⽽触发蓝屏。
强制对象分配到池⻚⾯的尾部
我们假设漏洞对象的⼤⼩为 0x40 字节(包含 Pool Header),则池⻚⾯初始 chunk 块的⼤⼩需要为 0x1000 – 0x40 =0xFC0 字节(包含 Pool Header)。
之后再分配池⻚⾯中余下的 0x40 字节。
如果溢出利⽤中需要借助其它对象,则在漏洞对象的特定偏移处进⾏相应的分配操作。
0x03 池内存的破坏
引起池内存破坏的原因有很多,如释放后重⽤(UAF)、池的线性溢出以及池的越界写(OOBW)等。
⽆符号整型溢出
⽆符号整型溢出是由于在计算过程中没有进⾏相应的检查使得计算结果超出了整型数所能表示的最⼤范围 MAX_UINT(0xFFFFFFFF, 32 位),从⽽导致最终结果远⼩于预期,⽽按其后溢出值的不同使⽤情形⼜会造成不同的错误影响。
为了更好的理解⽆符号整型数的溢出,我们来看个例⼦:假设⽬标系统为 x86 架构,因此 UINT 占 4 个字节(32 位),考虑如下的加法运算:
0xFFFFFF80 + 0x81 = 00000001 ??
对 x86 系统来说上述计算结果为 0x1,然⽽真实的计算结果应该是 0x100000001,但这超出了 x86 系统上 4 字节 UNIT数所能表示的范围,因此截断后得到结果 0x1。
⽽在 64 位系统中,虽然此概念仍然存在,但由于要求的数值过⼤,因此很难找到纯粹的 64 位整型数溢出的情况。不过,许多存在漏洞的函数在实际使⽤前会先将数值保存到 32 位寄存器中,所以⼜出现了前述解释的整数截断情形。
我们考虑如下的程序执⾏过程:
1. ⼊参变量为整型数,针对此整型数会进⾏⼀些运算操作;
2. 运算结果会导致整型数溢出;
3. 之后按此溢出值(较预期结果偏⼩)的⼤⼩进⾏新缓冲区分配操作;
4. 再按最初的⼊参整型数(未经过运算操作)进⾏相关操作:
a. 将原先内容拷⻉到新申请的缓冲区(这会导致线性溢出);
b. 向本应落在新分配缓冲区内的偏移进⾏写⼊操作(这会导致越界写, OOB Write)。
接着就来具体看⼀下。
线性溢出在对象数据拷⻉过程中,如果没有对边界进⾏检查,那么就有可能发⽣线性溢出。其原因有多种,例如传给内存分配函数的⼤⼩是⼀个溢出值,这会导致新分配的空间偏⼩,⽽拷⻉函数却按原先的⼤⼩将数据拷⻉到新分配的内存空间,⼜或者对象本身是以固定⼤⼩分配的,⽽拷⻉时使⽤的⼤⼩却是未经校验的⽤户输⼊值。
越界写(OOB Write)
对于越界写的情况,⾸先需有⼀对象,其⼤⼩⼤于某⼀特定值。⽽当该⼤⼩变量传给分配函数后发⽣了整型溢出,使结果较期望值偏⼩。随后,程序尝试在新分配对象中按预期索引值进⾏读写操作,但由于分配的⼤⼩值发⽣了溢出,导致该对象⼤⼩⼩于预期,从⽽造成了越界读写。
通常对 Exp 开发⽽⾔,某些经第⼀阶段内存破坏后的对象可被利⽤在获取第⼆阶段内存破坏的 primitives 中。这些对象⼀般拥有实现这些利⽤操作的特定成员,例如某些对象成员可以控制对象或对象中数据块的⼤⼩,因⽽能够实现相对的内存。
0x04 利⽤ GDI 对象获取 ring0 层 ARW Primitives
读写操作,在某些情形中这⾜以实现
bug 的利⽤。更进⼀步,如果对象同时还拥有另⼀成员,即指向对象数据块的指针,那么就能将内存破坏的 primitives 转换成内存 ARW
primitives,这会让利⽤程序的开发变得更加容易。要实现此利⽤技术通常需要借助两个对象,其中⼀个对象(manager)将⽤于修改第⼆个(通常是相邻)对象(worker)的数据指针,使其获得
ARW primitives(Game Over)。
在 Windows 内核中, GDI 对象恰好能够满⾜这些要求,如
Bitmap 对象利⽤技术,该技术⾸先是由 k33n 团队提出的[3],后续被 Nicolas Economou 和 Diego Juarez
做了详细补充[4]。⽽我则⾜够幸运的发现了另⼀个能被利⽤的 GDI 对象,即Palette 对象, Vulcan
团队同样也提及了此利⽤技术[10]。 Palette 对象利⽤技术和 Bitmap 对象利⽤技术⼀样强⼤,也能够⽤于获取内核的任意内存读写能⼒。
相对内存读写
相对内存 RW primitives 允许我们对特定地址区域进⾏读写操作。通过破坏 GDI 对象的内存可增加其⼤⼩,这通常是触发bug 后⽤于获取任意内存 RW primitives 所需迈出的第⼀步。
任意内存读写
对⽤于实现任意内存读写的对象,我们通常要求其拥有⼀个能够指向对象数据的指针成员。如果该指针被修改了,那么当调⽤相应对象数据读写函数时就会转⽽读写修改后指针所指向的地址,从⽽获取强⼤的任意内存 RW primitives。
为了便于理解,我们来考虑这样的
manager/worker 对象组合。对于 A 对象(manager),我们扩增了其⼤⼩,因⽽能够实现相对的越界读写,即实现对 B
对象(worker)数据指针的读写,⽽后将该指针替换为我们需要进⾏读写的地址,这就使得 B 对象的数据读写操作能够被我们所控制。
0x05 SURFOBJ – Bitmap 对象
Bitmap 对象在内核中对应的 Pool 标识为 Gh?5 或 Gla5 ,其结构体 _SURFOBJ 的定义在 msdn[5]、 ReactOS 项⽬(32 位) [6] 以及 Diego Juarez 的博⽂(64 位) [7] 中有说明。
SURFOBJ 结构体
SURFOBJ
结构中最值得我们关注的成员当属 sizlBitmap,它是⼀个 SIZEL 结构体,系统由该变量来确定 bitmap 位图的⻓宽。⽽
pvScan0 和 pvBits 成员变量均表示指向 bitmap 位图的指针,按 bitmap 类型的不同,系统会选⽤⼆者之⼀。此外,在内存中
bitmap 位图通常位于 SURFOBJ 结构之后。
分配
CreateBitmap 函数⽤于分配 Bitmap 对象,其定义如下:
分配 2000 个 bitmap 对象:
for (int y = 0; y < 2000; y++) {
HBITMAP bmp[y] = CreateBitmap(0x3A3, 1, 1, 32, NULL);
}
释放
DeleteObject 函数则⽤于 Bitmap 对象的释放:
DeleteObject(hBITMAP);
读内存
GetBitmapBits
函数可⽤于读取由 pvScan0 或 pvBits(取决于 bitmap 类型) 指针指向的 cBytes 字节的 bitmap
位图内容,其中 cBytes 的取值需⼩于 * * BitsPerPixel
乘积。
写内存
相对的,
SetBitmapBits 函数则⽤于向 pvScan0 或 pvBits(取决于 bitmap 类型)指针指向的 bitmap
位图写⼊cBytes 字节的内容,同样 cBytes 的取值也需⼩于 * *
BitsPerPixel 的乘积。
相对内存读写 – sizlBitmap
sizlBitmap 成员变量为 SIZEL 类型的结构体,其中包含了 bitmap 位图的⻓宽, SIZEL 结构体和 SIZE 结构体是等价的,定义如下:
后续的所有
Bitmap 对象操作,例如 bitmap 位图读写,都依赖此变量来计算 bitmap 位图的⼤⼩以执⾏对应操作,其中Size = Width
* Height * BitsPerPixel。通过破坏对象的 sizlBitmap 变量可实现相对内存读写。
任意内存读写 – pvScan0/pvBits
pvScan0
指针⽤于指向 bitmap 位图的第⼀⾏,但如果 bitmap 的格式为 BMF_JPEG 或 BMF_PNG ,那么此成员变量会被置为
NULL,转⽽由 pvBits 指针来指向 bitmap 位图数据。这两个指针在读写 bitmap
位图数据时会⽤到,通过对其进⾏控制可以实现任意内存读写。
利⽤思路
Diego Juarez 和 Nicolas
Economou 在之前的演讲中对借助Manager/Worker ⽅式的 Bitmap 对象利⽤技术做了详尽分析,其思路是通过控制
Manager Bitmap 对象的 sizelBitmap 或 pvScan0 成员,从⽽达到控制 Worker Bitmap 对象
pvScan0成员的⽬的,最终实现内核任意内存读写(ARW primitives)。
我们这⾥给出的思路是通过控制 Manager Bitmap 对象的 sizlBitmap 成员,以扩增 bitmap 的⼤⼩来获取相对内存读写的能⼒,接着再控制相邻 Worker Bitmap 对象的 pvScan0 指针达到任意内存读写。
0x06 XEPALOBJ – Palette 对象
下⾯我们将介绍基于
Palette 对象的新利⽤技术。此对象在内核中的Pool 标识为 Gh?8 或 Gla8 ,调试中相应的符号名为 _PALETTE 、
XEPALOBJ 或 PALOBJ 。 msdn 上并没有关于该对象的公开内核结构信息,但我们可以在 ReactOS项⽬中[8]找到其 x86
版的定义,⽽在 Deigo Juarez 的 WinDbg 插件项⽬ GDIObjDump 中[9]则可同时找到 x86 版和 x64版的定义。
PALETTE 结构体
对于
XEPALOBJ 结构体,我们⽐较感兴趣的成员变量是 cEntries,它表示 PALETTEENTRY 数组中的元素个数,此外还有
pFirstColor 成员变量,它是指向PALETTEENTRY 数组 apalColors 的指针,可以看到, apalColors
表示的数组位于此 0x06 XEPALOBJ – Palette 对象结构体的尾部。
分配
CreatePalette 函数⽤于分配 Palette 对象,唯⼀的⼊参 lplgpl 为 LOGPALETTE 结构体指针类型,对 x86 系统其分配⼤⼩需不⼩于 0x98 字节,相应的对 x64 系统其分配⼤⼩需不⼩于 0xD8 字节。
不论 x86 系统还是 x64 系统, PALETTEENTRY 结构都是占 4 个字节:
分配 2000 个 Palette 对象:
LOGPALETTE *lPalette;
lPalette = (LOGPALETTE*)malloc(sizeof(LOGPALETTE) + (0x1E3 – 1) * sizeof(PALETTEENTRY));
lPalette->palNumEntries = 0x1E3;
lPalette->palVersion = 0x0300;
for (int k = 0; k < 2000; k++) {
hps[k] = CreatePalette(lPalette);
}
释放
而 Palette 对象的释放则由 DeleteObject 函数来完成:
DeleteObject(hPALETTE);
读内存
GetPaletteEntries
函数被⽤来读取 Palette 对象中的内容,即对应 hpal 句柄表示的 XEPALOBJ 结构中
pFirstColor指针指向的apalColors 数组⾃偏移 iStartIndex 开始的 nEntries 个元素,并将其保存到缓冲区
lppe 上。函数定义如下:
写内存
相对的,
SetPaletteEntries 和 AnimatePalette 这两个函数可⽤来向 Palette 对象写⼊内容,即将缓冲区
lppe上的 nEntries 个元素写⼊ hpal 句柄表示的 XEPALOBJ 结构中 pFirstColor 指针指向的
apalColors 数组⾃偏移 iStart 或iStartIndex 开始的位置。
相对内存读写 – cEntries
XEPALOBJ 结构体的 cEntries 成员⽤于表示 Palette 对象中apalColors 数组元素的个数,若将其覆盖为⼀个⼤的数值,那么借助破坏后的 Palette 对象可以实现内存的越界读写。
任意内存读写 – pFirstColor
pFirstColor 指针指向的是 Palette 对象中 apalColors 数组的起始位置,通过控制该指针,可以实现内核态下内存的任意读写。
利⽤思路
针对
Palette 对象的利⽤思路和之前讨论的 Bitmap 对象利⽤思路是类似的,通过控制 Manager Palette 对象的
cEntries 或 pFirstColor 成员来达到控制相邻 Worker Palette 对象 pFirstColor
成员的⽬的,从⽽获取内核下的 ARW primitives。
0x07 基于 Palette 对象利⽤技术的⼏点限制
⾸先,在
x86 系统中要求 cEntires 成员溢出后得到的结果必须⼤于 0x26,相应的在 x64 系统中必须⼤于 0x36,这是因为在
XEPALOBJ 对象分配时, x86 系统要求其⼤⼩不⼩于 0x98 字节,⽽ x64 系统要求其⼤⼩不能⼩于 0xd8
字节。例如cEntires 成员经溢出后由 0x1 变为 0x6,但这显然不满⾜条件,该⼤⼩ 0x6 * 0x4 = 0x18
字节⼩于所要求分配 Palette 对象时的最⼩值。
其次,如果利⽤程序通过 SetPaletteEntries 函数向内存写⼊数据,那么需保证 XEPALOBJ 结构中的 hdcHead、ptransOld 以及ptransCurrent 成员不会被覆盖掉。
ring3
层的 SetPaletteEntries 调⽤会经由 NTSetPaletteEntries ⾄ GreSetPaletteEntries
函数,此时会对 hdcHead 成员进⾏检查,如果该值不为零,则程序执⾏流程会报错或直接蓝屏死机,即下图⻩⾊区域所示。
不过在此之前 GreSetPaletteEntries 还会先调⽤ XEPALOBJ::ulSetEntries 函数对 pTransCurrent 和 pTransOld 成员进⾏检查,如果它们⾮零,程序会进⼊下图所示的橘⾊区域,这有可能会导致蓝屏死机。
最后我们看下使⽤
AnimatePalettes 函数向 Palette 对象进⾏写操作的情况,唯⼀限制是要求 pFirstColor
指针所指内容的最⾼字节为奇数,对应的 XEPALOBJ::ulAnimatePalette
函数代码段如下。虽然不会导致蓝屏死机,但这使得我们⽆法完成写⼊操作。
0x08 Token 的替换
内核借助 _EPROCESS 结构来表示系统上运⾏的每⼀个进程,该结构包含很多重要成员,例如 ImageName、SecurityToken、ActiveProcessLinks 以及 UniqueProcessId,这些成员的偏移值因系统版本⽽异。
Windows 8.1 x64:
Windows 7 SP1 x86:
另外,内核中 SYSTEM 进程所对应的 EPROCESS 结构地址可通过如下⽅式计算得到:
KernelEPROCESSAddress = kernelNTBase + (PSInitialSystemProcess() – UserNTImageBase)
SecurityToken
SecurityToken 表示当前进程所持有的安全级别标识,当进程请求获取特定权限时,系统会借此判断其是否拥有所请求资源的权限。
ActiveProcessLinks
ActiveProcessLinks 是⼀个 LIST_ENTRY 对象,可借此遍历各进程对应的 EPROCESS 结构。
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY;
UniqueProcessId
UniqueProcessId 表示进程 PID。
步骤
1. 获取内核中 SYSTEM 进程对应的 EPROCESS 结构地址;
2. 借助 Read primitive 得到相应的 SecurityToken 和 ActiveProcessLinks;
3. 遍历 ActiveProcessLinks 得到当前进程的 EPROCESS 结构地址,即 ActiveProcessLinks->Flink.UniqueProcessId 和 GetCurrentProcessId() 的值相同;
4. 借助 Write primitive 将当前进程的 SecurityToken 替换为 SYSTEM 进程的SecurityToken。
*参考部分详⻅原⽂
原文链接:[翻译]基于 GDI 对象的 Windows 内核漏洞利用
本文由看雪论坛 BDomne 编译,来源media@Saif El-Sherei 转载请注明来自看雪论坛
1.文章《【奥迪a3蓝屏怎么退出】基于GDI对象的Windows内核脆弱性收益》援引自互联网,为网友投稿收集整理,仅供学习和研究使用,内容仅代表作者本人观点,与本网站无关,侵删请点击页脚联系方式。
2.文章《【奥迪a3蓝屏怎么退出】基于GDI对象的Windows内核脆弱性收益》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。
相关推荐
- . 现代买票为什么带上携程保险
- . 潮阳怎么去广州南站
- . 湖南马拉河怎么样
- . 烧纸为什么到三岔路口
- . 百色为什么这么热
- . 神州租车怎么样
- . 芜湖方特哪个适合儿童
- . 护肤品保养液是什么类目
- . 早晚的护肤保养有哪些项目
- . 女孩护肤品怎么保养的最好