There is a certain irony in the fact that one of the most powerful defensive technologies in the Linux kernel has become one of the most effective offensive ones. eBPF (extended Berkeley Packet Filter) was designed to make Linux systems more observable: you could attach small, sandboxed programs directly to kernel tracepoints, syscalls, and network hooks without loading a full kernel module. Security vendors fell in love with it immediately. Falco, Tetragon, Cilium, countless EDR sensors: they all depend on eBPF to provide that deep visibility into what a system is actually doing. The problem is that the same kernel primitives that make eBPF ideal for monitoring also make it ideal for hiding. And the attacker who fully understood this arrived several years before most defenders did.

cover

Why eBPF changed the rootkit calculus

Traditional Linux rootkits loaded as LKM (Loadable Kernel Modules). The technique was effective but loud: every serious kernel configuration ships with module signing requirements, and any mature Linux EDR will alert on an unsigned or unexpected module load. The forensic artifact trail was also manageable. Volatility has had solid module enumeration and hidden module detection for years, and a skilled analyst could often find the implant by comparing lsmod output against the kernel’s internal module list, hunting for DKOM (Direct Kernel Object Manipulation) discrepancies, or scanning for hooked syscall table entries.

eBPF programs require no kernel module. They run inside the kernel’s own eBPF virtual machine, loaded via the bpf() syscall, verified by the kernel’s built-in verifier, and JIT-compiled to native code. On modern kernels with BTF (BPF Type Format) support, they have precise, stable access to kernel data structures without any of the fragile offset arithmetic that made old-school rootkits break between kernel versions. An attacker who loads a malicious eBPF program gets code running in kernel space, with access to kernel data, and without the traditional module loading artifacts that every EDR is watching for. The attack surface is real, the tooling exists, and the forensic community spent several years scrambling to catch up.

The proof-of-concept landscape is instructive. TripleCross demonstrated full rootkit capabilities: backdoor activation via magic packet, library injection into running processes, execution hijacking, and persistent access, all built on eBPF primitives. Boopkit showed a working TCP reverse shell triggered by a crafted packet, with the eBPF program intercepting raw socket traffic before it reached any userspace tool. ebpfkit went further: network scanning, container breakouts, and RASP bypass from a single eBPF-based implant. And Pamspy, the least flashy of the bunch, quietly hooks pam_get_authtok via eBPF uprobes and dumps credentials for every PAM-authenticated session to a log file, with no kernel module, no modified binary, and no obvious indicator of compromise.

The most operationally mature example discovered in a real-world incident is LinkPro, analyzed by Synacktiv in late 2025 during an investigation into a compromised AWS-hosted infrastructure. LinkPro installs two eBPF modules: a Hide module using tracepoint and kretprobe programs to intercept getdents (hiding files and processes) and sys_bpf (hiding its own BPF programs from the bpf() syscall), and a Knock module using XDP and TC programs to wait for a magic TCP packet (window size 54321) before opening the C2 channel. If the kernel lacks CONFIG_BPF_KPROBE_OVERRIDE, it falls back to the classic /etc/ld.so.preload trick and injects a shared library to maintain userspace hiding. The entire design philosophy is layered deniability: if the kernel-level hiding fails, the userspace hiding takes over.

What the kernel actually stores, and where

To understand what a memory analyst can recover, you need a clear picture of how the kernel manages eBPF programs. When a program is loaded via bpf(BPF_PROG_LOAD, ...), the kernel allocates a struct bpf_prog in kernel memory and assigns it a unique integer ID tracked in the prog_idr (ID radix tree). The program metadata includes its type (socket filter, kprobe, tracepoint, XDP, etc.), its name (a 16-character string, often left as a generic or deliberately misleading identifier), the set of BPF helper functions it calls, and a pointer to the JIT-compiled native code. Attached programs are referenced via struct bpf_link objects that bind a program to a specific attachment point, such as a syscall or network hook.

This structure is the forensic analyst’s primary target. The prog_idr is a kernel-global data structure, and on a non-tampered system, it provides a complete inventory of all loaded eBPF programs. The immediate problem is that a sufficiently clever eBPF rootkit can intercept the sys_bpf syscall and hide its own entries from userspace tools that call bpf(BPF_PROG_GET_NEXT_ID, ...). LinkPro does exactly this. If you run bpftool prog list on a compromised system, the malicious programs simply do not appear. The rootkit is hiding inside the very mechanism you would normally use to detect it.

This is where memory forensics regains its advantage. A memory dump captures the raw state of physical RAM, including the kernel’s internal prog_idr data structure, regardless of what the sys_bpf syscall filter has been instructed to hide. The rootkit can manipulate the syscall interface. It cannot retroactively alter the memory snapshot you already acquired.

The Volatility 3 eBPF plugin

The ebpf_programs plugin for Volatility 3, originally submitted to the Volatility Foundation’s 2023 plugin contest, addresses this gap directly. The plugin enumerates all eBPF programs by walking the kernel’s prog_idr radix tree in the memory image, completely bypassing the syscall interface. For each program found, it extracts the name, type, load time, and the list of BPF helper functions the program references. It then applies heuristic classification based on which helpers are called: research published by Ben-Gurion University has shown that a specific set of helper functions appears consistently in malicious eBPF samples, including bpf_probe_write_user, bpf_override_return, and bpf_send_signal, among others.

The plugin also supports dumping the JIT-compiled binary of each program for offline disassembly:

# Basic enumeration
python3 vol.py -f /path/to/linux_memory.raw linux.ebpf_programs

# Enumerate and dump all programs to disk for further analysis
python3 vol.py -f /path/to/linux_memory.raw linux.ebpf_programs --dump

A clean system running a standard EDR agent might show output like this:

Name                Type            LoadTime            Suspicious  Helpers
------------------- --------------- ------------------- ----------- -------------------------------------------------------
falcon_sensor_bpf   BPF_PROG_TYPE   2026-05-25 06:11:02  False      bpf_get_current_pid_tgid, bpf_perf_event_output, ...
tetragon_enforce    BPF_PROG_TYPE   2026-05-25 06:11:04  False      bpf_get_current_comm, bpf_map_lookup_elem, ...

A compromised system running LinkPro might show something quite different, particularly if the rootkit failed to load its sys_bpf hiding module but succeeded with other components:

Name                Type            LoadTime            Suspicious  Helpers
------------------- --------------- ------------------- ----------- -------------------------------------------------------
falcon_sensor_bpf   BPF_PROG_TYPE   2026-05-25 06:11:02  False      bpf_get_current_pid_tgid, bpf_perf_event_output, ...
                    KRETPROBE       2026-05-25 14:33:17  True       bpf_override_return, bpf_probe_write_user, bpf_get_current_task
                    XDP             2026-05-25 14:33:19  True       bpf_xdp_adjust_head, bpf_redirect, bpf_get_prandom_u32

The empty name field and the specific combination of helpers are immediate red flags. bpf_override_return in particular is a strong indicator: it is used to modify the return value of a kernel function, precisely the mechanism used by programs that hide files by manipulating getdents return values. bpf_probe_write_user allows kernel-space eBPF programs to write into userspace memory, a capability with extremely limited legitimate use and obvious offensive applications.

For fast triage, it helps to map helpers to likely investigative value:

Helper Why it matters
bpf_override_return Often associated with return-value tampering and hiding logic
bpf_probe_write_user Strong signal for user-space memory tampering
bpf_send_signal Can be used for process disruption or forced termination
bpf_map_update_elem Common in legitimate tooling, but worth correlating with the full program behavior

The FKIE-CAD BPF rootkit workshop provides a structured methodology that complements the plugin, particularly for classifying eBPF program types and attachment points. Volexity’s research on Linux kernel tracing and memory forensics extends this further by discussing the enumeration of bpf_link structures in memory, which allows the analyst to determine exactly where each suspicious program is attached within the kernel. Knowing that an unnamed KRETPROBE-type program is attached to __x64_sys_getdents64 is considerably more actionable than knowing that a suspicious program exists somewhere.

Correlating memory artifacts with system behavior

Finding suspicious eBPF programs in a memory dump is the beginning of the analysis, not the conclusion. The next step is correlating what the memory tells you with everything else you can observe about the system’s behavior. This is where the integration with other Volatility capabilities matters.

If a rootkit is hiding processes via getdents manipulation, the process list visible through normal userspace tools will be incomplete. Volatility’s linux.pslist and linux.pstree plugins walk the kernel’s internal task lists directly, bypassing the filesystem layer entirely. If you find processes in the Volatility output that do not appear in the system’s ps output, and you also find eBPF programs with bpf_override_return attached to getdents, you have a coherent story. The rootkit’s method of hiding and the evidence of hidden processes are visible in the same memory image.

For network activity, the connection is even more direct. Suspicious eBPF programs of type XDP or TC (Traffic Control) attached to network interfaces should immediately prompt an examination of active network connections via linux.netstat and, if the system uses IPsec, an analysis of the XFRM state as discussed in an earlier post on this site. A magic-packet-activated backdoor like LinkPro will not appear in ss or netstat on a compromised system, but the XDP program waiting for that magic packet will be visible in the prog_idr via memory analysis, and any active tunnel it has established will leave XFRM state in memory.

The BPF memory forensics walkthrough by lolcads provides useful practical detail on the specific kernel data structures involved, including the layout of struct bpf_prog across different kernel versions and the offset arithmetic required for manual parsing when the plugin cannot resolve a symbol. This matters in practice because you will not always have perfectly matched symbol tables for the exact kernel version you are analyzing, and understanding the underlying structures allows you to perform targeted manual searches when the automated tooling fails.

The helper function heuristic and its limits

The classification approach based on suspicious helper functions is effective but not infallible. The research by Ben-Gurion University that underpins the plugin’s classification logic achieved optimal precision on known rootkit samples, but it was evaluated against a bounded set of existing public tools. A sufficiently motivated attacker can write an eBPF rootkit that avoids the most obviously suspicious helpers and instead chains together benign-looking primitives to achieve the same effect through multiple steps. The bpf_map_update_elem helper, for example, is used by virtually every legitimate eBPF application, but it can be combined with careful map design to pass data between programs and implement covert channels that do not require the obviously malicious helpers.

The more important point is that helper function analysis is a starting point for triage, not a definitive verdict. Any eBPF program found in a memory dump that was not explicitly loaded by a known, authorized process should be investigated, regardless of whether the heuristic flags it as suspicious. The dump-and-disassemble workflow is essential here. Once you have the JIT-compiled binary extracted to disk, standard reverse engineering tools apply: Ghidra with the eBPF architecture plugin, or even direct disassembly with the llvm-objdump eBPF target, can reconstruct the program’s logic well enough to determine its purpose. An unnamed program of type KRETPROBE that disassembles to code clearly manipulating getdents return values does not need a helper function heuristic to be classified as malicious.

There is also the question of what happens when the rootkit successfully hides itself from the prog_idr. TripleCross specifically includes a capability to hide its own BPF programs by intercepting bpf(BPF_PROG_GET_NEXT_ID, ...). If the prog_idr walking in memory also gets somehow corrupted or modified, the memory forensics approach faces a harder problem. The Ben-Gurion research addressed this by acquiring the memory image via hypervisor, ensuring that the rootkit running in the guest cannot tamper with the acquisition process itself. For most incident responders operating without a hypervisor layer, this means that memory acquisition methodology matters: acquiring from a known-good kernel using tools like LiME before the system is fully compromised, or leveraging cloud provider memory snapshots that bypass the guest OS entirely, is the correct approach when eBPF-based threats are suspected.

A practical starting point

The operational takeaway for a DFIR analyst encountering a suspected eBPF-based compromise is straightforward: treat every suspicious Linux system as a potential prog_idr concealment scenario. Do not trust the live system’s output of bpftool, ss, ps, or any other userspace tool. Acquire memory via a trusted, out-of-band mechanism, whether that is a hypervisor snapshot, a cloud provider memory dump API, or LiME loaded from read-only media before any in-scope process starts. Then use Volatility 3 with current Linux profiles and the ebpf_programs plugin as the first pass, followed by linux.pslist, linux.netstat, and if applicable, XFRM state analysis.

A short response checklist can keep the workflow disciplined:

  1. Acquire memory from a trusted source before relying on any live-system output.
  2. Enumerate eBPF programs from the image, not from bpftool on the host.
  3. Correlate suspicious helpers with process, file, and network artifacts.
  4. Dump and disassemble anything that lacks a clear authorized owner.
  5. Preserve the raw image and the extracted JIT binaries for later review.

The eBPF subsystem is not going away. Neither is its attractiveness as an offensive platform. The kernel’s own investment in eBPF’s capabilities means that every new kernel version brings new hook types and new opportunities for both defenders and attackers. Understanding the forensic artifacts that eBPF programs leave in memory, regardless of what the syscall interface is told to report, is no longer an advanced research topic. It is a baseline requirement for anyone doing Linux incident response in 2026.