CVE-2025-55680 - Windows Cloud Files Mini Filter Driver Elevation of Privilege Vulnerability

Ghostbyt3 Ghostbyt3
Table of Contents

The vulnerability exists in HsmpOpCreatePlaceholders(), which uses MmMapLockedPagesSpecifyCache() to map userspace buffers into kernel space, creating shared physical memory. During placeholder file creation, the driver validates filenames for path traversal characters (\, :) by reading from this shared memory, then uses the same memory for FltCreateFileEx2() file creation. An attacker exploits the race window by rapidly flipping a single character (e.g., 'D'\') in the shared buffer between validation (“JUSTASTRINGDfile.dll” passes) and file creation (“JUSTASTRING\file.dll” enables traversal). Combined with a pre-created junction (JUSTASTRING → C:\Windows\System32), this allows creating arbitrary files in System32 with SYSTEM privileges, leading to privilege escalation via DLL hijacking.

CVE-2025-55680: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-55680
Vulnerability Type: Time-of-check Time-of-use (TOCTOU) Race Condition
Driver Version: cldlft.sys - 10.0.26100.6725

Vulnerability analysis

The function HsmpOpCreatePlaceholders() was identified as the vulnerable routine addressed in the latest CVE. By reviewing the call graph, the shortest path to this function originates from HsmFltPreFILE_SYSTEM_CONTROL().

image.png

HsmFltPreFILE_SYSTEM_CONTROL():

HsmFltPreFILE_SYSTEM_CONTROL() is a pre-operation callback invoked by the Filter Manager before the actual I/O operation occurs (“Pre” indicates this; a corresponding “Post” callback also exists).

This function effectively acts as an IOCTL handler. If the IOCTL code equals 0x903BC, it calls HsmFltProcessHSMControl(), which is one step closer to the vulnerable function.

This IOCTL is triggered when user space calls CfCreatePlaceholders(), which creates one or more placeholder files or directories under a registered sync root. The API resides in cldapi.dll, which issues the IOCTL. The I/O manager (ntoskrnl.exe) processes the request, and then the Filter Manager (fltmgr.sys) invokes the HsmFltPreFILE_SYSTEM_CONTROL callback.

image.png

The CallbackData passed to the HsmFltProcessHSMControl() function is a _FLT_CALLBACK_DATA structure. Where Iopb is an important member, because it contains all the user-provided parameters.

typedef struct _FLT_CALLBACK_DATA {
  FLT_CALLBACK_DATA_FLAGS     Flags;
  PETHREAD                    Thread;
  PFLT_IO_PARAMETER_BLOCK     Iopb;
  IO_STATUS_BLOCK             IoStatus;
  struct _FLT_TAG_DATA_BUFFER *TagData;
  union {
    struct {
      LIST_ENTRY QueueLinks;
      PVOID      QueueContext[2];
    };
    PVOID FilterContext[4];
  };
  KPROCESSOR_MODE             RequestorMode;
} FLT_CALLBACK_DATA, *PFLT_CALLBACK_DATA;

The CfCreatePlaceholders() function accepts the following user input, where BaseDirectoryPath represents the registered sync root directory, PlaceholderArray is the pointer to an array of CF_PLACEHOLDER_CREATE_INFO.

HRESULT CfCreatePlaceholders(
  [in]      LPCWSTR                    BaseDirectoryPath,
  [in, out] CF_PLACEHOLDER_CREATE_INFO *PlaceholderArray,
  [in]      DWORD                      PlaceholderCount,
  [in]      CF_CREATE_FLAGS            CreateFlags,
  [out]     PDWORD                     EntriesProcessed
);

Each placeholder describes either a file or a directory:

typedef struct CF_PLACEHOLDER_CREATE_INFO {
  LPCWSTR                     RelativeFileName;
  CF_FS_METADATA              FsMetadata;
  LPCVOID                     FileIdentity;
  DWORD                       FileIdentityLength;
  CF_PLACEHOLDER_CREATE_FLAGS Flags;
  HRESULT                     Result;
  USN                         CreateUsn;
} CF_PLACEHOLDER_CREATE_INFO;

When creating placeholders, each file or subfolder under the sync root receives one CF_PLACEHOLDER_CREATE_INFO. It is also valid to create a placeholder even when the file does not yet exist because placeholders represent metadata.

BaseDirectoryPath (the sync root)

    ├─── Placeholder 1 (file or subfolder)
    ├─── Placeholder 2 (file or subfolder)
    ├─── Placeholder 3 (file or subfolder)
    └─── ...

After the IOCTL is issued, the input buffer is converted into an undocumented structure referred to here as CREATE_PLACEHOLDER_STRUCT.

Offset  Length  Field                                Description
------- ------- -------------------------            ---------------------------------------------
0x00    4       Tag                                  Set to 0x9000001A.
0x04    4       OpType                               Set to 0xC0000001.
...
0x0C    4       size                                 Size of the 'CREATE_PLACEHOLDER_STRUCT'.
...
0x10    8       placeholder_payload                  Pointer to the 'CREATE_PLACEHOLDER_STRUCT' data structure

The CREATE_PLACEHOLDER_STRUCT (placeholder_payload) is similar to CF_PLACEHOLDER_CREATE_INFO structure with few changes.

Offset  Length  Field                                Description
------- ------- -------------------------            ---------------------------------------------
0x08    2       relativeName_offset                  The offset to the 'RelativeFileName' start.
0x0A    2       relativeName_len                     The 'RelativeFileName' size.
0x0C    2       fileidentity_offset                  The offset to the 'FileIdentity' start.
0x0e    2       fileidentity_len                     The 'FileIdentityLength' size.
...
0x2e    4       fileAttributes                       The file attributes to apply to the created file.
...
0x50    VAR     relName                              The relative file name content.
VAR     VAR     fileid                               The file identity content.

Basically the following structure is sent as input buffer to 0x903BC IOCTL call.

typedef struct ioctl_0x903BC {
    DWORD Tag;                              // 0x9000001A
    DWORD OpType;                           // 0xC0000001
    DWORD unknown1;
    DWORD size;
    DWORD unknown2;
    PLACEHOLDER_CREATE_KERNEL* placeholder_payload;  // ← USERSPACE POINTER!
    // ...
} ioctl_0x903BC;

From the IOCTL code, we can determine the method being used, because the Iopb contains the user input and we need to know whether its Buffered or Direct or Neither.

image.png

HsmFltProcessHSMControl():

Before calling HsmFltProcessCreatePlaceholders() function, it checks if the input buffer length is not lesser than 0x98 bytes. Then it checks if OpType is 0xC0000001, this is set by cldapi.dll when it makes the IOCTL call.

image.png

HsmFltProcessCreatePlaceholders()

  • The function checks the user input buffer size (1️⃣) and and it calls HsmpRelativeStreamOpen() function (2️⃣) which verifies the directory provided in BaseDirectoryPath (member of CfCreatePlaceholders() function) is a registered sync root and check permissions and returns a handle.
  • Finally, it calls (3️⃣) the vulnerable function HsmpOpCreatePlaceholders() with the user buffer, also with the sync root directory handle.

image.png

HsmpOpCreatePlaceholders()

The CREATE_PLACEHOLDER_STRUCT structure is Payload here and first it calls IoAllocateMdl() to allocates a memory descriptor list (MDL) large enough to map a buffer, this is to map the user space buffer to kernel space.

Following that, ProbeForRead() is called which validates that the buffer (Payload) is in userspace and readable and calls MmProbeAndLockPages() which locks the physical pages in memory and prevents pages from being paged out to disk, the parameter 1 = UserMode (indicates this is a userspace buffer).

Finally, MmMapLockedPagesSpecifyCache() maps those locked physical pages that are described by the MDL (v9) into kernel virtual address space (MappedSystemVa_Payload). This returns a kernel virtual address (KVA) that the driver can use to directly access the same physical memory as the user buffer. No copying occurs. Because both the original user-mode pointer and the new kernel virtual address refer to the same underlying physical memory, any changes made through the user buffer will be visible through the kernel mapping—and any changes made through the kernel mapping will be visible to user space.

A while loop begins where it allocates some space in stack and copies the first placeholder values from the virtual kernel address (v17) to the stack (Payload_in_Stack) but this does not copy everything, the relative file name (relName) is not copied yet.

Another nested while loop, where it checks all the wide characters of the relative file name to see if any of the characters is equal to the\or the:character. This check is implemented to avoid any path traversal or junction in the file name (..\..\Windows\System32\file.dll or C:\Windows\file.dll), this is the TIME-OF-CHECK (TOC). It as a vulnerability fix implemented in CVE-2020-17136 and it’s writeup here.

Finally it calls, FltCreateFileEx2() function to create a new file or open an existing file, where ObjectAttributes (_OBJECT_ATTRIBUTES) structure contains ObjectName member which is pointer to aUnicode stringthat contains the name of the object for which a handle is to be opened.Here you see the relative file name is passed to it, but it takes it directly from the kernel virtual address (v68 → v54 → v17 → MappedSystemVa_Payload). This is the TIME-OF-USE (TOU).

Because the relative file name is taken from the KVA directly, if it’s changed in user space address, it will be changed here as well. So after the path traversal or junction check there is a time window available before it calls FltCreateFileEx2(). If we tamper the name after the check then with race condition it is possible to exploit this. Also, junction does not requires any elevation privilege.

__int64 __fastcall HsmpOpCreatePlaceholders(
        PFLT_INSTANCE *a1,
        __m128i *a2,
        int a3,
        void *Payload,
        ULONG Length,
        _DWORD *a6)
{

[::]

  v9 = IoAllocateMdl(Payload, Length, 0, 0, 0);
  Mdl = v9;
  if ( !v9 )
  {
  
[::]

  ProbeForRead(Payload, Length, 4u);
  v13 = Feature_2594491707__private_IsEnabledDeviceUsageNoInline() != 0;
  MmProbeAndLockPages(v9, 1, v13);
  if ( (v9->MdlFlags & 5) != 0 )
    MappedSystemVa_Payload = (char *)v9->MappedSystemVa;
  else
    MappedSystemVa_Payload = (char *)MmMapLockedPagesSpecifyCache(v9, 0, MmCached, 0, 0, 0x40000010u);
  v48 = MappedSystemVa_Payload;
  if ( !MappedSystemVa_Payload )
  {
  
[::]


  while ( 1 )
  {
    memset(Payload_in_Stack, 0, sizeof(Payload_in_Stack));
    v17 = (__m128i *)&MappedSystemVa_Payload[v56];
    v54 = v17;
    v68 = 0;
    memset(&ObjectAttributes, 0, 44);
    IoStatusBlock = 0;

[::]
  
    Payload_in_Stack[0] = *v17;
    Payload_in_Stack[1] = v17[1];
    Payload_in_Stack[2] = v17[2];
    Payload_in_Stack[3] = v17[3];
    Payload_in_Stack[4] = v17[4];
    
[::]

    while ( 1 )
    {
      v24 = *(__int16 *)((char *)&v54->m128i_i16[v23] + epi16);
      if ( v24 == '\\' || v24 == ':' )
        break;
      if ( ++v23 >= (unsigned __int16)(WORD1(v19) >> 1) )
        goto LABEL_52;
    }
    
[::]

[<------- RACE WINDOW -------->]

[::]

          *((_QWORD *)&v68 + 1) = (char *)v54 + Payload_in_Stack[0].m128i_u16[4];
          LOWORD(v68) = Payload_in_Stack[0].m128i_i16[5];
          WORD1(v68) = Payload_in_Stack[0].m128i_i16[5];
          ObjectAttributes.Length = 48;
          ObjectAttributes.RootDirectory = v57;
          ObjectAttributes.Attributes = 576;
          ObjectAttributes.ObjectName = (PUNICODE_STRING)&v68; // Relative Name of the File in Memory Mapped Region
          *(_OWORD *)&ObjectAttributes.SecurityDescriptor = 0;
          CreateOptions = (v34 != 0) | 0x208020;
          LODWORD(v25) = FltCreateFileEx2(
                           Filter,
                           Instance,
                           &FileHandle,
                           &FileObject,
                           0x100180u,
                           &ObjectAttributes,
                           &IoStatusBlock,
                           &AllocationSize,
                           FileAttributes,
                           0,
                           2u,
                           CreateOptions,
                           0,
                           0,
                           0x800u,
                           &DriverContext);
                           

[::]

  } [ WHILE LOOP CONTINUES TO NEXT PLACEHOLDER ]

[::]

Triggering the Vulnerability

To exploit this vulnerability,

Step 1: Register a directory as sync root directory, which can be done by using the CfRegisterSyncRoot() API.

Step 2: Create a new directory inside this sync root directory, for e.g, JUSTASTRING.

Step 3: Create a junction for this JUSTASTRING to C:\Windows\System32\ directory (which is non-writable by normal user but junction can be created).

Step 4: Create 3 threads

  • First thread will keep checking if C:\Windows\System32\newfile.dll is created.
  • Second thread will keep modifying the filename string which is "JUSTASTRINGDnewfile.dll" in the shared memory buffer, changing the character at position 11 from 'D' to '\' and back repeatedly. This causes the filename to oscillate between "JUSTASTRINGDnewfile.dll" (safe) and "JUSTASTRING\newfile.dll" (exploits junction).
  • Third thread will keep making IOCTL call to 0x903BC, inorder to invoke the HsmpOpCreatePlaceholders() vulnerable function.

The function will create JUSTASTRINGDnewfile.dll file but once the validation for the relName field is success, i.e. check that the relName does not contain any \ character, reaching the FltCreateFileEx2() function with relName set to JUSTASTRING\newfile.dll. Once this is set, the junction will create the file in C:\Windows\System32\newfile.dll.

Step 5: Once we have written a DLL in C:\Windows\System32\, then we can directly change the content of the DLL, so if a privileged service load this DLL we can escalate the privilege.

Patch Analysis

Analysing the patched HsmpOpCreatePlaceholders() function, instead of directly calling IoAllocateMdl() to allocate a memory descriptor list, it allocates a POOL_FLAG_PAGED pool called as Pool2 (also saved as Src) and then copies the data to that pool (Line 123). The old code path is still there for backward compatibility (Line 126).

image.png

Now all the user input values are retrieved directly from the newly allocated kernel memory (Src).

image.png

References: