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.
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.
| AV | Detection Method | Bypass Approach |
|---|---|---|
| Windows Defender | AMSI + ETW + Signatures | Patch AMSI/ETW, encrypt strings, obfuscate signatures |
| Kaspersky KIS | Kernel drivers + Behavioral + Cloud | Minimal behavior, unknown signature, no file operations |
| CrowdStrike Falcon | Behavioral AI + Process graph | Legitimate parent chain, avoid high-risk APIs in sequence |
| SentinelOne | Story-based detection + Kernel hooks | Break 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.
| Behavior | Why It Triggers |
|---|---|
Reading browser Login Data SQLite | Known credential theft path |
Reading wallet paths (%AppData%\Exodus\*) | Known stealer behavior |
VirtualAlloc + VirtualProtect RWX | Shellcode injection signature |
| Connecting to unknown IP on port 4444/443/80 | C2 beacon heuristic |
| Registry Run key writes | Persistence pattern |
| Process hollowing (suspend + unmap + map) | Behavioral signature |
| Reading LSASS memory | Credential 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.
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.
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.
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.
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.
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.
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
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
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.
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:
PAGE_READWRITE) — not executable, benignPAGE_EXECUTE_READ) via direct syscallUsing 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.
The correct testing order validates functionality before adding evasion complexity. Diagnosing an evasion failure in untested code is significantly harder.
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.
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.
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)