What this covers
The baseline technique: get a handle to a process, allocate a few bytes in it, write the
path of a DLL on disk, and start a thread at LoadLibraryA with that path as the argument.
The target’s own loader does the rest — mapping the DLL, resolving its imports, and calling
DllMain.
It is the easiest injection to write and the easiest to catch. It is worth knowing properly
anyway, because every more sophisticated technique on this list exists to avoid one of its
three giveaways: the DLL on disk, the CreateRemoteThread, and the module appearing in the
target’s module list.
Prerequisites
- A handle with
PROCESS_CREATE_THREAD,PROCESS_VM_OPERATIONandPROCESS_VM_WRITE. Against a process in another session or owned by another user, that meansSeDebugPrivilege— so in practice, local administrator. - Architecture must match: a 64-bit process cannot load a 32-bit DLL.
- The DLL must exist on disk at a path the target process can read. This is the technique’s defining weakness — there is a file to find, hash and submit.
Walkthrough
| # | Call | Purpose | Artefact |
|---|---|---|---|
| 1 | OpenProcess | handle to the target | Sysmon event 10, GrantedAccess 0x1F3FFF or similar |
| 2 | VirtualAllocEx(..., MEM_COMMIT, PAGE_READWRITE) | space for the path string | small RW private allocation — not suspicious alone |
| 3 | WriteProcessMemory | the DLL path | cross-process write |
| 4 | GetModuleHandle("kernel32") + GetProcAddress("LoadLibraryA") | the start routine | — |
| 5 | CreateRemoteThread(..., LoadLibraryA, pPath, ...) | the loader runs in the target | Sysmon event 8 — the loudest part |
| 6 | target’s loader maps the DLL, calls DllMain | execution | Sysmon event 7 (ImageLoad) naming your DLL |
Note that the allocation here is PAGE_READWRITE, not RWX: you are writing a string, not
code. Scanners hunting private executable memory do not find this one — the module shows up
properly mapped, image-backed, in the module list. That is the trade: it is quiet in memory
and loud everywhere else.
What it looks like from the outside
The injected DLL executes with the host’s identity and token, so whatever it spawns inherits the host’s lineage. A payload that runs a shell produces a process tree that makes no sense for the host application.
Detection
This is the technique detection was built for, so there are several independent catches and you do not need all of them.
Sysmon event 8, CreateRemoteThread. A remote thread whose StartAddress resolves to
kernel32!LoadLibraryA or LoadLibraryW is close to conclusive. Legitimate uses of
CreateRemoteThread exist — debuggers, some injectors in AV products themselves — so
baseline them rather than alerting blind.
Sysmon event 7, ImageLoad. The DLL is a real mapped module, so it is named in full.
Flag modules loaded from user-writable paths (%TEMP%, %APPDATA%, C:\Users\Public),
unsigned modules in signed processes, and any module whose load is not explained by the
host’s import table or a known plugin directory.
Sysmon event 10, ProcessAccess. GrantedAccess containing PROCESS_CREATE_THREAD
(0x0002) together with PROCESS_VM_WRITE (0x0020) is the access combination this
technique needs and little else does.
Module-list review. Process Hacker → Modules, or listdlls, shows the DLL plainly.
Compare each loaded module against the expected set for that binary; a rundll32 or an
unknown DLL inside notepad.exe is not ambiguous.
On disk. There is a file. Hash it, and check its path against application allowlists. Of the techniques on this page, this is the only one where the payload is recoverable from the filesystem after the fact — which makes it the best case for the responder and the worst for the operator.
References
- MITRE ATT&CK T1055.001
- MITRE ATT&CK T1055.002 — portable executable injection, the variant with no DLL on disk and no module-list entry
- Thread execution hijacking — starting
code without
CreateRemoteThread