Технология eBPF не нова. Её используют повсеместно, ведь она упрощает написание кода для ядра ОС. Классно и удобно, а главное безопасно! Но, как оказывается на практике, не все так гладко… Это не только удобное средство для написания кода, но и новые потенциальные векторы для атак. Поэтому давайте подробно разберём, как она работает, и как можно избежать потенциальных проблем. Для меня как безопасника интереснее всего использование eBPF сервисами и инструментами в продакшене. Именно там открываются возможные пути обхода для злоумышленников.
Меня зовут Лев Хакимов, я DevOps и Kubernetes Security Lead в MWS Cloud Platform, а ещё преподаю в ИТМО. Занимаюсь обеспечением и построением процессов безопасности платформ Kubernetes в облаке MWS, организую CTF-соревнования по всей стране для школьников, студентов и действующих специалистов.
Как писать код в Kernel Space: модифицируем ядро
Общий подход к запуску кода в Linux достаточно простой. У нас есть:
User Space:
Для прикладного ПО, где проводится множество проверок, чтобы как можно реже «стрелять себе в колено».
Защита при работе с памятью (SIGSEGV), чтобы было сложнее залезть в чужой сегмент памяти.
Общение с ядром через syscall’ы.
Тёмная лошадка — Kernel Space:
Здесь живёт ядро, его модули, драйверы.
Нет защиты при работе с памятью.
Есть прямой доступ ко всем устройствам памяти.
Требует особого внимания, поэтому мало кто пишет программы под Kernel Space. Это определённое искусство, которым надо владеть.
Написать код в Kernel Space можно разными способами.
Патч в ядро Linux
Первое, что мы можем сделать — пропатчить ядро Linux. Иногда такое действительно практикуется в компаниях, у которых есть собственные ядра Linux. Но у этого способа есть проблемы:
Сложный код на C: далеко не все умеют писать патчи для Linux.
Плохая переносимость между версиями ядра. А ещё нужно не забыть своевременно всё перенести.
Перекомпиляция ядра для изменений.
Модули ядра
Можно делать это прямо в модуле ядра, но проблемы остаются:
Всё ещё код на C.
Его всё ещё сложно писать.
В придачу, никакой защиты работы с памятью и проверок в принципе нет.
Из плюсов — есть динамическая загрузка и выгрузка ядер, что упрощает загрузку, тестирование и выгрузку кода.
eBPF
eBPF — новый механизм, который появился относительно недавно и покорил сердца многих:
Вместо сложного С — его ограниченное подмножество. Да, сложный код на C всё ещё есть, но он уже не такой сложный, поскольку код на eBPF ограничен небольшим подмножеством. А с недавних версий можно писать ещё на Rust, хотя в основном все по-прежнему пишут на C.
Продуманный интерфейс общения с User Space: как отдавать результаты работы программы.
Ограничение песочницей ВМ и различными верификаторами. Это вызвало отдельную симпатию у многих, так как не даёт сломать ничего серьёзного.
Подробнее про eBPF
Теперь подробнее про eBPF. Новая технология резко снизила порог вхождения в написание кода для ядра и сделала его универсальным для разных дистрибутивов/версий ядра Linux.
Вот уже ставшая каноничной картинка, которую многие видели, когда читали документацию к Cilium:

eBPF (extended Berkeley Packet Filter) — это мощный инструмент для разработки, который позволяет запускать собственный код прямо внутри ядра операционной системы.
У eBPF есть три крупных зоны применения: сеть, безопасность и трейсинг. Различные проекты работают с использованием технологии eBPF, например, Cilium, Katran, множество самописных. Например, если вы разрабатываете облако и пишете сетевую подсистему, то, чтобы не обсчитывать сетевые пакеты в User Space, это можно проделать с помощью eBPF-программ.
В различных языках есть библиотеки, позволяющие запускать eBPF-программы и удобно работать с их результатами.
А ещё есть подсистема, предоставляемая самой Linux: верификаторы, JIT-компиляция, позволяющая собрать байт-код и затем компилировать его на конкретной системе JIT под конкретную архитектуру, интерфейс Maps, runtime для eBPF программ.
Основные фишки eBPF
Запускаем код, который должен работать быстро прямо в ядре.
Собранный код переносим между версиями ядер — с определёнными ограничениями, но сравнительно легко за счёт встроенных механизмов совместимости.
Есть встроенный верификатор, защищающий от «стрельбы очередью по коленям».
Каждая eBPF-программа выполняет строго определённую задачу — например, фильтрация пакетов или трассировка системных вызовов. Это повышает надёжность: нельзя сделать больше, чем разрешено. eBPF-программы позволяют нам делать что-то одно, но хорошо. А главное, нельзя сделать больше, чем задумано изначально.
Обмен данными между ядром и пользовательским пространством (User Space) реализован через специальные структуры — eBPF maps. Это даёт удобный способ передачи и хранения информации.
Теперь рассмотрим, где это применять:
Наблюдение за точками в ядре. Когда нужно безопасно перехватить вызов или событие в ядре без использования отладчика.
Работа с сетевыми пакетами. Обнаружение, фильтрация, модификация сетевых пакетов на лету — без задержек и с максимальной гибкостью.
Трассировка и профилирование. уИЗА — идеальный инструмент для создания инструментов анализа производительности и поведения приложений на уровне ядра.
Безопасность. Контроль доступа, выявление подозрительных действий, мониторинг поведения процессов и сетевых соединений.
Как работает eBPF на примере Hello World
Для понимания, как работает eBPF, рассмотрим примитивную программу, печатающую Hello World в Stacktrace.
program = """
#include <uapi/linux/ptrace.h>
int hello(void *ctx) {
bpf_trace_printk("Hello, World!\\n");
return 0;
}
"""
Примеры будут на Python, потому что на слайдах Go менее лаконичен и выглядит не так хорошо. Но в реальной жизни проекты, связанные с eBPF, чаще загружаются при помощи обвязки из Go.
Мы написали программу на очень ограниченном C-языке. Например, в нём даже циклы в классическом представлении появились относительно недавно.
from bcc import BPF
b = BPF(text=program)
hello_function = b.load_func("hello", BPF.KPROBE)
b.attach_kprobe (event=b.get_syscall_fnname("sync"), fn_name="hello")
while True:
try:
b.trace_print()
except KeyboardInterrupt:
break
Мы загружаем eBPF в программу и прикрепляем её к системному вызову sync. Каждый раз при срабатывании syscall sync, из трейса будет выводиться фраза «hello world».
Любая eBPF-программа — это функция-обработчик на некоторые события, а не фоновый процесс. Она не запускается сама по себе — её обязательно должен запустить какой-то триггер — например, системный вызов, приход сетевого пакета или событие в ядре. И она обязательно должна завершиться! Соответственно, eBPF-программа не может существовать бесконечно и крутиться фоново как демон или служба. Обязательно должны быть запуск, работа и результат. Выполнение строго ограничено как по времени, так и по доступным операциям.
Немного о Capabilities
Чтобы загружать eBPF-программы и управлять ими, требуются определённые capabilities — специальные привилегии в Linux, которые позволяют выполнять чувствительные действия без необходимости полного root-доступа.
Самая универсальная и часто используемая из них — это CAP_SYS_ADMIN
. Она даёт очень широкий набор прав, почти как у суперпользователя. И именно с ней связано множество проблем. У безопасников при упоминании этой capability обычно начинает нервно дёргаться глаз. В системах, где применяют eBPF или другие технологии, часто появляется избыточное количество прав, что открывает лишние риски.
Когда безопасники проверяют такие системы, они получают гигантские отчёты с флагами нарушений — кто, где и зачем задействовал SYS_ADMIN
, хотя мог бы обойтись более узкими привилегиями. В некоторых случаях CAP_SYS_ADMIN
превращается в технический долг — систему уже сложно перестроить на более безопасные права без полной переработки.
С выходом ядра Linux 5.8 появилась новая capability — CAP_BPF
(Kernel >5.8) даёт право на syscallbpf()
.
Важно понимать, что не у всех установлены свежие версии ядра, а eBPF существовал и до этого. Поэтому часто во многих источниках рекомендуют ставить именно CAP_SYS_ADMMIN
.
Вот выдержка из официальный документации Cilium прямо говорит нам — запускайте Cilium под CAP_SYS_ADMMIN
.

Но если у вас современное ядро Linux, безопаснее использовать CAP_BPF
и CAP_NET_ADMIN
. Этого будет достаточно, к тому же не понадобится устанавливать сверх-широкую capability, которая полезна всем, кроме безопасников.
CAP_BPF
достаточно, чтобы загружать программы и получать результаты их работы, хотя как рекомендация он используется далеко не везде.
CAP_PERFMON
необходима для загрузки программ для трассировки системы.
CAP_NET_ADMIN
— для загрузки программ, работающих с сетевым стеком XDP.
И вот наша eBPF программа работает, получает результат, который нам бы хотелось забрать. Рассмотрим, как это сделать и к чему это может привести.
EPBF Maps
Чтобы делиться данными между User Space и Kernel Space, существует механизм обычных карт EPBF Maps:
Особые структуры данных в виде карт ключ-значение.
Хранятся в пространстве ядра.
К ним могут обращаться как eBPF-программы, так и процессы в User Space.
По факту это общая область памяти между пользовательским пространством, где работают наши приложения и пространством ядра. И туда может писать и оттуда читать как сам EBPF-код, так и приложения из пользовательского пространства. Запомним это, ведь ряд атак будет построен на этой особенности.

Maps — дальше «карты» — бывают разных типов:
различные словари;
массивы;
очереди;
представления IPv4-и IPv6-адресов с префиксом сети;
хранилища для сокетов;
карты ключей для cgroups;
и так далее.
Карты — основной механизм общения eBPF программ с User Space.
Посмотрим на прекрасный security мир, в котором существует eBPF.

Проекты Tetragon, Falco, Tracee позволяют проводить аудит на системах в Kubernetes. А ещё их легко установить на операционную систему и провести аудит происходящих там событий. Например, зафиксировать, какие syscall вызываются, а файлы и папки — открываются.
Надо понимать, что сейчас злоумышленнику не столь важно получить root в системе. Ведь в этом случае его, скорее всего, быстро обнаружат, SOC выкинет из системы, а по адресу местонахождения вызовут наряд.
Немного о защите: в eBPF есть механизм Linux LSM — это специально расставленные разработчиками Linux ловушки в коде ядра. Когда выполняется какое-либо действие (например, открытие файла), эта ловушка срабатывает и подает сигнал нашему приложению. Достаточно на эти события реагировать, и система аудита на нужные вам события появится, отчасти, сама собой. Именно на них работает практически весь security tooling, построенный вокруг eBPF. Они включаются при компиляции. Найти включенные можно по пути:
/sys/kernel/security/lsm
Объявляются они через директиву SEC в eBPF-программах. У них есть статус возврата:
«
0
» — проверка прошла успешно,«
-EPERM
» — ошибка, то есть всё плохо и надо разбираться, что произошло.
Разберёмся, какие атаки кроме доступа к root и защиты бывают.
Атака на shared maps
Первая и самая простая — атака на shared maps. Да, те самые, которые позволяют делиться данными между User Space и Kernel Space
Рассмотрим тестовый стенд. У нас есть Ubuntu 22.04 и установленный на неё Falco. В качестве PoC рассмотрим следующий сценарий:
Map — это общий ресурс.
Допустим, злоумышленник каким-то образом попадает в систему. Ему бы хотелось с этой системой работать так, чтобы Falco его не заметил.
К этой map’е имеется доступ у всех, у кого в User Space есть доступ к
CAP_BPF
илиCAP_SYS_ADMIN

Можно доставить и запустить на стенде eBPF-программу. Можно загрузить свою eBPF программу разными способами — например, скриптом sploit, который может запуститься из-под привилегий
CAP_BPF
. Или провести атаку типа supply chain и подшить в устанавливаемую на сервер программу другую нехорошую eBPF-программу. Она запустится, когда приложение приедет на сервер и там развернётся.И вот злоумышленник может смело удалять/засорять maps.
Суть в том, что Falco общается с eBPF-программами через maps. А значит эти maps можно удалить и зачистить вот таким несложным скриптом:
void delete_keys(int fd, const char *map_name) {
std::cout << "Targeting map " << map_name <<
std::endl;
struct map_elem map_item = {};
while (bpf_map_get_next_key(fd, &map_item, &map_item)
== 0) {
__u32 key = static_cast<__u32>(map_item.key);
if (bpf_map_delete_elem(fd, &key) == 0) {
std::cout << "\tDeleted key " << key << "
from " << map_name << std::endl;
}
key++;
}
}
Даже простое удаление MAP приведёт к тому, что Falco замолчит.
Просто очистим ключи, и Falco замолчит, не выкинув в лог никаких ошибок — солдат спит, а служба идёт.
io_uring & eBPF.
Читать файлы в операционной системе Linux можно несколькими способами:
Стандартный вызов
open()
.Через io_uring (io_uring_setup) интерфейсы, появившиеся относительно недавно. Они дают возможность делать асинхронное чтение и запись файлов.
Через системный вызов
open_by_handle_at (CAP_DAC_READ_SEARCH)
.
Механизм асинхронной работы с операциями «ввод-вывод» в io_uring :
read() -> aio_read()
write() -> aio_write()
С ними есть забавный момент. Falco по умолчанию это не отслеживает. Он смотрит на системные вызовы open из коробки, но не смотрит на aio_-системные вызовы. Мы можем воспользоваться таким скриптом и попытаться открыть файлы с использованием io_uring.

Можно просто использовать скрипт, приведённый выше, и читать любой файл на системе, при этом Falco ничего не заметит.
Чтобы гарантировать, что он будет вызван до libc, можно сделать это через LD_ PRELOAD
, указав скрипт с bypass в переменную LD_ PRELOAD
:
LD_PRELOAD=io_uring_bypass.so ls –la
Overflow maps
Но у maps есть ограничения:
Количество экземпляров maps ограничено (как правило, около 10 000 штук).
Maps всё ещё общие.
Если их заполнить мусором, то в потоке таких данных можно не увидеть что-то интересное.
Например, Falco криво настроен и следит не только за важными файлами, касающимися приложения, но за всеми файлами в операционной системе. Тогда нам даже не нужны какие-то дополнительные сложные capability. Создаем в файловой системе файл, заполняем его случайными данными и в бесконечном цикле открываем и закрываем.
Maps Falco переполняются из-за обилия событий открытия файлов. В то время, пока скрипт занимается бесполезной работой открывая и закрывая файлы, злоумышленник может сделать что-то другое, на что Falco не среагирует — потому что занят.
С maps разобрались — теперь немножко про rootkits
ebpfkit
Вернёмся к rootkit. Вот сравнительно шуточный rootkit от разработчиков DataDog, который, тем не менее, работает. Посмотрим на его примере как работает любой eBPF-rootkit:

Здесь C2-сервер — это командно-контрольный сервер, который будет подавать нам какие-то команды. Общение идёт по сети, в данном случае работают XDP-сокеты. Мы можем написать eBPF-программу, которая будет подключаться в XDP, забирать пакеты и каким-то образом их обрабатывать. Например, откидывать, игнорировать, перенаправлять в обход основного сетевого стека. Таким образом пакеты будут доезжать, например, до виртуалок, крутящихся на сервере с гипервизором. Но при этом сам сервер с гипервизором ничего об этом знать не будет.
Внутри rootkit масса тулинга: от разных видов bypass до скана сети, есть возможность скрытого исполнения на хосте. Но работает для злоумышленника это примерно так: мы действительно можем запустить rootkit, оставить поработать какое-то время и получить схему сети всех запросов — кто, откуда, с какого порта обращался к нашему серверу. Всё это — лишь по анализу входящих сетевых пакетов.

Если бы такая программа торчала в User Space, скорее всего, её бы быстро заметили.
Но это rootkit, а он умеет скрываться от ищущего, например, перехватывать системный вызов getdents64syscall
. Представьте запуск программы psaux с выходом в каталог /proc и выводом списка процессов. Если наш системный вызов перехватит rootkit и из него заботливо удалят все процессы, связанные с rootkit’ом, то вывод psaux нам волшебным образом ничего не покажет, потому что его уже очистили до нас.
Выводы
Технология eBPF открыла новые горизонты для наблюдения, сетевого взаимодействия и обеспечения безопасности на уровне ядра Linux. Гибкость и мощь сделали её незаменимым инструментом для DevOps-инженеров. Однако с этими возможностями приходит и ответственность.
eBPF — это не только средство наблюдения, но и потенциальный вектор атаки при неправильной настройке. Примеры с удалением maps, обходом через io_uring и eBPF-rootkit'ами доказывают, что теоретически безопасные механизмы могут быть обойдены злоумышленником при недостаточном контроле.
Ключевые меры защиты:
Используйте минимально необходимые capabilities (вместо
CAP_SYS_ADMIN — CAP_BPF
,CAP_NET_ADMIN
иCAP_PERFMON
).Контролируйте доступ к eBPF maps и следите за утечкой привилегий.
Регулярно обновляйте ядро и eBPF-инструменты — уязвимости быстро закрываются.
Используйте LSM-хуки и современные решения (Tracee, Tetragon, Falco) для детектирования аномалий.
И главное — обсуждайте, делитесь опытом и учитесь у других. Именно для этого и существует DevOpsConf. На конференции вы сможете пообщаться с экспертами по безопасности, обсудить реальные кейсы, eBPF-инфраструктуру и подходы к защите — как в теории, так и в боевых условиях. Приходите, будем разбираться в безопасности инфраструктуры вместе!