Service DLL Hijacking
Replacing the binary of a service
is a very effective way to attempt privilege escalation on Windows systems.
However, because our user doesn’t often have permissions to replace these binaries
we’ll need to adapt with a more advanced way of abusing Windows services.
Dynamic Link Libraries provide functionality
to programs or the Windows operating system.
DLLs contain code or resources,
such as icon files,
for other executable files or objects to use.
These libraries provide a way for developers to use
and integrate already existing functionality
without reinventing the wheel.
Windows uses DLLs to store functionality needed by several components.
Otherwise, each component would need the functionality in their own source code
resulting in a huge resource waste.
On Unix systems, these files are called Shared Objects.
There are several methods we can use to exploit how DLLs work on Windows
and they can often be an effective way of elevating our privileges.
One method is similar to the privilege escalation vector
performed in the previous section.
Instead of overwriting the binary,
we merely overwrite a DLL the service binary uses.
However, the service may not work as expected
because the actual DLL functionality is missing.
In most cases, this would still lead us to code execution of the DLL’s code and then,
for example, to the creation of a new local administrative user.
Another method is to hijack the DLL search order.
The search order is defined by Microsoft
and determines what to inspect first when searching for DLLs.
By default,
all current Windows versions have safe DLL search mode enabled.
This setting was implemented by Microsoft
due to the high number of DLL hijacking vectors
and ensures that DLLs are more difficult to hijack.
Let’s quickly review the standard search order
taken from Microsoft’s Documentation:
Windows first searches the application’s directory.
Interestingly, the current directory is at position 5.
When safe DLL search mode is disabled,
the current directory is searched at position 2
after the application’s directory.
A special case of this method is a missing DLL.
This means the binary attempted to load a DLL
that doesn’t exist on the system.
This often occurs with flawed installation processes or after updates.
However, even with a missing DLL,
the program may still work with restricted functionality.
To exploit this situation,
we can try placing a malicious DLL,
with the name of the missing DLL,
in a path of the DLL search order so it executes when the binary is started.
Let’s show how we can abuse a missing DLL in an example
by connecting to CLIENTWK220 with RDP as steve.
We’ll start PowerShell and enumerate the services
as we did in the previous section.
One finding we skipped is the BetaService service.
Let’s check our permissions on the binary file of the service.
The results show we don’t have permissions to replace the binary
since we only have Read and Execute permissions as steve.
We need to investigate a bit more.
We can use Process Monitor to display real-time information about any process,
thread, file system, or registry related activities.
Our goal is to identify all DLLs loaded by BetaService
as well as detect missing ones.
Once we have a list of DLLs used by the service binary,
we can check their permissions and if they can be replaced with a malicious DLL.
Alternatively, if we find that a DLL is missing,
we could try to provide our own DLL by adhering to the DLL search order.
Unfortunately, we need administrative privileges
to start Process Monitor and collect this data.
However, the standard procedure in a penetration test
would be to copy the service binary to a local machine.
On this system,
we can install the service locally
and use Process Monitor with administrative privileges
to list all DLL activity.
In this example, we’ll simulate this step
by starting Process Monitor as backupadmin.
Let’s browse to C:\tools\Procmon\ and double-click on Procmon64.exe.
A window will appear asking us for administrative user credentials.
Once we enter the password for backupadmin,
the program starts.
Without any filters,
the information provided by Process Monitor can be quite overwhelming.
Multiple new list entries are added every second.
For now,
we are only interested in events related to the
BetaServ process for the target service,
so we can create a filter to only include events related to it.
Let’s click on the Filter menu
then on Filter again to get into the filter configuration.
The filter consists of four conditions.
Our goal is that Process Monitor
only shows events related to the BetaServ Process.
We’ll enter the following arguments:
Process Name as Column,
is as Relation,
BetaServ.exe as Value,
and Include as Action.
Once entered, we’ll click on Add, then Apply, then OK.
After applying the filter, the list is empty.
In order to analyze the service binary,
we should try restarting the service,
since the binary will then attempt to load the DLLs.
In PowerShell,
we’ll enter Restart-Service with BetaService as the argument
while Process Monitor is running in the background.
We were able to successfully restart BetaService.
Checking Process Monitor,
we notice that numerous events appeared.
Scrolling down in the list,
various CreateFile calls can be found in the Operation column.
The CreateFile function can be used to create or open a file.
The CreateFile calls attempted to open a file named myDLL.dll in several paths.
The Result column states NAME NOT FOUND for these calls,
which means that a DLL with this name couldn’t be found in any of these paths.
The consecutive function calls follow the DLL search order we reviewed earlier,
starting with the directory the application is located
in and ending with the directories in the PATH environment variable.
We can confirm this
by displaying the contents of this environment variable in PowerShell.
The directories in the PATH environment variable
match the paths used in the CreateFile calls recorded by Process Monitor.
So far, we know that the service binary tries to locate a file called myDLL.dll,
but fails to do so.
To abuse this,
we can attempt to write a DLL file with this name
to a path used by the DLL search order.
The first call attempts to locate the DLL in the Documents folder for steve.
Because it’s a home directory for steve,
we have permissions to write to this folder
and can place a malicious DLL there,
then restart the service to load it.
Before we create a DLL,
let’s briefly review how attaching a DLL works
and how it may lead us to code execution.
Each DLL can have an optional entry point function named DllMain,
which is executed when processes or threads attach the DLL.
This function generally contains four cases.
These cases handle situations when the DLL is loaded
or unloaded by a process or thread.
They are commonly used to perform initialization tasks for the DLL
or tasks related to exiting the DLL.
If a DLL doesn’t have a DllMain entry point function,
it only provides resources.
Let’s review a code example from Microsoft,
outlining a basic DLL in C++ containing these four cases.
The DLL code contains the entry point function DllMain
and the previously mentioned cases in a switch statement.
Depending on the value of ul_reason_for_call,
one of these cases gets executed.
As of now, all cases only use a break statement.
The provided comments from Microsoft state that DLL_PROCESS_ATTACH
is used when a process is loading the DLL.
Since the target service binary process in our example tries to load the DLL,
this is the case we need to add our code to.
Let’s reuse the C code from the previous section
by adding the include statement
as well as the system function calls to the C++ DLL code.
Additionally, we need to use an include statement for the header file windows.h,
since we use Windows specific data types such as BOOL.
Now, let’s cross-compile the code with mingw.
We’ll use the same command as in the previous section
but change the input code file,
the output name,
and add --shared to specify that we want to build a DLL.
Once the DLL is compiled, we can transfer it to CLIENTWK220.
We can start a Python3 web server on Kali in the directory the DLL is located in,
and use iwr in a PowerShell window on the target machine.
Before we download the file,
we’ll change our current directory to the Documents folder for steve
to download it into the correct directory for our attack.
Additionally,
let’s confirm that dave2 doesn’t exist yet on the system with the net user command.
myDLL.dll is now located in the Documents folder for steve,
which is the first path in the DLL search order.
After we restart BetaService,
our DLL should be loaded into the process,
and the code to create the user dave2
as member of the local Administrators group
in DLL_PROCESS_ATTACH should be executed.
User dave2 was created and added to the local Administrators group,
once the service is restarted.
Excellent!
Let’s briefly summarize what we did in this section.
Through the information obtained from Process Monitor,
we identified that the BetaService binary tried to load myDLL.dll.
Because the service binary is located in the Documents folder for steve,
we could write to this directory,
which is also the first directory of the DLL search order specified by Microsoft.
Because of this, our malicious DLL was loaded and executed
once the service was restarted.