Linux Detection Engineering - Fileless Execution
We reproduced five Linux fileless execution patterns with FENIX, including memfd_create staging, interpreter one-liners, deleted binaries, and in-memory kernel module loads, then mapped each to the Elastic Defend rules that catch it.
Fileless execution on Linux has moved from niche tradecraft into real-world intrusion chains. By executing payloads from memory or anonymous file descriptors, attackers can reduce on-disk artifacts and weaken controls that rely heavily on file inspection.
In our own analysis of VoidLink, we observed how fileless execution can be paired with a rootkit. The loader scans for processes running from memfd and passes their PIDs to the rootkit, allowing an already running fileless implant to be hidden immediately after activation. Socket’s research into a malicious PyPI package impersonating SymPy, named sympy-dev, showed the execution side of this tradecraft, using memfd_create and /proc/self/fd/<fd> to execute a downloaded Linux ELF payload from an anonymous memory-backed file descriptor. Trend Micro’s research into Quasar Linux reflects the same broader shift.
The process of building these payloads is also getting simpler. Large language models (LLMs) make it easier to prototype loaders and combine primitives such as memfd_create, /proc/<pid>/fd, encryption, compression, and runtime execution. Our Global Threat Report highlights how adversaries are already using AI to lower the barrier to entry for cybercrime, including the generation of simple but effective malicious loaders and tools.
From a detection engineering perspective, the key point is that memfd-based fileless execution doesn’t mean invisible execution. These payloads may avoid a durable executable on disk, but they still depend on observable behavior: creating an anonymous memory-backed file and writing an ELF payload into it, and then executing it through a file descriptor.
Starting with Elastic Security 9.4.0, Elastic Defend records process events for memfd_create on Linux (kernel 5.10.16+ required for eBPF-based event sourcing), improving visibility into one of the core primitives behind modern fileless ELF execution. Additionally, visibility for loadable kernel module activity was implemented by reporting init_module and finit_module-based loads through the load_module event action on process events.
In this publication, we’ll break down how fileless execution works on Linux and reproduce common approaches through proofs of concept (PoCs). We’ll also map the resulting behavior to detection opportunities using Elastic Defend telemetry.
What is fileless execution on Linux?
Before diving into specific techniques, it’s important to define what fileless actually means on Linux. The term is often used broadly, but the underlying techniques aren’t all the same. Some payloads are truly backed by anonymous memory, and some are reconstructed at runtime by a script or interpreter. Still others briefly touch disk before being deleted.
To understand these techniques, we first need to cover a few Linux building blocks.
How Linux file descriptors work
On Linux, processes interact with files, sockets, pipes, devices, and many other resources through file descriptors. A file descriptor is simply a numeric handle that refers to an open resource inside a process. Standard input, standard output, and standard error are usually file descriptors 0, 1, and 2, but a process can open many more.
For example, when a process opens a file, the kernel returns a file descriptor such as 3 or 4. The process can then read from it, write to it, map it into memory, or pass it to another process, depending on how it was opened and what type of object it represents.
How processes execute through /proc/<pid>/fd
Linux exposes process information through the proc filesystem. One useful part of this is /proc/<pid>/fd/, which contains references to the open file descriptors of a process. For example, /proc/1337/fd/3 refers to file descriptor 3 in process 1337. The special path /proc/self/fd/3 refers to file descriptor 3 in the current process. In other words, self is a shortcut for “this process.”
What memfd_create does
One of the most important primitives behind Linux fileless execution is memfd_create. This system call creates an anonymous memory-backed file and returns a file descriptor to it. The object doesn’t have a normal path on disk, but it can still behave like a file; that is, a process can write bytes to it, resize it, map it into memory, and reference it through procfs paths, such as /proc/<pid>/fd/<fd>.
This makes memfd_create useful for fileless execution because a payload can be stored and executed through a file descriptor rather than a conventional filesystem path. We’ll go deeper into this pattern in a later section.
Deleted executables that keep running
Not all fileless execution is truly memory-only. Another common pattern is deleted-file execution. In this case, a payload is written to disk and executed. It’s then removed, while the process keeps running.
For example, a running process may expose /proc/1337/exe -> /tmp/payload (deleted) through /proc/<pid>/exe.
The original file is gone from the filesystem, but the process is still running from it. This isn’t the same as memfd execution, but it’s often grouped under fileless or semi-fileless tradecraft because the durable file artifact has been removed.
Interpreters and runtime staging
Fileless execution can also happen without a standalone ELF payload. Attackers may use shell, Python, Perl, or another interpreter to download, decode, decrypt, decompress, and execute logic directly at runtime.
For example, a simplified chain may look like curl → base64 -d → openssl → gzip -d → bash or python -c '<download, decode, execute logic>'. In these cases, the detection focus shifts away from executable files and toward process lineage, command-line behavior, network activity, suspicious pipelines, and interpreter abuse.
Five types of Linux fileless execution
For this publication, we’ll use “fileless execution” as an umbrella term for execution patterns where the final payload doesn’t exist as a normal, durable executable on disk at the time defenders inspect the system.
Type | Meaning | Example |
Anonymous memory-backed execution | The payload exists in a |
|
Interpreter-backed execution | The malicious logic runs through an interpreter without a standalone ELF payload. |
|
Fileless staging | A loader exists on disk or in a script, but the final payload is reconstructed, decoded, decrypted, or decompressed at runtime. |
|
Deleted-file execution | The payload existed on disk but was unlinked after execution, while the process kept running. |
|
Fileless loadable kernel module loading | Kernel module bytes are loaded from memory, a file descriptor, or another transient source. |
|
These different classes are important to understand as they require a different approach to detection engineering. A memfd-backed payload may avoid a durable file artifact, but it still relies on observable behavior: creating an anonymous file descriptor and writing executable content into it, along with executing through a descriptor-based path. A deleted executable may disappear from disk, but the running process can still expose traces through /proc/<pid>/exe. An interpreter-backed payload may never create an ELF file, but it still leaves process lineage, command-line, network, and runtime behavior.
Each of the following sections will go deeper into one of these approaches and show one or more PoCs. They’ll also map the resulting behavior to Elastic Defend telemetry and detection opportunities.
memfd_create fields in Elastic Defend
From Elastic Security 9.4.0, Elastic Defend records a process event when a process calls memfd_create, the syscall that creates an anonymous memory-backed “file” and returns a file descriptor. That call is often the first clear step in memfd-based fileless ELF execution, before the payload is written and executed through /proc/self/fd/<n> or an exec syscall on the fd. An example event with the relevant fields highlighted is displayed below:
The extension fields under process.Ext.memfd mirror the syscall arguments. process.Ext.memfd.name is the name passed to memfd_create(). It isn’t a path on disk; the kernel uses it for identification in /proc, where the fd often shows up as something like memfd:<name>. process.Ext.memfd.flags is the raw flags bitmask from the syscall. The Boolean fields flag_cloexec, flag_allow_seal, flag_hugetlb, flag_noexec_seal, and flag_exec decode that bitmask into the main options documented on the man page: MFD_CLOEXEC (close-on-exec on the new fd), MFD_ALLOW_SEALING (sealing allowed), MFD_HUGETLB (huge-page backing), and on Linux 6.3+, MFD_NOEXEC_SEAL and MFD_EXEC for non-executable versus explicitly executable memfds when vm.memfd_noexec policies apply.
Together with process and parent process context, these fields make the memfd creation step queryable for hunts and sequences (for example, download → memfd_create → exec), without relying only on later /proc/self/fd paths in child processes.
Linux fileless execution patterns
In the following sections, we walk through the main Linux fileless execution patterns from a detection engineering perspective. For each one, we summarize how the technique works in the wild and tie it to public research or open-source tooling, where possible. We also reproduce the resulting behavior in a controlled lab so it can be mapped to Elastic Defend telemetry.
Reproducing fileless execution patterns with FENIX
To keep comparisons fair, we use Fileless Execution for NIX (FENIX), a lab-only framework of small C helpers, benign sample payloads, and a command-line interface (CLI) that implements the same syscall chains described in malware write-ups. A brief overview of the framework is displayed below:
The typical workflow is as follows:
git clone https://github.com/elastic/FENIX.git && cd FENIX
make all
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
export FENIX_BIN_DIR=$PWD/bin
fenix check
fenix run <technique> [options]
fenix cleanupFor bulk coverage testing, fenix run-all runs a matrix of techniques in one pass (lab matrix docs). In this publication, we highlight one representative command per pattern; you can correlate the resulting events in Kibana and extend runs with fenix info <technique> for variants (fexecve, execveat, --fchmod, and others).
Anonymous memory-backed execution with memfd_create
Anonymous memory-backed execution is the most direct form of memfd-based fileless execution. Instead of writing an ELF payload to /tmp, /dev/shm, or another filesystem location, a loader creates an anonymous memory-backed file and writes the ELF bytes into the returned file descriptor. It then executes that descriptor through procfs.
The core primitive is small. The following snippet omits error handling and setup code but shows the essential flow:
// Create an anonymous memory-backed file.
int fd = memfd_create("payload", 0);
// Write ELF bytes into the file descriptor.
// In real malware, these bytes may be downloaded, embedded,
// decrypted, or decompressed at runtime.
write(fd, elf_payload, elf_payload_size);
// Reference the anonymous file through procfs.
char path[64];
snprintf(path, sizeof(path), "/proc/self/fd/%d", fd);
// Execute the ELF from the file descriptor path.
char *argv[] = { "payload", NULL };
execve(path, argv, NULL);The important detail is that the payload isn’t executed from a conventional filesystem path, such as /tmp/payload or /usr/bin/payload. Instead, execution is routed through a file descriptor that points to an anonymous memory-backed file. This removes the durable executable artifact from disk, but it doesn’t remove the behavior. The loader still has to create the memfd and populate it with executable content, along with executing it through /proc/self/fd/<fd> or a related descriptor-based execution path.
In real-world loaders, the ELF bytes may come from many different sources. They may be embedded in the loader, downloaded from a remote server, decrypted at runtime, decompressed from an encoded blob, or received from another process. Those staging choices change the surrounding telemetry, but the execution pattern remains the same: anonymous file creation, payload write, and descriptor-backed execution.
Executing a native ELF from a memfd
Native ELF execution via memfd is the pattern most often discussed in threat reports and PoC tooling. A small native binary (or injector) calls memfd_create() and copies an ELF into the fd. It then executes it. Socket’s analysis of the malicious sympy-dev PyPI package describes a Linux path that stages a downloaded ELF and runs it through memfd and /proc/self/fd/. The same building blocks appear in public research code, such as fileless-elf-exec (fee), fireELF, and classic write-ups on in-memory-only ELF execution. Malware development blog posts, such as “In-Memory-Only ELF Execution (Without tmpfs)” and “Linux Malware Development: Fileless Execution with memfd_create and Python,” document the same sequence.
The exec step in FENIX’s helper matches the snippet above: memfd_create → write → execve on /proc/self/fd/<fd>:
snprintf(fd_path, sizeof(fd_path), "/proc/self/fd/%d", mfd);
execve(fd_path, exec_argv, environ);To simulate this pattern, we can execute the following FENIX command:
fenix run memfd-exec \
--payload payloads/hello_elf/hello \
--method procfs-fd \
--name fenix_payload
Step-by-step (this run)
1. open(2) — read benign ELF from payloads/hello_elf/hello
2. memfd_create(2) — anonymous RAM file fenix_payload
3. write(2) — copy ELF bytes into memfd
4. execve(2) — path /proc/self/fd/<N> points at memfd-backed inode
hello from fenixFENIX prints the steps executed by this run and displays the “hello from fenix” message, indicating that the in-memory execution was successful. Looking at the documents generated by this run in Kibana:
Shows the most basic fileless execution sequence imaginable, the memfd_create() syscall, followed by the execution of a memory file descriptor named “memfd:fenix_payload”. Zooming in on the memfd_create document, we can see that we do have more fine-grained telemetry available via the process.Ext.memfd.* fields:
However, we won’t be digging deep into this, as most attack chains can easily be differentiated from benign activity through the use of process/parent relationships.
The main detection opportunities here are twofold:
Sequencing the
memfd_create()syscall, followed by the execution of a memory file descriptor by the same process in a short time span.Detecting the sole execution of a memory file descriptor, potentially through a suspicious parent process.
The main endpoint rules that trigger on this activity can be found here:
Executing a script from a memfd
Attackers don’t always drop an ELF. The same anonymous fd primitive can hold script bytes (shell, Python, Perl, awk, and similar), which are then executed from memory. Public tooling and write-ups often combine download or decode stages with interpreter abuse; Perly Shells is a recent example of Perl-centric staging and memfd-style execution in that vein.
Two execution shapes matter for detection. In the shebang case, the loader writes a script that starts with #! into a memfd and calls execve on /proc/self/fd/<fd>. The kernel reads the shebang from the memfd-backed file and starts the named interpreter (for example, /bin/sh). In the explicit interpreter case, the loader calls execve on python, perl, or any other interpreter with similar functionality, with /proc/self/fd/<fd> as the script argument. FENIX uses an inheritable memfd (without MFD_CLOEXEC) for shebang runs, so the fd remains usable across that exec. If MFD_CLOEXEC is set, the file descriptor is closed during execve; for script/shebang-style execution, the descriptor often needs to remain inheritable so the interpreter can still read it. This matches how the pattern is implemented in the wild:
int mfd = fenix_create_memfd_inheritable(memfd_name);
write_all(mfd, script, len);
snprintf(fd_path, sizeof(fd_path), "/proc/self/fd/%d", mfd);
execve(fd_path, argv_exec, environ); /* shebang: kernel picks interpreter */To simulate the shebang path with FENIX:
fenix run memfd-script-exec \
--script payloads/scripts/hello_shebang.sh \
--method shebang \
--name fenix_script
Step-by-step (this run)
1. read — load script bytes from payloads/scripts/hello_shebang.sh
2. memfd_create(2) — inheritable memfd fenix_script (no CLOEXEC)
3. write(2) — script content into memfd
4. execve(2) — kernel reads shebang → spawns with script fd
hello from fenix shebang scriptIn Kibana, the timeline differs from native ELF-in-memfd; you still see memfd_create on fenix-memfd-script-exec, but the follow-on exec shows the interpreter from the shebang (rather than the helper binary) executing the file descriptor (payload) directly.
When trying to cover this flow, we need to be aware of the fact that passing file descriptors via process arguments is a common, benign activity. Inherited FDs, pipes, sockets, containers, build tooling, shells, and runtimes that pass an already open FD path may trigger on the exec event. For this reason, a detection focusing on the entire chain (memfd_create → exec process relationship) is preferred.
It’s important to be aware of the main variations of this flow. As we’re dealing with interpreters for the execution, we must scope the detection logic appropriately and try not to miss any highly targeted living-off-the-land binaries (LoLbins). For example, if we want to execute the same payload via awk, we can:
fenix run memfd-script-exec \
--script payloads/scripts/hello.awk \
--method shebang \
--name fenix_script
hello from fenix awkUsing awk modifies the telemetry only slightly:
But it does show that we need to be careful about restricting the exec process entries too much when crafting detection logic that isn’t prone to FPs and isn't brittle to trivial in-the-wild bypasses.
The important detection lesson is the relationship between anonymous file descriptor creation and a capable interpreter consuming /proc/self/fd/<fd> as executable input, not the specific interpreter. Whether the script is handled by sh, python, perl, awk, or another interpreter, the defensive anchor remains the same; that is, a memfd-backed script is created and then consumed through a descriptor-backed execution path.
The detection logic we crafted for this flow looks as follows:
The sequence of a
memfd_createsyscall, followed by a directexecof its self-created file descriptor through aprocess.entity_idrelationship.Direct execution of a file descriptor through a capable living-off-the-land interpreter.
The main endpoint rules triggering on this sequence are listed below:
memfd_create inside an interpreter one-liner
A third variant keeps the loader inside the interpreter process. Instead of a separate stager binary calling memfd_create, the attacker runs python3 -c '…', perl -e '…', or php -r '…'. That one-liner creates the memfd, reads the ELF (commonly from stdin), writes it into the fd, and execs via /proc/self/fd/<fd>. There’s no second native helper in the final chain, only the interpreter and a large argument blob. This shows up in fileless-elf-exec (fee), Perly Shells. Similarly, THC’s hackshell tool implemented a similar technique in its _memexec() function; the same applies to cases where Python or Perl leads staging, even when the last stage isn’t a standalone dropper.
In this variant, the ELF bytes never need to exist as a standalone payload path. The interpreter receives the bytes through stdin, creates the memfd internally, writes the payload into that anonymous file descriptor, and then replaces itself with the descriptor-backed payload. This makes the interpreter both the staging environment and the execution launcher, rather than relying on a separate native helper binary.
A minimal Python-shaped loader looks like this:
fd = os.memfd_create("", MFD_CLOEXEC)
os.write(fd, sys.stdin.buffer.read())
os.execve(f"/proc/self/fd/{fd}", ["fenix_payload"], os.environ)Unlike the shebang case, MFD_CLOEXEC is safe here because the Python process calls os.execve() to replace itself; no child interpreter needs to inherit the fd.
Perl loaders in the wild often resolve memfd_create via syscall() for portability; the exec target is the same idea:
exec {"/proc/$$/fd/$f"} "fenix_payload" or die "exec failed: $!\n";For now, we can simulate this activity by piping the output of a script to the interpreter. In another example later in this article, we’ll stage it remotely and pull it down before piping it, which is more realistic. However, the same pattern (just without the egress network event) applies. While we’re at it, let’s list our available interpreters and run the same command; for example, perl, python3, and php.
# run 3x, with --interpreter perl, python3 & php
cat payloads/hello_elf/hello | fenix run interpreter-memfd-exec --interpreter perl --mode one-liner
Step-by-step (this run)
1. subprocess — /usr/bin/perl -e <see payload below>
2. memfd_create(2) — syscall or libc API inside interpreter
3. read stdin — ELF bytes piped by FENIX (payloads/hello_elf/hello)
4. write(2) — copy ELF into memfd fd
5. execve(2) — /proc/self/fd/N with argv[0]=fenix_payload
hello from fenixTo showcase the variance, the screenshot below shows the documents for all three executions:
Depending on the language, we can see variances in language syntax and process arguments. However, we do see an overlapping execution flow, where the interpreter executes, followed by the execution of the memfd_create() syscall, and finalized by the execution of the anonymous file descriptor.
For the detection of this execution flow, we mainly focus on the execution of an interpreter with a command flag (-c, -r, and -e), calling the memfd_create() syscall. But also the sequence of memfd_create → exec via an interpreter (with potentially suspicious process arguments) and the execution of an anonymous file descriptor via an exec event can result in strong detection opportunities. The rules associated with this vector are as follows:
Fileless staging and runtime payload assembly
Fileless staging is the delivery layer before the memfd patterns earlier in this article. Something in the environment still acts as a loader, a curl one-liner, a Python dropper, an installer, a paste fetch, but the payload bytes are often assembled only at runtime: downloaded, base64-decoded, XOR-decrypted, or extracted from an archive. Only after that transform does execution move to anonymous memory, an interpreter, or an embedded memfd loader.
That’s a different question from Is the final hop fileless? Staging is about how bytes arrive. memfd execution is about how the ELF or script is run. Public chains, such as RemoteELFMemExec (remote fetch + XOR + in-memory ELF), the malicious sympy-dev PyPI package (download + memfd), and Perly Shells (curl → layers → then Perl-centric execution), mix both. FENIX models the staging half in a single technique, fileless-staging, with optional decode/decompress and two execute back ends.
Piping a downloaded payload to an interpreter
The most realistic pattern in the wild is to download or pull encoded content, decode in the loader process, and run a script through a living-off-the-land interpreter without writing the script to a stable path. Let’s execute the scenario:
fenix run fileless-staging \
--source-file payloads/scripts/hello.rb \
--remote \
--remote-backend uguu \
--remote-fetch curl \
--execute interpreter \
--interpreter ruby \
--mode stdin
Technique leveraged
Stage → optional decode/decompress → execute. Mimics remote droppers.
Step-by-step (this run)
1. upload — push local payload to remote bin (auto)
2. fetch — download via curl: https://<url>/nytOFXqs.rb
3. stdin — feed downloaded script to ruby via subprocess
hello from fenix rubyThis results in the following Elastic Defend events:
In the figure above, we see curl being used to pull down remote Ruby source code and piping it straight into the interpreter. Since curl was used for downloading the payload, and ruby for executing it, we can no longer rely on a process → process sequence. Since both processes in this and other similar scenarios will have the same parent (in this case, FENIX [Python], but this could be malware, a shell, or any other interpreter), we can chain the network (or process) event to the execution event.
When a payload is piped to an interpreter, we’ll see the interpreter to which the command is piped in the process.command_line field, together with the process that ends up being called through this command line.
As a simple example, if we execute cat payloads/scripts/hello.rb | ruby, ruby will resolve to whatever the path is set to resolve it to. In my case, which ruby returns /usr/bin/ruby. However, because the Linux CLI is friendly, it tries to make life convenient for us and allows us to run processes without having to deal with process versions. Because of this, we’ll see that when we execute ls -lah /usr/bin/ruby, we invoke a symlink that resolves to lrwxrwxrwx [...] /usr/bin/ruby -> ruby3.2. This means that when we pipe a command to ruby, we expect the process.command_line to show up as ruby, while the process.executable is expected to be a resolved symlink of ruby, which is /usr/bin/ruby3.2. The documents are shown below:
So, if we sequence any process, reading/downloading a file, with the execution of an interpreter, where process.command_line points to that interpreter and is limited to one process argument, we can detect the flow of a pipe. Red Canary put out an interesting read back in 2024, called “The detection engineer’s guide to Linux,” where more information about process flows can be found.
Since there are many interpreters available to pipe code to and many LoLbins capable of pulling remote files, focusing on the most common tooling, such as curl/wget, won’t suffice. To avoid being error-prone, we do, however, need to rely on an inclusion list, as this activity is very frequent. By using an inclusion list of processes, with a clever Event Query Language (EQL) trick that allows us to compare fields (stringcontains(process.executable, process.command_line)), and a limit on the number of process arguments (process.args_count == 1), we can sequence the events on process.parent.entity_id and detect this flow.
We split this logic into several different detection and endpoint rules, based on the frequency of occurrence in benign system processes:
Deleted-file execution
Deleted-file execution is best understood as semi-fileless execution; the payload briefly existed on disk, but the durable artifact is removed before or during investigation. The binary is gone from ls and from most file-centric hunts, but the kernel still holds the inode open via a file descriptor; execution continues through fexecve (or equivalent open-fd exec). This isn’t memfd-backed fileless execution; there’s no memfd_create, and /proc/<pid>/exe often shows a path such as /tmp/payload (deleted).
Attackers use it to reduce forensic artifacts on disk without going to anonymous memory. It appears in older dropper chains and tooling and remains common in post-exploitation tradecraft alongside download-to-/tmp staging. It sits between fully fileless memfd patterns and a normal persistent dropper: brief create → exec → delete, with the process surviving on an open inode.
The important syscall chain in the child is:
fd = open("/tmp/payload", O_RDONLY);
unlink("/tmp/payload"); /* name removed; inode kept via fd */
fexecve(fd, argv, environ);Let’s take a look at this example through FENIX:
fenix run deleted-file-exec \
--payload payloads/sleep_elf/sleep \
--path /tmp/fenix_sleep \
--args "60" \
--no-wait
Technique leveraged
Classic 'file on disk then deleted while running':
Copy ELF to path → child opens → unlinks → fexecve.
Step-by-step (this run)
1. read/copy — payload to /tmp/fenix_sleep
2. fork(2)
3. Child: open → unlink(2) → fexecve(2)
4. Parent returns (--no-wait)
fenix sleep payload: sleeping for 60 secondsOn Linux, the (deleted) suffix is visible in /proc/<pid>/exe and in some endpoint process-executable fields, depending on the deleted executable's activity. Looking at the CLI, we do see the executable suffix stating that the process is running but has been deleted:
readlink /proc/$(pgrep -f fenix_sleep | head -1)/exe
/tmp/fenix_sleep (deleted)Looking at the documents in Kibana:
We see a sequence of a process execution, followed by a file deletion via the same process.entity_id. This is a suspicious sequence, since generally, most executables remain on disk after execution. To detect this sequence, we created two rules:
Process Execution Followed by Self-Deletion (and Elastic Defend counterpart)
File Creation, Execution, and Self-Deletion in Suspicious Directory
Loading kernel modules from memory with finit_module
Starting with Elastic Security 9.4.0, Elastic Defend adds improved Linux visibility for loadable kernel module activity by reporting init_module and finit_module-based loads through the load_module event action on process events. This gives defenders a direct process-level view into one of the most important kernel-code execution paths on Linux.
Unlike userland memfd execution, the fileless loadable kernel module loading path crosses into kernel execution and generally requires root or equivalent module-loading capability, making it both higher-impact and more constrained. Kernel module loading targets the kernel; a .ko image must be loaded through init_module(2) or finit_module(2) without keeping a module file on disk for the load itself. The staging story is usually:
memfd_create → write module bytes → init_module / finit_moduleAttackers and PoCs (including fileless_loader-style research and supply-chain cases, such as Quasar Linux) differ mainly in where the bytes sit before the syscall and whether init_module or finit_module is used, not in the overall idea of staging in memory and then loading.
The most direct memfd pattern copies the module into an anonymous file descriptor and calls finit_module on that fd. The kernel reads the image from memory; there is no need for a persistent .ko path at load time. A closely related variant still uses memfd_create, but the loader mmap’s the fd and passes the mapped image to init_module. The telemetry is the same for defenders: memfd staging followed by a module-load syscall.
Some loaders skip memfd entirely and read the .ko into a heap buffer, then call init_module on that userspace copy. Others embed the module inside the loader binary and call init_module from a static array, so nothing reads a .ko from disk at runtime. A forked loader creates and fills the memfd in the parent while the child performs the mmap and init_module, same memfd-to-load flow, but parent and child split the syscalls for correlation and evasion. Finally, the least “fileless” shape opens a normal .ko file and uses finit_module on the open fd; it is useful as a baseline but still leaves a file on disk until unlink or cleanup.
FENIX implements these patterns under lkm-load. For detection engineering, the anchor remains module bytes staged in memory (or on a transient fd) → init_module / finit_module, typically as root, often near memfd_create. Because the sequence is similar, we only run one scenario in this article; specifically, the most general memfd → load_module chain, memfd-finit-module.
export FENIX_BIN_DIR=$PWD/bin
F="$PWD/.venv/bin/fenix"
sudo env FENIX_BIN_DIR=$PWD/bin $F run lkm-load \
--module payloads/hello_lkm/hello_lkm.ko \
--method memfd-finit-module \
--i-understand-this-loads-kernel-code
Technique leveraged
In-memory kernel module load (benign hello_lkm — logs only).
Step-by-step (this run)
1. Read .ko → memfd_create → write
2. finit_module(2) — load from memfd fd (loader2)Let’s take a look at the documents generated by Elastic Defend in Kibana through this method:
In the screenshot above, we see the fenix-finit-module executing, followed by the immediate loading of the hello_lkm.ko loadable kernel module (LKM) through the load_module event action. This sequence can easily be tied together using the process.entity_id. For instances where the loader forks and loads the LKM via the fork, we’ll see a discrepancy in the process relationship. For these instances, we need to sequence the events using the parent → child relationship.
To detect these and the other scenarios, we created several rules related to (fileless) LKM loading via init_module and finit_module:
Potential Loadable Kernel Module Load via Memory File Descriptor
Loadable Kernel Module Load via Forked Memory File Descriptor
Detecting Linux fileless execution: What to anchor on
Fileless execution on Linux isn’t invisible execution. Whether attackers use memfd_create, /proc/self/fd/<fd>, interpreter-based staging, deleted executables, or in-memory kernel module loading, they still rely on observable behavior.
The key for detection engineering is to focus on those unavoidable transitions: anonymous file creation, payload writes, descriptor-backed execution, suspicious interpreter flows, self-deletion, and module-load syscalls.
With Elastic Defend telemetry, including memfd_create and init_module/finit_module process tracking from Elastic Security 9.4.0, these patterns can be sequenced and modeled across different variants. The FENIX examples show that implementation details may change, but the core execution behaviors remain detectable.




