By Philippe Laulheret
ClipSP (clipsp.sys) is a Windows driver used to implement client licensing and system policies on Windows 10 and 11 systems.
Cisco Talos researchers have discovered eight vulnerabilities related to clipsp.sys ranging from signature bypass to elevation of privileges and sandbox escape:
- TALOS-2024-1964 (CVE-2024-38184)
- TALOS-2024-1965 (CVE-2024-38185)
- TALOS-2024-1966 (CVE-2024-38186)
- TALOS-2024-1968 (CVE-2024-38062)
- TALOS-2024-1969 (CVE-2024-38187)
- TALOS-2024-1970 (CVE-2024-38062)
- TALOS-2024-1971 (CVE-2024-38062)
- TALOS-2024-1988 (CVE-2024-38062)
This research project was also presented at both HITCON and Hexacon. A recording of the latter’s presentation is embedded at the end of this article.
What is ClipSp?
ClipSp is a first-party driver on Microsoft Windows 10 and 11 that is responsible for implementing licensing features and system policies, and as such it is one of the main components of the Client Licensing Platform (CLiP). Little is known about this driver; while most Microsoft drivers and DLLs have publicly available debug symbols, in the case of ClipSp, those were removed from Microsoft's symbol server. Debug symbols provide function names and other related debug information that can be leveraged by security researchers to infer the intent behind the many functions of a binary; their absence hinders that. Surprisingly, the driver is also obfuscated, a very rare occurrence in Microsoft binaries, likely to deter reverse engineering even further. Limited public research exists, much of which either predates our findings or was released in response to our reports. The latter research also shares symbols from an older version of ClipSp, which could be a useful springboard for anyone wanting to research this driver. The most interesting aspect of this software involves implementing features related to licensing Windows applications from the Windows App store and activation services for Windows itself.
Deobfuscation
The driver is obfuscated with Warbird, which is Microsoft’s proprietary obfuscator. Luckily, past research comes in handy, and we can adapt to suit our needs. The plan to deobfuscate the driver is to leverage the binary emulation framework Qiling, to emulate the part of the driver responsible for deobfuscating the obfuscated sections, and dump the executable memory range to import it into our favorite reversing tool.
During normal operation, the obfuscation appears as follows:
We can see that a decrypt function is called twice with different parameters, followed by a call to the actual function being deobfuscated and, finally, two calls to re-obfuscate the relevant section.
Using Ida Python, we can track all the references to the decrypt functions (there are actually two distinct functions), and recover their arguments by looking at the instructions that precede the function call where the RCX and RDX registers are being assigned. Per calling conventions, these two registers are the first and second arguments of the function. Then, we can feed this information to our modified Qiling script to emulate the decryption functions and dump the whole deobfuscated binary. Once the driver is deobfuscated, we can start reversing it to understand how Windows communicates with the driver, understand various business logic elements, and look for vulnerabilities.
Driver communication
Usually, drivers either register a device that can be reached from userland or export the functions that are meant to be used by other drivers. In the ClipSp case, things behave slightly differently. The driver exports a “ClipSpInitialize” function that takes a pointer to an array of callback functions that get populated by ClipSp, to then be used by the calling driver to invoke ClipSp functionalities. Grepping for “ClipSpInitialize” throughout the System32 folder shows that the best candidate for using ClipSp is “ntoskrnl.exe”, followed by a handful of filesystem drivers that use a limited amount of ClipSp functions. For the rest of this report, we will focus on how “ntoskrnl” interacts with ClipSp.
Analyzing the cross-references within the Windows’ kernel to ClipSp functions, it becomes clear that, to interact with them, a call to “NtQuerySystemInformation” with the SystemPolicy class is required. Other binaries in the CLiP ecosystem will issue these system calls, while also providing a remote procedure call (RPC) interface to decouple other software from the undocumented API. However, nothing stops us from interacting with the “NtQuerySystemIformation” endpoint directly, which becomes a handy trick to bypass some of the additional checks that are enforced by the intended RPC client library.
Obfuscated structures
Unfortunately for us, looking at how a legitimate binary interacts with the SystemPolicy class, we can see the following (from wlidsvc!DeviceLicenseFunctions::SignHashWithDeviceKey):
This is another layer of obfuscation that encapsulates the data passed over to the API. The idea here is that a network of binary transformations (also known as a Feistel cipher) is used to encrypt the data with the various operations inline in the code (as seen above). Part of the API call will provide the list of operations that were used, and the kernel will call them directly with the appropriate parameters to decrypt the data. As such, the easier approach to dealing with this is to simply rip out both the encryption code and the associated parameters and re-use them in our own invocation of the API. Copying and pasting the decompiler’s output into Visual Studio is a little tedious but usually works fine. Before returning from the syscall, the resulting data is obfuscated in a similar fashion, and, once again, ripping out the data from a working implementation is the most straightforward way to deal with it. Overall, the data format looks as such:
The inner payload (left) is an array of size-value entries that contain the command number that needs to be executed, followed by the Warbird material used to encrypt the reply from the kernel, and finally command-specific data that depends on which ClipSp function is being invoked.
This data is then encapsulated into a structure that mostly specifies the number of entries there are in the provided array and the whole thing then gets encrypted. The remaining Warbird data in the righ-most part of the diagram is to instruct the kernel how to decrypt the provided data.
Here’s our best guess at the various available commands:
Most of them call into ClipSp, but a few (especially in the <100 range) may be solely handled by the Windows kernel.
Sandbox considerations
Microsoft provides a tool to test if a piece of code can be run within a low-privilege context called a Less Privileged Application Container (LPAC) sandbox. Using this with our proof of concept, we can confirm that ClipSp’s APIs are actually reachable from an LPAC context. This is particularly interesting as these application containers are usually used to sandbox high-risk targets, such as parsers and browser rendering processes. As such, any elevation of privilege vulnerabilities we could find would likely double as sandbox escapes as well.
Processing licenses
Throughout the reversing process, we observed that the license files handled by ClipSp were quite interesting. They are usually obtained silently from Microsoft when interacting with UWP applications (both coming from the App Store and those installed by default, such as Notepad). They can also be used for other purposes, such as Windows activation, hardware binding, and generally providing cryptographic material for various applications.
At first, license files appear to be opaque blobs of data that are installed via the “SpUpdateLicense” command. This can be invoked following the process described above with the command “_id = 100”. Existing licenses are stored in the Windows registry at the following location:
HKLN\SYSTEM\CurrentControlSet\Control\{7746D80F-97E0-4E26-9543-26B41FC22F79}
Only the SYSTEM user can access this registry key. From an elevated prompt, the following command can open regedit as SYSTEM:
PsExec64.exe -s -i regedit
The format for these licenses is mostly undocumented, but looking at how they are being parsed is pretty informative. These licenses are in a tag-length-value (TLV) format, where the list of authorized tags is contained in an array of tuples of the form (tag, internal_index) hardcoded inside ClipSp. Upon parsing, a pointer to each valid TLV entry is stored in an array at the location indicated by the internal_index:
Signature bypass (TALOS-2024-1964)
Licenses are signed by various signing authorities whose public keys are hardcoded in ClipSp. Verification code looks as such:
The “entry_of_type_24” value is a pointer saved during the parsing of the license and points to its signature. The difference between “entry_of_type_24” and “License_data” is pointer arithmetic used to count the number of bytes from the beginning of the license blob up to its signature.
During the parsing, this looks as such:
If the internal index associated with the entry’s tag is 24, then the processing loop is temporarily exited. A pointer to the signature is saved, and if more data remains, the license processing is resumed.
We can see that this approach is flawed: If there is data after the license’s signature, it will still be parsed but not checked against the signature, effectively enabling an attacker to bypass the signature check of any license as long as they can get one that is already signed with the proper signing authority.
Out-of-bound read vulnerabilities (TALOS-2024-1965,TALOS-2024-1968, TALOS-2024-1969, TALOS-2024-1970, TALOS-2024-1971, TALOS-2024-1988)
We can cross reference where the license structure and its array of pointers to the TLV data is being used, and what we find is many wrapper functions that return either the length/size of a given entry or the data associated with it. In most cases, this is done in a secure fashion, but there are a few entries that make assumptions on the size of the data provided in the license blob, which leads to a handful of out-of-bound read vulnerabilities. An example of such vulnerabilities can be seen in the following screenshots:
These two functions retrieve either the size of the DeviceID field or its content. However, if the data is formatted in such a way that line 11 is reached (i.e., no entry of type 5 in the license provided) then the data field of entry 18 is used to provide both size and value by dereferencing its pointer, without checking if enough data was provided for that. For instance, if we append a DeviceID entry (type 18) at the end of a valid license blob, but make it so its data field is only one byte long, then the “get_DeviceIDSize” function will read one byte out of bound, as it is expecting two bytes of data. Furthermore, any function that calls “get_DeviceID” will receive a pointer that is pointing one byte past the end of the license file and will likely act on wrong information from the “get_DeviceIDSize” function for further out of bound (OOB)-read problems.
Turning an OOB-read into an OOB-write (TALOS-2024-1966)
If we look specifically at the case described above where the DeviceIdSize field can be read out of bound, this creates a particularly interesting situation where the expected size of the DeviceID object can change throughout its lifetime if the data immediately adjacent in memory changes in a meaningful way. The first byte of data after the license blob will also be read as the leading byte of the (unsigned short) value defining the size of the DeviceID. Looking at how these two functions are used in ClipSp, we can see that during the installation of a hardware license, the following happens:
We can see multiple calls to the “get_DeviceIDSize” function, with one providing the size field to a memory allocation routine, while another call is used as a parameter to a “memcpy”. If the size field changes in between the two calls, this may lead to an out-of-bounds write vulnerability.
Exploiting a vulnerability like this is far from trivial, as one would have to win a race condition between the two fetches while being able to shape the PagedPool heap in such a way that there’s meaningful data located right after the malicious license blob.
Conclusion
As we have just seen, obfuscated code can hide low hanging fruit, trivial memory corruptions, and simple logic bugs. In the case of ClipSp, this issue is even more serious, as this attack vector may lead to sandbox escapes and potentially significant impact to the compromised user.
As such, this is a reminder for security researchers on the value of taking the less traveled path, even if it begins with a bramble of Feistel functions. And for the software engineers and project managers who decide to leverage obfuscation for their projects, this is also a stark reminder that this approach may hinder normal bug finding processes that would detect trivial bugs early on.