Skip to content

DLL Hijacking & COM Hijacking

DLL hijacking is the attack where an application loads an attacker-controlled DLL because the legitimate one is missing, in a writable directory, or named ambiguously. COM hijacking is the same principle applied to the registry: an application looks up a COM object by CLSID, and an attacker redirects the lookup to their own in-process server. Both are persistence and elevation primitives that survive across user logons and reboots because they live in well-known search paths the OS itself walks.

Windows loads a DLL by walking a search order defined by SafeDllSearchMode (enabled since XP SP2):

1. The directory from which the application loaded (most trusted)
2. The system directory (C:\Windows\System32)
3. The 16-bit system directory (C:\Windows\System)
4. The Windows directory (C:\Windows)
5. The current working directory ← writable from user context
6. The directories listed in PATH ← often user-writable
7. App Paths registry keys

If a high-privileged process references a DLL that does not exist in locations 1-4, and the attacker can drop a malicious DLL into location 5, 6, or 7, the privileged process loads the attacker’s code into its own address space.

COM hijacking targets the registry lookup instead:

HKCU\Software\Classes\CLSID\{GUID}\InprocServer32 ← per-user, no admin needed
HKCR\CLSID\{GUID}\InprocServer32 ← machine, admin needed
│
└─► Default value = path to DLL

Per-user COM registration under HKCU\Software\Classes\CLSID\ overrides the machine-wide HKCR mapping, so a standard user can hijack any COM object the system uses.

DLL hijacking (search order exploit):

  1. Enumerate the target process’s loaded DLLs with procmon (procmon /accepteula /quiet /minimized), filter by Path = ends with .dll and Result = NAME NOT FOUND.
  2. Identify a DLL the process tries to load but cannot find in the system directory.
  3. Drop a DLL with that name in a directory earlier in the search order (e.g., next to a scheduled task binary in C:\Tasks\).
  4. When the scheduled task runs as SYSTEM, it loads your DLL, runs your DllMain, and your code now runs as SYSTEM.

Phantom DLL (LoadLibrary with no DLL):

If the binary calls LoadLibraryW(L"SomeLib.dll") and the library does not exist, it walks the search order. Any DLL you can write earlier in the search order wins. notepad.exe, explorer.exe, and many MSI installers exhibit this.

DLL proxying (forward exports):

A real DLL hijack can break the application. The technique is to write a “proxy DLL” that exports the same symbols as the original and forwards them via __declspec(dllexport) + a .def file with EXPORTS MyFunc = real.MyFunc:

proxy_dll.c
__declspec(dllexport) void OriginalFunc() {
// malicious code first
RunPayload();
// then forward to the real DLL
HMODULE h = LoadLibraryA("original_real.dll");
((void(*)())GetProcAddress(h, "OriginalFunc"))();
}

COM hijacking:

  1. Enumerate COM objects used by privileged processes: Process Monitor on HKCR\CLSID\... reads.
  2. Pick a CLSID that the target process queries but does not exist in HKCU.
  3. Write HKCU\Software\Classes\CLSID\{guid}\InprocServer32 = path to your DLL.
  4. The next time the process resolves the CLSID, the per-user key wins, and your DLL is loaded.

Classic targets: TaskScheduler\{0f87369f-a4e2-4e6f-bc2d-1f1c2f3c4f5d} (Task Scheduler CLSID), {B196B286-BAB4-101A-B69C-00AA00341D07} (IADsAccessControlList), and various Shell\.dll handlers.

TechniqueTriggerPrivilege Gain
Phantom DLLApp LoadLibraryW for non-existent DLLRun as app’s account
Search-order hijackApp + writable dir + missing DLLRun as app’s account
Side-loading (signed binary)Legitimate signed app loads DLL from same dirBypass AV (signed caller)
COM inproc hijack (HKCU)App instantiates CLSIDStandard user → app privileges
COM inproc hijack (HKLM)Same, but admin already heldPersistence after admin
DLL redirector (AppDir)SetDllDirectory in app dirLocal privesc
Terminal window
# Find DLL hijack candidates with procmon (run as admin, filter to the target binary)
procmon /accepteula /quiet /minimized
# Filter: Process Name is <target.exe>, Result is NAME NOT FOUND, Path ends with .dll
# Export the resulting list to CSV, then:
grep -oE '[A-Za-z0-9_]+\.dll' procmon.csv | sort -u > candidates.txt
# Find writable directories in PATH for the current user
where.exe pathtest 2>nul
# Equivalent check via PowerShell
$env:Path -split ';' | Where-Object { $_ -and (Test-Path $_) } |
Where-Object { (Get-Acl $_).Access | Where-Object {
$_.IdentityReference -match $env:USERNAME -and $_.FileSystemRights -match 'Write' } }
# Identify COM hijack targets that run in the current user context
Get-ChildItem 'HKCU:\Software\Classes\CLSID' -ErrorAction SilentlyContinue | Measure-Object
Get-ChildItem 'HKLM:\Software\Classes\CLSID' -ErrorAction SilentlyContinue | Measure-Object
# A CLSID present in HKCU\...\CLSID\{guid}\InprocServer32 overrides HKLM — that's a hijack
# Test a hijack with an exported dummy DLL (PowerShell)
$dll = @"
using System;
using System.Runtime.InteropServices;
public class Pwn {
[DllExport("DllMain", CallingConvention = CallingConvention.StdCall)]
public static void DllMain(IntPtr hModule, int reason, IntPtr lpReserved) {
if (reason == 1) System.Diagnostics.Process.Start("cmd.exe", "/c whoami > C:\\pwn.txt");
}
}
"@
# Compile to payload.dll, place next to a vulnerable binary
# Check what scheduled task binaries load
Get-ChildItem 'C:\Tasks\' | ForEach-Object {
& "C:\Tools\peview.exe" $_.FullName -exported
}

Sysmon Event ID 7 — ImageLoad — alert when a SYSTEM process loads a DLL from a user-writable directory (C:\Users\, C:\Tasks\, C:\Temp\). Filter on ImageLoaded path matching \\Users\\ or \\Temp\\.

Sysmon Event ID 12, 13, 14 — RegistryEvent — alert on HKCU\Software\Classes\CLSID\*\InprocServer32 writes from a non-Explorer process. This is the canonical COM hijack signature.

MITRE ATT&CK T1574.001 — DLL Search Order Hijacking.

MITRE ATT&CK T1574.007 — Path Interception by PATH Environment Variable.

MITRE ATT&CK T1546.011 — Event Triggered Execution: Application Shimming (related).

MITRE ATT&CK T1546.010 — Event Triggered Execution: AppInit DLLs (similar persistence vector).

MITRE ATT&CK T1129 — Shared Modules (loading unauthorized DLL).

Wazuh rule 31105 [verify] — DLL loaded from non-standard directory.

Wazuh rule 31172 [verify] — COM CLSID write under HKCU.

ELK Sigma rule proc_susp_win_susp_dll_load_from_user_dir.yml — SigmaHQ has published rules for this.

DLL proxying. A direct hijack breaks the app. A proxy DLL forwards the original exports so the app keeps working and the malicious code runs from DllMain. Detection becomes harder because the loaded module name matches the original — only a hash comparison catches it.

Side-loading via signed binaries. Microsoft-signed binaries like moblivetools.exe, xcopy.exe’s variants, or third-party-signed AV updaters often search for DLLs in their own directory. Drop your DLL next to the signed binary, invoke the signed binary (which passes SmartScreen and Application Control), and your code inherits the signed binary’s reputation.

AppInit DLLs (HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs). Any process that loads user32.dll loads the AppInit DLLs too. Disable per-app via LoadAppInit_DLLs and RequireSignedAppInit_DLLs. As an attacker, set both to 1 and 0 respectively to enable unsigned AppInit loading.

EvasionDefeatsWhat it requires
DLL proxyingApp functional checkSource code of original or .def file reverse
Signed-binary side-loadingAppLocker / WDAC code-integrity rulesLegitimate signed binary with writable side dir
COM hijack in HKCUUAC, admin-required checkOnly standard user
AppInit_DLLsPer-process DLL monitoringHKLM admin

⚠️ What caught me: I planted a DLL hijack in C:\Tasks\ against a scheduled task that ran every 5 minutes as SYSTEM. The DLL worked — but the SOC’s Sysmon EID 7 rule keyed on ImageLoaded containing C:\Users\Public\ lit up because my DLL still had the IExpress stub I’d used for compilation. The lesson: the path of the loaded DLL is not the only signature; the import table of the DLL itself is its own fingerprint. Sysmon EID 7 also logs the hash of the loaded module, and the SOC had baselined hashes.

VM type: Windows 10 VM with Sysinternals installed, plus a writable C:\Tasks\ directory. Two Windows Server 2019 boxes for the COM hijack persistence test.

Objective:

  1. Build a proxy DLL that loads as SYSTEM via a scheduled task binary.
  2. Plant a COM hijack under HKCU\Software\Classes\CLSID\ and confirm it triggers on a privileged process startup.

Steps:

  1. Use procmon to find a DLL the task binary attempts to load but cannot find.
  2. Write a proxy DLL in C with DllMain that writes a sentinel file to C:\pwn.txt.
  3. Drop the DLL in the task’s directory, wait for the next run, and read C:\pwn.txt to confirm SYSTEM execution.
  4. For COM: pick a CLSID queried by explorer.exe at startup, register under HKCU, restart explorer, confirm your DLL is loaded.

Expected flag format: flag{proxy_dll:SYSTEM_via_hijack:clsid:{...}}

Detection that should fire: Sysmon EID 7 (DLL from user-writable path) and EID 13 (HKCU\Software\Classes\CLSID… write).

Explain the Windows DLL search order. Where can a standard user typically place a DLL to be loaded by a SYSTEM process?

Scenario: a DLL hijack lands but the SOC’s Sysmon EID 7 rule fires. What two aspects of the loaded DLL, beyond its path, would a mature detection also key on?

Technical: what does “DLL proxying” mean, and what file is required to make LoadLibraryA("proxy.dll") forward calls transparently to the real DLL?

Conceptual: why is a per-user COM hijack (HKCU\Software\Classes\CLSID) a privilege-equal to an admin-level HKLM COM hijack, and what AppLocker rule structure would mitigate it?

What is the difference between T1574.001 (DLL Search Order Hijacking) and T1574.007 (Path Interception by PATH Environment Variable) from the defender’s detection perspective?

→ PE5 (Persistence Mechanisms) · PE2 (Windows Privilege Escalation) · B7 (DLL Injection Techniques)