Skip to content
KitploitKITPLOIT
工具漏洞利用博客
Log in
提交
工具漏洞利用博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

订阅源联系隐私© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2016-3308 — 利用CVE-2016-3308破坏win32k桌面堆 | Kitploit
工具/GitHubGitHub/jackhuyh/cve-2016-3308
内存取证漏洞分析漏洞利用二进制利用
GitHubjackhuyh/cve-2016-3308

CVE-2016-3308

利用CVE-2016-3308破坏win32k桌面堆

查看仓库
1211110年前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

利用 CVE-2016-3308 破坏 win32k 桌面堆

作者:@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

为了触发该漏洞,需要完成以下步骤:

  1. 创建一个菜单。
  2. 创建一个子菜单,作为菜单的第一个 ITEM,ID 为 0x123,必须将 MENUITEMINFO.hbmpItem 设置为 HBMMENU_SYSTEM。
  3. 向菜单中添加其他 7 个 ITEM,ID 从 0x1001 到 0x1007。
  4. 向子菜单中添加一个 ID 为 1 的 ITEM。
  5. 向子菜单中添加其他 8 个 ITEM,以便获得一个具有 16 个槽的 tagMENU.rgItems。但如果只需要触发崩溃,这一步并非必须。
  6. 向菜单中添加第 9 个 ITEM(ID 为 0x123),从而以错误的参数调用预期的 RtlMoveMemory()。

桌面堆

桌面堆是所有 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。

下图展示了堆的变化过程:

根据图示,我逐步破坏桌面堆,并通过以下阶段实现利用:

  1. 创建一些具有 WndText 大小为 1 的窗口,循环 FILL_HOLE_COUNT 次来填充旧的空洞。
  2. 创建 WND_0 ~ WND_5 来操作块数据。
  3. 创建一个菜单,并创建一个子菜单作为菜单的第一个 ITEM,ID 为 0x123。然后向菜单中添加其他 7 个 ITEM,ID 为 0x1001~0x1007。
  4. 向子菜单中添加一个 ID 为 1 的 ITEM,以及其他 7 个 ID 为 0x2001~0x2007 的 ITEM。
  5. 向子菜单中添加第 9 个 ITEM(ID 为 0x2008),以便获得一个具有 16 个槽的 tagMENU.rgItems。
  6. 将 WND_5 的文本设置为 0x360 字节,以填充子菜单 ITEM*8 的原始空洞。
  7. 将 WND_1 的文本设置为 0x70 字节,准备伪造的块头部。
  8. 将 WND_2 的文本设置为 0x70 字节,为伪造的块构建一个占位符。
  9. 将 WND_0 的文本设置为 0x6c0 字节,为将来的菜单 ITEM*16 构建一个占位符。
  10. 创建一个带有自动创建的 tagPROPLIST 的原始窗口(Primitive-WND)。
  11. 创建一个带有自动创建的 tagPROPLIST 的损坏窗口(Corrupt-WND)。
  12. 将 WND_3 的文本设置为 0x10 字节,用于步骤 8 中伪造块之后的下一个伪造块。
  13. 保存堆布局,以便在退出阶段恢复。
  14. 将 WND_0 的文本重置为 0x700 字节,从而释放一个 0x6c0 大小的空洞。
  15. 向菜单中添加第 9 个 ITEM,它会重用步骤 14 中释放的块,并且漏洞会被触发。
  16. 将 WND_2 的文本重置为 0x80 字节,导致伪造的块被推到空闲列表的顶部。
  17. 将 Corrupt-WND 的文本设置为 0x8e0 字节,然后伪造的块被重用。
  18. 通过设置 Primitive-WND 的文本,执行写入原语来覆盖 nt!HalDispatchTable[1]。
  19. 通过调用 NtQueryIntervalProfile 触发 shellcode。
  20. 恢复保存的堆布局并退出。

上面列出的关键步骤用于构建堆风水:

在步骤 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 处的如下代码:

下载工具