Новости

24.08.2020

Книга «BPF для мониторинга Linux»

Дэвид Калавера и Лоренцо Фонтана помогут вам раскрыть возможности BPF. Расширьте свои знания об оптимизации производительности, сетях, безопасности. — Используйте BPF для отслеживания и модификации поведения ядра Linux. — Внедряйте код для безопасного мониторинга событий в ядре — без необходимости перекомпилировать ядро или перезагружать систему. — Пользуйтесь удобными примерами кода на C, Go или Python. — Управляйте ситуацией, владея жизненным циклом программы BPF.

Безопасность ядра Linux, его возможности и Seccomp


BPF предоставляет мощный способ расширения ядра без ущерба для стабильности, безопасности и скорости. По этой причине разработчики ядра подумали, что было бы неплохо использовать его универсальность для улучшения изоляции процессов в Seccomp путем реализации фильтров Seccomp, поддерживаемых программами BPF, известными также как Seccomp BPF. В этой главе мы расскажем, что такое Seccomp и как он применяется. Затем вы узнаете, как писать фильтры Seccomp с помощью программ BPF. После этого рассмотрим встроенные ловушки BPF, которые есть в ядре для модулей безопасности Linux.

Модули безопасности Linux (LSM) — это платформа, предоставляющая набор функций, которые можно применять для стандартизированной реализации различных моделей безопасности. LSM может использоваться непосредственно в дереве исходного кода ядра, например Apparmor, SELinux и Tomoyo.

Начнем с обсуждения возможностей Linux.

Возможности


Суть возможностей Linux заключается в том, что вам нужно предоставить непривилегированному процессу разрешение на выполнение определенной задачи, но без использования suid для этой цели, или иным образом сделать процесс привилегированным, уменьшая возможность атак и обеспечивая процессу возможность выполнения определенных задач. Например, если вашему приложению нужно открыть привилегированный порт, допустим, 80, вместо запуска процесса от имени root можете просто предоставить ему возможность CAP_NET_BIND_SERVICE.

Рассмотрим программу Go с именем main.go:

Эта программа обслуживает HTTP-сервер на порте 80 (это привилегированный порт). Обычно мы запускаем ее сразу после компиляции:

Однако, поскольку мы не предоставляем привилегии root, этот код выдаст ошибку при привязке порта:

capsh (средство управления оболочкой) — это инструмент, который запускает оболочку с определенным набором возможностей.

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

Немного разберемся в этой команде.

  • capsh — используем capsh в качестве оболочки.
  • --caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' — поскольку нам нужно сменить пользователя (мы не хотим запускаться с правами root), укажем cap_net_bind_service и возможность фактически изменить идентификатор пользователя с root на nobody, а именно cap_setuid и cap_setgid.
  • --keep=1 — хотим сохранить установленные возможности, когда выполнено переключение с аккаунта root.
  • --user=«nobody» — конечным пользователем, запускающим программу, будет nobody.
  • --addamb=cap_net_bind_service — задаем очистку связанных возможностей после переключения из режима root.
  • — -c "./capabilities" — просто запускаем программу.

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

Вероятно, вам интересно, что значит +eip после указания возможности в опции --caps. Эти флаги используются для определения того, что возможность:

-должна быть активирована (p);

-доступна для применения (е);

-может быть унаследована дочерними процессами (i).

Поскольку мы хотим использовать cap_net_bind_service, нужно сделать это с флагом e. Затем запустим оболочку в команде. В результате запустится двоичный файл capabilities, и нам нужно пометить его флагом i. Наконец, мы хотим, чтобы возможность была активирована (мы это сделали, не меняя UID) с помощью p. Это выглядит как cap_net_bind_service+eip.

Можете проверить результат с помощью ss. Немного сократим вывод, чтобы он поместился на странице, но он покажет связанный порт и идентификатор пользователя, отличные от 0, в данном случае 65 534:

В этом примере мы использовали capsh, но вы можете написать оболочку с помощью libcap. За дополнительной информацией обращайтесь к man 3 libcap.

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

Чтобы лучше понять возможности нашей программы, можем взять инструмент BCC capable, который устанавливает kprobe для функции ядра cap_capable:

Добиться того же самого мы можем, используя bpftrace с однострочным kprobe в функции ядра cap_capable:

Это выведет что-то вроде следующего, если возможности нашей программы будут активированы после kprobe:

Пятый столбец — это возможности, в которых нуждается процесс, и, поскольку эти выходные данные включают в том числе неаудитные события, мы видим все неаудитные проверки и, наконец, требуемую возможность с флагом аудита (последний в выводе), установленным в 1. Возможность, которая нас интересует, — CAP_NET_BIND_SERVICE, она определяется как константа в исходном коде ядра в файле include/uapi/linux/ability.h с идентификатором 10:

Возможности часто задействуются во время выполнения контейнеров, таких как runC или Docker, чтобы они работали в непривилегированном режиме, но им были разрешены только те возможности, которые необходимы для запуска большинства приложений. Когда приложению требуются определенные возможности, в Docker можно обеспечить их с помощью --cap-add:

Эта команда предоставит контейнеру возможность CAP_NET_ADMIN, что позволит ему настроить сетевую ссылку для добавления интерфейса dummy0.

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

Seccomp


Seccomp означает Secure Computing, это уровень безопасности, реализованный в ядре Linux, который позволяет разработчикам фильтровать определенные системные вызовы. Хотя Seccomp сопоставим с возможностями Linux, его способность управлять определенными системными вызовами делает его намного более гибким по сравнению с ними.

Seccomp и возможности Linux не исключают друг друга, их часто используют вместе, чтобы получить пользу от обоих подходов. Например, вы можете захотеть предоставить процессу возможность CAP_NET_ADMIN, но не разрешить ему принимать соединения через сокет, блокируя системные вызовы accept и accept4.

Способ фильтрации Seccomp основан на фильтрах BPF, работающих в режиме SECCOMP_MODE_FILTER, и фильтрация системных вызовов выполняется так же, как и для пакетов.

Фильтры Seccomp загружаются с использованием prctl через операцию PR_SET_SECCOMP. Эти фильтры имеют форму программы BPF, которая выполняется для каждого пакета Seccomp, представленного с помощью структуры seccomp_data. Эта структура содержит эталонную архитектуру, указатель инструкций процессора во время системного вызова и максимум шесть аргументов системного вызова, выраженных как uint64.

Вот как выглядит структура seccomp_data из исходного кода ядра в файле linux/seccomp.h:

 

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

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

— SECCOMP_RET_KILL_PROCESS — завершение всего процесса сразу же после фильтрации системного вызова, который из-за этого не выполняется.

— SECCOMP_RET_KILL_THREAD — завершение текущего потока сразу же после фильтрации системного вызова, который из-за этого не выполняется.

— SECCOMP_RET_KILL — алиас для SECCOMP_RET_KILL_THREAD, оставлен для обратной совместимости.

— SECCOMP_RET_TRAP — системный вызов запрещен, и сигнал SIGSYS (Bad System Call) отправляется вызывающей его задаче.

— SECCOMP_RET_ERRNO — системный вызов не выполняется, и часть возвращаемого значения фильтра SECCOMP_RET_DATA передается в пространство пользователя как значение errno. В зависимости от причины ошибки возвращаются разные значения errno. Список номеров ошибок приведен в следующем разделе.

— SECCOMP_RET_TRACE — используется для уведомления трассировщика ptrace с помощью — PTRACE_O_TRACESECCOMP для перехвата, когда выполняется системный вызов, чтобы видеть и контролировать этот процесс. Если трассировщик не подключен, возвращается ошибка, errno устанавливается в -ENOSYS, а системный вызов не выполняется.

— SECCOMP_RET_LOG — системный вызов разрешен и зарегистрирован в журнале.

— SECCOMP_RET_ALLOW — системный вызов просто разрешен.

ptrace — это системный вызов для реализации механизмов трассировки в процессе, называемом tracee, с возможностью наблюдения и контроля за выполнением процесса. Программа трассировки может эффективно влиять на выполнение и изменять регистры памяти tracee. В контексте Seccomp ptrace используется, когда запускается кодом состояния SECCOMP_RET_TRACE, следовательно, трассировщик может предотвратить выполнение системного вызова и реализовать собственную логику.

С полным содержанием статьи можно ознакомиться на сайте "Хабрахабр":

https://habr.com/ru/company/piter/blog/514984/


Комментарии: 0

Пока нет комментариев


Оставить комментарий






Все поля обязательны к заполнению.

Перед публикацией комментарии проходят модерацию.