Skip to content

PersistenceAnti-forensicsNetwork

eBPF Programs and Pinned Maps: Linux Kernel Implants

Loaded eBPF programs, pinned objects in /sys/fs/bpf and their loaders on disk: how eBPF rootkits and BPF backdoors hide on Linux and how to find them.

Location
Kernel memory (bpftool prog/map/link), /sys/fs/bpf/ (pinned objects), loader binaries and .o files on disk
Proves
Which eBPF programs are attached to the kernel, what they hook, who loaded them and when, and which on-disk loader restores them
Timestamps
bpftool loaded_at (wall clock) per program; audit BPF records (kernel 5.8+); loader file times
Access
root (CAP_BPF / CAP_SYS_ADMIN) to list programs; kernel.unprivileged_bpf_disabled usually blocks others
Retention
Programs live until unloaded or reboot; pins in /sys/fs/bpf vanish at reboot; loaders persist on disk
Collection
UAC, Velociraptor, bpftool, LiME, AVML

What it is

eBPF lets verified bytecode run inside the kernel, attached to kprobes, tracepoints, network hooks (XDP, tc), cgroups, LSM hooks and sockets. It powers legitimate observability and security tools such as Cilium, Falco, Tetragon, bcc and systemd's own cgroup filters. It also gives attackers a rootkit without a kernel module: programs that hide processes and files by rewriting syscall results, intercept credentials, or wait for a magic packet to open a backdoor (public examples include ebpfkit, TripleCross and Boopkit). Older families use classic BPF instead: BPFDoor attaches a socket filter to wait for its magic packet, and Symbiote injects filters to hide its traffic.

eBPF programs are not files. They exist in kernel memory, which makes this a live-response and memory artifact first. What survives on disk is the loader that puts them back after a reboot.

Where it lives

ItemLocationNotes
Loaded programsKernel; bpftool prog showID, type, name, tag, loaded_at, uid, map IDs, owning PIDs
Mapsbpftool map showData shared with user space; rootkits store hidden PIDs or keys here
Links / attachmentsbpftool link, bpftool net (XDP, tc), bpftool cgroup tree, bpftool perfWhere each program is attached
Pinned objects/sys/fs/bpf/ (bpffs)Keep programs and maps alive without a process; lost at reboot
Classic BPF socket filtersss -0 -p -b (packet sockets with filters), /proc/net/packetBPFDoor-style magic packet listeners
LoadersAny ELF that calls bpf(); eBPF object files (ELF ... eBPF)Started by systemd units, cron, rc scripts or LD preload
Kernel settingskernel.unprivileged_bpf_disabled, net.core.bpf_jit_hardenSee sysctl

What it proves

  • Which programs run in the kernel right now, their type and attach point: a kprobe on __x64_sys_getdents64 or a tracepoint/syscalls/sys_exit_read program from an unknown loader is a strong rootkit indicator.
  • When each was loaded (loaded_at) and by which UID and process (pids on bpftool builds with that support).
  • With auditd, every load and unload since kernel 5.8 (type=BPF ... op=LOAD), even if the program is gone.
  • It does not prove intent on its own; many hosts run dozens of legitimate programs.

Key fields

# bpftool prog show
27: cgroup_device  name sd_devices  tag 3a0e8e9b8ab9e0a9  gpl
        loaded_at 2026-09-19T22:01:11+0000  uid 0
        xlated 504B  jited 309B  memlock 4096B
        pids systemd(1)
311: tracepoint  name handle_getdents  tag 9f1c6a0d2b7e4411  gpl
        loaded_at 2026-09-20T03:14:07+0000  uid 0
        xlated 2120B  jited 1287B  memlock 4096B  map_ids 88,89
        pids dbus-daemon-lnch(4302)
FieldMeaning
IDKernel program ID; restarts at boot
Typekprobe, tracepoint, raw_tracepoint, xdp, sched_cls (tc), lsm, cgroup_*, socket_filter, tracing
nameUp to 15 characters, set by the loader; can be anything
tagHash of the program instructions; same code gives the same tag
loaded_at, uidLoad time and loading user
pidsProcesses holding a file descriptor to it; empty for pinned-only programs
map_idsMaps used; dump with bpftool map dump id N

Timestamps

loaded_at is wall-clock time from the kernel's boot-time offset, printed in the host's zone or UTC depending on the bpftool build. The audit BPF record carries the usual epoch timestamp and the program ID. On disk, loader and .o files carry normal file system times, and the systemd unit or cron entry that starts the loader dates the persistence.

Retention

Nothing survives a reboot except what a loader re-creates. Pins under /sys/fs/bpf keep programs alive after the loader exits, which is why rootkits pin and then delete their loader from disk; the program remains in memory with no process attached. Capture RAM before shutting such a host down (see memory acquisition).

Collection

# Live, as root, before anything else changes
bpftool prog show  > /media/ir/bpf_prog.txt;  bpftool -j prog show > /media/ir/bpf_prog.json
bpftool map show   > /media/ir/bpf_map.txt
bpftool link show  > /media/ir/bpf_link.txt;  bpftool net show > /media/ir/bpf_net.txt
bpftool cgroup tree > /media/ir/bpf_cgroup.txt; bpftool perf show > /media/ir/bpf_perf.txt
ls -laR /sys/fs/bpf > /media/ir/bpffs.txt
ss -0 -p -b > /media/ir/packet_sockets.txt     # classic BPF filters on packet sockets
for id in $(bpftool prog show | awk -F: '/^[0-9]+:/{print $1}'); do bpftool prog dump xlated id "$id" > /media/ir/prog_$id.txt; done

UAC's bpftool and ebpf artifacts list programs, dump them and list /sys/fs/bpf. Velociraptor can run the same commands through an execve artifact. Always add a memory image.

Parsing

# Memory: recent Volatility 3 releases enumerate eBPF programs from a Linux image
vol -f mem.lime linux.ebpf.EBPF

# Audit trail of loads and unloads
ausearch -if /cases/2026-017/var/log/audit -m BPF -i

# On disk: eBPF object files and loaders
find /mnt/evidence -xdev -type f -size -20M -exec file {} + 2>/dev/null | grep -i 'eBPF'
readelf -S suspicious.o | grep -E 'kprobe|tracepoint|xdp|tc|lsm|maps'

Investigator tips

  • Build an allowlist: systemd loads small cgroup_* programs (names starting sd_), and installed agents (Falco, Cilium, Tetragon, Datadog, CrowdStrike and others) load theirs. Anything else needs an owner.
  • Programs with no pids and a pin under /sys/fs/bpf survived their loader. Search the disk and deleted files for the loader, and check systemd units and ld.so.preload for what starts it.
  • The kernel log warns when a program uses bpf_probe_write_user, the helper most rootkits use to tamper with user memory. Search kern.log and the journal for it.
  • If ps, ls or ss output disagrees with /proc or with the memory image, an eBPF hook may be rewriting results. Compare with offline analysis before trusting any live tool.
  • kernel.unprivileged_bpf_disabled=0 or a recent change to it in /etc/sysctl.d/ lowers the bar for loading programs without root.

See also