Ancient Win32k heap UAF detector.
This blog post is a reshare of a personal note I wrote 15 years ago, and I’m sharing it here for nostalgic reasons. fast forward to 2024, and we now have KASAN integrated into the Windows kernel.
Original content:
In recent days, Windows kernel vulnerabilities have become more valuable due to the limitations imposed by sandboxes. Every RCE exploit now requires an additional phase to bypass these restrictions in order to achieve full system access.
Many local privilege escalation vulnerabilities are based on flaws in Win32k
, particularly how it handles or uses objects. In most cases, Win32k
uses freed memory, leading to use-after-free vulnerabilities.
Win32k uses HMAllocObject
to allocate memory for its objects, and the function uses different memory management subsystems based on the object type, either from the heap or from the kernel pool.
int __stdcall HMAllocObject(int a1, PVOID Object, char a3, ULONG Size)
{
....
....
if ( v5 & 0x10 && Object )
{
v7 = DesktopAlloc((int)Object, Size, ((unsigned __int8)a3 << 16) | 5);
if ( !v7 )
{
LABEL_28:
UserSetLastError(8);
return 0;
}
LockObjectAssignment(v7 + 12, Object);
*(_DWORD *)(v7 + 16) = v7;
}
else
{
if ( v5 & 0x40 )
{
v8 = SharedAlloc(Size);
}
else
{
v9 = !Object && v5 & 0x20;
if ( !(v5 & 8) || v9 )
v8 = Win32AllocPoolWithTagZInit(Size, dword_BF9F191C[v4]);
else
v8 = Win32AllocPoolWithQuotaTagZInit(Size, dword_BF9F191C[v4]);
}
v7 = v8;
....
....
....
....
}
}
These memory management functions include:
DesktopAlloc
function uses heap,SharedAlloc
function uses heap.Win32AllocPoolWithQuotaTagZInit
,Win32AllocPoolWithTagZInit
functions use pool.
For example, a Menu object
uses DesktopAlloc
, while an Accelerator
object uses Kernel Pool
.
For objects that use heap memory, when the object’s life ends, the OS calls RtlFreeHeap
to free the used memory. However, after RtlFreeHeap
returns, the freed memory still contains the old/valid contents. If another part of win32k.sys
uses the freed memory, nothing will happen because it uses memory with old contents (no BSOD occurs), and the bug will be missed.
Until now, researchers have typically discovered these types of bugs through reverse engineering. but if someone is fuzzing Win32k, they will miss vulnerabilities, due to having valid content even after free, They need to allocate a large amount of memory to ensure that the freed memory is reallocated with different content, which can trigger a crash. But how can one know when the OS will use the freed memory? and allocating memory in each iteration has runtime overhead.
In user-mode code, we can use GFlags to enable PageHeap system-wide. This doesn’t affect the heap implementation in the kernel. There is also a special pool
that can be enabled with the verifier, but it doesn’t help us with heap-based objects.
gflags.exe /i iexplore.exe +hpa +ust to to enable the page Heap (HPA)
So, my idea is to patch RtlFreeHeap
and fill the freed memory with invalid content, such as 0x0c0c0c0c
.
With the help of the RtlSizeHeap
function (thanks to @ponez ), we can find the size of heap object and fill it with 0x0c0c0c0c
.
in alomst all cases the object contains a pointer to another object, so an pointer needs to be dereferenced, it means OS will dereference 0x0c0c0c0c
address and we will get a kernel BSOD when it uses freed memory.
__declspec(naked) my_function_detour_RtlFreeHeap()
{
//PVOID func=RtlSizeHeap;;
__asm
{
// exec missing instructions
mov edi,edi
push ebp
mov ebp,esp
push ebx
mov ebx,dword ptr [ebp+10h]
int 3;
/*
BOOLEAN RtlFreeHeap
(
IN PVOID HeapHandle,
IN ULONG Flags,
IN PVOID HeapBase
);
mov ebx,dword ptr [ebp+10h] get HeapBase
*/
PUSHAD
PUSH dword ptr [ebp+10h]
PUSH dword ptr [ebp+0Ch]
PUSH dword ptr [ebp+08h]
call RtlSizeHeap;
sub ecx,ecx;
mov ecx, eax; // size from RtlSizeHeap
mov eax, 0x0c
mov edi, ebx; // address of heap chunk
rep stos byte ptr es:[edi]
POPAD
}
}
I tested my Win32k heap sanitizer detector with some old UAF vulnerabilities in Win32k and it worked like a charm.