About a year have passed since CVE-2019-0685 vulnerability that I reported to MSRC got patched. Initially I planned to write about it sooner but time flies so fast. So here is a long overdue blog post about the vulnerability.
CVE-2019-0685 is a reference count leak vulnerability that existed in DirectComposition component of win32k. It allows an attacker to force-free an kernel object by overflowing reference count to 0 while other kernel objects still have reference to it causing use-after-free. This vulnerability existed since Windows 10 RS4 until when it was patched in April 2019. This vulnerability was submitted to Windows Insider Preview Bug Bounty Program and was rewarded.
What is DirectComposition?
DirectComposition is a graphics component introduced in Windows 8. According to Microsoft, it enables high-performance bitmap composition with transforms, effects, and animations. Basically it abstracts everything (visual, transforms, effects, animations …) as kernel objects and make references with each other and serialize them to be sent to DWM to be displayed on screen.
You can watch this youtube video to get better understanding of the concepts.
Only public security research about DirectComposition that I found at that time was “Win32k Dark Composition: Attacking the Shadow Part of Graphic Subsystem” presented at CanSecWest 2017 by Peng Qiu, SheFang Zhong. You can reference history and attack surfaces in DirectComposition which also helped me a lot starting the research. If you haven’t read this, I advise to read before going through the post.
One of the reasons DirectComposition is an interesting attack surface is that in userland it can issue commands to the kernel directly and create new objects, set properties, set references etc.
How objects make reference to others
Graphic objects related to DirectComposition is called “Marshaler”s. Each Marshaler implements method called SetReferenceProperty to set reference to other Marshalers.
Marshalers implementing SetReferenceProperty()
To manage the life cycle of Marshaler objects, there is a reference count value inline in every object. When some resource references others, reference count of referenced Marshaler object is increased by one. This mechanism prevents objects accidentally being freed even though there are still references to it, possibly leaving dangling pointers. Freshly created Marshaler object has reference count set to 1.
There is a pattern that every Marshaler follows when setting a new reference to it.
- Checks whether the pointer to trying-to-reference Marshaler, which is passed to the function, is null.
- If not, checks if type of trying-to-reference Marshaler is compatible.
- If it is, saves the pointer to trying-to-reference Marshaler inline and increase reference count of referenced Marshaler by 1.
What if it tries to repeatedly set same reference? Kernel checks to see if reference property trying to currently set has been already set before by checking the value of inline member which is used to save pointer to referenced Marshaler (pointer saved at step 3 above). If not null, it means there is already a Marshaler being referenced, so it does clean up by decreasing the reference count of the previously referenced Marshaler and set a new reference following steps above. If it is trying to set a reference to the same Marshaler as previously done, then it does nothing (the pointer value will be the same). This is basically of reference counting mechanism works in DirectComposition.
If you look at the DirectComposition::CApplicationChannel::ReleaseResource() code, at the beginning it first releases the reference by decreasing the reference count by one and only if resulting value is 0 it actually frees the resource object (Marshaler with no reference has value of 1).
Although in normal cases there is no reference count leak, but I found exceptional one case that doesn’t decrement the reference count of previously set reference property before setting a new one. Also in this case, it doesn’t check if the newly setting reference property value (pointer to a Marshaler object) is same as the old one when trying to reference same Marshaler object multiple times. It just blindly set the new reference property and increases the reference count. This vulnerable resource marshaler is CConditionalExpressionMarshaler.
If you see, it doesn’t call ReleaseResource(), which means it doesn’t do any cleanup of previous reference. Also only checks the type of trying-to-reference Marshaler or whether the pointer passed is null. Trying to set reference property of CConditionalExpressionMarshaler with same Marshaler object multiple times will cause the reference counter of the referenced Marshaler object to keep increasing even though actually only one CConditionalExpressionMarshaler is referencing it. The reference count is a 4-byte value (in 64 bit also), so if the reference count is increased enough it will overflow to 0 after 0xFFFFFFFF, and ultimately become 1 which then the referenced Marshaler object can be freed by ReleaseResource() with CConditionalExpressionMarshaler still having dangling pointer to it.
Triggering the vulnerability
- Create a CConditionalExpressionMarshaler object
- Create a to-be referenced resource object (CExpressionMarshaler is the compatible Marshaler)
- Set reference property of CConditionalExpressionMarshaler object with the same CExpressionMarshaler 0xFFFFFFFF times (reference count will wrap to 1)
- Release CExpressionMarshaler (FREE)
- Release CConditionalExpressionMarshaler (will dereference dangling pointer to CExpressionMarshaler while releasing, USE)
And you get a crash. Here I have controlled the RAX register by spraying the kernel pool with fake object with same size, and RAX should point to the virtual function table of CConditionalExpressionMarshaler. So, while the kernel tries to do a virtual function call, the control flow can be hijacked. Also it doesn’t do check when doing virtual function call.
FAULTING_IP: win32kbase!DirectComposition::CApplicationChannel::ReleaseResource+4e ffffe491`36e0c2b6 488b4028 mov rax,qword ptr [rax+28h] CONTEXT: ffff810d3171ef50 -- (.cxr 0xffff810d3171ef50) rax=4141414141414141 rbx=ffffe4aec4ef7ca0 rcx=ffffe4aec4ef7ca0 rdx=ffffe4aec0771a78 rsi=0000000000000000 rdi=ffffe4aec07718a0 rip=ffffe49136e0c2b6 rsp=ffff810d3171f940 rbp=ffffe4aec07718a0 r8=0000000000000000 r9=0000000000000000 r10=0000000000000008 r11=ffffe4aec4fafaf0 r12=00007ff71d711460 r13=00007ff71d711460 r14=00007ff71d711460 r15=00000000000003e9 iopl=0 nv up ei pl zr na po nc cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00010246 win32kbase!DirectComposition::CApplicationChannel::ReleaseResource+0x4e: ffffe491`36e0c2b6 488b4028 mov rax,qword ptr [rax+28h] ds:002b:41414141`41414169=???????????????? Resetting default scope STACK_TEXT: ffff810d`3171f940 ffffe491`36f8d696 : ffffe4ae`c0684af0 ffffe4ae`c07718a0 ffffe4ae`c07718a0 ffffe70a`00000000 : win32kbase!DirectComposition::CApplicationChannel::ReleaseResource+0x4e ffff810d`3171f970 ffffe491`36e0c3df : ffffe4ae`c0684af0 00000000`00000000 ffff810d`3171fa80 ffffbcbf`fb8eb328 : win32kbase!DirectComposition::CConditionalExpressionMarshaler::ReleaseAllReferences+0x66 ffff810d`3171f9a0 ffffe491`36e465f8 : ffffe4ae`c0684af0 00000000`00000000 ffff810d`3171fa90 00000000`00010286 : win32kbase!DirectComposition::CApplicationChannel::ReleaseResource+0x177 ffff810d`3171f9d0 ffffe491`36e4651a : ffffe4ae`c07718a0 ffff810d`3171fb00 ffff810d`3171fa90 00007ff7`1d711460 : win32kbase!DirectComposition::CApplicationChannel::ReleaseResource+0x8c ffff810d`3171fa10 ffffe491`36e46cf6 : 00000000`000003e9 ffffe4ae`c07718a0 ffff810d`3171fb00 0000007a`be4ff720 : win32kbase!DirectComposition::CApplicationChannel::ReleaseAllResources+0x52 ffff810d`3171fa40 fffff802`3106d285 : 00007ff7`00000015 0000007a`be4ff720 00000000`00000000 ffffe4ae`c07718a0 : win32kbase!NtDCompositionReleaseAllResources+0x76 ffff810d`3171fa80 00007ff7`1d680055 : 00000000`00000000 00000000`00000000 0000007a`00000000 0057005c`00000000 : nt+0x1c2285 0000007a`be4fd298 00000000`00000000 : 00000000`00000000 0000007a`00000000 0057005c`00000000 00000000`00000000 : 0x00007ff7`1d680055
But it takes a lot of time to overflow the reference count, right?
You might wonder whether this vulnerability has any practical value since it takes time to issue 0xFFFFFFFF number of commands to the kernel. Well, DirectComposition allows userland to issue batch of commands in a single syscall. So you don’t need to issue 0xFFFFFFFF syscalls, which is slow, but issue small number of syscalls with large number of “set reference” commands in batches which will be extremely fast compared to doing 0xFFFFFFFF syscalls.