eBPF Rootkit
Loading an eBPF program attached to kernel tracepoints or kprobes to hide processes, files, or network connections from userspace tools — a kernel-assisted rootkit that needs no loadable kernel module.
The extended Berkeley Packet Filter (eBPF) lets a userspace program load small,
verified bytecode programs into the kernel and attach them to hooks —
tracepoints, kprobes, or network-socket filters — without writing a full
kernel module. That legitimate observability mechanism (the basis of tools
like bpftrace and modern EDR sensors) also gives an attacker with sufficient
privilege a way to run code at the kernel boundary: an eBPF program attached to
a syscall like getdents64 (used by ls, ps, and most directory-listing
code) can filter out entries matching the rootkit's own PID or file names
before the result ever reaches userspace, hiding it from every tool that walks
/proc or the filesystem through the normal API — no unsigned .ko for a
kernel-module signature check to flag.
How it works
The rootkit's userspace loader compiles or embeds an eBPF bytecode blob and
attaches it via the bpf() syscall:
// simplified loader
int prog_fd = bpf_prog_load(BPF_PROG_TYPE_KPROBE, ebpf_bytecode, ...);
int perf_fd = perf_event_open_for_kprobe("sys_getdents64");
ioctl(perf_fd, PERF_EVENT_IOC_SET_BPF, prog_fd);
ioctl(perf_fd, PERF_EVENT_IOC_ENABLE, 0);The attached program runs on every invocation of the hooked syscall and
rewrites its return buffer in kernel space before the calling process ever
sees it — filtering out dirent entries whose name matches the rootkit's
hidden PID, file, or connection, entirely transparently to ps, ls, or
netstat, none of which do anything wrong; they simply never receive the
hidden data from the kernel.
Detection & analysis
Static analysis: A userspace binary that calls bpf() with
BPF_PROG_TYPE_KPROBE (or the newer BPF_PROG_TYPE_TRACING) attached to
syscalls like getdents64, bpf itself, or network-facing hooks, especially
combined with perf_event_open, is a strong signal outside of known
observability tooling (Falco, Cilium, Tetragon, standard eBPF-based EDR
agents) — context and provenance matter here, since the mechanism itself is
also entirely legitimate.
Dynamic analysis: bpftool prog list (and bpftool prog show <id> --json
for detail) enumerates every eBPF program currently loaded in the kernel,
including its type and attach point; diff this against an expected baseline
for the host. Cross-view detection is the strongest signal available: compare
what /proc or ps report against an independent enumeration path — reading
task lists via a separate kernel interface, or querying from a hypervisor/host
outside the affected guest — since a discrepancy proves something is filtering
one view but not the other.
Detection rule hint: Enable kernel lockdown mode (lockdown=confidentiality
or stricter) where feasible, which restricts unprivileged BPF program loading.
Alert on bpf() syscalls from processes outside an allow-listed set of
observability agents, and periodically run bpftool prog list via an
out-of-band collector (not something the rootkit could also hook) to catch
programs a compromised in-guest agent might otherwise hide from itself.