# Detection primitives for eBPF rootkits

Analysis of three in-the-wild eBPF rootkits (VoidLink, LinkPro, Atomic Arch) abusing rare helpers to hide sockets, BPF programs, and kill debuggers, plus load-time detections.

- Payout: Real-world hack
- Program: Linux kernel
- Analysis by: Datadog Security Labs
- Disclosed: 2026-07-27
- Write-up quality: 8/10
- Original: https://securitylabs.datadoghq.com/articles/detection-primitives-for-ebpf-rootkits/
- Library page: https://bugbountylibrary.com/r/blog%3Asecuritylabs-datadoghq-com-articles-detection-primitives-for-ebpf-rootkits
- Tags: area:os domain:desktop impact:dos impact:integrity kind:incident platform:threat-intel tech:linux technique:reverse-engineering technique:source-review vuln:dos vuln:info-disclosure

**Target:** eBPF programs / kernel syscall paths (recvmsg, bpf, ptrace)
**Root cause:** Rare eBPF helpers (bpf_probe_write_user, bpf_override_return, bpf_send_signal) with almost no legitimate production-tracing use let rootkits tamper with user buffers, override syscall return values, and kill debuggers from kernel context.
**Steps:**
1. VoidLink: kprobe on `__sys_recvmsg` entry captures user buffer pointer from registers; paired kretprobe fires after kernel fills buffer.
2. kretprobe calls `bpf_probe_write_user` to inflate the nlmsg_len of the message preceding the one to hide by exactly `NLMSG_ALIGN(B.nlmsg_len)`, so `NLMSG_NEXT` skips message B while `NLMSG_OK` still passes.
3. LinkPro: tracepoint on `sys_enter_bpf` stashes ctx for BPF_PROG_GET_NEXT_ID (11), BPF_MAP_GET_NEXT_ID (12), BPF_LINK_GET_NEXT_ID (31) keyed by pid_tgid.
4. kretprobe on `__x64_sys_bpf` reads next_id, checks against allowlist map, and calls `bpf_override_return(ctx, -ENOENT)` to truncate enumeration.
5. Atomic Arch: tracepoint on `sys_enter_ptrace` checks request == PTRACE_ATTACH (0x10) or PTRACE_SEIZE (0x4206), looks up target pid in hidden_pids map, and calls `bpf_send_signal(9)` to SIGKILL the calling debugger.
6. Detection: kprobe on `check_helper_call` reads raw helper ID from insn->imm before do_misc_fixups rewrites it; LSM security_bpf_prog hook reads prog type/name/tag; rules match `bpf.cmd == BPF_PROG_LOAD && <helper> in bpf.prog.helpers`.
**How found:** Authors picked one instructive feature per malware family, read the eBPF helper semantics and kernel verifier flow (check_helper_call, do_misc_fixups, __bpf_call_base offset encoding), and reasoned that rare helpers plus narrow program types form a load-time fingerprint before the program can act.
**Exploitation:** Rootkits load eBPF programs via bpf(2) that attach kprobes/tracepoints to syscall paths. VoidLink rewrites Netlink inet_diag responses in place so ss skips hidden-port sockets; LinkPro returns -ENOENT from GET_NEXT_ID walks so bpftool/libbpf stop enumerating at the hidden program ID; Atomic Arch kills any process attempting PTRACE_ATTACH/SEIZE on protected PIDs before the attach completes.
**Escalation:** LinkPro's -ENOENT override truncates the entire enumeration at the hidden ID, hiding all higher-ID programs regardless of allowlist membership, stronger than intended.
**Insight:** Hook check_helper_call to read insn->imm before do_misc_fixups rewrites helper IDs into __bpf_call_base offsets; the raw helper bitmap at BPF_PROG_LOAD is ground truth a running rootkit cannot corrupt, unlike post-attach enumeration.

## Proof of concept (copied exactly from the original report)

**Detection rule for bpf_probe_write_user helper**

```
bpf.cmd == BPF_PROG_LOAD && BPF_PROBE_WRITE_USER in bpf.prog.helpers
```

**Detection rule for bpf_override_return helper**

```
bpf.cmd == BPF_PROG_LOAD && BPF_OVERRIDE_RETURN in bpf.prog.helpers
```

**Detection rule for bpf_send_signal helpers**

```
bpf.cmd == BPF_PROG_LOAD && (BPF_SEND_SIGNAL in bpf.prog.helpers || BPF_SEND_SIGNAL_THREAD in bpf.prog.helpers)
```

