DLL Hijacking on Windows: The Search-Order Flaw Hiding in Your App Folders
Attackers rarely need an exploit when Windows will load a malicious DLL for them. Here is how DLL search-order and phantom-DLL hijacking work, why ordinary installers create the flaw, and how to audit every writable directory on your machine's DLL search path - free.
Most discussions of Windows privilege escalation reach for a CVE. But the technique that has quietly shipped inside more red-team engagements and real intrusions than almost any single bug is not a bug at all — it is the documented way Windows finds a DLL. When a process calls LoadLibrary("helper.dll") without a full path, the loader walks an ordered list of directories looking for that file. If an attacker can place a file named helper.dll in a directory that gets searched before the legitimate one — or place it where the real DLL is simply missing — the process loads attacker code into its own address space, with that process’s identity and privileges. No memory corruption, no shellcode gadget, no patch will ever fix it, because nothing is broken. This is MITRE ATT&CK T1574.001, and it is one of the most reliable primitives on the platform.
The Search Order, and Why It Bites
With safe DLL search mode enabled (the default since Windows XP SP2), a process that requests a DLL by name searches roughly this order:
- The directory the application loaded from (the EXE’s own folder).
- The system directory (
C:\Windows\System32). - The 16-bit system directory, then the Windows directory.
- The current working directory.
- Each directory listed in the
PATHenvironment variable, in order.
Two of those entries do the damage. The application directory is searched first, so if a SYSTEM service or an elevated app lives in a folder a standard user can write to, that user drops a same-named DLL and owns the next launch. And the PATH directories are searched last — which sounds safe until you realise that a missing dependency (a DLL the program references but Windows can’t find in any earlier location) falls all the way through to PATH. If any PATH directory is user-writable, that missing DLL becomes a free code-execution slot. This second case is called phantom DLL hijacking, and it is everywhere: applications routinely try to load optional or version-specific DLLs that aren’t present on a given machine.
Why Ordinary Installers Create the Flaw
You almost never introduce this yourself. It arrives bundled with software. The three recurring root causes:
- App folders outside
C:\Program Files. Anything installed toC:\Tools\,C:\Vendor\, or a per-user-but-machine-run location often inherits ACLs that letAuthenticated UsersorUserswrite. A privileged binary in that folder is a hijack waiting to happen. - Writable directories on the system
PATH. Developer toolchains, language runtimes, and database clients love to prepend their own folder toPATH— sometimes with(Modify)for the install user. Every elevated process on the box now searches that writable folder for any DLL it can’t otherwise find. - Missing dependencies. Side-by-side assembly quirks, optional codecs, and DLLs that only exist on newer builds leave named gaps the loader will happily fill from a writable directory.
Auditing It on the Machine in Front of You
The whole attack reduces to one question: can a non-administrator write to any directory that a privileged process searches for DLLs? You can start answering it by hand. First, find writable directories on the machine PATH — these are the phantom-hijack slots:
# Machine PATH entries that a standard user can write to
$me = [Security.Principal.WindowsIdentity]::GetCurrent().Name
[Environment]::GetEnvironmentVariable('Path','Machine') -split ';' |
Where-Object { $_ -and (Test-Path $_) } |
ForEach-Object {
$acl = Get-Acl $_
$writable = $acl.Access | Where-Object {
$_.FileSystemRights -match 'Write|Modify|FullControl' -and
$_.IdentityReference -match 'Users|Authenticated|Everyone'
}
if ($writable) { [pscustomobject]@{ Path = $_; GrantedTo = ($writable.IdentityReference -join ', ') } }
}
Then check the directories that privileged programs actually run from. A service whose binary sits in a user-writable folder is the highest-value hijack target, because the loader searches that folder first:
# Service binary folders, then test each for non-admin write access
Get-CimInstance Win32_Service |
ForEach-Object {
if ($_.PathName -match '^"?([A-Za-z]:\\[^"]+\.exe)') {
$dir = Split-Path $Matches[1]
[pscustomobject]@{ Service = $_.Name; Account = $_.StartName; Dir = $dir }
}
} | Sort-Object Dir -Unique
For deeper confirmation that a specific app is reaching into a writable location, Sysinternals Process Monitor filtered on Result is NAME NOT FOUND and Path ends with .dll will show you the exact phantom DLLs a process probes for — each one a candidate. That is a great way to understand one binary. It is a terrible way to clear a machine, because the flaw is never in the app you thought to check; it is in the dependency you didn’t.
Where a Posture Audit Earns Its Keep
This is exactly the kind of broad, mechanical enumeration that a configuration audit is built for: walk every PATH entry, every service and autostart binary, every app directory, resolve the effective ACLs, and flag any privileged search path a non-admin can write. On the machine you own, that full sweep is free and runs at complete depth in seconds:
# Audit writable PATH dirs, app-folder ACLs, and service binary permissions
winsentinel --audit
# Snapshot the writable-path surface as a baseline you can diff later
winsentinel --audit --format json --out dll-surface-baseline.json
# Then close the gaps it found
winsentinel --fix-it
None of this shows up in a patch-level scan. A vulnerability scanner sees a fully-updated machine and moves on; the writable directory on your PATH has no CVE, no version number, nothing to flag. It is pure configuration state — file ACLs and environment variables — which is precisely the gap a posture audit reads and a CVE scanner walks straight past.
At Fleet Scale, the Same Bad Folder Is Everywhere
On a single machine, finding the handful of writable search-path directories and tightening their ACLs is genuinely complete — every check above runs free, at full depth, on the box in front of you. The problem changes shape across an organisation. The reason DLL hijacking is dangerous to a fleet is not any one laptop; it is that the same installer that prepends a world-writable folder to PATH, or drops an app into a loose-ACL directory, does it identically on every endpoint it touches. One sloppy developer-tool rollout is not one hijack primitive — it is the same primitive seeded across hundreds of machines by your own software deployment.
That is the boundary where fleet orchestration earns its keep. WinSentinel Pro runs an agent on each endpoint performing the same writable-path audit into a central node — the depth is identical to the free single-machine scan; Pro does not unlock extra checks. What it adds is the organisation-level view: a fleet rollup that says “38 of your 200 machines have the same vendor folder on a writable PATH” in one report instead of 200 separate scans; drift alerts when a new deployment opens a writable search path across the fleet; and compliance evidence that your endpoints’ DLL search paths stayed locked down over an audit window. Single-machine hardening is free and total. The “which of my 200 machines did that installer just weaken?” question — answered as one report — is the org problem Pro exists to solve.
The Takeaway
DLL hijacking is not an exploit you patch; it is a permission you left open. Windows will faithfully load whatever DLL it finds first, and attackers know your application folders and PATH better than you do. Treat the DLL search path the way you treat your firewall rules — enumerated, permission-checked, and baselined — so the next installer can’t quietly hand a standard user a SYSTEM-level launch slot.
# Close the DLL search-path gaps on this machine right now
winsentinel --audit
winsentinel --fix-it
An attacker only needs one writable directory on a privileged search path. Auditing all of them is how you make sure they don’t find it first.