Windows内核利用——从任意地址写到任意地址读写

Windows内核利用:使用GDI结构从任意地址写到任意地址读写利用技术的攻防演进

前言#

假设我们已经存在一个任意地址写任意值的内核漏洞,我们的最终目的是要实现提权。

这个时候我们就需要先回答这个问题:什么地址,写什么值可以让我们实现提权呢?

我们比较常用的方法是将exp进程的token地址替换为System进程的token地址,启动system cmd,因此这个问题也很好回答:

什么地址:exp进程的token地址

写什么值:System进程的token值

但是处于普通用户权限的exp进程并不能知道当前进程的token地址,也不知道System进程的token值,我们只有一个内核里的任意地址写任意值的漏洞。那什么东西可以让我们实现最终的提权呢?这就是本文的主要内容。

Windows对象#

Windows 中存在着 3 种类型的对象,分别为 user object、GDI object、Kernel object,一共有 40 多种对象

其中:

  • bitmap对象属于 GDI object,存在于换页会话池中

  • Accelerator table 对象属于 user object,存在于换页会话池中

  • 桌面堆对象属于 user object,存在于换页会话池中

  • Palette对象属于GDI object,存在于换页会话池中

存在于同一会话池中的对象,可以通过堆风水的技术使得攻击者可以稳定的分配到同一块内存空间,实现内核地址的泄露

BitMap RS1(v1607)前#

BitMap结构#

BitMap是Windows GDI的一个对象,存在于内核中,可以帮助我们实现从任意地址写到任意地址读写。

img

当新建一个BitMap对象时,会在内核中分配三个结构,分别是BaseObject,SURFObj和Pixel Data,其中SURFOBJ中的pvScan0指向Pixel Data,另外,用户态进程可以通过GetBitmapBitsSetBitmapBits来读写pvScan0指向的PixelData数据区 => 用户态进程可以操作内核数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
typedef struct _BASEOBJECT32 // 5 elements, 0x10 bytes (sizeof)
{
ULONG32 hHmgr;
ULONG32 ulShareCount;
WORD cExclusiveLock;
WORD BaseFlags;
ULONG32 Tid;
}BASEOBJECT32, *PBASEOBJECT32;

typedef struct _BASEOBJECT64 // 0x18
{
ULONG64 hHmgr;
ULONG32 ulShareCount;
WORD cExclusiveLock;
WORD BaseFlags;
ULONG64 Tid;

void operator=(const BASEOBJECT32 &base )
{
hHmgr = base.hHmgr;
ulShareCount = base.ulShareCount;
cExclusiveLock = base.cExclusiveLock;
BaseFlags = base.cExclusiveLock;
Tid = base.Tid;
}

bool operator==(const _BASEOBJECT64 &base )
{
return ((hHmgr == base.hHmgr) && (Tid == base.Tid));
}

void dump()
{
dprint("BASEOBJECT:\n");
dprint("\thHmgr: %08lx\n", hHmgr);
dprint("\tulShareCount: %08lx\n", ulShareCount);
dprint("\tcExclusiveLock: %04lx\n", cExclusiveLock);
dprint("\tBaseFlags: %04lx\n", BaseFlags);
dprint("\tTid: %016I64x\n\n", Tid);
}

}BASEOBJECT64, *PBASEOBJECT64;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
typedef struct _SURFOBJ32 { pvScan0偏移0x20
ULONG32 dhsurf; // +4
ULONG32 hsurf; // +4
ULONG32 dhpdev; // +4
ULONG32 hdev; // +4
SIZEL sizlBitmap; // +8
/*
typedef struct tagSIZE
{
LONG cx;
LONG cy;
} SIZE, *PSIZE, *LPSIZE;

typedef SIZE SIZEL;
*/
ULONG cjBits; // +4
ULONG32 pvBits; // +4
ULONG32 pvScan0;
LONG lDelta;
ULONG iUniq;
ULONG iBitmapFormat;
USHORT iType;
USHORT fjBitmap;
} SURFOBJ32 ;


typedef struct _SURFOBJ64 { pvScan0偏移0x38
ULONG64 dhsurf; // +8
ULONG64 hsurf; // +8
ULONG64 dhpdev; // +8
ULONG64 hdev; // +8
SIZEL sizlBitmap; // +8
ULONG64 cjBits; // +8
ULONG64 pvBits; // +8
ULONG64 pvScan0;
LONG32 lDelta;
ULONG32 iUniq;
ULONG32 iBitmapFormat;
USHORT iType;
USHORT fjBitmap;

void operator=(const SURFOBJ32 &surf )
{
dhsurf = surf.dhsurf;
hsurf = surf.hsurf;
dhpdev = surf.dhpdev;
hdev = surf.hdev;
sizlBitmap = surf.sizlBitmap;
cjBits = surf.cjBits;
pvBits = surf.pvBits;
pvScan0 = surf.pvScan0;
lDelta = surf.lDelta;
iUniq = surf.iUniq;
iBitmapFormat = surf.iBitmapFormat;
iType = surf.iType;
fjBitmap = surf.fjBitmap;
}

} SURFOBJ64;

泄露内核信息#

CreateBitmap 创建 Bitmap 对象后,会返回对应的资源句柄,该句柄的低16位是数组的访问下标

1
UINT index = hBitmap & 0xFFFF;

通过下标可以找到对应的GDI对象

1
2
3
4
5
6
7
8
9
10
11
struct _GDI_CELL GdiSharedHandleTable[0x8fff];

struct _GDI_CELL
{
IntPtr pKernelAddress;
UInt16 wProcessId;
UInt16 wCount;
UInt16 wUpper;
UInt16 wType;
IntPtr pUserAddress;
}

GdiSharedHandleTable可以在用户态通过TEB->PEB->GdiSharedHandleTable获取到。
GDI_CELL结构在32位下大小为0x10,64位下为0x18。它的第一个指针 pKernelAddress 指向了 BASEOBJECT 的第一个字节

由于SURFACE OBJ结构体的偏移固定,可以泄露pvScan0的内核信息,过程如下:

pvScan0 = *(PDWORD32)pKernelAddress + (x86:0x10,x64:0x18) + (x86:0x20,x64:0x38)

此时,我们可以在用户态找到内核态的PvScan0地址

单次任意地址写 -> 单次任意地址读写#

当我们有了一个任意地址写任意值的能力时,我们可以通过以下流程使用BITMAP进行利用

  1. 新建BITMAP结构
  2. 用户态获取位于内核中的pvScan0地址
  3. 任意地址写漏洞改写 pvScan0 的值为我们想要读写的地址
  4. 使用GetBitMapBits和SetBitMapBits读写任意值

但是上面的流程好像只比原来的漏洞多了一个读的能力,当漏洞只能触发一次时,一次读写根本不能满足我们想要达到的目的

单次任意地址写 ->任意次任意地址读写#

img

通过两个BitMap的Manager + Worker模式,实现任意次任意地址读写,完全控制内核

  1. 用户态新建Manager和Worker两个BitMap
  2. 使用任意地址写覆盖Manager的pvScan0为Worker的pvScan0地址
  3. 在用户态,操作Manager,可以替换Worker的pvScan0地址,再通过Worker来读写任意内存
  4. 通过任意地址读获取System进程的token
  5. 通过任意地址写替换当前进程token

Accelerator Table RS1(v1607)#

在RS1中,微软将pKernelAddress指向了异常地址,阻止了BitMap方式的pvScan0地址泄露

可以想到的两个想法:

  1. 寻找其它方法来泄露 bitmap GDI 对象的地址
  2. 寻找 bitmap 的替代方案

泄露内核地址#

由于堆风水技术的存在,如果我们可以让BitMap分配到一个我们已知地址的空间,那么我们还是能泄露出pvScan0的地址,实现任意次的任意地址读写

  • bitmap对象属于 GDI object,存在于换页会话池中

  • Accelerator table 对象属于 user object,存在于换页会话池中

如果可以泄露Accelerator table的地址,再让BitMap分配到释放的Accelerator table的那块内存,就可以泄露pvScan0地址

泄露Accelerator table内核地址的结构体如下:

user32.dll 模块中有一个全局变量叫做 gSharedInfo,结构如下:

1
2
3
4
5
6
7
8
9
10
typedef struct _SHAREDINFO {
PSERVERINFO psi;
PUSER_HANDLE_ENTRY aheList;
ULONG HeEntrySize;
ULONG_PTR pDispInfo;
ULONG_PTR ulSharedDelts;
ULONG_PTR awmControl;
ULONG_PTR DefWindowMsgs;
ULONG_PTR DefWindowSpecMsgs;
} SHAREDINFO, * PSHAREDINFO;

其中第二个变量aheList指向一张结构为 USER_HANDLE_ENTRY 的表,如下:

1
2
3
4
5
6
7
8
9
10
11
12
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指向对应的User Object在内核中的位置

堆风水#

  1. 调整构造函数,使得创建的Accelerator Table对象和BitMap对象大小一致
  2. 使用Accelerator Table占位
  3. 释放Accelerator Table并分配BitMap,实现内存复用
  4. 根据偏移,计算出pvScan0内核地址
1
pvScan0 = *(PDWORD32)pKernel + (x86:0x10,x64:0x18) + (x86:0x20,x64:0x38)

lpszMenuName RS2(v1703)#

在RS2中,Windows再次将pKernel指向异常地址,阻止了Accelerator Table + 堆风水泄露pvScan0的方式

lpszMenuName关联一个windows窗口对象, 其在内核当中对应结构体对象为tagWND

tagCLS对应于windows窗口类,其中一个对象是lpszManuName

如果可以泄露lpszManuName地址,就可以通过设置wndclass.lpszMenuName控制对应内存的大小

img

tagWND对应一个桌面堆. 内核的桌面堆会映射到用户态去.。

泄露内核地址#

  • bitmap对象属于 GDI object,存在于换页会话池中

  • 桌面堆对象属于 user object,存在于换页会话池中

使用桌面堆对象和堆风水泄露BitMap对象的pvScan0的内核地址。

桌面堆会在用户态和内核态各保存一份,我们通过下面的方式泄露桌面堆的内核地址

img

指向用户模式版本的指针被存在TEB.Win32ClientInfo+0x28的位置,ulClientDelta即是内核地址减去用户地址

img

img

首先在用户态找到函数HMValidateHandle的地址

1
2
3
4
5
6
7
8
9
10
kd> u user32!IsMenu
USER32!IsMenu:
00007fff`17d489e0 4883ec28 sub rsp,28h
00007fff`17d489e4 b202 mov dl,2
00007fff`17d489e6 e805380000 call USER32!HMValidateHandle (00007fff`17d4c1f0)
00007fff`17d489eb 33c9 xor ecx,ecx
00007fff`17d489ed 4885c0 test rax,rax
00007fff`17d489f0 0f95c1 setne cl
00007fff`17d489f3 8bc1 mov eax,ecx
00007fff`17d489f5 4883c428 add rsp,28h

在用户态调用HmValidateHandle,可以返回一个用户桌面堆中的tagWND对象指针

img

具体泄露过程如下:

  1. 从User32.dll中的IsMenu函数中找到HMValidateHandle函数的指针:pIsMenu --> pHMValidateHandle
  2. 通过调用HMValidateHandle函数找到用户桌面堆中的tagWND指针:pWnd = HMValidateHandle(hWnd,1)
  3. 获取内核桌面堆地址:pSelf=pWnd+0x20,获取内核tagCLS地址:kernelTagCLS=pWnd+0xa8,这里的pSelf其实是Kernel中的tagWND地址
  4. 内核桌面堆和用户桌面堆的地址偏移:ulClientDelta=pSelf-pWnd
  5. 获取用户态TagCLS:userTagCLS=kernelTagCLS-ulClientDelta
  6. lpszMenuName位于userTagCLS0x90偏移处(该处指向paged pool中的lpszMenuName成员)

堆风水#

  1. 调整构造函数,使得创建的桌面堆对象的lpszMenuName和BitMap对象大小一致
  2. 使用桌面堆对象占位
  3. 释放桌面堆对象并分配BitMap,实现内存复用
  4. 根据偏移,计算出pvScan0内核地址
1
pvScan0 = *(PDWORD32)pKernel + (x86:0x10,x64:0x18) + (x86:0x20,x64:0x38)

Palette RS3 (v1709)#

在RS3中,微软彻底解决了BitMap的问题,把Bitmap header与Bitmap data分离,无法通过Bitmap header取得pvscan0指针的内核地址

Palette结构#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
}
1
2
3
4
5
6
7
class PALETTEENTRY(Structure):
_fields_ = [
("peRed", BYTE),
("peGreen", BYTE),
("peBlue", BYTE),
("peFlags", BYTE)
]

0x90处是一个4字节的数组apalColors,相当于BitMap里的pixel data,pFirstColor是一个指针,指向apalColors,相当于BitMap里的pvScan0。

  • pvScan0 -> pFirstColor

  • pixelData -> apalColors

img

泄露内核地址#

使用桌面堆风水泄露Pallete结构中的pFirstColor的内核地址

评论