Talos Vulnerability Report

TALOS-2024-1988

Microsoft CLIPSP.SYS License Update out-of-bounds read vulnerability

August 13, 2024
CVE Number

None

SUMMARY

An out-of-bounds read vulnerability exists in the License Update functionality of Microsoft CLIPSP.SYS 10.0.22621 Build 22621. A specially crafted license blob can lead to privilege escalation. An attacker can use the NtQuerySystemInformation function call to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

Microsoft CLIPSP.SYS 10.0.22621 Build 22621
Microsoft Windows 11 Pro 23H2 22631.3296
Microsoft Windows 11 Pro 24H2 Insider Preview 26085.1
Microsoft Windows 11 Pro 24H2 Insider Preview 26100.1

PRODUCT URLS

CLIPSP.SYS - https://www.microsoft.com/en-us/windows/windows-11 Windows - https://www.microsoft.com/en-us/windows/

CVSSv3 SCORE

7.4 - CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-170 - Improper Null Termination

DETAILS

CLIPSP.SYS is a driver used to implement Client License System Policy on Windows 10 and 11. It provides the functions used when handling most of the requests involving licensing, notably the implementation of many use cases involved with the SystemPolicyInformation class used in conjunction with NtQuerySystemInformation.

Context

When calling NtQuerySystemInformation with the SystemPolicyInformation class, ntoskrnl will call ExHandleSPCall2 that will process the data provided. The format is mostly undocumented and encrypted using Microsoft’s Warbird. Upon decryption of the data provided, a call handler is invoked based on the command_id provided and dispatches the payload to the relevant function (e.g. SPCallServerHandleClepKdf, SPCallServerHandleUpdateLicense, etc.). A substantial amount of these functions are wrapers around clipsp functions that are stored as function pointers in the nt!g_kernelCallbacks globlal array.

The SPCallServerHandleUpdateLicense (command_id:100) will accept a License blob whose format is also undocumented. Once installed, these license files are stored in the HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\{7746D80F-97E0-4E26-9543-26B41FC22F79}\{A25AE4F2-1B96-4CED-8007-AA30E9B1A218} key, only accessible to the SYSTEM user. The format of this license file is TLV (Tag-length-value) following this format:

struct __unaligned __declspec(align(1)) LicenseParsing_entry
{
  __int16 type;
  __int16 reserved;
  int entry_size;
  char value[ANYSIZE_ARRAY]; //expected to be of size entry_size
};

The vulnerability

The code snippets below are decompiled output and variables names were assumed from context or retrieved from public symbol servers, sdk, etc. The addresses provided are for the build 10.0.22621.3374 of ClipSp, but was also tested on Windows Canary build 26100.1.amd64fre.ge_release.240331-1435

There exists multiple out-of-bound reads instances when handling license data of type 0xCE (the license’s PFN) that could possibly be turned into an out-of-bound write situation. The purpose of the type 0xCE is to set the PFN (package full/family name) of the license, which is a unicode string representing the package name. This vulnerability can be triggered by feeding to the SPCallServerHandleUpdateLicense function a license blob that has been tampered with to include a non-null terminated PFN.

While other strings are checked for null-termination, ClipSP fails to do so for the PFN string which can lead to an out-of-bound read when handling the data (for brevity, we only share one example of such instance, but we counted 7 locations that use the PFN in a potentially vulnerable way).

For instance, if one installs a license with a tampered PFN, and that license has an associated License whose Keyholder information (blob type 0xDD) requires to match a given expression (data of type 1 inside that keyholder blob), the vulnerable code path can be followed:

  1. While verifying the license being installed, its associated keyId is retrieved and the relevant keyholder license is retrieved from cache (the word keyholder might not match internal nomenclature):

        //00000001C00FB152  
       keyIdSize = LicenseStruct_get_KeyIdSize_for_type23(License_);
       keyId = (char *)LicenseStruct_get_KeyID_ptr_for_type23(License_);
       status = Keyholder_get_from_cache(context, keyId, keyIdSize, &keyHolder);
       if ( status < 0 )
       {
         Keyholder = keyHolder;
         goto DONE;
       }
       licenseType = LicenseStruct_get_licensetype_dref_at_2_for_type15(License);
       Keyholder = keyHolder;
       if ( licenseType == 8 || (status = License_verify_keyholder_and_keys(License, keyHolder), status >= 0) )
    
  2. License and keyholder are being matched:

    //0001C00FD674   
    _int64 __fastcall License_verify_keyholder_and_keys(LicenseStruct *License, Keyholder *keyHolder)
    {
    (...)
      status = 0;
      if ( keyHolder->maybe_depth_or_priority_max_is_10 )
      {
        if ( (unsigned int)LicenseStruct_get_licensetype_dref_at_2_for_type15(License) != 8 )// if value is 8 we're good
        {
          for ( i = 0i64; (unsigned int)i < keyHolder->nKeyslots; i = (unsigned int)(i + 1) )
          {
            keySlot = keyHolder->Keyslots[i];
            keyslot_type = keySlot->keyslot_type;
            if ( keyslot_type )
            {
              type_minus1 = keyslot_type - 1;
              if ( type_minus1 )
              {
                    ...
                }
              }
              else                                  // type 1
              {
    [1]            pfn_ptr = (WCHAR *)LicenseStruct_get_PFN_ptr_type14(License);
    [2]            status = check_if_expression_in_pfn(pfn_ptr, (PWSTR)&keySlot->blob_first_byte);
                if ( status < 0 )
                  return (unsigned int)status;
              }
            }                                       // type 0
            else if ( (unsigned int)LicenseStruct_get_licensetype_dref_at_2_for_type15(License) != keySlot->blob_first_byte )
            {
              return (unsigned int)STATUS_NO_MATCH;
            }
          }
        }
      }
      else
      {
        return (unsigned int)STATUS_NO_MATCH;
      }
      return (unsigned int)status;
    }
    
  3. The PFN is retrieved at [1] and the so called Expression from the type 1 keyslot data is then used to see if the Expression is contained inside the PFN by calling the function at [2]:

    // 00000001C00FD51C
    _int64 __fastcall check_if_expression_in_pfn(PWSTR PFN, PWSTR expression) { __int64 pfn_len; // rdi __int64 expression_len; // rsi void *v6; // r14 int status; // ebx USHORT v8; // di int v9; // eax __int64 v10; // rdi wint_t v11; // ax struct _UNICODE_STRING Expression; // [rsp+20h] [rbp-20h] BYREF struct _UNICODE_STRING Name; // [rsp+30h] [rbp-10h] BYREF __int64 v15; // [rsp+90h] [rbp+50h] BYREF WCHAR *ExpressionBuffer; // [rsp+98h] [rbp+58h] BYREF

    pfn_len = -1i64;
    ExpressionBuffer = 0i64;
    v15 = 0i64;
    expression_len = -1i64;
    v6 = 0i64;
    Expression = 0i64;
    Name = 0i64;
    do
      ++expression_len;
     while ( expression_[expression_len] ); 
    status = CLIP_allocate_buffer((unsigned __int16)(2 * (expression_len + 1)), (__int64 *)&ExpressionBuffer);
    if ( status >= 0 )
    {
      do
        ++pfn_len;   [3] while ( PFN[pfn_len] );  //OOB READ HAPPENS HERE   [5] v8 = 2 * (pfn_len + 1);
      v9 = CLIP_allocate_buffer(v8, &v15);
      v6 = (void *)v15;
      status = v9;
      if ( v9 >= 0 )
      {
        Expression.Buffer = ExpressionBuffer;
        Expression.MaximumLength = 2 * (expression_len + 1);
        Name.MaximumLength = v8;
        Name.Buffer = (PWSTR)v15;
        status = unciode_copy_wstring(&Expression, (char *)expression_);
        if ( status >= 0 )
        {
          status = unciode_copy_wstring(&Name, (char *)PFN);
          if ( status >= 0 )
          {
            if ( (_DWORD)expression_len )
            {
              v10 = 0i64;
              expression_len = (unsigned int)expression_len;
              do
              {
                v11 = towupper(Expression.Buffer[v10]);
                Expression.Buffer[v10++] = v11;
                --expression_len;
              }
              while ( expression_len );
              v6 = (void *)v15;
            }   [4]         if ( !FsRtlIsNameInExpression(&Expression, &Name, 1u, 0i64) )
              status = STATUS_NO_MATCH;
          }
        }
      }
    }
    ExFreePoolWithTag_noTag(ExpressionBuffer);
    ExFreePoolWithTag_noTag(v6);
    return (unsigned int)status;   }
    

Here we can see that an OOB read can occur at [3] given that the PFN can be non-null-terminated; also because Expression can be attacker controlled (by tampering with the relevant license), it is possible to infer memory content read OOB at [4] as the match/no match information gets propagated to the user installing the license blob.

Side note on the potential for an OOB Write to occur

As a sidenote, although there’s a potential for integer overflow / integer truncation at [5], in this case this is unconsequential as the truncated size value in every subsequent reference to that size field in the remaining of the function. However other functions that process PFN strings show similar pattern and may act on the non-truncated 64-bit integer size, which could lead to an OOB Write if the length of the data read without crashing exceeds MAX_UINT. This is because the memory allocation function CLIP_allocate_buffer takes a unsigned int variable for the size parameter. For instance, such pattern can be seen here while processing EFS headers:

// 00001C00FDC0C  
   pfn = (wint_t *)LicenseStruct_get_PFN_ptr_type14(a1.License);
    status = wcpy_and_lower_risk_overflow_if_large_str(pfn, -1, 0, &lowerPFN, 0i64);

// 001C00B8BE8   

__int64 __fastcall wcpy_and_lower_risk_overflow_if_large_str(wint_t *wstr, int len, int bToLower, char **pBufferOut, _DWORD *pLen)
{
  __int64 len_; // rbx
  NTSTATUS status; // ebp
  char *v10; // rdi
  char *v11; // r15
  __int64 v12; // r12
  signed __int64 v13; // r14
  _DWORD *pLen_1; // rax
  char *v15; // rcx
  char *buffer_copy; // [rsp+50h] [rbp+8h] BYREF

  buffer_copy = 0i64;
  LODWORD(len_) = len;
  status = 0;
  v10 = 0i64;
  if ( !wstr )
  {
LABEL_12:
    *pBufferOut = v10;
    v15 = 0i64;
    goto LABEL_13;
  }
  if ( len == -1 )
  {
    len_ = -1i64;
    do
      ++len_;
    while ( wstr[len_] ); //len_ is a QWORD
  }
  status = CLIP_allocate_buffer(2 * len_ + 2, &buffer_copy); // if len_ == (MAX_UINT+1)/2 we allocate a 2-byte buffer
  if ( status >= 0 )
  {
    v10 = buffer_copy;
    if ( bToLower )
    {
      if ( (_DWORD)len_ )
      {
        v11 = buffer_copy;
        v12 = (unsigned int)len_;
        v13 = (char *)wstr - buffer_copy;
        do
        {
          *(_WORD *)v11 = towlower(*(_WORD *)&v11[v13]);  // OOB Write HERE
          v11 += 2;
          --v12;
        }
        while ( v12 );
      }
    }
    else
    {
      some_memcpy(buffer_copy, (char *)wstr, 2i64 * (unsigned int)len_);  // size argument of memcpy is QWORD, 2i64*(unsigned int)len_  is  (MAX_UINT+1), not 0, so, OOB WRITE here as well.
    }

Impact

The SPCallServerHandleUpdateLicense function can be called from a low privilege user or from within a LPAC container and does not require any special permission. As such, the vulnerabilty described above can be used as an Escalation of Privilege primitive either standalone or for an (LPAC) Sandbox Escape and as such should be considered in the sandbox escape attack scenario.

TIMELINE

2024-05-03 - Vendor Disclosure
2024-07-09 - Vendor Patch Release
2024-08-13 - Public Release

Credit

Discovered by Philippe Laulheret of Cisco Talos.