Introduction
In this article, we'll look at the concept of DLL hijacking and how it can be used to achieve userland persistence on Windows systems. This method is described in MITER ATT & CK under: "Intercepting DLL Search Order (T1038) ".
DLL spoofing can be used by attackers for many different purposes, but this article will focus on achieving resiliency with auto-start applications. For example, since Slack and Microsoft Teams are launched at boot (by default), DLL spoofing in one of these applications would allow an attacker to gain persistent access to their target - whenever a user logs in.
After introducing the concept of DLLs, DLL lookup order, and DLL spoofing, I'll walk you through the process of automating DLL interception detection . This article will talk about detecting DLL interception paths in Slack, Microsoft Teams, and Visual Studio Code.
Finally, I discovered multiple DLL interception paths used by different applications, investigated the root cause, and found that applications using certain Windows API calls are prone to DLL interception when not running from
C:\Windows\System32\
.
I want to thank my colleague Josiah Massari (
@Airzero24
) for being the first to spot some of these DLL hooks, explain their methodology, and inspire me to automate the detection.
What is a DLL?
A DLL is a library containing code and data that can be used simultaneously by more than one program. ( Source ) The
functionality of a DLL can be used by a Windows application using one of the functions
LoadLibrary*
. Applications can reference DLLs designed specifically for those applications, or Windows DLLs already on disk in System32. Developers can load DLLs from System32 to use functionality already implemented in Windows in their applications without having to write this functionality from scratch.
For example, a developer who needs to make HTTP requests can use the WinHTTP (
winhttp.dll
) library instead of implementing HTTP requests using raw sockets.
DLL search order and interception
Since DLLs exist as files on disk, you may be wondering how an application knows where to load the DLL from? Microsoft has documented the DLL lookup order in detail here .
Starting with Windows XP SP2, DLL Safe Search Mode is enabled by default (
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode
). When safe mode is enabled, the DLL search order is as follows:
- The directory from which the application was loaded.
- System directory. Use the GetSystemDirectory function to get the path to this directory.
- 16-bit system directory. There is no function that provides a path to this directory, but it is searched.
- Windows directory. Use the GetWindowsDirectory function to get the path to this directory.
- Current directory.
- , PATH. , , App Paths. App Paths DLL.
A system can contain multiple versions of the same DLL. Applications can control the choice of the location from which the DLL should be loaded by specifying the full path or by using another mechanism such as a manifest. ( Source )
If the application does not specify where to load the DLL from, Windows uses the default DLL search order shown above. The first position in the DLL search order (the directory from which the application is loaded) is of interest to attackers.
If the application developer intends to load the DLL from
C:\Windows\System32
, but did not explicitly write it in the application, the malicious DLL placed in the application directory will be loaded before the legitimate DLL from System32. Loading a malicious DLL is called DLL spoofing (or interception) and is used by attackers to load malicious code into trusted / signed applications.
Using DLL Spoofing to Achieve Resilience
DLL spoofing can be used to achieve resiliency when a vulnerable application / service is started and a malicious DLL is placed in a vulnerable location. A colleague of mine
@Airzero24
discovered DLL spoofing in Microsoft OneDrive, Microsoft Teams and Slack as userenv.dll
.
It was these programs that became the target of the interception, because by default they are configured to start at Windows startup. This can be seen below in Task Manager:
Windows Applications Configured to Autostart
To test DLL spoofing, I created a DLL shellcode loader that started Cobalt Strike Beacon. I renamed the malicious DLL to
userenv.dll
and copied it to the affected application directory. I launched the application and saw my new Beacon callback.
Cobalt Strike Beacon Through DLL Interception
UsingProcess Explorer , I can check if my malicious DLL was actually loaded by a vulnerable application.
Process Explorer showing loaded malicious DLL
Automatic detection of DLL interception potential
After confirming the previously known DLL hijacking, I wanted to see if I could find other DLL spoofing capabilities that could be exploited.
The code used in my checkout can be found here .
Using Slack as an example
To start this process, I ran Process Monitor (ProcMon) with the following filters:
- Process name -
slack.exe
- Result contains
NOT FOUND
- The path ends with
.dll
.
Find missing DLLs in ProcMon.
Then I started Slack and examined ProcMon for any DLLs that Slack was looking for but couldn't find.
Possible DLL Interception Paths Discovered by ProcMon
I exported this data from ProcMon as a CSV file for easier parsing in PowerShell.
With my current shellcode loader DLL, I couldn't easily figure out the DLL names that were successfully loaded by Slack. I created a new DLL, which is used
GetModuleHandleEx
, and GetModuleFileName
to determine the name of the loaded DLL and write it to a text file .
My next goal was to parse the CSV file for DLL paths in the list, view that list, copy my test DLL to the specified path, start the target process, stop the target process, and delete the test DLL. If the test DLL was loaded successfully, it will write its name to the resulting file.
When this process finishes, I will have a list of possible DLL hijacks (I hope) written to a text file.
All the magic in my DLLHijackTest project is done by a PowerShell script . It accepts the path to the CSV file generated by ProcMon, the path to your malicious DLL, the path to the process you want to run, and any arguments you want to pass to the process.
Get-PotentialDLLHijack Parameters
Get-PotentialDLLHijack.ps1
After a few minutes, I check the text file listed in my "malicious" DLL for possible DLL hijacks. I found the following possible interception paths for Slack:
PS C:Users\John\Desktop> Get-PotentialDLLHijack -CSVPath .\Logfile.CSV -MaliciousDLLPath .\DLLHijackTest.dll -ProcessPath "C:\Users\John\AppData\Local\slack\slack.exe"
C:\Users\John\AppData\Local\slack\app-4.6.0\WINSTA.dll
C:\Users\John\AppData\Local\slack\app-4.6.0\LINKINFO.dll
C:\Users\John\AppData\Local\slack\app-4.6.0\ntshrui.dll
C:\Users\John\AppData\Local\slack\app-4.6.0\srvcli.dll
C:\Users\John\AppData\Local\slack\app-4.6.0\cscapi.dll
C:\Users\John\AppData\Local\slack\app-4.6.0\KBDUS.DLL
Using Microsoft Teams as an example
We carry out the process described above again:
- Use ProcMon to identify potential DLL interception paths, export this data as a CSV file.
- Determine the path to start the process.
- Define any arguments you want to pass to the process.
- Run
Get-PotentialDLLHijack.ps1
with appropriate arguments.
I found the following possible interception paths for Microsoft Teams:
PS C:Users\John\Desktop> Get-PotentialDLLHijack -CSVPath .\Logfile.CSV -MaliciousDLLPath .\DLLHijackTest.dll -ProcessPath "C:\Users\John\AppData\Local\Microsoft\Teams\Update.exe" -ProcessArguments '--processStart "Teams.exe"'
C:\Users\John\AppData\Local\Microsoft\Teams\current\WINSTA.dll
C:\Users\John\AppData\Local\Microsoft\Teams\current\LINKINFO.dll
C:\Users\John\AppData\Local\Microsoft\Teams\current\ntshrui.dll
C:\Users\John\AppData\Local\Microsoft\Teams\current\srvcli.dll
C:\Users\John\AppData\Local\Microsoft\Teams\current\cscapi.dll
C:\Users\John\AppData\Local\Microsoft\Teams\current\WindowsCodecs.dll
C:\Users\John\AppData\Local\Microsoft\Teams\current\TextInputFramework.dll
Note : I had to make small changes to the PowerShell script to completeTeams.exe
, as my script is trying to terminate the process it was trying to start, in this case it isUpdate.exe
.
Using Visual Studio Code as an example
By repeating the above process, I found the following potential interception paths for Visual Studio Code:
PS C:Users\John\Desktop> Get-PotentialDLLHijack -CSVPath .\Logfile.CSV -MaliciousDLLPath .\DLLHijackTest.dll -ProcessPath "C:\Users\John\AppData\Local\Programs\Microsoft VS Code\Code.exe"
C:\Users\John\AppData\Local\Programs\Microsoft VS Code\WINSTA.dll
C:\Users\John\AppData\Local\Programs\Microsoft VS Code\LINKINFO.dll
C:\Users\John\AppData\Local\Programs\Microsoft VS Code\ntshrui.dll
C:\Users\John\AppData\Local\Programs\Microsoft VS Code\srvcli.dll
C:\Users\John\AppData\Local\Programs\Microsoft VS Code\cscapi.dll
Sharing DLLs
I noticed that Slack, Microsoft Teams and Visual Studio Code share the following DLLs:
WINSTA.dll
LINKINFO.dll
ntshrui.dll
srvcli.dll
cscapi.dll
I found this interesting, and I wanted to understand what causes this behavior.
Methodology: Understanding Ways to Intercept Shared DLLs
I watched Tracy stack when Slack tried to load
WINSTA.dll
, LINKINFO.dll
, ntshrui.dll
, srvcli.dll
and cscapi.dll
.
DLL with lazy loading
I noticed similarities in Tracy stack when loading
WINSTA.dll
, LINKINFO.dll
, ntshrui.dll
and srvcli.dll
.
Stack trace when Code.exe tries to load
WINSTA.dll
a stack trace when
Teams.exe
trying to load LINKINFO.dll
,
stack trace when Slack tries to load
ntshrui.dll
a stack trace constantly contains a call
_tailMerge_<
dllname>_dll
, delayLoadHelper2
followed LdrResolveDelayLoadedAPI
. This behavior was the same for all three applications.
I have determined that this behavior is related to lazy DLL loading . From the trace stack at boot
WINSTA.dll
I could see that the module responsible for this lazy loading was wtsapi32.dll
.
I opened
wtsapi32.dll
in Ghidra and used Search -> For Strings -> Filter: WINSTA.dll
. Double click on the found line will take you to its location in memory.
The line "
WINSTA.dll
" inwtsapi32.dll
By right-clicking on a location in memory, we can find any references to this address.
Links to
WINSTA.dll
Following the links, we can see that the string
WINSTA.dll
is passed to a structure named ImgDelayDescr
. Looking at the documentation for this structure, we can confirm that it is related to lazy DLL loading.
typedef struct ImgDelayDescr {
DWORD grAttrs; //
RVA rvaDLLName; // RVA dll
RVA rvaHmod; // RVA
RVA rvaIAT; // RVA IAT
RVA rvaINT; // RVA INT
RVA rvaBoundIAT; // RVA IAT
RVA rvaUnloadIAT; // RVA IAT
DWORD dwTimeStamp; // 0, ,
// O.W. / DLL, (Old BIND)
} ImgDelayDescr, * PImgDelayDescr;
This structure can be passed to
__delayLoadHelper2
, which will use LoadLibrary
/ GetProcAddress
to load the specified DLL and fix the imported function address in the lazy load import address table (IAT).
FARPROC WINAPI __delayLoadHelper2(
PCImgDelayDescr pidd, // ImgDelayDescr
FARPROC * ppfnIATEntry // IAT
);
By finding other references to our structure
ImgDelayDescr
, we can find a call __delayLoadHelper2
that then calls ResolveDelayLoadedAPI
. I have renamed the function name, types and variables to make it easier to understand.
__delayLoadHelper2
and ResolveDelayLoadedAPI
at Ghidra
Excellent! This is consistent with what we saw in our ProcMon stack trace when Slack tried to load
WINSTA.dll
.
__delayLoadHelper2
and ResolveDelayLoadedAPI
in ProcMon.
This behavior was uniformly for
WINSTA.dll
, LINKINFO.dll
, ntshrui.dll
and srvcli.dll
. The main difference between each lazy-load DLL was the "parent" DLL. In all three applications:
wtsapi32.dll
deferred loadedWINSTA.dll
shell32.dll
lazy loadedLINKINFO.dll
LINKINFO.dll
deferred loadedntshrui.dll
ntshrui.dll
deferred loadedsrvcli.dll
Did you notice anything interesting? It looks like it
shell32.dll
downloads LINKINFO.dll
, which downloads ntshrui.dll
, which finally downloads srvcli.dll
. This brings us to our last common potential DLL spoofing option - cscapi.dll
.
DLL substitution in NetShareGetInfo and NetShareEnum
I followed the stack trace when Slack tried to load
cscapi.dll
and saw a call LoadLibraryExW
that apparently came from srvcli.dll
.
I opened the
stack trace at boot
cscapi.dll
in Ghidra and used . Double clicking on the found line and following the links leads to the expected call. calls LoadLibrary for
Renaming the function containing the call and following the links, I got two places where the function is used:
srvcli.dll
Search -> For Strings -> Filter: cscapi.dll
LoadLibrary
srvcli.dll
cscapi.dll
LoadLibrary
NetShareEnum downloads cscapi.dll
NetShareGetInfo downloads
cscapi.dll
I checked this with PoC programs that called
NetShareEnum
and NetShareGetInfo
:
NetShareEnum.exe
downloads cscapi.dll
NetShareGetInfo.exe
downloadscscapi.dll
results
The following DLL spoofing paths are available in Slack:
C:\Users\John\AppData\Local\slack\app-4.6.0\WINSTA.dll
C:\Users\John\AppData\Local\slack\app-4.6.0\LINKINFO.dll
C:\Users\John\AppData\Local\slack\app-4.6.0\ntshrui.dll
C:\Users\John\AppData\Local\slack\app-4.6.0\srvcli.dll
C:\Users\John\AppData\Local\slack\app-4.6.0\cscapi.dll
C:\Users\John\AppData\Local\slack\app-4.6.0\KBDUS.DLL
The following DLL spoofing paths are available in Microsoft Teams:
C:\Users\John\AppData\Local\Microsoft\Teams\current\WINSTA.dll
C:\Users\John\AppData\Local\Microsoft\Teams\current\LINKINFO.dll
C:\Users\John\AppData\Local\Microsoft\Teams\current\ntshrui.dll
C:\Users\John\AppData\Local\Microsoft\Teams\current\srvcli.dll
C:\Users\John\AppData\Local\Microsoft\Teams\current\cscapi.dll
C:\Users\John\AppData\Local\Microsoft\Teams\current\WindowsCodecs.dll
C:\Users\John\AppData\Local\Microsoft\Teams\current\TextInputFramework.dll
The following DLL spoofing paths are available in Visual Studio Code:
C:\Users\John\AppData\Local\Programs\Microsoft VS Code\WINSTA.dll
C:\Users\John\AppData\Local\Programs\Microsoft VS Code\LINKINFO.dll
C:\Users\John\AppData\Local\Programs\Microsoft VS Code\ntshrui.dll
C:\Users\John\AppData\Local\Programs\Microsoft VS Code\srvcli.dll
C:\Users\John\AppData\Local\Programs\Microsoft VS Code\cscapi.dll
In addition, I found that programs using
NetShareEnum
and NetShareGetInfo
provide the ability to replace DLLs in the form cscapi.dll
due to the hard-coded call LoadLibrary
. I have verified this behavior with Ghidra and PoC.
Conclusion
As a reminder, DLL interception is a method by which attackers can interfere with code execution in signed / trusted applications. I have created tools to help automate DLL interception path detection. Using this tool, I discovered DLL interception paths in Slack, Microsoft Teams and Visual Studio Code.
I noticed that the DLL interception paths of these three applications overlap and investigated the cause. I highlighted my method of understanding this coincidence. I learned about lazy loading DLLs and discovered two API calls that make it possible to intercept DLLs in any program that calls them:
NetShareEnum
loadscscapi.dll
NetShareGetInfo
loadscscapi.dll
Thanks for taking the time to read this article, I hope you've learned a thing or two about Windows APIs, Ghidra, ProcMon, DLLs and DLL interception!
Links
Big hello to my colleagues Daniel Heinsen (
@hotnops
), Lee Christensen ( @tifkin_
) and Matt Hand ( @matterpreter
) for helping out with Ghidra / ProcMon!
Checking public PoCs for use in pentesting