core-jmp core-jmpdeath of core jump

Singularity Rootkit: Evading Elastic Defend Module Load Detection

How Singularity rootkit bypasses Elastic Defend 9.5's enhanced module load detection through source obfuscation, BPF hook manipulation, and exploitation of trusted process lists.

oxfemale September 2, 2026 10 min read 77 reads
Export PDF
Singularity Rootkit: Evading Elastic Defend Module Load Detection
Original text: “Singularity Rootkit: Evading Elastic Defend Module Load Detection”0xMatheuZ, August 31, 2026. Code blocks, tables, and diagrams below are reproduced verbatim with attribution captions.

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.

elastic-agent status and bpftool showing module_load prog and elastic_ebpf_ev maps
Map id 33 is hash map named elastic_ebpf_ev with key_size=4, representing elastic_ebpf_events_trusted_pids. Source: original article.

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.

make compiling Singularity from /dev/shm/Singularity
Source: original article.

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.

Kibana showing 76 alerts: 69x Linux.Rootkit.Generic, 6x Linux.Rootkit.BrokePKG, 1x Kernel Object File Creation
Source: original article.

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
make obfuscate running: module name fs_util_1hj, scanning modules/ with function counts per file
Source: original article.

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.

file renaming output: trace.c -> sys_cf941g.c, hooks_write.c -> drv_tuzd0g.c, clear_taint_dmesg.c -> kern_q8s6yd.c, etc.
Source: original article.

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.

compilation complete showing fs_util_1hj.ko, modinfo output: license GPL, name fs_util_1hj, vermagic 6.8.0-138-generic
Source: original article.

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
Source: original article.

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.

loader output: found module_load prog fd=3, map id=33 type=1 key=4 name=elastic_ebpf_ev, tgid 3212 -> trusted_pids (bypassed), module loaded. dmesg grep fs_util_ returns empty. dmesg grep taint returns empty.
Source: original article.

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.

Kibana Alerts page showing 0 alerts, No items found
Source: original article.

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
make obfuscate OBF_OUT=/var/lib/dkms/sg_build, modinfo showing license GPL name sys_core_nke vermagic 6.8.0-138-generic, loader output tgid 3183 trusted_pids bypassed module loaded, ps aux showing Elastic agent still running
Source: original article.

What Got Bypassed

The following table summarizes all Elastic Defend detection mechanisms and the evasion techniques that neutralize them:

DetectionHow 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
Source: original article.

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.

oxfemale Vulnerability research, reverse engineering, and exploit development.
// Discussion