← All posts

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:

  1. The directory the application loaded from (the EXE’s own folder).
  2. The system directory (C:\Windows\System32).
  3. The 16-bit system directory, then the Windows directory.
  4. The current working directory.
  5. Each directory listed in the PATH environment 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:

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.