0x00 前言
内核池常见类型:
桌面堆:主要用于窗口、类、菜单等桌面对象,函数为RtlAllocateHeap和DesktopAlloc,释放函数为RtlFreeHeap()
非分页池:对应的虚拟地址和物理地址存在映射关系,eg:信号量、事件对象。
分页池:对应的虚拟地址和物理地址不存在一一映射,只保证在当前执行会话有效,其余内核操作时,并不要求这些对象必须在内存中,eg:GDI对象和一些用户对象。
后两者函数均为ExAllocatePoolWithTag和ExFreePool。
另外,内核池的内存空间以0x1000字节大小划分成页,对于每个池页面来说,初次分配的chunk块位于页面的起始处,而接下来的chunk块大部分将从页底开始分配。
Pool Feng shui(池喷射):通过适当操作可将池内存置于一种可预测的状态。通过一系列的分配操作和释放操作来构造与漏洞大小相同的内存空洞,以便将存在漏洞的对象分配到我们可控对象的相邻处,从而完成利用对象的内存布局。
如果该漏洞对象在执⾏过程中没有被释放,那么需要构造的空洞可位于池页面的任何地方,但如果该对象最终被释放掉了,那么就需要确保漏洞对象被置于池页面的最后,即下一chunk 头将不再有效,这样对象被释放后不会由于 BAD POOL HEADER 而触发蓝屏。
利用GDI对象获取Ring0 ARW Primitives(原语)
1 | tips:内核或微核提供核外调用的过程或函数称为原语(primitive)。 |
对 Exp 开发而言,某些经第一阶段内存破坏后的对象可被利用在获取第二阶段内存破坏的 primitives 中。这些对象一般拥有实现这些利用操作的特定成员,例如某些对象成员可以控制对象或对象中数据块的大小,因而能够实现相对的内存读写操作,在某些情形中这足以实现 bug 的利用。更进一步,如果对象同时还拥有另一成员,即指向对象数据块的指针,那么就能将内存破坏的 primitives 转换成内存 ARW primitives,这会让利用程序的开发变得更加容易。要实现此利用技术通常需要借助两个对象,其中一个对象(manager)将用于修改第二个(通常是相邻)对象(worker)的数据指针,使其获得 ARW primitives(Game Over)。
在Windows内核中,GDI对象刚好满足这些要求。
①Bitmap对象
②Palette对象
相对内存读写
相对内存RW Primitives允许我们对特定区域进行读写操作。通过破坏GDI对象的内存可增加其大小,这通常是触发bug后用于获取任意内存RW Primitives所需迈出的第一步。
任意内存读写
对用于实现任意内存读写的对象,通常要求其拥有一个能够指向其对象数据的指针成员。如果该指针被修改了,那么当调用相对用对象数据读写函数时就会转而读写修改后指针所指地址,从而获取强大的任意内存RW primitives。
考虑这样的Manager/Worker组合。对A对象(Manager),我们扩增了其大小,因而能够实现相对的越界读写,即实现了B对象(Worker)数据指针的读写,而后将该指针替换为我们需要进行读写的地址,这就使得B对象数据读写操作能够被我们控制。
0x01 Bitmap 对象
SURFOBJ结构体
其中关注的成员sizelBitmap,它是⼀个 SIZEL 结构体,系统由该变量来确定 bitmap 位图的
⻓宽。⽽ pvScan0 和 pvBits 成员变量均表示指向 bitmap 位图的指针,按 bitmap 类型的不同,系统会选⽤⼆者之⼀。此外,在内存中 bitmap 位图通常位于 SURFOBJ 结构之后。
1 | typedef struct _SURFOBJ { |
分配
CreateBitmap函数用于分配Bitmao对象,定义如下:
1 | BOOL CreateBitmap( |
分配2000个Bitmap对象:
1 | for (int y = 0; y < 2000; y++) { |
释放
DeleteObject函数用于释放:
1 | BOOL DeleteObject( |
1 | DeleteObject(hBITMAP); |
读内存
GetBitmapBits 函数可⽤于读取由 pvScan0 或 pvBits(取决于 bitmap 类型) 指针指向的 cBytes 字节的 bitmap 位图内容,其中 cBytes 的取值需⼩于 sizlBitmap.Width * sizlBitmap.Height * BitsPerPixel 乘积。
1 | LONG GetBitmaps( |
写内存
相对的,SetBitmapBits 函数则⽤于向 pvScan0 或 pvBits(取决于 bitmap 类型)指针指向的 bitmap 位图写⼊cBytes 字节的内容,同样 cBytes 的取值也需⼩于 sizlBitmap.Width * sizlBitmap.Height * BitsPerPixel 的乘积。
1 | LONG SetBitmapBits( |
相对内存读写 - sizlBitmap
sizlBitmap成员变量为SIZEL类型结构体,其中包含了Bitmap位图的长宽,SIZEL结构体和SIZE结构体是等价的:
1 | typedef SIZE SIZEL; |
后续的所有 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 Bitmap 对的 sizelBitmap 或 pvScan0 成员,从⽽达到控制 Worker Bitmap 对象pvScan0 成员的⽬的,最终实现内核任意内存读写(ARW primitives)。
②还有一种通过控制 Manager Bitmap 对象的 sizlBitmap 成员,以扩增 Bitmap 的⼤⼩来获取相对内存读写的能⼒,接着再控制相邻 Worker Bitmap 对象的 pvScan0 指针达到任意内存读写。
0x02 Palette对象
PALETTE结构体
⽐较感兴趣的成员变量是 cEntries,它表示 PALETTEENTRY 数组中的元素个数,此外还有 pFirstColor 成员变量,它是指向 PALETTEENTRY 数组 apalColors 的指针,可以看到, apalColors 表示的数组位于此结构体尾部。
1 | typedef struct _PALETTE |
分配
CreatePalette函数用于分配Palette对象,唯一的入参lplgpl为LOGPALETTE结构体指针类型,对x86系统其分配大小需不小于0x98个字节,相应的对x64系统其分配大小需不小于0xD8字节。
1 | HPALETTE CreatePalette( |
分配2000个Palette对象:
1 | LOGPALETTE *lPalette; |
释放
而Palette对象的释放则是由DeleteObject函数来完成:
1 | DeleteObject(hPALETTE); |
读内存
相对的, SetPaletteEntries 和 AnimatePalette 这两个函数可⽤来向 Palette 对象写⼊内容,即将缓冲区 lppe
上的 nEntries 个元素写⼊ hpal 句柄表示的 XEPALOBJ 结构中 pFirstColor 指针指向的 apalColors 数组⾃偏移 iStart 或iStartIndex 开始的位置。
1 | UINT SetPaletteEntries( |
相对内存读写 - cEntries
XEPALOBJ 结构体的 cEntries 成员⽤于表示 Palette 对象中 apalColors 数组元素的个数,若将其覆盖为⼀个⼤的数值,那么借助破坏后的 Palette 对象可以实现内存的越界读写。
任意内存读写 - pFirstColor
pFirstColor 指针指向的是 Palette 对象中 apalColors 数组的起始位置,通过控制该指针,可以实现内核态下内存的任意读写。
利用思路
针对 Palette 对象的利⽤思路和之前讨论的 Bitmap 对象利⽤思路是类似的,通过控制 Manager Palette 对象的 cEntries 或pFirstColor 成员来达到控制相邻 Worker Palette 对象 pFirstColor 成员的⽬的,从⽽获取内核下的 ARW primitives。
我们这⾥讨论 Manager Palette 对象中 cEntries 成员可控的情况,通过对 cEntries 进⾏溢出使得 Manager Palette 对象获取相对内存读写的能力,再借此修改相邻 Worker Palette 对象的 pFirstColor 指针可实现任意内存读写。
基于 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 函数代码段如下。虽然不会导致蓝屏死机,但这使得我们⽆法完成写⼊操作。
Token的替换
内核借助 _EPROCESS 结构来表示系统上运⾏的每⼀个进程,该结构包含很多重要成员,例如 ImageName、SecurityToken、 ActiveProcessLinks 以及 UniqueProcessId,这些成员的偏移值因系统版本⽽异。
Windows 7 x64
1 | dt _EPROCESS UniqueProcessId ActiveProcessLinks Token |
Windows7 x86
1 | kd> dt _EPROCESS UniqueProcessId ActiveProcessLinks Token |
SecurityToken
SecurityToken 表示当前进程所持有的安全级别标识,当进程请求获取特定权限时,系统会借此判断其是否拥有所请求资源的权限。
ActiveProcessLinks
ActiveProcessLinks 是⼀个 LIST_ENTRY 对象,可借此遍历各进程对应的 EPROCESS 结构。 .
1 | typedef struct _LIST_ENTRY { |
UniqueProcessId
UniqueProcessId 表示进程 PID。
提权步骤
- 获取内核中 SYSTEM 进程对应的 EPROCESS 结构地址;
- 借助 Read primitive 得到相应的 SecurityToken 和 ActiveProcessLinks;
- 遍历 ActiveProcessLinks 得到当前进程的 EPROCESS 结构地址,即 ActiveProcessLinks->Flink.UniqueProcessId 和GetCurrentProcessId() 的值相同;
- 借助 Write primitive 将当前进程的 SecurityToken 替换为 SYSTEM 进程的 SecurityToken。