The file scanner reported nothing. No binary reached disk, no harmful script appeared in a temporary directory, and no unfamiliar file had its executable bit set. The alert came from eBPF: an application server's Python interpreter opened a socket and connected to an external IP, then invoked execve for a shell command absent from the process's historical behavior profile. The attack existed entirely in memory and the process tree. There was nothing to scan.
Fileless execution is an established technique. Its continued effectiveness comes from detection infrastructure built largely to locate malicious files. eBPF shifts that view by observing execution at the kernel boundary, whether or not the filesystem was involved.
What "Fileless" Means at the Kernel Level
"Fileless" is not entirely precise. Usually, practitioners mean that no new binary is left on disk for a scanner to flag. Execution still takes place, code still runs, and the operating system still handles syscalls. In eBPF terms, the relevant event is execve, the kernel function that replaces a process's memory image with a new executable. Even when code loads from memory instead of a file, execve-related syscalls remain part of the chain.
More specifically, fileless execution follows these patterns:
- Reflective loading: Shellcode or a shared object is loaded directly into an existing process's memory via mmap() with PROT_EXEC, without being written to disk first. The loader resolves symbols and fixes up relocations in memory.
- Trusted process injection: Malicious code is injected into an existing trusted process (Python, bash, perl, curl) and executed via ptrace() or by writing to /proc/<pid>/mem. The process that executes the payload has a legitimate name and a legitimate parent chain up to the injection point.
- Command execution via scripting interpreters: The attacker's payload reaches a scripting interpreter (bash, Python, Perl, Node.js) as an argument string. execve is called on the interpreter, not on a malicious binary, so the binary being executed is trusted. The malicious content is in the argument.
- memfd_create: Available since Linux kernel 3.17, this syscall creates an anonymous file backed by memory only. Code written to the file descriptor can then be executed via execve on /proc/self/fd/<fd>. No filesystem entry exists for the file.
Why File Scanning Overlooks Fileless Attacks
File scanners (both AV-style and hash-based) operate on files at rest or on file creation events via inotify or fanotify. Reflective loading that uses mmap() to bring executable pages into memory without a filesystem write never triggers the file creation event. memfd_create files do not appear in normal filesystem walks. Script interpreter execution does not create a new file because the interpreter binary already existed.
Hash-based allow listing, which permits only known-good hashes to execute, offers better coverage when a new binary is dropped. It does not address known-good binaries such as Python or bash running malicious arguments, nor reflective shellcode loading that creates no new process image.
MITRE ATT&CK identifies the core techniques as T1055 (Process Injection) and T1059 (Command and Scripting Interpreter), each with several sub-techniques. Both families focus on environments whose detection controls are file-based.
What eBPF Detects Beyond File Scanners
At the kernel boundary, fileless attacks remain visible. They produce distinct syscall traces:
mmap() with executable permissions
A kprobe on mmap(), or on the do_mmap kernel function, records every memory-mapping request and its permission flags. PROT_EXEC | PROT_WRITE is unusual in most application contexts: the process is mapping memory that it will both modify and execute. Legitimate JIT compilers, including V8 and JVM JIT, use this pattern and belong in the process's behavioral baseline. An mmap() with PROT_EXEC | PROT_WRITE from a Python interpreter or an application server that does not normally JIT is a strong signal.
/* Example: detect anomalous PROT_EXEC|PROT_WRITE mmap */
SEC("kprobe/do_mmap")
int detect_exec_write_mmap(struct pt_regs *ctx) {
unsigned long prot = (unsigned long)PT_REGS_PARM3(ctx);
if ((prot & (PROT_EXEC | PROT_WRITE)) == (PROT_EXEC | PROT_WRITE)) {
/* filter by process comm to exclude known JIT processes */
bpf_get_current_comm(&event.comm, sizeof(event.comm));
/* submit to ring buffer for userspace analysis */
bpf_ringbuf_submit(event, 0);
}
return 0;
}
ptrace() on another process
Injection through ptrace() produces a clear kernel trace: ptrace() with PTRACE_POKEDATA or PTRACE_POKETEXT lets one process write code into another process's memory. Production application processes rarely use ptrace legitimately. A kprobe that captures the ptrace() request type and correlates it with the source process identity provides high-confidence process-injection detection.
execve() chains with unusual argument patterns
The parent-child process tree comes from tracking execve() calls alongside the current PID and PPID. When a web application server starts Python, Python starts bash, and bash runs a command using curl with a data URL, the chain itself is the signal rather than the binary name. Normal applications produce predictable execve chains. Attacker chains stand out against that baseline.
memfd_create() followed by execve on /proc/self/fd
A kprobe on memfd_create(), paired with a later execve() whose filename argument falls under /proc/self/fd/, reliably indicates the memfd_create execution technique. Because legitimate production applications seldom use this pattern, an observation warrants alert-level attention.
A Hands-On Forensic Walk-Through
An early-access deployment observed this sequence inside a containerized Python application:
- The web application process (gunicorn worker, PID 4812) received a request that triggered a CWE-502 deserialization vulnerability in a data processing library.
- The deserialization executed attacker-controlled code that called os.system() inside the Python interpreter, which maps to execve() at the kernel level. Arguments included a base64-decoded command string.
- A child process (bash, PID 4817, parent 4812) was spawned. Bash executed curl with arguments pointing to an external IP, downloading a second-stage payload to a file descriptor without touching the filesystem (using a process substitution pattern).
- The downloaded content was piped directly to bash for execution.
The eBPF trace recorded the full chain: gunicorn invoked bash, which was anomalous because gunicorn had never spawned bash during the 14-day behavioral baseline; bash then made a connect() call to the external IP, also anomalous because bash does not normally make outbound connections; finally, bash invoked the second-stage commands. Syscall-level telemetry reconstructed the complete attack. Nothing not already present before the attack began was written to disk.
The Visibility Boundary
eBPF observation at the kernel boundary captures execution events despite evasion at the application layer. This does not mean eBPF detection cannot be bypassed. An attacker who knows kernel-level observability is active and adapts their technique can reduce some signals. Still, code must execute, and execution at the kernel level leaves a trace that is harder to suppress than a filesystem artifact.
Detection models perform best with behavioral baselines: understand the normal execve and mmap pattern for each process, then alert on deviations. Processes with consistent, predictable behavior, as most production services have, create a tight baseline, making deviations high signal. Generic process-name allow lists apply the same concept more weakly because they lack per-application behavioral context.