In May, I published a blog post detailing a Remote Code Execution vulnerability in Dell SupportAssist. Since then, my research has continued and I have been finding more and more vulnerabilities. I strongly suggest that you read my previous blog post, not only because it provides a solid conceptual understanding of Dell SupportAssist, but because it's a very interesting bug.
This blog post will cover my research into a Local Privilege Escalation vulnerability in Dell SupportAssist. Dell SupportAssist is advertised to "proactively check the health of your system’s hardware and software". Unfortunately, Dell SupportAsssist comes pre-installed on most of all new Dell machines running Windows. If you're on Windows, never heard of this software, and have a Dell machine - chances are you have it installed.
Amusingly enough, this bug was discovered while I was doing my write-up for the previous remote code execution vulnerability. For switching to previous versions of Dell SupportAssist, my method is to stop the service, terminate the process, replace the binary files with an older version, and finally start the service. Typically, I do this through a neat tool called Process Hacker, which is a vamped up version of the built-in Windows Task Manager. When I started the Dell SupportAssist agent, I saw this strange child process.
I had never noticed this process before and instinctively I opened the process to see if I could find more information about it.
We straight away can tell that it's a .NET Application given the ".NET assemblies" and ".NET performance" tabs are populated. Browsing various sections of the process told us more about it. For example, the "Token" tab told us that this was an unelevated process running as the current user.
While scrolling through the "Handles" tab, something popped out at me.
For those who haven't used Process Hacker in the past, the cyan color indicates that a handle is marked as inheritable. There are plenty of processes that have inheritable handles, but the key part about this handle was the process name it was associated with. This was a THREAD handle to SupportAssistAgent.exe, not SupportAssistAppWire.exe. SupportAssistAgent.exe
was the parent SYSTEM
process that created SupportAssistAppWire.exe - a process running in an unelevated context. This wasn't that big of a deal either, a SYSTEM
process may share a THREAD handle to a child process - even if it's unelevated, but with restrictive permissions such as THREAD_SYNCHRONIZE
. What I saw next is where the problem was evident.
This was no restricted THREAD handle that only allowed for basic operations, this was straight up a FULL CONTROL thread handle to SupportAssistAgent.exe
. Let me try to put this in perspective. An unelevated process has a FULL CONTROL handle to a thread in a process running as SYSTEM
. See the issue?
Let's see what causes this and how we can exploit it.
Every day, Dell SupportAssist runs a "Daily Workflow Task" that performs several actions typically to execute routine checks such as seeing if there is a new notification that needs to be displayed to the user. Specifically, the Daily Workflow Task will:
The important thing to remember is that all of these checks were performed on a daily basis. For us, the last check is most relevant, and it's why Dell users will receive this "Optimize System" notification constantly.
If you haven't run the PC Doctor optimization scan for over 14 days, you'll see this notification every day, how nice. After determining a notification should be created, the method OptimizeSystemTakeOverNotification
will call the PushNotification
method to issue an "Optimize System" notification.
For most versions of Windows 10, PushNotification
will call a method called LaunchUwpApp
. LaunchUwpApp
takes the following steps:
SupportAssistAppWire.exe
, with InheritHandles
set to true.SupportAssistAppWire.exe
will then create the notification.The flaw in the code can be seen in the call to CreateProcessAsUser
.
As we saw in the Discovery section of this post, SupportAssistAgent.exe
, an elevated process running as SYSTEM
starts a child process using the unelevated explorer token and sets InheritHandles
to true. This means that all inheritable handles that the Agent has will be inherited by the child process. It turns out that the thread handle for the service control handler for the Agent is marked as inheritable.
Now that we have a way of getting a FULL CONTROL thread handle to a SYSTEM
process, how do we exploit it? Well, the simplest way I thought of was to call LoadLibrary
to load our module. In order to do this, we need to get past a few requirements.
LoadLibrary
the buffer address.QueueUserApc
to call LoadLibrary
. This means that we need to have the thread become alertable.I thought of various ways I could have my string loaded into the memory of the Agent, but the difficult part was finding a way to predict the address of the buffer. Then I had an idea. Does LoadLibrary accept files that don’t have a binary extension?
It appeared so! This meant that the file path in a buffer only needed to be a file we can access; not necessarily have a binary extension such as .exe
or .dll
. To find a buffer that was already in memory, I opted to use Process Hacker
which includes a Strings utility with built-in filtering. I scanned for strings in an Image that contained C:\
. The first hit I got shocked me.
Look at the address of the first string, 0x180163f88
... was a module running without ASLR? Checking the modules list for the Agent, I saw something pretty scary.
A module named sqlite3.dll
had been loaded with a base address of 0x180000000
. Checking the module in CFF Explorer
confirmed my findings.
The DLL was built without the IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE
characteristic, meaning that ASLR was disabled for it. Somehow this got into the final release build of a piece of software deployed on millions of endpoints. This weakness makes our lives significantly easier because the buffer contained the path c:\dev\sqlite\core\sqlite3.pdb
, a file path we could access!
We already determined that the extension makes no difference meaning that if I write a DLL to c:\dev\sqlite\core\sqlite3.pdb
and pass the buffer pointer to LoadLibrary
, the module we wrote to c:\dev\sqlite\core\sqlite3.pdb
should be loaded.
Now that we got the first problem sorted, the next part of our exploitation is to get the thread to be alertable. What I found in my testing is that this thread was the service control handler for the Agent. This meant that the thread was in a Wait Non-Alertable state because it was waiting for a service control signal to come through.
Checking the service permissions for the Agent, I found that the INTERACTIVE
group had some permissions. Luckily, INTERACTIVE
includes unprivileged users, meaning that the permissions applied directly to us.
Both Interrogate
and User-defined control
sends a service signal to the thread, meaning we can get out of the Wait state. Since the thread continued execution after receiving a service control signal, we can use SetThreadContext
and set the RIP
pointer to a target function. The function NtTestAlert
was perfect for this situation because it immediately makes the thread alertable and executes our APCs.
To summarize the exploitation process:
SupportAssistAppWire.exe
process.C:\dev\sqlite\core\sqlite3.pdb
.NtQuerySystemInformation
works just as well).RIP
register of the Agent's thread to NtTestAlert
.LoadLibraryA
for the user routine and 0x180163f88
(buffer pointer) as the first argument.INTERROGATE
service control to the service.NtTestAlert
triggering the APC which causes the APC DLL to be loaded.SYSTEM
process, causing local privilege escalation.Dell's advisory can be accessed here.
After spending a long time reversing the Dell SupportAssist agent, I've come across a lot of practices that are in my opinion very questionable. I'll leave it up to you, the reader, to decide what you consider acceptable.
You can disable some of this, but it’s enabled by default. Think about the millions of endpoints running Dell SupportAssist…
04/25/2019 - Initial write up and proof of concept sent to Dell.
04/26/2019 - Initial response from Dell.
05/08/2019 - Dell has confirmed the vulnerability.
05/27/2019 - Dell has a fix ready to be released within 2 weeks.
06/19/2019 - Vulnerability disclosed by Dell as an advisory.
06/28/2019 - Vulnerability disclosed at RECON Montreal 2019.