Phantom DLL Hijacking: Auditing the DLLs Windows Searches For But Never Ships
Windows processes routinely try to load DLLs that do not exist on disk. Here's how phantom DLL hijacking works, why it's a stealthy persistence and privilege-escalation path, and how to audit for it.
Open Process Monitor on a freshly booted Windows machine, filter for Result is NAME NOT FOUND and Path ends with .dll, and you will see something that unsettles most engineers the first time: dozens of trusted, signed processes — including services running as SYSTEM — repeatedly asking the OS to load DLLs that simply are not there. The loader walks its search path, fails to find the file, shrugs, and carries on. These are ghost or phantom DLLs, and the gap between "a process will load this file" and "this file does not exist" is a privilege-escalation and persistence primitive hiding in plain sight.
Why Windows looks for DLLs that aren't there
A phantom DLL reference is almost always a benign side effect of modular code. An application links against an import library for an optional feature, probes for a plugin that was never installed, or carries a dependency that only ships in a different SKU. When LoadLibrary is called with a bare filename rather than a fully qualified path, the loader consults the standard DLL search order:
- The directory the executable was launched from
- The system directory (
C:\Windows\System32) - The 16-bit system directory, then the Windows directory
- The current working directory (when safe search is off)
- Each directory listed in the
PATHenvironment variable
If none of those locations hold the named DLL, the import fails quietly. The danger is the first and last entries: if an attacker can drop a malicious DLL with the phantom's exact name into a directory that appears earlier in the search order than where the real file would have lived — or into any writable PATH directory — their code loads instead, inside the host process, with the host's token.
Why this is worse than classic DLL search-order hijacking
Traditional DLL hijacking competes with a real, on-disk DLL and relies on winning the search-order race. Phantom hijacking has no competition at all: the file genuinely does not exist, so the attacker's DLL is the only candidate. That removes the timing fragility and the risk of breaking the host application by shadowing a legitimate dependency. It also evades a surprising amount of tooling — integrity checks that verify "every DLL on disk is signed" never fire, because the malicious DLL is a brand-new file, not a modification of something Microsoft shipped.
The payoff scales with the host:
- Privilege escalation — a phantom reference in a service that auto-starts as
SYSTEMturns a writable directory into aSYSTEMshell. - Persistence — the DLL reloads every time the host process starts, with no registry Run key, no scheduled task, and no new service to flag.
- Defense evasion — execution happens inside a trusted, signed binary, so allowlisting and parent-process heuristics see only
services.exeor a vendor's updater.
Hunting phantom DLLs with Process Monitor
The canonical discovery technique is a Procmon capture across a boot and a few minutes of normal use, filtered to surface the misses:
Operation is Load Image -> Exclude
Path ends with .dll -> Include
Result is NAME NOT FOUND -> Include
For each hit, the question is simple: is the directory where the DLL was expected writable by a non-admin? A phantom under System32 is uninteresting because only admins can write there. A phantom probed from an application folder under C:\Program Files\Vendor\ whose ACLs grant Authenticated Users: Modify — or from a PATH entry pointing at a user-writable directory — is an exploitable finding.
Validating writability
PowerShell makes the ACL check scriptable so you can triage dozens of hits quickly:
Get-Acl 'C:\Program Files\Vendor' |
Select-Object -ExpandProperty Access |
Where-Object {
$_.FileSystemRights -match 'Write|Modify|FullControl' -and
$_.IdentityReference -match 'Users|Authenticated|Everyone'
}
Pair that with an audit of every directory on the machine and service PATH — a single user-writable PATH entry turns every phantom reference on the box into a hijack opportunity.
Hardening against phantom loads
- Fix the directory ACLs. Application install folders and any custom
PATHdirectories should never grant write access to standard users. This is the root cause for most exploitable phantoms. - Prefer safe DLL search mode and explicit paths. Developers should call
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32)and load with absolute paths;CWDIllegalInDllSearchhardening removes the working directory from the search. - Deploy WDAC or AppLocker DLL rules. Allowlisting that covers DLLs — not just EXEs — blocks an unsigned phantom from loading even when the directory is writable.
- Watch the writable surface, not just the files. The vulnerability is the gap between a probe and a permission, so monitoring must correlate both.
Where WinSentinel fits
Phantom DLL hijacking is invisible to signature-based tools precisely because there is nothing malicious on disk until the moment of attack. WinSentinel approaches it structurally: its auditing engine enumerates writable directories that sit on service and process search paths, flags Program Files subtrees and PATH entries with weak ACLs, and surfaces the exact misconfiguration — a SYSTEM service searching a user-writable folder — as a prioritized finding with remediation guidance. Every WinSentinel install runs the full set of 64 audit modules with no feature gating, so a single engineer hardening one workstation gets the same depth of search-path analysis as a managed endpoint.
For organizations, WinSentinel Pro rolls those findings up across the fleet: the central node aggregates writable-path and DLL-search-order risk from every agent, alerts when a new deployment or a vendor update re-introduces a weak ACL (drift), and lets you confirm the whole estate is clean rather than auditing machines one at a time.
The files that aren't there are as dangerous as the ones that are. Audit the search path, lock down the writable directories, and a whole class of signed-process code execution closes before an attacker can use it.