
Executive Summary
Elastic Defend 9.5.0 strengthened kernel module load detection by introducing the taint_flags field in module_load BPF events and implementing a new EQL-based detection rule. However, this detection mechanism relies on collecting telemetry from the kernel module loading subsystem—a process that can be circumvented by manipulating BPF hook behavior through trusted process list poisoning. The Singularity rootkit demonstrates a multi-layered evasion strategy: source code obfuscation defeats pattern-based YARA detection, strategic insertion of the loader process into Elastic’s trusted_pids BPF map causes module load events to be silently suppressed, and compilation to excluded filesystem paths bypasses file creation rules. The result is a kernel rootkit that loads with zero detection alerts across all three Elastic detection layers.
This analysis examines the technical mechanisms behind Elastic Defend’s module load detection, reverse-engineers the BPF bytecode of the offending hook, demonstrates the trusted_pids bypass, and documents the complete attack chain including source obfuscation, BPF map manipulation, and filesystem exclusion path exploitation.
Setup
The test environment consists of an Ubuntu system running kernel 6.8.0-138-generic with Elastic Defend 9.5.2 deployed. The Elastic agent is fully operational and actively monitoring system events. Using the bpftool utility, we can verify that the module_load BPF program is loaded and attached to the kernel’s module loading tracepoint.

The Detection Rules
Rule cc3dab79: “Kernel Module Loaded with Tainting Flags” (new in 9.5.0)
Elastic introduced this new EQL-based detection rule in version 9.5.0 to catch kernel modules that trigger taint flags during loading. The rule monitors the process event stream for module_load actions and fires when the process.Ext.load_module.taint_flags field is populated.
min_stack_version = "9.5.0"
index = ["logs-endpoint.events.process*"]
language = "eql"
process where host.os.type == "linux" and event.type == "start" and
event.action == "load_module" and
process.Ext.load_module.taint_flags != null
The taint_flags field itself is populated via BPF_CORE_READ(mod, taints) inside the kernel’s tp_btf/module_load tracepoint handler. This tracepoint fires after the module’s initialization function completes, capturing the kernel’s assessment of whether the module carries problematic flags (GPL violation, unsigned, etc.).
Rule 05cad2fb: “Tainted Kernel Module Load” (2023, still active)
This earlier detection rule, deployed in 2023, remains active and continues to catch module load events through a different vector: syslog message inspection. It depends on the Filebeat agent reading kernel messages from /var/log/syslog and triggers on a specific kernel message pattern indicating signature or key verification failures.
index = ["logs-system.syslog-*"]
language = "kuery"
host.os.type:linux and data_stream.dataset:"system.syslog" and
process.name:kernel and
message:"module verification failed: signature and/or required key missing - tainting kernel"
Both detection rules operate independently: YARA rules fire on the compiled kernel object file itself on disk, and the BPF hook fires at runtime during module initialization. Neither depends on the other, meaning both must be defeated to achieve silent loading.
Two Separate Detection Problems
A test run without evasion measures generates approximately 76 individual alerts originating from two distinct detection paths. The first path consists of 69 YARA rule matches for Linux.Rootkit.Generic and 6 matches for Linux.Rootkit.BrokePKG—these fire when Elastic scans the compiled kernel object file on disk before it is loaded. The second detection path is the BPF module_load hook, which fires at runtime. Defeating these requires independent strategies: source code obfuscation addresses the YARA detections, while manipulation of the trusted_pids BPF map addresses the runtime BPF hook.
Without Evasion
To establish a baseline, we compile and load Singularity without any evasion measures applied. The compilation happens in a temporary directory (/dev/shm), and the resulting kernel object file immediately triggers Elastic’s detection pipeline.

Within seconds of the module object appearing on disk and being loaded, Elastic’s Kibana dashboard populates with alerts. The YARA detection matches (69 Linux.Rootkit.Generic, 6 Linux.Rootkit.BrokePKG) represent file-based scanning, while additional alerts track the module loading event itself and attempt to correlate related activity.

Obfuscating the Source Code
The YARA rules that fire on the unobfuscated kernel object primarily match against known rootkit function names and patterns embedded in the binary. Singularity includes an obfuscation tool (name_randomizer.py) that systematically randomizes function names, renames source files, escapes sensitive strings in read-only data sections (.rodata), and generates a new Makefile containing only the randomized identifiers. This approach ensures that no recognizable function name or symbol remains visible to static analysis tools.
make obfuscate OBF_OUT=/root/deploy

The obfuscation process scans all source modules and generates random identifiers for each function. The output includes the new module name (e.g., fs_util_1hj) and logs each file transformation.

After the transformation, compilation proceeds with the randomized identifiers. The resulting kernel object file carries none of the original recognizable strings. Functions like singularity, hook_*, hide_module, or kallsyms_lookup_name do not appear in the binary, leaving YARA with nothing to match.

The trusted_pids Bypass
While source obfuscation defeats file-based YARA detection, it does nothing to prevent the kernel’s BPF module_load hook from firing at runtime. Analysis of the BPF bytecode reveals the key insight: when the module_load program executes, it retrieves the current process’s TGID (thread group ID) and performs a lookup in a specific BPF map named elastic_ebpf_events_trusted_pids (map ID 33). If the process TGID is found in this map, the entire program exits early without emitting any telemetry. This is the intended behavior for whitelisting trusted processes—but it becomes an attack surface when an attacker can manipulate the map.
![bpftool prog dump xlated name module_load showing bpf_get_current_pid_tgid call, map[id:33] lookup (elastic_ebpf_events_trusted_pids), and conditional jump at line 12](https://core-jmp.org/wp-content/uploads/2026/09/2.png)
The exploitation is elegant: before calling finit_module(), the loader process retrieves its own TGID and inserts it into the trusted_pids map. The kernel module loads silently because the BPF hook immediately exits. Once loading completes, the TGID is removed from the map.
uint32_t my_tgid = (uint32_t)getpid();
map_upsert(map_fd, my_tgid); /* insert BEFORE the syscall */
int ko_fd = open(argv[1], O_RDONLY | O_CLOEXEC);
syscall(__NR_finit_module, ko_fd, "", 0);
close(ko_fd);
map_delete(map_fd, my_tgid); /* remove immediately after */
The following output demonstrates successful execution: the loader finds the module_load BPF program (fd=3), identifies the trusted_pids map (id=33), inserts the current TGID (3212), loads the module, and cleans up. Notably, neither dmesg | grep fs_util_ nor dmesg | grep taint produces any output—the kernel logs no module loading event.

Zero Alerts
With both evasion techniques applied—source obfuscation to prevent YARA matches and trusted_pids map insertion to suppress the BPF module_load hook—Singularity loads completely undetected. The Kibana alerts dashboard reflects zero events.

Bonus: The Kernel Object File Creation Rule
Beyond the module load detections, Elastic maintains an additional rule that triggers on kernel object file creation events. This rule applies a new_terms detection on the tuple (process.name, file.name) and fires when a .ko file is created for the first time within a rolling 5-day window. However, the rule includes an exclusion list of common filesystem paths where legitimate out-of-tree drivers are built and compiled, including /var/lib/dkms/*—the standard path for Debian and Ubuntu DKMS (Dynamic Kernel Module Support) builds.
event.category:file and host.os.type:linux and event.type:creation and file.extension:ko and
not (
file.path:(/tmp/mkinitramfs* or /var/cache/uptrack/* or /var/tmp/dracut.* or
/build/* or /var/lib/dkms/* or /var/tmp/portage/* ...)
or process.executable:("/sbin/dracut" or "/usr/bin/pacman" or /snap/* ...)
or process.name:"cpio"
)
By compiling to a path under /var/lib/dkms/, the .ko file creation event falls into the exclusion list and bypasses this detection layer entirely.
make obfuscate OBF_OUT=/var/lib/dkms/sg_build

What Got Bypassed
The following table summarizes all Elastic Defend detection mechanisms and the evasion techniques that neutralize them:
| Detection | How Bypassed |
|---|---|
| YARA on function names (hook_, hide_module, singularity) | Source obfuscation + rename |
| Rule cc3dab79 (BPF module load probe, taint_flags field) | trusted_pids insertion before finit_module |
| Rule 05cad2fb (syslog pattern via filebeat) | read() hook in clear_taint_dmesg.c filters line |
| Rule 1965eab8 (.ko file creation) | OBF_OUT=/var/lib/dkms/sg_build hits exclusion path |
| lsmod / /proc/modules / /sys/module/ | Module list erasure during init |
| dmesg | grep <module_name> | dmesg filter using THIS_MODULE->name |
Key Takeaways
- BPF hook design assumptions matter: The trusted_pids bypass exploits a legitimate feature—whitelisting trusted processes to reduce noise—but demonstrates the risk when kernel memory structures are accessible to unprivileged code.
- Multi-layer detection requires multi-layer evasion: File-based YARA detection, runtime BPF hooks, syslog inspection, and file system rules operate independently. Defeating one does not defeat the others; an attacker must address each separately.
- Source obfuscation is effective but not novel: Randomizing function names and strings is a well-known technique, but it remains effective against pattern-based detections that rely on recognizable symbols.
- Filesystem exclusions create blind spots: Rules that exclude common paths like /var/lib/dkms/ reduce false positives but also reduce coverage. Legitimate tools using those paths become suitable disguises for malicious compilation.
- Kernel module loading is a privileged operation: An attacker must already have root access to insert into BPF maps and load kernel modules. This is not a vulnerability in Elastic Defend for unprivileged users, but rather a showcase of post-exploitation techniques for the already-compromised host.
- Runtime behavior evasion extends beyond code: Even after obfuscation, the rootkit must prevent itself from appearing in module listings, dmesg logs, and proc filesystem interfaces. These secondary evasions compound the difficulty of detection.
Defensive Recommendations
- Lockdown BPF map access: Restrict or audit access to BPF maps through kernel LSM policies or eBPF verifier constraints. Consider whether process whitelisting maps should be readable/writable by user-space code post-load.
- Baseline kernel module inventory: Establish a known-good list of kernel modules expected to load, signed and verified, rather than relying solely on runtime detection of taint flags. Detect loading of modules not in the baseline.
- Combine detection signals: Alert not only on individual events (module load, file creation, syslog message) but on suspicious correlations: a process that creates a .ko file AND inserts itself into a BPF map AND loads a module within a short time window.
- Monitor BPF map modifications: Consider kernel LSM hooks or eBPF programs that log all BPF map insert/delete operations. Even if an attacker inserts a trusted_pids entry, the insertion itself becomes detectable.
- Validate syslog completeness: The syslog rule assumes kernel messages reach the logging system. Implement kernel module hooks that log directly to a protected audit facility or network socket, bypassing the syslog pipeline entirely.
- Expand filesystem exclusion reviews: Periodically audit and tighten exclusion paths in file creation rules. Paths like /var/lib/dkms/ are legitimate but should be monitored for unusual process executables or activity patterns.
- Implement kernel module signing enforcement: On systems where kernel lockdown is feasible, enforce module signature verification and disable the loading of unsigned modules entirely, eliminating the attack surface for custom kernel code.
Conclusion
Elastic Defend 9.5.0 introduced the taint_flags field to module_load events, strengthening runtime detection of kernel module loading activity. However, the underlying BPF infrastructure includes a whitelisting mechanism (trusted_pids) designed to suppress false positives from legitimate system processes. This same mechanism becomes an evasion vector when an attacker with root privileges can manipulate BPF maps. Combined with source code obfuscation to defeat YARA pattern matching, placement of compiled modules in excluded filesystem paths, and suppression of kernel log messages, the Singularity rootkit demonstrates how multi-layered evasion can circumvent comprehensive endpoint detection capabilities. The attack is not a flaw in Elastic Defend’s design but rather a consequence of the inherent difficulty in detecting privileged malicious activity once root access is already established. Defense requires not only runtime detection but also hardened kernel interfaces, comprehensive process integrity monitoring, and baseline verification of system state.
Original text: “Singularity Rootkit: Evading Elastic Defend Module Load Detection” by 0xMatheuZ, August 2026.


