Windows GDI Bitmap
0x1. Windows GDI Bitmap
GDI(Windows Graphics Device Interface),是提供图形、文字显示的 API 接口。
Bitmap 是 GDI 中的图形对象,Bitmap 事实上包含一个二元数组,用于存放图形像素(当然还有图形大小等信息),可以通过 CreateBitmap 和 SetBitmapBits 来创建和修改图形。
0x2. 用户态到内核态的 Bitmap 访问过程
在通过 CreateBitmap 创建 Bitmap 对象后,返回对应的资源句柄,同 CreateFile 类似,其实 Windows 都用句柄(Handle)来标识用户态对内核对象的引用。这个句柄低 16 位其实是数组索引:
UINT index = hBitmap & 0xFFFF;
这个数组指针位于 PEB->GdiSharedHandleTable 中,定义如下:
struct _GDI_CELL GdiSharedHandleTable[0x8fff];
GDI_CELL 的定义如下,这个对象在 x86 下大小是 0x10,x64 是 0x18:
struct _GDI_CELL
{
IntPtr pKernelAddress;
UInt16 wProcessId;
UInt16 wCount;
UInt16 wUpper;
UInt16 wType;
IntPtr pUserAddress;
}
看到 pKernelAddress
了吗?所以在用户态中,通过 PEB 结构,可以索引到 Bitmap 在内核中的真实对象地址:
ULONG_PTR baseObj = PEB->GdiSharedHandleTable + (hBitmap & 0xFFFF) * 0x10
Bitmap 内核对象的真实结构如下:
typedef struct _SURFACE {
BASEOBJECT BaseObject;
SURFOBJ surfobj;
[...]
}
BASEOBJECT
对内核对象进行标记,用于描述最基础的对象信息,真实的 Bitmap 对象信息存放在 SURFOBJ
中:
typedef struct _SURFOBJ {
DHSURF dhsurf;
HSURF hsurf;
DHPDEV dhpdev;
HDEV hdev;
SIZEL sizlBitmap;
ULONG cjBits;
PVOID pvBits;
PVOID pvScan0;
LONG lDelta;
ULONG iUniq;
ULONG iBitmapFormat;
USHORT iType;
USHORT fjBitmap;
} SURFOBJ, *PSURFOBJ;
在这个结构中,存在一个非常重要的成员:pvScan0
。这个指针指向 Bitmap 的图形像素。无论是GetBitmapBits
还是SetBitmapBits
,都是通过这个指针来读取对应的像素信息。
0x3. 内核利用中的 Bitmap
在内核漏洞利用中,如果获取到一个任意内存写(Arbitrary Memory Write)漏洞,则可以向任意地址写入一个 unsigned long 值。
第一个能想到的,应该是替换某个内核分支地址,然后跳转到我们控制的代码。
但是如果存在 SMEP 等保护,或者说需要通过这个漏洞,来达到可以操作任意其他内核内存的目的。后者听起来好像很难实现?
这时候就需要引入一个 Primitive,用本原这个上帝视角,来构建整个内核世界:
- 首先找到一个内核内存访问接口,这个 API 中,内核为我们分配内核内存,虽然限制我们只能对这部分内存进行读写
- 能通过用户态的方式,找到内核内存的地址,这个地址存放在某个数据结构中
如果能符合上面的两个条件,首先我们通过 API 分配一个内核内存(CreateBitmap)。接着,通过内核漏洞,替换数据结构中指向限制内存区域的指针(SURFACE->pvScan0)。最后,再通过 API(GetBitmapBits / SetBitmapBits),对这个被间接引用的指针进行访问,可以自由向这个地址写入、读取数据。
但是如果需要更换地址,都需要重新利用漏洞,来覆盖这个指针。这时候我们可以再叠加一次 :
- 创建两个 Bitmap 对象
- 第一个 Bitmap 对象,通过内核漏洞,覆盖 pvScan0 为另外 Bitmap 对象的 pvScan0 地址
- 往后,操作第一个 Bitmap,可以替换第二个 Bitmap->pvScan0 地址,再通过第二个 Bitmap 来读写任意内存
因此第一个 Bitmap 称为 Manager,用于管理写入地址;第二个 Bitmap 则称为 Worker,负责对真实内存区域进行操作。