CVE-2021-26900: Privilege Escalation Via a Use After Free Vulnerability In win32k

May 04, 2021 | Guest Blogger

In March 2021, Microsoft released a patch to correct a vulnerability in the Windows kernel. The bug could allow an attacker to execute code with escalated privileges. This vulnerability was reported to the ZDI program by security researcher JeongOh Kyea (@kkokkokye) of THEORI. He has graciously provided this detailed write-up and Proof-of-Concept detailing ZDI-21-331/CVE-2021-26900 and how it bypasses the fix for CVE-2020-1381, which was patched in July 2020.


DirectComposition

The DirectComposition component was added in Windows 8 and enables efficient support for graphical effects such as image conversion and animations. A presentation on finding vulnerabilities in DirectComposition was given by @360Vulcan at CanSecWest 2017 - Win32k Dark Composition [PDF].

DirectComposition can be accessed via win32k system calls that begin with NtDComposition. Before Windows 10 RS1, the caller makes a separate system call for each action, such as creating or releasing a resource. After Windows 10 RS1, these are merged into one system call, NtDCompositionProcessChannelBatchBuffer, which processes several commands in batch mode. The work presented by @360Vulcan at CanSecWest 2017 fuzzes this function to find vulnerabilities. Since then, many vulnerabilities related to DirectComposition have been discovered, including a Pwn2Own bug, CVE-2020-1382.

There are three essential system calls for triggering any DirectComposition vulnerability: NtDCompositionCreateChannel, NtDCompositionProcessChannelBatchBuffer and NtDCompositionCommitChannel.

To create DirectComposition objects, the caller must first create a channel using the NtDCompositionCreateChannel system call.

After creating the channel, several commands can be sent using the NtDCompositionProcessChannelBatchBuffer system call. Each command has its own format with various sizes.

The mapped section address, pMappedAddress, is used for storing a batch of commands. After storing several commands at pMappedAddress, the caller can invoke NtDCompositionProcessChannelBatchBuffer to process the commands.

To trigger the vulnerability, we need to use 3 commands: CreateResource, SetResourceBufferProperty, and ReleaseResource.

First, CreateResource is used to create a specific type of object. The size of the CreateResource command is 16 bytes and the format is as follows. The resource type may be different according to the Windows version. You can easily get the resource type number by analyzing the win32kbase!DirectComposition::CApplicationChannel::CreateResource function.

Second, SetResourceBufferProperty is used to set the data for a target object. The size and format of this command depends on the resource type.

Finally, ReleaseResource is used to release the resource. The size of the ReleaseResource command is 8 bytes and the format is as follows.

NtDCompositionCommitChannel system call sends these commands, after serialization, to the Desktop Window Manager (dwm.exe) through the Local Procedure Call (LPC) protocol. After receiving the commands from the kernel, the Desktop Window Manager (dwm.exe) will render these commands to the screen.

The Vulnerability

The CVE-2021-26900 vulnerability is related to CInteractionTrackerBindingManagerMarshaler and CInteractionTrackerMarshaler. This vulnerability is very similar to CVE-2020-1381, so we will explain CVE-2020-1381 first before discussing CVE-2021-26900.

CVE-2020-1381

CVE-2020-1381/ZDI-20-872 was patched in July 2020. The vulnerability occurs in the DirectComposition::CInteractionTrackerBindingManagerMarshaler::SetBufferProperty function, which is the handler for the SetResourceBufferProperty command of a CInteractionTrackerBindingManagerMarshaler object.

The CInteractionTrackerBindingManagerMarshaler object takes 12 bytes as data for a SetResourceBufferProperty command. The data consists of three DWORDs: resource1_id, resource2_id, and new_entry_id

This function first retrieves resources from resource1_id and resource2_id specified by the user ([1]). It then checks that the type of each of these resources is 0x58, which is the resource type of CInteractionTrackerMarshaler ([2]).

Next, the pair of CInteractionTrackerMarshaler resources is appended to the tracker list of the CInteractionTrackerBindingManagerMarshaler object. As indicated by their names, the two object types, CInteractionTrackerMarshaler and CInteractionTrackerBindingManagerMarshaler, are related to each other. The CInteractionTrackerBindingManagerMarshaler object keeps a list of pairs of CInteractionTrackerMarshaler objects, and each of these CInteractionTrackerMarshaler objects has a pointer back to the CInteractionTrackerBindingManagerMarshaler object.

When the DirectComposition::CInteractionTrackerBindingManagerMarshaler::SetBufferProperty function is called for the first time, the tracker pair is added to the list because the list is empty.

To add the new entry to the tracker list, the size of tracker_list is increased by 1 and the new tracker pair data is written ([3]). Then, it sets a reference from each CInteractionTrackerMarshaler object to the CInteractionTrackerBindingManagerMarshaler ([4]) object using the DirectComposition::CInteractionTrackerMarshaler::SetBindingManagerMarshaler function, which is as follows.

The DirectComposition::CInteractionTrackerMarshaler::SetBindingManagerMarshaler function updates tracker->binding_obj to a new CInteractionTrackerBindingManagerMarshaler object.

After appending the CInteractionTrackerMarshaler object pair to tracker_list, the relationship between the CInteractionTrackerMarshaler object and the CInteractionTrackerBindingManagerMarshaler object is as follows:

Because they are referenced by each other, the references must be cleared when an object is released. Let's see the situation if the CInteractionTrackerMarshaler object is released. To release the resources related with the CInteractionTrackerMarshaler object, the DirectComposition::CInteractionTrackerMarshaler::ReleaseAllReferences function is called.

If the CInteractionTrackerMarshaler object has a binding to a CInteractionTrackerBindingManagerMarshaler object, DirectComposition::CInteractionTrackerBindingManagerMarshaler::RemoveTrackerBindings is called to remove the corresponding tracking entry.

In DirectComposition::CInteractionTrackerBindingManagerMarshaler::RemoveTrackerBindings, if one of the two tracker objects in the entry has a resource id that matches the object being deleted, the entry_id of that entry will be set to zero. Finally, it calls DirectComposition::CInteractionTrackerBindingManagerMarshaler::CleanUpListItemsPendingDeletion to clean those entries that have entry_id equal to zero.

However, what happens if a single CInteractionTrackerMarshaler is added to multiple CInteractionTrackerBindingManagerMarshaler tracker lists? Because there is no check while adding a new entry, the CInteractionTrackerMarshaler object, which is already bound to a CInteractionTrackerBindingManagerMarshaler object, can become bound to a second CInteractionTrackerBindingManagerMarshaler object.

The picture below shows that situation:

In this situation, if Tracker1 is freed, only the entry in TrackerBindingB is removed because Tracker1 is bound to TrackerBindingB. Eventually, the entry of the TrackerBindingA object has the freed object pointer.

This dangling object pointer is later dereferenced in the DirectComposition::CInteractionTrackerBindingManagerMarshaler::EmitBoundTrackerMarshalerUpdateCommands function, which can be triggered via the NtDCompositionCommitChannel system call. This system call references the resource during serialization of the batched commands.

The function shown above calls the EmitUpdateCommands method for objects in the tracker_list. The freed object will get referenced in the process, which leads to a use-after-free vulnerability.

CVE-2021-26900

CVE-2021-26900/ZDI-21-331 will re-trigger the above vulnerability by bypassing the patch of CVE-2020-1381. The patch of CVE-2020-1381 is as follows.

The part marked with [*] was added to check the binding_obj of the CInteractionTrackerMarshaler object. it checks that the CInteractionTrackerMarshaler is not already bound to another CInteractionTrackerBindingManagerMarshaler.

However, this patch can be bypassed by updating the tracker entry. Let's see the code for updating the tracker entry:

First, the above code tries to find the entry that has tracker pair, (tracker1, tracker2) or (tracker2, tracker1). If there is an entry, the entry_id is updated to new_entry_id ([1]).

The most important part related to this vulnerability is [2]. When the new_entry_id is zero, the CInteractionTrackerBindingManagerMarshaler object regards this entry as not necessary. To handle this entry, it calls the DirectComposition::CInteractionTrackerBindingManagerMarshaler::RemoveBindingManagerReferenceFromTrackerIfNecessary function. However, this function will not remove this entry. It only removes the binding.

The above function tries to find an entry whose resource id is tracker1_id or tracker2_id. If there are no other entries whose resource id is tracker1_id or tracker2_id, it means that the two objects don't have to reference each other. Thus, the DirectComposition::CInteractionTrackerMarshaler::SetBindingManagerMarshaler function is called with a NULL binding object to remove the binding of the CInteractionTrackerMarshaler object.

However, the pointer of tracker1 or tracker2 remains in the tracker list although the binding from CInteractionTrackerMarshaler to CInteractionTrackerBindingManagerMarshaler is removed. Updating entry with a zero new_entry_id produces the state shown below:

Now, the binding_obj of the CInteractionTrackerMarshaler object is set to zero, which can bypass the patch of CVE-2020-1381. If we bind tracker1 to another CInteractionTrackerBindingManagerMarshaler object, the state is changed as follows.

Next, updating the entry_id in TrackerBindingA to a non-zero value will produce the same state as in CVE-2020-1381

The Patch

The patch applied to win32kbase.sys to fix the vulnerability, CVE-2021-26900, is as follows:

The patch applies to the code that adds the entry to tracker_list, modifies the entry_id, and releases the resource.

When modifying the entry_id, the binding is not removed although the entry_id is 0.

Next, when adding the entry, the listref field is added to the resource. This field is used to free the object properly when the same objects are inserted to tracker_list.

Finally, when releasing the resource, the binding is actually removed in the DirectComposition::CInteractionTrackerBindingManagerMarshaler::CleanUpListItemsPendingDeletion function.

Proof-of-concept code demonstrating this vulnerability can be found here.


Thanks again to JeongOh Kyea for providing this thorough write-up and PoC. He has contributed several Windows bugs to the ZDI program over the last couple of years, and we certainly hope to see more submissions from him in the future. Until then, follow the team for the latest in exploit techniques and security patches.