eBPF видит все! Или нет?

  • #SSH

О чем материал

Разбираемся, для чего применяется технология eBPF в ИБ и можно ли обмануть такую защиту

eBPF — мощная технология, которая позволяет загружать свой код в ядро Linux быстро, безопасно и с минимальными накладными расходами. Однако ее популярность в ИБ объясняется не только этим. Одно из ключевых преимуществ программных средств защиты на базе eBPF — возможность работы с более доверенной средой. Мы получаем телеметрию напрямую из ядра, а не из пользовательского пространства, где злоумышленник может ее изменить. Но так ли это на самом деле?

Что, если у злоумышленника достаточно прав доступа для полноценного использования eBPF на целевом узле? Что, если он может читать структуры eBPF-данных и оказывать влияние на возвращаемые значения функций ядра? Кажется, мы с трудом сможем доверять такой системе... Да и сможем ли мы вообще понять, что атакующий что-то изменил? Иными словами, можно ли обмануть средства защиты, использующие eBPF? Давайте разбираться!

Как и почему используют eBPF в ИБ

Для начала рассмотрим основные этапы работы eBPF-программы (см. рис. 1).

Рисунок 1. Этапы работы eBPF-программы
  1. Загрузка eBPF-программы. Начинается с того, что ее инициирует процесс в пользовательском пространстве (агент) с помощью системного вызова bpf().
  2. Проверка. Ту самую надежность и безопасность обеспечивает специальный механизм в ядре — eBPF Verifier. Он выполняет ряд проверок: если они прошли успешно, программа переходит на этап компиляции, если нет — ее загрузка отменяется.
  3. Подключение к источнику событий. Одна из главных особенностей eBPF — событийно-ориентированный подход, который использует события как триггер для запуска программы. Для получения событий применяются разные механизмы трассировки (kprobe, kretprobe и tracepoint) и работы сети (TC, XDP).
  4. Запуск программы. Указанные выше механизмы определяют конкретные объекты ядра Linux (например, функции), запуск которых инициирует запуск eBPF-программы. При этом она получает доступ к метаданным выбранного объекта (в случае с функциями это могут быть ее аргументы или возвращаемое значение).

eBPF-программа позволяет подключиться к функциям ядра (kprobe, kretprobe, tracepoint) или библиотеки пользовательского пространства (uprobe, uretprobe) и получить телеметрию: аргументы функций и возвращаемые значения. Эти данные могут содержать сведения, необходимые для обнаружения аномалий, и помогают в трассировке происходящего в системе. Например, к каким файлам был получен доступ, что было загружено в память, какой сетевой порт был открыт и т. д. Кроме того, eBPF-программы могут реагировать на определенные события с помощью специальных функций-помощников, которые позволяют влиять на объекты пользовательского пространства или ядра.

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

Полученные из таких источников события можно обогащать и обрабатывать различными правилами поиска вредоносной активности. Поскольку это происходит в реальном времени, мы получаем систему обнаружения и реагирования на угрозы, которая дает следующие преимущества:

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

Нам доступно несколько источников для получения событий. В этой статье я сфокусируюсь на трассировке действий в системе с помощью eBPF-программ через пространство ядра Linux (события сетевого стека и пользовательского пространства сознательно опускаю — это тема для отдельного материала), поэтому список источников для получения телеметрии будет выглядеть следующим образом (см. табл. 1): 

 ПлюсыМинусы
Системные вызовы
  • Стабильность относительно версии ядра. Поскольку это базовые кирпичики API, то изменения, которые потребуют дополнительной адаптации eBPF-программ, сведены к минимуму.
  • Хорошо задокументированы.
  • Легко адаптировать существующие правила. Если у вас уже есть правила обнаружения на auditd, их можно легко адаптировать под соответствующие eBPF-программы.
  • Однозначные возвращаемые значения. Помогают, когда важно обнаруживать не только успешные, но и безуспешные действия злоумышленников.
  • Аргументы системных вызовов могут не содержать нужных переменных. Например, относительные пути к файлам или директориям и файловые дескрипторы требуют дополнительных ресурсов для обработки и приведения к человекочитаемому виду.
  • Часто становятся целью для атаки. Их недостатки и способы обхода хорошо изучены и задокументированы.
  • Сложно покрыть полностью. Одно и то же действие можно выполнить, используя разные системные вызовы. Это вынуждает ставить их на мониторинг и тратить ресурсы на их обработку.
     
LSM-функции
  • Разработаны с целью обеспечить защиту. Помогают выстраивать политики безопасности и реагирования, а не только мониторинга.
  • Могут быть единой точкой для разных системных вызовов. Это позволяет сэкономить ресурсы.
  • Поддерживают большинство операций, но не все.
  • Внедрены с версии ядра 5.7. Если по какой-то причине вы используете более ранние версии, придется обойтись без них.
Остальные функции
  • Большой выбор функций ядра Linux.
  • Функции могут содержать значения аргументов и их сочетания в формате, который требует минимальной обработки.
  • Могут быть единой точкой для разных системных вызовов. Это позволяет сэкономить ресурсы.
  • Нестабильность. Код функции и ее аргументы могут меняться в зависимости от версии ядра или даже дистрибутива.
  • Высокий порог входа. Функций очень много: потребуются время, метод и соответствующая экспертиза для поиска.
  • Не все функции ядра доступны для eBPF-программ, есть ограничения.
     
Таблица 1. Источники получения eBPF-телеметрии

Исходя из табл. 1 и предыдущих утверждений, можно сделать два вывода:

  • От качества выбранного источника событий будет зависеть эффективность обнаружения.
  • Для разработки экспертизы необходимо понимать не только откуда вы берете события, но и как сведения из них помогут обнаружить вредоносную активность.

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

falco — экспертиза построена на событиях, полученных от функций системных вызовов, а правила обнаружения реализованы в виде псевдокода (что упрощает их адаптацию к реальности и создание собственных правил). А вот расширение источников событий в простом виде, к сожалению, не предусмотрено.

tracee — экспертиза построена на тщательно отобранных функциях ядра, которые используют различные механизмы дополнительного обогащения. Изменение источников и правил предусмотрено, однако требует глубокой экспертизы и опыта написания eBPF-программ с нуля.

runtime-radar — это наша разработка. В качестве источников используем микс из системных вызовов, LSM-функций и специально отобранных функций ядра. Также реализован экспертный режим, который позволяет самостоятельно адаптировать сбор событий с учетом специфики инфраструктуры прямо в интерфейсе продукта и добавлять собственные правила обнаружения. Подробности ищите здесь.

Теперь, когда у нас есть общее представление о решениях на базе eBPF, давайте разберемся, как можно их обмануть.

Способы избежать обнаружения

В сегодняшней статье мы сосредоточимся на атаках в пространстве ядра, поскольку они наиболее опасны. Доступные на этом уровне техники можно разделить на две группы:

  • Использование возможностей eBPF. Широкий набор инструментов, который предоставляет eBPF, может использовать атакующий. Вкупе с отсутствием разграничения доступа на уровне eBPF-программ, получаем идеальный микс для сокрытия своей деятельности.
  • Компрометация ресурсов и компонентов eBPF. Технология eBPF использует собственную инфраструктуру — транспорт, объекты хранения и сбора данных. Это объекты пространства ядра, с которыми при наличии определенных прав можно взаимодействовать (в том числе менять).

Пара слов о необходимых правах доступа. Прежде всего для работы с eBPF в Linux необходимы права суперпользователя (root) и возможности (capabilities) CAP_PERFMON, CAP_BPF или CAP_SYS_ADMIN. Для тех, кто задается вопросом «Стоп! Злоумышленник уже получил максимальные права в системе, зачем ему эти сложности с eBPF?», отвечаю: мало получить доступ, еще нужно решить задачи закрепления, эксфильтрации, да и дальнейшего обнаружения желательно избежать. Такая комбинация позволит долго оставаться незамеченным и бесшумно собирать нужные данные.

Использование возможностей eBPF 

В последнее время исследователи (и злоумышленники) показали много разных способов применения eBPF в качестве инструмента нападения. Один из распространенных примеров — eBPF-руткиты, которые скрывают артефакты своего присутствия в системе. Обычно подобные атаки сводятся к загрузке вредоносной eBPF-программы, которая мониторит аргументы и возвращаемые значения определенных функций ядра Linux и при необходимости их меняет.

Такие возможности обеспечивают два механизма:

  • krpobe / kretprobe hook, лежащий в основе событийно-ориентированного подхода eBPF;
  • helper functions — специальный набор функций, который доступен в eBPF-программе для взаимодействия с другими объектами Linux, включая userspace.
Рисунок 2. Упрощенная последовательность работы системного вызова

Нас в первую очередь интересует значение, которое возвращает системный вызов. Обычно это 0 (что указывает на успешную реализацию) либо числовое значение кода ошибки. Рассмотрим, как злоумышленник может вмешаться в этот процесс с помощью вредоносной eBPF-программы.

Рисунок 3. Последовательная схема изменения возвращаемого значения системного вызова с помощью eBPF-программы

Представим ситуацию: EDR-агент обнаружил процесс eBPF-руткита в пользовательском пространстве. Cогласно настроенной политике реагирования, он пытается его завершить. Для этого процесс агента использует системный вызов kill(), в который передается PID процесса (12345) и значение сигнала (в нашем случае SIGKILL, что означает немедленную принудительную остановку процесса).

Во время установки eBPF-руткит злоумышленника загрузил eBPF-программу, которая использует kprobe, чтобы в момент запуска системного вызова kill() в пространстве ядра перехватить контекст вызова и получить содержимое ее аргументов (логика ее работы отображена в блоке alt на рис. 3). Если аргумент PID содержит ID процесса руткита в пользовательском пространстве, eBPF-программа с помощью helper-функции bpf_override_return() меняет возвращаемое значение на ESRCH, что означает «процесс не найден».

Стоит упомянуть интересную особенность bpf_override_return(): если использовать функцию вместе с kretprobe, то она меняет возвращаемое значение функции ядра. Если же использовать ее вместе с kprobe, работа функции полностью пропускается. Применительно к нашему примеру это означает, что работа системного вызова kill() будет полностью пропущена. Его возвращаемое значение сообщит EDR-агенту, что процесс не найден, хотя в действительности он существует и продолжает работать.

Это один из классических примеров применения eBPF-руткитов для сокрытия артефактов от сканеров безопасности и EDR, в том числе использующих eBPF. Такая же техника может использоваться и для сокрытия других следов деятельности руткита: например, файлов из определенных директорий, запущенных процессов, используемых сетевых портов и т. п.

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

Компрометация ресурсов и компонентов eBPF 

В качестве примера возьмем недавнее исследование и PoC-реализацию руткита Singularity. Он представляет собой набор способов компрометации ИБ-инструментов на базе eBPF. В основе исследования же лежат идеи перехвата и изменения данных, которые поступают от eBPF-программ в пространство пользователя. Для этого необходимо атаковать транспортную инфраструктуру eBPF, которая организована между пространством ядра и пользовательским пространством. При этом руткит вместо eBPF использует ftrace — альтернативный метод трассировки действий ядра, который основан на другой технологии, но дает примерно те же возможности. Но обо всем по порядку.

Начнем со структур данных map, которые eBPF использует для обмена информацией между пользовательским пространством и пространством ядра (в них могут храниться любые данные, взаимодействие с которыми осуществляется как со стороны пользовательского пространства, так и из пространства ядра). Обычно map используют для обмена информацией между eBPF-программами или между eBPF-программой и процессом-агентом в пользовательском пространстве. В частности, приложения безопасности могут хранить в них данные для отслеживания состояния процессов или сетевых соединений, а также некоторые настройки. Схема работы с map представлена на рис. 4.

Рисунок 4. Работа со структурами данных eBPF  

Обращение к данным осуществляется с помощью функций bpf_map_lookup_elem() и bpf_map_update_elem(). Поскольку никаких разграничений доступа к этим структурам нет, злоумышленник может практически беспрепятственно влиять на данные как со стороны пространства ядра, так и со стороны пользовательского пространства. Метод, предложенный автором руткита Singularity, выглядит следующим образом (см. рис. 5):

Рисунок 5. Последовательность действий при изменении возвращаемых значений функции ядра с помощью ftrace

В результате ни процесс со стороны пространства пользователя, ни сами eBPF-программы не будут знать о существовании артефактов руткита. Какие конкретно артефакты будут скрыты? Зависит от того, какое средство защиты используется и какие данные оно хранит в структурах map.

Похожим образом можно атаковать и другие объекты, которые eBPF-программы используют для передачи данных (разница заключается лишь в функциях и результатах):

  • Итераторы — специальные eBPF-программы для перебора и сбора объектов ядра (в том числе map). Средства безопасности на базе eBPF обычно используют их для сбора данных об активных процессах.
  • Транспортные потоки (eBPF ring buffer, perf event) — применяются для передачи данных из пространства ядра в пользовательское пространство.
МеханизмФункцииРезультат
Итераторы
  • bpf_iter_run_prog()
  • bpf_seq_write()
  • bpf_seq_printf()
Событие, полученное от eBPF-программы, может быть неполным или содержать ошибочные сведения
eBPF ring buffer
  • bpf_ringbuf_reserve()
  • bpf_ringbuf_submit()
  • bpf_ringbuf_output()
Событие может отсутствовать совсем
perf event
  • perf_event_output()
  • perf_trace_run_bpf_submit()
Событие может отсутствовать совсем
Таблица 2. Последствия атаки на объекты, которые eBPF-программы используют для передачи данных

Основной недостаток этого метода заключается в том, что его реализацию нужно адаптировать под конкретный ИБ-инструмент. Без этого есть риск не получить ожидаемый эффект или вовсе сломать работу eBPF-программ (ftrace в таких случаях не так надежен, как eBPF), что поставит крест на развитии атаки. 

Например, на данный момент Singularity во многом адаптирован под обход обнаружения с помощью вышеупомянутых falco и tracee. Но мы смогли обнаружить многие артефакты руткита с помощью политик на базе tetragon, которые используем в нашем runtime-radar. Так, Singularity скрывает использование открываемых для входящих соединений портов (по умолчанию: tcp 8081). На рис. 6 представлена наша политика, которая отправляет событие каждый раз, когда на узле открывается новый порт.

Рисунок 6. Политики tetragon обнаружили открытие порта, скрытое с помощью Singularity

Таким образом, руткит скрыл имя процесса, но сам факт открытия порта скрыть не удалось. Аналогично со способом повышения привилегий с помощью отправки сигнала -59 текущему процессу. Здесь мы снова получаем сообщение от политики мониторинга: процесс повысил привилегии до пользователя с uid = 0, то есть root (см. рис. 7).

Рисунок 7. Политики tetragon обнаружили повышение привилегий

Встроенные механизмы защиты

Как от всего этого защищаться? Начнем со встроенных механизмов — их мало, но они есть.

Использовать eBPF может только тот процесс, у которого присутствуют следующие capabilities (см. табл. 3):

capabilitiesТипы eBPF-программ
CAP_PERFMON, CAP_BPFПрограммы трассировки (kprobe, tracepoint и т. д.)
CAP_NET_ADMIN, CAP_BPFПрограммы для работы с сетевым стеком (TC, XDP)
CAP_SYS_ADMINВсе типы
Таблица 3. Capabilities, необходимые процессу для использования eBPF

Однако настройка ядра CONFIG_BPF_UNPRIV_DEFAULT_OFF позволяет обойти это ограничение, поэтому за ней нужно приглядывать. Она может принимать следующие значения (см. табл. 4):

ЗначениеОписание
0Непривилегированные вызовы bpf() разрешены
1Непривилегированные вызовы bpf() запрещены и не могут быть переопределены в рантайме
2Непривилегированные вызовы bpf() запрещены, но могут быть переопределены в рантайме с помощью параметра unprivileged_bpf_disabled
Таблица 4. Возможные значения CONFIG_BPF_UNPRIV_DEFAULT_OFF


Мы уже выяснили, что функции-помощники обладают достаточно широкими возможностями. Примечательно, что некоторые из них изначально были добавлены в экспериментальных целях». В табл. 5 перечислены самые опасные.

Функция-помощникНазначение
bpf_probe_write_userЗапись в пользовательскую память любого процесса
bpf_probe_read_userЧтение пользовательской памяти любого процесса
bpf_override_returnИзменяет код возврата функции ядра
bpf_send_signalПосылает сигнал на завершение любого процесса
Таблица 5. Опасные возможности функций-помощников

Из существующих ограничительных мер функций-помощников есть только настройка ядра CONFIG_BPF_KPROBE_OVERRIDE, которая позволяет ограничить использование функции bpf_override_return. 

Есть и более радикальные способы ограничить работу eBPF, но это методы из разряда «все или ничего» (см. табл. 6).

ОпцияНазначение
CONFIG_KPROBESЗапретить или разрешить использование kprobe
CONFIG_BPF_EVENTSЗапретить или разрешить прикреплять программы eBPF к событиям kprobe, uprobe и tracepoint
Таблица 6. Радикальные способы ограничить работу eBPF

В итоге мы получаем небольшой список контролей, которым не хватает гранулярности для полноценного использования. Хотя некоторые из них могут помочь в системах, где eBPF совсем не применяется. Основная проблема кроется в том, что большинство современных средств защиты на базе eBPF используют эти функции. Поэтому отключать их — уже не вариант. В таком случае следует рассмотреть возможности MAC средств защиты на базе SElinux и AppArmor. Например, SElinux позволяет настраивать политику в контексте возможности загрузки или запуска eBPF-программы, а также доступа к структурам данных. Это позволяет организовать доступ только для вашего eBPF-приложения.

Возможности для обнаружения

Рассмотрим способы обнаружения описанных ранее техник. Начнем с нескольких встроенных методов и доступных инструментов, которые, к сожалению, легко обмануть (полагаться на них не стоит, но знать полезно).

Прежде всего можно отслеживать запуск системного вызова bpf() с помощью auditd. Проблема в том, что полезных данных там немного: например, вы точно не узнаете, используется опасная функция-помощник в загружаемой eBPF-программе или нет. Тем не менее можно отслеживать все загружаемые eBPF-программы, обращения к структурам данных, а также процессы-агенты, которые их инициируют.

Для изучения уже загруженных eBPF-программ подойдет утилита ebpftool. С ее помощью можно сделать листинг загруженных программ:

bpftool prog show

Также можно сделать дамп инструкций и получить список используемых eBPF-программой функций-помощников. Для этого потребуется получить ID загруженной eBPF-программы (ее можно взять из листинга с помощью команды выше):

bpftool prog dump xlated id <PROG_ID> | grep call

Рисунок 8. Получаем список функций-помощников eBPF-руткита bad-bpf с помощью bpftool

При загрузке eBPF-программы, использующей функцию-помощник bpf_probe_write_user, в логах ядра появится следующая запись:

[Mar 5 10:42] exechijack[11954] is installing a program with bpf_probe_write_user helper that may corrupt user memory!

Я уже упоминал, что, если руткит загружен в систему, эти данные могут быть замаскированы, а единственными следами останутся события auditd, которые связаны с первоначальной загрузкой eBPF-программы. Но их, опять же, может быть недостаточно для полноценного анализа и оценки рисков. В связи с этим предлагаю два подхода к обнаружению подобного руткита.

Первая тактика. Чтобы eBPF-руткит мог применить все вышеописанные техники, ему необходимо загрузить eBPF-программу. Именно в этот момент мы можем его обнаружить. Однако для этого мы должны получить максимум информации при его загрузке:

  • Реквизиты процесса агента: PID, права доступа, имя исполняемого файла, аргументы запуска, информация о родителе и т. д.
  • Сведения о загружаемой eBPF-программе: тип, имя функции, которую программа использует в качестве триггера, имя функции-помощника.

В качестве одного из способов реализации такой тактики можно воспользоваться open source продуктом tetragon (подробнее о применении tetragon в нашем продукте Runtime-Radar читайте здесь и здесь). Он позволяет писать политики мониторинга на базе eBPF-программ, используя нативный синтаксис Kubernetes. Чтобы получить нужную информацию, я подготовил следующую политику:

apiVersion: cilium.io/v1alpha1

kind: TracingPolicy

metadata:

name: "bpf"

spec:

kprobes:

- call: "bpf_check"

syscall: false

args:

- index: 1

type: "bpf_attr"

- call: "security_perf_event_alloc" # since 5.5

syscall: false

args:

- index: 0

type: "perf_event"

- call: "check_helper_call" # since 5.13

syscall: false

args:

- index: 1

type: "int"

label: "func_id"

resolve: "imm"

selectors:

- matchArgs:

- index: 1

operator: "Equal"

values:

- "36" # bpf_probe_write_user

- "112" # bpf_probe_read_user

- "58" # bpf_override_return

- "109" # bpf_send_signal

Политика использует kprobe и специальный набор функций ядра, аргументы которых содержат нужные нам сведения (см. табл. 7). Этого набора функций достаточно, чтобы собрать необходимую первичную информацию о загружаемой eBPF-программе, провести оценку рисков ее использования (например, если сработала функция check_helper_call(), сразу следует насторожиться), а также получить список функций или tracepoint, которые она использует.

ФункцияОписаниеИнформация в аргументах
bpf_checkОсновная функция ядра Linux, осуществляет проверку eBPF-программы (eBPF Verifier)
  • Тип программы (kprobe, tracepoint, XDP и т. д.)
  • Имя программы
security_perf_event_allocОдна из функций, осуществляющих проверку прав доступа к подсистеме perfИспользуемое событие (tracepoint, uprobe)
check_helper_callПроверка запрашиваемых eBPF-программой функций-помощников, которая проводится в рамках работы bpf_checkИспользуемые eBPF-программой функции-помощники (в политике мы сразу фильтруем по самым опасным функциям)
Таблица 7. Функции ядра

Вторая тактика — это обнаружение использования функции kallsyms_lookup_name(). Прежде, чем влиять на работу функций ядра, злоумышленнику необходимо получить их адреса в памяти. Для этого он использует функцию kallsyms_lookup_name(), где в аргументе строкой передается имя функции. Таким образом, мы можем использовать kprobe и отслеживать запуск этой функции. Чтобы уменьшить количество ложных срабатываний, можно сузить круг поиска до конкретных критичных функций или системных вызовов (кстати, эта функция также используется при загрузке kprobe и ftrace, что тоже поможет нам обнаружить злоумышленника).

Ниже приведена политика, которую вы можете самостоятельно испытать с помощью tetragon:

apiVersion: cilium.io/v1alpha1

kind: TracingPolicy

metadata:

name: "rootkit"

spec:

kprobes:

- call: "kallsyms_lookup_name"

syscall: false

return: false

args:

- index: 0

type: "string"

***

Суперспособности eBPF — это обоюдоострый меч, который может как обеспечить надежную защиту, так и стать отличным инструментом для ее компрометации. 

Получив достаточно прав, злоумышленник сможет оставаться в тени и ускользать даже от СЗИ на базе eBPF. Однако для этого нужно адапитровать руткит к конкретной среде, что не всегда возможно и требует определенной подготовки. Хотя бесследно такие манипуляции все равно не проходят. Если правильно выстроить тактику обнаружения, определенные шаги атакующего удастся отследить — это поможет выстроить процесс реагирования на инцидент. 

Надеюсь, материалы этой статьи помогут инженерам по обнаружению выстроить успешную стратегию, а форенизкам — расследовать подобные инциденты.

Мы дěлаем Positive Research → для ИБ-экспертов, бизнеса и всех, кто интересуется ✽ {кибербезопасностью}