CVE-2024-38185
Multiple out-of-bounds read vulnerabilities exists in the License update functionality of Microsoft CLIPSP.SYS 10.0.22621 Build 22621, 10.0.26080.1 and 10.0.26085.1. A specially crafted license blob can lead to information disclosure. An attacker can use the NtQuerySystemInformation function call to trigger this vulnerability.
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 CLIPSP.SYS 10.0.26080.1
Microsoft CLIPSP.SYS 10.0.26085.1
CLIPSP.SYS - https://www.microsoft.com/en-us/windows/windows-11
8.4 - CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-125 - Out-of-bounds Read
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.
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 substential 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 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 canary build 26085.1.amd64fre.ge_release.240315-1352
There exists multiple out-of-bound reads vulnerabilities when handling license data of type 0x1. This vulnerability can be triggered by feeding to the SPCallServerHandleUpdateLicense
function a license filed that has been tampered with to include a malformed type 0x1 blob.
When calling SPCallServerHandleUpdateLicense
the data will eventually be past to an obfuscated function inside clipsp
and stored in an array (see 0x0001C00E8BA3):
License->field_0[v13].entry_size = v10->entry_size;
License->field_0[v13].entry_field2 = v10->field_2;
License->field_0[v13].entry_ptr = (__int64)&v10->first_byte;
Where v13
is the internal type associated with an entry type in the license file.
The problem is with how type 0x1
(internal value representation is 17
) is handled throughout the code especially during the DeviceLicenseInstall
function (see 0x001C00EDB0C). The data is expected to be a blob of 4100 bytes with a 4 bytes header and 4096 bytes of data used for generating key material / authentication data in the case of a device-bound license. However the size of the data is never verified and as such, out-of-bound reads can occur in multiple location of the code when provided with a type 0x1 blob whose data has been truncated. Be advised that in normal circumstances, a signature check would prevent from tampering with a license file, but that check can also be bypassed and was filed in TALOS-2024-1964 report.
First out-of-bound read occurs when dereferencing the first two bytes of the data provided as no size check is ever performed:
__int64 __fastcall __spoils<rdx,rax> License_get_version_or_size_for_type17_via_deref_clepstuff(LicenseStruct *License) // at 00001C00E7658
{
unsigned __int16 *entry_ptr; // rdx
__int64 result; // rax
entry_ptr = (unsigned __int16 *)License->field_0[17].entry_ptr;
result = 0i64;
if ( entry_ptr )
return *entry_ptr; //2 bytes out of bound read
return result;
}
A second and third out-of-bound reads occur reading the next two bytes in the payload used to determine which version of the code path to use: when going through the DeviceLicenseInstall
code path (at 001C00EDBF2) a pointer to the data is obtained, and passed to ReleaseKeyFromEncState_internal
which calls ClepReleaseKeyFromEncState
then reads the first two bytes of that payload:
Indeed, consider the following code:
Get the pointer:
//01C00E763C
__int64 __fastcall __spoils<rax,rcx> License_get_ptr_for_clep_type17(LicenseStruct *License)
{
return (License->field_0[17].entry_ptr + 2) & -(__int64)(License->field_0[17].entry_ptr != 0);
}
Use the data provided to release the key:
//In DeviceInstall at 001C00EDBF2
if ( (License->license_status_bitmask & 4) == 0 )
{
version_or_size_for_type17_via_deref_clepstuff = License_get_version_or_size_for_type17_via_deref_clepstuff(License);
ptr_for_clep_type17 = (int *)License_get_ptr_for_clep_type17(License);
status = ReleaseKeyFromEncState_internal(
UNUSED_ARG(),
[0] ptr_for_clep_type17,
version_or_size_for_type17_via_deref_clepstuff,
&hKey);
if ( status < 0 )
goto DONE;
...
}
The key release process:
_int64 __fastcall ReleaseKeyFromEncState_internal(
__unused __int64 a1,
int *ClepStuff,
int size,
BCRYPT_KEY_HANDLE *keyHandle)
{
int v6; // ebx
BCRYPT_ALG_HANDLE hAlgorithm; // [rsp+30h] [rbp+8h] BYREF
int v9; // [rsp+40h] [rbp+18h] BYREF
hAlgorithm = 0i64;
if ( size )
{
v6 = CLIP_open_alg(aAes, (PUCHAR)aChainingmodecb, &hAlgorithm, &v9);
if ( v6 >= 0 )
v6 = ((__int64 (__fastcall *)(int *, BCRYPT_ALG_HANDLE, BCRYPT_KEY_HANDLE *))ClepReleaseKeyFromEncState)(
ClepStuff,
hAlgorithm,
keyHandle);
if ( hAlgorithm )
BCryptCloseAlgorithmProvider(hAlgorithm, 0);
}
And the problematic derefencing at [1] and [2]:
//0001C00F0248
__int64 __fastcall ClepReleaseKeyFromEncState(int *ClepStuff, void *hAlgorithm, BCRYPT_KEY_HANDLE *keyHandle)
{
int v7; // [rsp+40h] [rbp+8h] BYREF
if ( (unsigned int)g_log_context > 5 )
{
v7 = *ClepStuff;
CLIP_log_stuff3(UNUSED_ARG(), a2, UNUSED_ARG(), UNUSED_ARG(), (ULONGLONG)&v7);
}
[1] if ( *ClepStuff == 2 )
return ClepTpmReleaseKeyFromEncState_version2((__int64)ClepStuff, hAlgorithm, keyHandle);
[2] if ( *ClepStuff == 4 )
return ClepTpmReleaseKeyFromEncState_version4_((__int64)ClepStuff, hAlgorithm, keyHandle);
return 0xC0000001i64;
The functions ClepTpmReleaseKeyFromEncState_version2
and ClepTpmReleaseKeyFromEncState_version4_
also process the data out of bound (the ClepStuff
variable is the same data we obtained at [0]).
Another series of out-of-bound reads occurs a little later in the DeviceLicenseInstall
function assuming previous steps where successful (which may depend on the data being read out-of-bound):
The same data is obtained in the clep_stuff
pointer and passed to the generate_license_auth_data
function that will then dereference the data to determine its type and how to handle it, and then copy part of its content into the context->license_auth_data
buffer:
//00001C00EDD5A
clep_stuff = (_DWORD *)License_get_ptr_for_clep_type17(License);
status = generate_license_auth_data(clep_stuff, deviceId, deviceIdSize, &context->license_auth_data);
//001C00F0218
__int64 __fastcall generate_license_auth_data(_DWORD *clep_stuff, char *deviceId, __int64 deviceIdSize, __int64 *a4)
{
if ( *clep_stuff == 2 ) //oob-read here
return generate_license_auth_data_v2(clep_stuff, deviceId, deviceIdSize, a4);
if ( *clep_stuff == 4 ) //oob-read here
return generate_license_auth_data_v4((__int64)clep_stuff, (__int64)deviceId, deviceIdSize, a4);
return 3221225473i64;
For example, in the case the data is of type 4, we can see:
//001C00F1CB0
__int64 __fastcall generate_license_auth_data_v4(
__int64 clep_stuff,
__int64 deviceId,
__int64 deviceIdSize,
__int64 *pDest)
{
int status; // r8d
__int64 pDest_1; // rdx
status = CLIP_allocate_buffer(148i64, pDest);
if ( buffer >= 0 )
{
pDest_1 = *pDest;
*(_OWORD *)pDest_1 = *(_OWORD *)(clep_stuff + 1722);
*(_OWORD *)(pDest_1 + 16) = *(_OWORD *)(clep_stuff + 1738);
*(_OWORD *)(pDest_1 + 32) = *(_OWORD *)(clep_stuff + 1754);
*(_OWORD *)(pDest_1 + 48) = *(_OWORD *)(clep_stuff + 1770);
*(_OWORD *)(pDest_1 + 64) = *(_OWORD *)(clep_stuff + 1786);
*(_OWORD *)(pDest_1 + 80) = *(_OWORD *)(clep_stuff + 1802);
*(_OWORD *)(pDest_1 + 96) = *(_OWORD *)(clep_stuff + 1818);
*(_OWORD *)(pDest_1 + 112) = *(_OWORD *)(clep_stuff + 1834);
*(_OWORD *)(pDest_1 + 128) = *(_OWORD *)(clep_stuff + 1850);
*(_DWORD *)(pDest_1 + 144) = *(_DWORD *)(clep_stuff + 1866);
}
return (unsigned int)status;
}
The case of type 2 (generate_license_auth_data_v2
) also reads data out of bound but is left out for brevity of this report.
It’s important to note that the data read out-of-bound into the context->license_auth_data
is also used in the ClipSpGetLicenseChallange
function (at 00001C00B7E70) to generate a challenge when the appropriate call is made (NtQuerySystemInformation
with the SystemPolicyInformation
and command_id=105
) thus potentially leaking back to userland part of the the data that was read out of bound.
2024-04-08 - Vendor Disclosure
2024-08-13 - Vendor Patch Release
2024-08-13 - Public Release
Discovered by Philippe Laulheret of Cisco Talos.