CVE-2020-1015 Analysis
This post is an analysis of the April 2020 security patch for CVE-2020-1015. The bug was reported by Shefang Zhong and Yuki Chen of the Qihoo 360 Vulcan team. The description of the bug from Microsoft:
An elevation of privilege vulnerability exists in the way that the User-Mode Power Service (UMPS) handles objects in memory. An attacker who successfully exploited the vulnerability could execute code with elevated permissions.
Microsoft assigned this bug an exploitability rating of 2. Information pulled from the Microsoft Exploitability Index shows this means an attacker would likely have difficulty creating the code, requiring expertise and/or sophisticated timing, and/or varied results when targeting the affected product.
This bug is likely not the most ideal candidate for a fully functioning and reliable exploit. Especially when taking in consideration other EoPs from the April 2020 security patches with an exploitability rating of 1. In any case, it will be interesting to see why the exploitability rating was assigned, and the root cause of the bug.
The remainder of this post will follow the steps I took to analyze the security patch and put together simple proof of concept code for a working crash.
Patch Diffing Windows 10 1903 umpo.dll
To get started we will first need to grab a patched and non-patched version of the relevant file. While not a foolproof method, search engining “User-Mode Power Service file” points us to umpo.dll. We will focus our efforts on the changes in this dll. A patched version can be grabbed by downloading and extracting the .dll from the relevant KB or from an updated Windows 10 system. The non-patched version can be grabbed from a Windows 10 system prior to updating it with April’s updates.
To perform the patch diff I will use Diaphora. Diaphora shows that two functions have been modified UmpoRpcLegacyEventRegisterNotification
and UmpoNotifyRegister
:
A third function, UmpoNotifyUnregister
, is unmatched and has been removed from the patched version of umpo.dll.
Reading these modified function names sounds like the vulnerability could be a use after free.
Modified Functions Analysis
Now that the modified functions are known it is time to start reviewing them to identify the bug. Quickly looking at both modified functions reveals that they are not very long. To get an idea of how the functions are called we will check the cross references to each function. There are no cross references from another function to UmpoRpcLegacyEventRegisterNotification
. Based on the name it is likely invoked via an RPC call. UmpoNotifyRegister
however has one:
UmpoRpcLegacyEventRegisterNotification
We will delve deeper into UmpoRpcLegacyEventRegisterNotification
. I prefer to do dynamic analysis whenever possible, so we will fire up WinDbg and put a breakpoint on the target function:
The function declaration:
__int64 UmpoRpcLegacyEventRegisterNotification(__int64 a1,
__int64 a2,
const wchar_t *a3,
int a4)
Since this is likely an RPC call (which we will verify later) we will assume the first argument is the RPC binding handle and ignore it. The arguments appear pretty straightforward:
a3
is a wchar_t
which in this case is a service name. a2
is some value that does not point to valid memory so is likely utilized as some constant or identifier. a4
is 0. We will keep track of a4
and see what other potential values it could be. The first section of interesting code marked up with comments is:
v14 = 0i64;
v4 = a4;
v5 = a3;
v6 = a2;
// Return if not local client (local RPC only)
if ( !(unsigned int)UmpoIsClientLocal() )
return 5i64;
// Get some object
v7 = UmpoGetSettingEntry();
// If not NULL
if ( v7 )
{
// Get another object at v7+32
v8 = (__int64 *)(v7 + 32);
// Walk circular linked list until back at head
for ( i = *v8; (__int64 *)i != v8; i = *(_QWORD *)i )
{
// Breaks if object offset 20 == 1 and
// if object offset 24 == a2
if ( *(_DWORD *)(i + 20) == 1 && *(_QWORD *)(i + 24) == v6 )
goto LABEL_11;
}
// If not found set i to 0
i = 0i64;
LABEL_11:
// If found within circular linked list set v14 to object pointer
v14 = i;
}
A reference to an object is obtained by calling UmpoGetSettingEntry()
. This object contains a pointer to another object at offset 32. This object appears to be a circularly linked list that is walked in a loop. If the object member at offset 24 is equal to a3
and the object member at offset 20 is equal to 1 the loop breaks.
The second section of interesting code is:
// if a4
if ( v4 )
{
// if section 1 code loop does not find anything i is 0
if ( !i )
return 0i64;
// otherwise unregister
result = UmpoNotifyUnregister(i);
}
// if a4 == 0 (our current case)
else
{
if ( i )
return 0i64;
// Get sessionID of service
v10 = WTSGetServiceSessionId();
// call UmpoNotifyRegister
result = UmpoNotifyRegister(v12, v11, v10, v6, v5, &v14);
}
This section reveals the purpose of a4
. If a4
is non-zero UmpoNotifyUnregister
is called with the address returned from the section one code loop. If a4
is 0 UmpoNotifyRegister
is called. For documentation purposes we can reduce this function to:
__int64 UmpoRpcLegacyEventRegisterNotification(
// RPC Binding Handle
__int64 a1,
// Handle
__int64 a2,
// Service Name
const wchar_t *a3,
// 0 to Register 1 to Unregister
int a4)
UmpoNotifyRegister
This brings us to the second modified function UmpoNotifyRegister
. This function is a bit longer than the previous, but still relatively short. Less relevant sections will be omitted. From our work done reversing the last function we already have a good idea of the arguments:
__int64 __fastcall UmpoNotifyRegister(
// Set to 0
STRSAFE_LPCWSTR pszSrc,
// Set to 0
__int64 a2,
// SessionID
int a3,
// Handle
__int64 a4,
// Service Name
const wchar_t *pszSrca,
// Reference to return to calling function?
__int64 *a6)
The first interesting section of the second target function:
v6 = a4;
v7 = a3;
v8 = 0;
EnterCriticalSection(&UmpoNotification);
if ( service_name )
{
v9 = service_name;
v10 = 256i64;
// walk through string until null char is encountered
// or 256 characters are read
do
{
if ( !*v9 )
break;
++v9;
--v10;
}
while ( v10 );
// v11 set to error code if error if str len > 256
v11 = v10 == 0 ? 0x80070057 : 0;
if ( v10 )
// v12 set to length of string
v12 = 256 - v10;
else
v12 = 0i64;
}
else
{
v12 = 0i64;
v11 = -2147024809;
}
EnterCriticalSection
is called to synchronize shared access to an object. Then service_name
(which is our service name argument) is walked until a null character is encountered to determine the length of the string.
The second section is the bulk of the function. This function allocates space for a struct that we will call registrant
(r
for short in the code comments). It turns out that this struct makes up the circular linked list (actually a doubly circular linked list) walked in the for
loop in section one of the first function analyzed. The function then populates the new object with the relevant data from our function call arguments, and inserts the object into the front of the list. The struct is defined (rust syntax) as:
struct registrant {
// pointer to next
next: usize,
// pointer to prev
prev: usize,
// set to 1 after alloc
count: u32,
// Flags
flags: u32,
// handle we pass
handle: usize,
// heap alloc which is size of service_name + 2 for null char
service_name: usize,
// sessionID
session_id: u32,
// unknown, might be two u16s
unknown: u32,
}
With the above struct definition the below code should be relatively straight forward to follow:
// if no error on service_name length check
if ( !v11 )
{
v13 = (_QWORD *)UmpoGetSettingEntry();
v11 = 8;
...
// allocate registrant struct memory 48 bytes
v14 = RtlAllocateHeap(UmpoHeapHandle, 8i64, 48i64);
v15 = v14;
// error case on alloc
if ( !v14 )
goto LABEL_20;
v16 = UmpoHeapHandle;
// r->count is set to 1
*(_DWORD *)(v14 + 16) = 1;
// r->prev is set to itself
*(_QWORD *)(v14 + 8) = v14;
// r->next is set to itself
*(_QWORD *)v14 = v14;
// allocate memory for r->service_name
v17 = (wchar_t *)RtlAllocateHeap(v16, 8i64, (unsigned int)(2 * v12 + 2));
// set r->service_name pointer to allocated memory
*(_QWORD *)(v15 + 32) = v17;
// error case on alloc
if ( !v17 )
{
LABEL_18:
if ( v15 )
UmpoDereferenceRegistrant((__int64 *)v15);
LABEL_20:
if ( v13 && v8 )
RtlFreeHeap(UmpoHeapHandle, 0i64);
goto LABEL_21;
}
// set r->flags set to 1
*(_DWORD *)(v15 + 20) = 1;
// set r->handle to a4 (handle)
*(_QWORD *)(v15 + 24) = v6;
// copy func arg service_name into r->service_name
StringCchCopyW(v17, v12 + 1, pszSrca);
// umpo!UmpoNotifyRegister+0x111: lea rax, [rsi+20h]
v18 = v13 + 4;
// set r->session_id
*(_DWORD *)(v15 + 40) = v7;
// v19 = settingEntry head->next
v19 = v13[4];
// check if linked list head->next->prev == head
if ( *(_QWORD **)(v19 + 8) == v13 + 4 )
{
// inserting at head of list
// set r->next to head->next
*(_QWORD *)v15 = v19;
// set r->prev to head
*(_QWORD *)(v15 + 8) = v18;
// set head->next->prev to r
*(_QWORD *)(v19 + 8) = v15;
// set head->next to r
*v18 = v15;
// set r+44 to 0
*(_BYTE *)(v15 + 44) = 0;
UmpoNotifyUnregister
The final function change was the removal of UmpoNotifyUnregister
. This function calls EnterCriticalSection
and then UmpoDereferenceRegistrant
. UmpoDereferenceRegistrant
performs some error checks, removes the registrant struct from the doubly linked list and frees both heap allocations performed in UmpoNotifyRegister
.
Bug Analysis
We now have a good understanding of the modified/removed functions. Let’s look at the April umpo.dll function changes to see if we can spot the security vulnerability. Solely looking at the Diaphora results it appears that there are quite a few modifications. However with our new understanding of the code, the modifications essentially boil down to one glaring change from a security perspective. EnterCriticalSection
and LeaveCriticalSection
have been moved from UmpoNotifyRegister
to UmpoRpcLegacyEventRegisterNotification
. Critical sections are used to synchronize simultaneous access to a shared object within a process. Errors in synchronizing access to shared data produces bugs. Conspicousouly, EnterCriticalSection
is moved right above the call to UmpoGetSettingEntry
(section one of first function analyzed).
At this point the bug is becoming apparent. UmpoGetSettingEntry
returns a global variable which contains a pointer to the head of our doubly circular linked list of registrant objects. The non-patched code does not correctly synchronize access to this object. This error results in a race condition.
Race conditions are bugs, but in a sense are not directly exploitable. Rather, they provide a pathway to a security violation. Initially, the race must be won, which allows the security violation. The violation then must be taken advantage of. With this in mind, let’s think of how this race condition can be taken advantage of. As mentioned earlier, the names of the modified functions invokes the use after free vulnerability class. Following this line of thought, it is straightforward to imagine a scenario where the race condition results in a use after free. Take for example the following scenario with two threads:
Thread 1 enters UmpoRpcLegacyEventRegisterNotication.
Thread 1 accesses linked list registrant shared object.
Thread 1 obtains pointer to target registrant struct.
Thread 1 enters UmpoNotifyUnregister.
Thread 2 enters UmpoRpcLegacyEventRegisterNotication.
Thread 1 enters UmpoDereferenceRegistrant.
Thread 2 accesses linked list registrant shared object.
Thread 2 obtains pointer to target registrant struct.
Thread 1 frees registrant struct memory.
At this point the thread 2 pointer to the target registrant struct is dangling.
Exploitation PoC to Trigger Crash
To verify that the bug analysis is accurate we must write a simple PoC to trigger the race condition and use after free. Based on the name of UmpoRpcLegacyEventRegisterNotication
we have previously assumed that this function is invoked via RPC. To verify this we will use the extremely useful tool NtObjectManager written by James Forshaw.
After importing the NtObjectManager module we can run the below command (more information on this tool here and here):
This provides all RPC functions we can call from umpo.dll. Reviewing the ouput we see our function:
HRESULT UmpoRpcLegacyEventRegisterNotification(
/* Stack Offset: 0 */ handle_t p0,
/* Stack Offset: 8 */ [In] UIntPtr p0,
/* Stack Offset: 16 */ [In] /* FC_SUPPLEMENT FC_C_WSTRING Range(0, 256) */
wchar_t* p1,
/* Stack Offset: 24 */ [In] int p2);
We will take all the output from the tool and use that to create our .idl file to make the RPC call. The crash PoC is very simple and the code is available here. The necessary RPC binding initialization is set up and two threads are started.
Thread one repeatedly registers a registrant struct. Thread two randomly flips between registering and unregistering. The service_name
argument to UmpoRpcLegacyEventRegisterNotication
is set up to be the same size heap allocation as the registrant struct itself (48 bytes) to populate the freed memory. After running the crash code for a bit we get a crash!
The crash occurs in UmpoRpcLegacyEventRegisterNotication
referencing the invalid memory 41414141`41414155, which is our data.
Thanks to Shefang Zhong, Yuki Chen, and James Forshaw for providing tools and knowledge that greatly helped: