Modern EDRs detect Python-based in-memory shellcode execution by hooking Win32 APIs at the ntdll layer, monitoring memory permission changes, and flagging anomalous ctypes call patterns. But attackers who abuse PyCapsule internals and function pointer casting can sidestep the most obvious signals. Knowing exactly what EDRs watch for is how defenders close the gap.
Pithy Security | Cybersecurity FAQs – The Details
Question: In Python malware evasion, what are the most effective ways defenders can detect multi-stage reflective DLL injection (or pure-Python equivalents via ctypes / cffi) while identifying abuse of _ctypes function pointers and PyCObject / PyCapsule internals used to hide shellcode in memory without obvious WriteProcessMemory calls?
Asked by: Perplexity AI
Answered by: Mike D (MrComputerScience) from Pithy Security.
Why ctypes-Based Shellcode Bypasses WriteProcessMemory Detection
EDRs learned to flag WriteProcessMemory years ago. Attackers responded by never calling it. Pure-Python shellcode execution via ctypes works by allocating RWX memory through VirtualAlloc directly, casting a Python integer (the allocation address) to a ctypes function pointer, and calling it. No WriteProcessMemory. No obvious injection API. The EDR never sees a cross-process write because the shellcode runs in the same process as the Python interpreter.
PyCapsule and the legacy PyCObject make this cleaner for attackers. Both types wrap an arbitrary C pointer with no type enforcement. An attacker stuffs a shellcode buffer address into a PyCapsule, extracts it later via PyCapsule_GetPointer(), casts it with ctypes.cast(), and calls it as a CFUNCTYPE. From an EDR’s behavioral telemetry, this looks like a Python script doing… C interop. Which is what ctypes is for. The signal-to-noise problem is real.
The detection pivot: watch for VirtualAlloc or VirtualProtect calls that set PAGE_EXECUTE_READWRITE (0x40) originating from python.exe or any embedded interpreter process. That permission combination is the meaningful signal, not the specific injection API used.
The Memory Permission Telemetry Gap Most EDRs Miss
Kernel-level ETW (Event Tracing for Windows) and API hooking at the ntdll layer catch a lot, but cffi and ctypes can make calls through Python’s own C extension layer, which some EDR hooks miss depending on hook placement depth. Microsoft Defender for Endpoint and CrowdStrike Falcon both instrument NtAllocateVirtualMemory and NtProtectVirtualMemory at the syscall boundary, which is the right layer.
The gap is in correlating those syscalls back to the originating Python stack. A raw syscall stub built from ctypes byte arrays bypasses userland hooks entirely, going straight to the kernel transition. This is the same technique used in C-based EDR evasion, now available to Python malware with no compiled code required.
Defenders should enable Sysmon Event ID 10 (process access) and Event ID 8 (CreateRemoteThread) alongside memory protection change logging. Pair this with YARA rules scanning Python process memory for shellcode signatures (MZ headers, common stub patterns) using tools like pe-sieve or Moneta. CISA’s guidance on memory-only malware detection (AA22-321A) covers the broader framework.
When Behavioral EDR Rules Actually Catch Python Shellcode Loaders
Behavioral detection works when rules are tuned to the interpreter context. CrowdStrike’s Falcon and SentinelOne both ship rules that flag python.exe spawning unexpected child processes, making unusual network connections post-memory-allocation, or calling VirtualProtect to flip memory from RW to RX (a hallmark of staged shellcode decryption).
The most reliable catch: staged loaders almost always need to change memory permissions twice. Initial allocation is RW for writing shellcode, then a VirtualProtect call flips it to RX before execution. That two-step permission sequence on the same memory region, originating from a scripting interpreter, is a high-fidelity behavioral indicator. MITRE ATT&CK T1055.001 (Dynamic-link Library Injection) and T1620 (Reflective Code Loading) both map to this pattern and have documented detection opportunities in the ATT&CK knowledge base.
What This Means For You
- Enable ETW and Sysmon memory protection change events (Event ID 10, 8) and correlate them specifically against interpreter processes like
python.exe,pythonw.exe, and embedded runtimes. - Deploy memory scanning tools (pe-sieve, Moneta, or EDR memory scanning modules) tuned to detect shellcode stubs and MZ headers inside Python process heap allocations.
- Audit any internal Python tooling or automation scripts that use
ctypesorcffifor legitimate purposes, because attackers blend into approved interpreter usage — your allowlist is their cover. - Monitor for the RW-to-RX
VirtualProtectpermission sequence on the same memory region from scripting interpreter processes, as this two-step pattern is a high-fidelity indicator of staged in-memory execution.
Related Questions
- 1
- 2
- 3
