Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,54 @@ grep -RInE 'iptables|bpfd|dockerd|hpas|/dev/shm|/var/tmp|/tmp/' \

If the host supports `bpftool`, also baseline legitimate BPF usage. Unexpected packet filters, raw sockets, or process names that do not match the backing executable are strong post-exploitation signals even when no listening port is visible.<sup>[[7]](#references)</sup>

## eBPF rootkit stealth, anti-debugging, and detection

These are post-compromise techniques: the operator must already be allowed to load and attach the required eBPF program types. Once attached, however, a program can tamper with user-space observations at kernel-controlled boundaries without changing the underlying socket, process, or BPF object.<sup>[[13]](#references)[[14]](#references)[[15]](#references)[[16]](#references)</sup>

### Skip Netlink records by extending the previous length

`ss -tn` opens `NETLINK_INET_DIAG`, sends a `SOCK_DIAG_BY_FAMILY | NLM_F_DUMP` request, and parses the returned chain of `nlmsghdr`/`inet_diag_msg` records. A kprobe on entry to `__sys_recvmsg` can save the destination user pointer; the paired kretprobe runs after the kernel fills the buffer but before `ss` parses it. To hide contiguous record **B** between **A** and **C**, use `bpf_probe_write_user()` to apply the following modification:<sup>[[16]](#references)</sup>

```c
A.nlmsg_len += NLMSG_ALIGN(B.nlmsg_len);
```

The bytes for B remain in the response, but `NLMSG_NEXT` advances by `NLMSG_ALIGN(A.nlmsg_len)` and lands on C. Because the enlarged length only incorporates bytes already present, the remaining-buffer check in `NLMSG_OK` still succeeds. This record-skipping primitive can affect any trusted consumer that walks an attacker-writable, length-delimited response using embedded sizes.<sup>[[16]](#references)</sup>

### Truncate BPF object enumeration with `-ENOENT`

An entry tracepoint on `sys_enter_bpf` can identify `BPF_PROG_GET_NEXT_ID` (11), `BPF_MAP_GET_NEXT_ID` (12), and `BPF_LINK_GET_NEXT_ID` (31), then cache the request by `pid_tgid`. A return probe on the architecture-specific `sys_bpf` wrapper reads the selected `next_id`; when it matches a protected ID, `bpf_override_return(ctx, -ENOENT)` makes `bpftool`/libbpf interpret the result as the natural end of the walk. This hides the protected object **and every object with a higher ID**, rather than only skipping one entry.<sup>[[14]](#references)[[16]](#references)</sup>

The helper is restricted to compatible kprobe programs and error-injectable kernel functions, and requires `CONFIG_BPF_KPROBE_OVERRIDE=y`. Compare iterative enumeration with direct-ID lookup, which uses a different BPF command; an object that opens directly but never appears in the walk indicates enumeration tampering. LinkPro samples also printed `BPF cmd: %d, start_id: %u` and `HIDING NEXT_ID: %u` to `trace_pipe`, although these mutable debug strings are only secondary indicators.<sup>[[14]](#references)[[16]](#references)</sup>

```bash
sudo bpftool prog list
sudo bpftool prog show id <suspected_id>
sudo bpftool map list && sudo bpftool map show id <suspected_id>
sudo bpftool link list && sudo bpftool link show id <suspected_id>
sudo cat /sys/kernel/debug/tracing/trace_pipe | grep -E 'BPF cmd:|HIDING NEXT_ID:'
```

Apply the same sequential-versus-direct lookup test to maps and links. A live-response workflow should not treat a clean `bpftool ... list` as kernel ground truth after untrusted BPF programs have attached.<sup>[[14]](#references)[[16]](#references)</sup>

### Kill a debugger before `ptrace()` attaches

A program on `tp/syscalls/sys_enter_ptrace` runs before the kernel processes the request. It can filter for `PTRACE_ATTACH` (`0x10`) or `PTRACE_SEIZE` (`0x4206`), extract the target PID, and query a protected-PID map. On a match, `bpf_send_signal(SIGKILL)` signals the **current process making `ptrace()`**—the debugger or tracer—not the requested target, so `gdb` or `strace` disappears before attachment completes. `bpf_send_signal_thread()` is the thread-scoped variant of the same anti-analysis capability.<sup>[[15]](#references)[[16]](#references)</sup>

### Fingerprint capabilities during `BPF_PROG_LOAD`

A helper call reaches the verifier as an 8-byte `BPF_CALL` whose 32-bit `imm` contains a stable `enum bpf_func_id`. `check_helper_call()` obtains the helper prototype and type-checks it; later, `do_misc_fixups()` replaces `imm` with a signed implementation offset from `__bpf_call_base`. A defensive kprobe on `check_helper_call` can therefore build a per-load helper bitmap before the original IDs disappear, while `security_bpf_prog` supplies the program type, expected attach type, ID, name, tag, and map metadata. Correlate both observations with the in-flight `bpf(2)` syscall and emit the immutable fingerprint at syscall exit.<sup>[[16]](#references)</sup>

`BPF_PROG_LOAD` is the highest-trust observation point because the submitted instructions are still inert and cannot yet falsify enumeration, rewrite response buffers, or detach monitoring probes. Capability-oriented rules can alert on rare helpers without depending on attacker-controlled program names:<sup>[[16]](#references)</sup>

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

Allowlist known loaders where necessary, but raise confidence when two or more of these helper families are loaded close together, especially by an interactive shell or a binary in a world-writable path.<sup>[[16]](#references)</sup>

## References

- [1] [0xdf – HTB Planning (Grafana env creds reuse, systemd BASIC_AUTH)](https://0xdf.gitlab.io/2025/09/13/htb-planning.html)
Expand All @@ -243,5 +291,9 @@ If the host supports `bpftool`, also baseline legitimate BPF usage. Unexpected p
- [10] [3snake - dump sshd and sudo credential-related strings](https://github.com/blendin/3snake)
- [11] [Gaming the System: How a Chinese-Speaking Actor Turned Brazilian Government Sites into an SEO Weapon](https://research.checkpoint.com/2026/gaming-the-system-how-a-chinese-speaking-actor-turned-brazilian-government-sites-into-an-seo-weapon/)
- [12] [`ptrace(2)` Linux manual page](https://man7.org/linux/man-pages/man2/ptrace.2.html)
- [13] [VoidLink: The Cloud-Native Malware Framework](https://research.checkpoint.com/2026/voidlink-the-cloud-native-malware-framework/)
- [14] [LinkPro: eBPF rootkit analysis](https://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis)
- [15] [Atomic Arch npm Campaign Adds Malicious Dependency](https://www.sonatype.com/blog/atomic-arch-npm-campaign-adds-malicious-dependency)
- [16] [Detection primitives for eBPF rootkits](https://securitylabs.datadoghq.com/articles/detection-primitives-for-ebpf-rootkits/)

{{#include ../../../banners/hacktricks-training.md}}