← Back to writeups Evasion

AV Evasion Fundamentals

Why behavioral AV is fundamentally different from AMSI — and what that means for loader design

// What is this?
How malware hides from antivirus — and why scanning for files is a losing game.

Traditional AV fingerprints known malware (like a most-wanted list). Attackers bypass it by encrypting or rewriting their payload so the signature doesn't match. Behavioral AV watches what programs *do* — create processes, inject into other processes, reach out to the internet. Fooling behavioral detection means avoiding suspicious actions entirely, which forces attackers to be more surgical. Understanding both sides helps defenders tune detection without drowning in false positives.

PowerShell — AV Evasion Detection
# Check AMSI bypass
try {
  [Ref].Assembly.GetType("System.Management.Automation.AmsiUtils").GetField("amsiInitFailed","NonPublic,Static").GetValue($null)
} catch { "AMSI check failed - may be tampered" }

# Defender exclusions (attacker added)
Get-MpPreference | Select-Object -ExpandProperty ExclusionPath
Get-MpPreference | Select-Object -ExpandProperty ExclusionProcess

# PowerShell execution policy bypass indicators
Get-WinEvent -FilterHashtable @{LogName="Microsoft-Windows-PowerShell/Operational"; Id=4104} -MaxEvents 200 -ErrorAction SilentlyContinue |
  Where-Object { $_.Message -match "bypass|hidden|-enc|FromBase64" }
If this flags: AMSI returning True for amsiInitFailed = bypassed. Any user folder in Defender exclusions is suspicious. Encoded PowerShell is evasion.

Most bypass tutorials target Windows Defender's AMSI hook. That works against Defender. Against behavioral AV engines (Kaspersky, CrowdStrike, SentinelOne), AMSI patching is irrelevant — these products run kernel-mode drivers that see syscalls directly, not managed .NET buffers. Understanding the difference changes every design decision.

Two Different Threat Models

AVDetection MethodBypass Approach
Windows DefenderAMSI + ETW + SignaturesPatch AMSI/ETW, encrypt strings, obfuscate signatures
Kaspersky KISKernel drivers + Behavioral + CloudMinimal behavior, unknown signature, no file operations
CrowdStrike FalconBehavioral AI + Process graphLegitimate parent chain, avoid high-risk APIs in sequence
SentinelOneStory-based detection + Kernel hooksBreak the story (unpredictable execution flow)

Kaspersky's kernel drivers (klif.sys file filter, klhk.sys syscall hooks, klids.sys intrusion detection, klwtp.sys web traffic) sit below the Windows subsystem. They see everything before user-mode AV hooks can be patched. Patching AMSI/ETW against KAV accomplishes nothing.

What Actually Triggers Behavioral Detection

BehaviorWhy It Triggers
Reading browser Login Data SQLiteKnown credential theft path
Reading wallet paths (%AppData%\Exodus\*)Known stealer behavior
VirtualAlloc + VirtualProtect RWXShellcode injection signature
Connecting to unknown IP on port 4444/443/80C2 beacon heuristic
Registry Run key writesPersistence pattern
Process hollowing (suspend + unmap + map)Behavioral signature
Reading LSASS memoryCredential dumping

A minimal reverse shell — connect, receive, execute, send — often evades behavioral engines because it matches zero of these patterns. Feature creep (adding a stealer, adding a keylogger) is what introduces detection surface. Capability and detectability scale together.

The Layered Evasion Model

Layer 0 — Delivery

Initial access vector. LNK with RTL filename spoof, macro-enabled document, DLL side-loading into signed binary, USB drop. The delivery mechanism determines the first detection surface. Signed binaries loading side-loaded DLLs create a trusted parent process.

Layer 1 — Sandbox Detection

Before any payload logic runs, detect automated analysis environments and exit cleanly. AV vendors submit suspicious files to sandboxes with limited resources, time acceleration, and no user interaction. Each check targets a different sandbox limitation.

Layer 2 — FUD Loader (Stage 1)

The on-disk artifact must have zero readable strings, zero imports, and an unknown signature. PEB walking resolves Win32 APIs without an import table. XOR encryption hides string constants. Random variable names prevent signature matching on symbol patterns.

Layer 3 — Memory-Only Execution (Stage 2)

Stage 1 fetches encrypted Stage 2 shellcode from C2 over HTTPS. Decrypt in-memory to RW, then change to RX via direct syscall (NtProtectVirtualMemory via syscall stub, not VirtualProtect). Stage 2 never touches disk — file system filter drivers see nothing.

Layer 4 — Minimal Payload

Stage 2 does only what is needed: WSAStartup → socket → connect → recv/CreateProcess/send loop. No file operations. No registry. No browser DB access. No wallet reads. Behavioral engines track action sequences — a short sequence with no known-bad patterns clears behavioral thresholds.

Sandbox Detection Checks

These five checks run before any payload logic. All are read-only — they trigger no behavioral alerts. A sandbox hit causes silent exit (not crash, not error dialog — clean exit is less suspicious than abnormal termination).

// 1. RAM check — sandboxes typically have < 4GB RAM
ULONGLONG mem;
GetPhysicallyInstalledSystemMemory(&mem);
if (mem < (4 * 1024 * 1024)) return 0;  // exit

// 2. CPU count — sandboxes often use 1 CPU
SYSTEM_INFO si;
GetSystemInfo(&si);
if (si.dwNumberOfProcessors < 2) return 0;

// 3. VM process check — common virtualization artifacts
// Check for: vmtoolsd.exe, VBoxService.exe, qemu-ga.exe
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
// [walk process list, compare against known VM process names]

// 4. Sleep acceleration — sandboxes compress time to speed analysis
DWORD t0 = GetTickCount();
Sleep(3000);
DWORD elapsed = GetTickCount() - t0;
if (elapsed < 2500) return 0;  // sandbox compressed the sleep

// 5. Mouse movement — sandboxes have no user interaction
POINT p1, p2;
GetCursorPos(&p1);
Sleep(5000);
GetCursorPos(&p2);
if (p1.x == p2.x && p1.y == p2.y) return 0;  // no mouse movement = no human

FUD Loader: Zero Imports via PEB Walking

A standard PE binary lists its imported DLLs and functions in the Import Address Table. AV engines scan the import table for known-dangerous functions (VirtualAlloc, CreateRemoteThread, WriteProcessMemory). PEB walking resolves these APIs at runtime without any import table entry.

The Process Environment Block (PEB) at FS:[0x30] / GS:[0x60] contains a linked list of loaded modules. Walking this list locates kernel32.dll by hash, then walks its export directory to find LoadLibraryA and GetProcAddress — without importing either.

// djb2 hash — deterministic API resolution without strings
static DWORD hash_name(const char* s) {
    DWORD h = 5381;
    while (*s) h = ((h << 5) + h) + *s++;
    return h;
}

// Walk PEB to find kernel32.dll (no LoadLibrary needed)
// 1. TIB → PEB via FS/GS segment register
// 2. PEB → PEB_LDR_DATA → InMemoryOrderModuleList
// 3. Walk list, compare module name hash against hash("KERNEL32.DLL")
// 4. Found → walk export directory for desired function hashes

// Result: call any Win32 API with zero import table entries
// objdump -p loader.dll | grep "DLL Name" → empty output

String Encryption (XOR)

Strings in an executable are readable by any scanner that runs strings(1) on the binary. All string constants — DLL names, API names, hardcoded paths — are encrypted at build time with a random XOR key. The key is embedded, but because each build uses a different key, there is no signature to match.

// BAD — string visible in binary, matchable by scanner
LoadLibraryA("amsi.dll");

// GOOD — encrypted at compile time, decrypted in-place
// "ntdll.dll" XOR 0x4B = {0x27,0x2F,0x29,0x25,0x65,0x27,0x25,0x25}
unsigned char s1[] = {0x27,0x2F,0x29,0x25,0x65,0x27,0x25,0x25,0x00};
char t[16];
for (int i = 0; i < 8; i++) t[i] = s1[i] ^ 0x4B;
t[8] = 0;
// Use t ("ntdll.dll"), then zero it
memset(t, 0, sizeof(t));  // remove from memory post-use

A build script generates a new random key per compile, encrypts every string constant into a header file, and produces a binary with no recognizable strings. The same binary never produces the same signature twice.

Memory-Only Execution: RW → RX Transition

The standard shellcode injection pattern (VirtualAlloc(RWX)) is a known behavioral trigger — allocating memory that is simultaneously writable and executable is an immediate flag. The correct approach separates write and execute phases:

  1. Allocate RW memory (PAGE_READWRITE) — not executable, benign
  2. Write decrypted shellcode into the RW buffer
  3. Change protection to RX (PAGE_EXECUTE_READ) via direct syscall
  4. Execute — memory was never RWX simultaneously

Using a direct syscall stub for NtProtectVirtualMemory (rather than calling VirtualProtect from kernel32.dll) avoids the userland hook that behavioral engines place on that function. The syscall number is resolved dynamically from the NTDLL export table at runtime.

Testing Protocol

The correct testing order validates functionality before adding evasion complexity. Diagnosing an evasion failure in untested code is significantly harder.

  1. No AV: Disable all AV. Verify the tool connects, commands execute, output returns. If broken here, it's a code bug, not an evasion gap.
  2. Defender only: Enable Defender RTP. If detected, identify which layer triggers — file scan (signature), memory scan (AMSI), or behavioral (ETW). Fix that layer.
  3. Behavioral AV: Full AV with behavioral detection enabled. Monitor what triggers — if detected, analyze whether it's signature (rebuild), behavioral (reduce payload actions), or network heuristic (change protocol).
  4. Iterate: Each detection event is diagnostic data. Signature match → change byte patterns. Behavioral hit → remove or delay the triggering action. Heuristic → change execution flow.

The Minimal Payload Principle

The most evaded payload in any test is often the simplest: connect socket, receive command, spawn cmd.exe with redirected stdio, send output, loop. It does nothing detectable because it does almost nothing at all. Feature additions (persistence, credential theft, lateral movement) each introduce new behavioral signals.

This is why capability separation matters architecturally: run a minimal persistent shell first, then send dedicated modules for each additional capability. A credential harvester that runs once and exits creates a shorter detection window than a persistent beacon that harvests on every connection.

Detection — Blue Team

Import table anomaly: PE files with empty or minimal import tables are statistically rare. A DLL with zero imports should be inspected — it's either a rare legitimate use case or a loader using PEB walking.

RW→RX transition: NtProtectVirtualMemory calls that change a previously-written region from non-executable to executable. Sysmon Event ID 8 (CreateRemoteThread) and ETW memory protection change events.

Syscall anomaly: Direct syscalls that bypass ntdll.dll (syscall instruction not preceded by expected ntdll address range). EDR kernel callbacks still fire, but the userland hook is bypassed.

Sandbox evasion IOC: Processes that call GetPhysicallyInstalledSystemMemory, GetSystemInfo, CreateToolhelp32Snapshot, GetCursorPos in sequence before any network activity are exhibiting sandbox fingerprinting behavior.

Process lineage: A process with a legitimate parent (signed binary) spawning a network connection followed by cmd.exe — the parent has no business creating network connections and shells.

Summary: Reducing Detection Surface

Demo

Loader execution against active AV — payload delivery using the techniques described above.

MITRE ATT&CK: T1055 (Process Injection) · T1027 (Obfuscated Files or Information) · T1497 (Virtualization/Sandbox Evasion) · T1620 (Reflective Code Loading)