上一篇研究了一下基础的内核整数溢出漏洞,漏洞利用还有瑕疵,但是原理上还是比较清楚的,后续有时间继续搞一哈,这篇我们继续下一个内核漏洞:未初始化栈变量。
实验环境:Win10专业版+VMware Workstation 15 Pro+Win7 x86 sp1
实验工具:VS2015+Windbg+KmdManager+DbgViewer
顾名思义,即内核函数栈中局部变量未初始化。
typedef struct _UNINITIALIZED_MEMORY_STACK { ULONG Value; FunctionPointer Callback; ULONG Buffer[58]; } UNINITIALIZED_MEMORY_STACK, *PUNINITIALIZED_MEMORY_STACK; NTSTATUS TriggerUninitializedMemoryStack( _In_ PVOID UserBuffer) { ULONG UserValue = 0; ULONG MagicValue = 0xBAD0B0B0; NTSTATUS Status = STATUS_SUCCESS; #ifdef SECURE //安全版本 UNINITIALIZED_MEMORY_STACK UninitializedMemory = { 0 }; #else //漏洞版本 UNINITIALIZED_MEMORY_STACK UninitializedMemory; #endif PAGED_CODE(); __try { 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) { UninitializedMemory.Callback(); } } __except (EXCEPTION_EXECUTE_HANDLER) { Status = GetExceptionCode(); DbgPrint("[-] Exception Code: 0x%X\n", Status); } return Status; }
很明显可以看到,不安全的版本中,结构体变量UninitializedMemory未进行初始化。栈上的局部变量,未初始化时,则会拥有前调用函数的随机垃圾值。
如果我们传入一个正确MagicValue,它会填充变量UninitializedMemory以及回调成员。如果传递的值不正确那么就不会填充。后面看到Callback检查,但是并没有什么用。
驱动程序拖进IDA看下:
此时数组偏移下标为(0xB)*4
var_C变量为控制码0x222003,那么IO控制码为0x222003+(0xB)*4 = 0x22202f
找到漏洞函数,我们看到:
查看代码逻辑,比较成功会命中绿色块,在这里变量被填充了适当的值,此后在红色块中当回调函数被调用时并不会出错。
(我的调试过程断点总是出问题,借用 fuzzsecurity的调试过程):
这个值是不固定的,如果你尝试重现,很可能会在Windbg中看到不同的值。在虚拟机BSOD之前,让我们快速的看一下该变量距离当前栈起始位置有多远。
计算方式:0x8a15ced0 - 0x8a15c9cc = 0x504 (1284 bytes)
让我们通过回溯对BSOD进行检查。
可以看到,回调指针指向了一个垃圾地址,执行出错。
覆盖内核栈上的整型指针为我们shellcode的地址是最终目标。
参考j00ru的文章。
其中提到了内核栈喷射(Kernel Stack-Spraying)的概念。
在Windows系统中,内核栈不同于用户栈,其为一片共用空间,使用常规的的喷射方法是有很大风险的,比如覆盖到其他关键的地址。而原作者提到的用NtMapUserPhysicalPages的方法,使我们在用户态完成对内核栈的操作。
NTSTATUS NtMapUserPhysicalPages ( __in PVOID VirtualAddress, __in ULONG_PTR NumberOfPages, __in_ecount_opt(NumberOfPages) PULONG_PTR UserPfnArray ) (...) ULONG_PTR StackArray[COPY_STACK_SIZE]; // // This local stack size definition is deliberately large as ISVs have told // us they expect to typically do up to this amount. // #define COPY_STACK_SIZE 1024
未文档化的函数,NtMapUserPhysicalPages,我们不关心它用于干什么,但它的一部分功能是拷贝输入的字节到内核栈上的一个本地缓冲区。最大尺寸可以拷贝1024*IntPtr::Size(32位机器上是4字节=>4096字节)。
seebug的文章中可以看到NtMapUserPhysicalPages的实现:
NTSTATUS __stdcall NtMapUserPhysicalPages(PVOID BaseAddress, PULONG NumberOfPages, PULONG PageFrameNumbers) if ( (unsigned int)NumberOfPages > 0xFFFFF ) return -1073741584; BaseAddressa = (unsigned int)BaseAddress & 0xFFFFF000; v33 = ((_DWORD)NumberOfPages << 12) + BaseAddressa - 1; if ( v33 <= BaseAddressa ) return -1073741584; v4 = &P;//栈地址 v39 = 0; v37 = &P; if ( PageFrameNumbers ) { if ( !NumberOfPages ) return 0; if ( (unsigned int)NumberOfPages > 0x400 )//如果要超过1024,就要扩展池,不过这里不用 { v4 = (char *)ExAllocatePoolWithTag(0, 4 * (_DWORD)NumberOfPages, 0x77526D4Du); v37 = v4; if ( !v4 ) return -1073741670; } v5 = MiCaptureUlongPtrArray((int)NumberOfPages, (unsigned int)PageFrameNumbers, v4);//v4 要拷贝的目标 内核栈 a2,要覆盖的EoPBuffer 长度是4*NumberOfPages
调用过程NtMapUserPhysicalPages -> MiCaptureUlongPtrArray -> memcpy。
int __fastcall MiCaptureUlongPtrArray(int a1, unsigned int a2, void *a3)//4*a1为长度 a2为构造的用户缓冲区 a3为内核地址 { size_t v3; // ecx@1 v3 = 4 * a1; //长度 if ( v3 ) { if ( a2 & 3 ) ExRaiseDatatypeMisalignment(); if ( v3 + a2 > (unsigned int)MmUserProbeAddress || v3 + a2 < a2 ) *(_BYTE *)MmUserProbeAddress = 0; } memcpy(a3, (const void *)a2, v3); return 0; }
那么在用户态下构造好栈缓冲区,填充好payload地址。在HacksTeam的利用代码中,可以看到这个过程。
整理一下:
exp工作流将如下:(1)将shellcode放在内存任意位置,(2)使用指向shellcode的指针喷射内核栈,(3)触发未初始化变量漏洞 (4)调用执行。
测试一哈,成功提权。
安全版本中提到的变量初始化的好习惯可以很好的帮助我们规避这类漏洞。
#ifdef SECURE //安全版本 UNINITIALIZED_MEMORY_STACK UninitializedMemory = { 0 }; #else