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.