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
Tools
Compare all tools- bpftoolCLI · open source
- Volatility 3CLI · open source
- ssCLI · built into Linux
- ausearchCLI · built into Linux
- readelfCLI · built into Linux
- findCLI · built into Linux
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
| Item | Location | Notes |
|---|---|---|
| Loaded programs | Kernel; bpftool prog show | ID, type, name, tag, loaded_at, uid, map IDs, owning PIDs |
| Maps | bpftool map show | Data shared with user space; rootkits store hidden PIDs or keys here |
| Links / attachments | bpftool link, bpftool net (XDP, tc), bpftool cgroup tree, bpftool perf | Where each program is attached |
| Pinned objects | /sys/fs/bpf/ (bpffs) | Keep programs and maps alive without a process; lost at reboot |
| Classic BPF socket filters | ss -0 -p -b (packet sockets with filters), /proc/net/packet | BPFDoor-style magic packet listeners |
| Loaders | Any ELF that calls bpf(); eBPF object files (ELF ... eBPF) | Started by systemd units, cron, rc scripts or LD preload |
| Kernel settings | kernel.unprivileged_bpf_disabled, net.core.bpf_jit_harden | See sysctl |
What it proves
- Which programs run in the kernel right now, their type and attach point: a
kprobeon__x64_sys_getdents64or atracepoint/syscalls/sys_exit_readprogram from an unknown loader is a strong rootkit indicator. - When each was loaded (
loaded_at) and by which UID and process (pidson 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)
| Field | Meaning |
|---|---|
| ID | Kernel program ID; restarts at boot |
| Type | kprobe, tracepoint, raw_tracepoint, xdp, sched_cls (tc), lsm, cgroup_*, socket_filter, tracing |
name | Up to 15 characters, set by the loader; can be anything |
tag | Hash of the program instructions; same code gives the same tag |
loaded_at, uid | Load time and loading user |
pids | Processes holding a file descriptor to it; empty for pinned-only programs |
map_ids | Maps 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 startingsd_), and installed agents (Falco, Cilium, Tetragon, Datadog, CrowdStrike and others) load theirs. Anything else needs an owner. - Programs with no
pidsand a pin under/sys/fs/bpfsurvived 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,lsorssoutput disagrees with/procor with the memory image, an eBPF hook may be rewriting results. Compare with offline analysis before trusting any live tool. kernel.unprivileged_bpf_disabled=0or a recent change to it in/etc/sysctl.d/lowers the bar for loading programs without root.
See also
Related artifacts
- Linux Kernel Modules: lsmod, Taint Flags and Boot Config
- /etc/ld.so.preload and LD_PRELOAD: Linux Linker Hijacking
- /proc Live Artifacts: Processes, Sockets and Deleted Files
- Linux Memory Acquisition: LiME, AVML and /proc/kcore
- systemd Unit Files: Linux Service Persistence
- dmesg, kern.log and the Kernel Ring Buffer on Linux
- sysctl, Boot Parameters and binfmt_misc on Linux