- PVSM.RU - https://www.pvsm.ru -
Все знают о том, как наблюдать за работающими процессами в Linux-системе. Но почти никто не добивается в подобных наблюдениях высокой точности. На самом деле, всем методам мониторинга процессов, о которых пойдёт речь в этом материале, чего-то не хватает.
Давайте, прежде чем приступить к экспериментам, определим требования к системе наблюдения за процессами:
cgroup ID процесса. Дело в том, что с точки зрения ядра нет такого понятия, как «контейнер» или «идентификатор контейнера». Ядро оперирует лишь такими понятиями, как «контрольные группы», «сетевые пространства имён», «пространства имён процессов», оно работает с различными независимыми API, с помощью которых средства контейнеризации вроде Docker реализуют механизмы контейнеризации. Если попытаться идентифицировать контейнеры посредством ID уровня ядра, нужен уникальный идентификатор контейнера. В случае с Docker данному требованию удовлетворяют идентификаторы контрольных групп.
Поговорим об обычных API Linux, которые могут помочь в решении этой задачи. Мы, чтобы не усложнять повествование, уделим особое внимание процессам, создаваемым с помощью системных вызовов execve. Если же говорить о более полном решении задачи, то при его реализации нужно, кроме того, мониторить процессы, созданные с помощью системных вызовов fork/clone и их вариантов, а так же — результаты работы вызовов execveat.
/proc. Этот способ, из-за проблемы короткоживущих процессов, нам не особенно подходит.PID процессов. Сведений о пути к исполняемому файлу процесса там не будет. В результате придётся возвращаться к чтению данных из /proc, сталкиваясь со сложностями при работе с короткоживущими процессами.auditd или osquery. Средства мультиплексирования событий, работающие в пользовательском режиме, вроде auditd и go-audit [2], в теории, могут смягчить эту проблему. Но в случае с решениями корпоративного класса нельзя заранее знать о том, используют ли клиенты подобные средства, а если используют — то какие именно. Нельзя заранее знать и о том, какие именно средства для обеспечения безопасности, работающие напрямую с API аудита, применяются клиентами. Второй недостаток заключается в том, что API аудита ничего не знают о контейнерах. И это — несмотря на то, что данный вопрос обсуждается уже много лет.Реализация этих механизмов предусматривает использование «датчиков» (probe) различных типов в единственном экземпляре.
Использование точек трассировки (tracepoint). Точки трассировки — это датчики, статически включённые в определённые места ядра во время его компиляции. Каждый такой датчик можно включить независимо от других, в результате чего он будет выдавать уведомления в тех случаях, когда достигается то место кода ядра, в которое он внедрён. Ядро содержит несколько подходящих нам точек трассировки, код которых выполняется в различные моменты работы системного вызова execve. Это — sched_process_exec, open_exec, sys_enter_execve, sys_exit_execve. Для того чтобы получить этот список, я выполнил команду cat /sys/kernel/tracing/available_events | grep exec и отфильтровал полученный список, пользуясь сведениями, полученными в ходе чтения кода ядра. Эти точки трассировки подходят нам лучше, чем вышеописанные механизмы, так как они позволяют организовать наблюдение за короткоживущими процессами. Но ни одна из них не даёт информацию о полном пути к исполняемому файлу процесса в том случае, если параметрами exec является относительный путь к такому файлу. Другими словами, если пользователь выполняет команду вроде cd /bin && ./ls, тогда мы получим сведения о пути в виде ./ls, а не в виде /bin/ls. Вот простой пример:
# Включение точки трассировки the sched_process_exec
sudo -s
cd /sys/kernel/debug/tracing
echo 1 > events/sched/sched_process_exec/enable
# запуск ls с использованием относительного пути
cd /bin && ./ls
# получение данных из точки трассировки sched_process_exec
# обратите внимание на то, что сведений о полном пути у нас нет
cd -
cat trace | grep ls
# отключение точки трассировки
echo 0 > events/sched/sched_process_exec/enable
Датчики kprobe позволяют извлекать отладочную информацию практически из любого места ядра. Они подобны особым точкам останова в коде ядра, которые выдают информацию, но при этом не останавливают выполнение кода. Датчик kprobe, в отличие от точек трассировки, можно подключить к самым разным функциям. Код такого датчика сработает в процессе выполнения системного вызова execve. Но я не нашёл в графе вызовов execve ни одной функции, параметрами которой является и PID процесса, и полный путь к его исполняемому файлу. В результате тут мы сталкиваемся с той же «проблемой относительных путей» что и при использовании точек трассировки. Тут можно, опираясь на особенности конкретного ядра, кое-что «подкрутить». В конце концов, датчики kprobe могут считывать данные из стека вызовов ядра. Но такое решение не будет стабильно работать в разных версиях ядра. Поэтому его я его не рассматриваю.
Тут речь идёт о том, что при выполнении некоего кода будут срабатывать точки трассировки или датчики, но при этом будет выполняться код eBPF-программ, а не код обычных обработчиков событий.
Использование этого подхода открывает перед нами некоторые новые возможности. Теперь мы можем запускать в ядре произвольный код при выполнении системного вызова execve. Это, теоретически, должно дать нам возможность извлекать из ядра любую необходимую нам информацию и отправлять её в пользовательское пространство. Есть два способа получения подобных данных, но ни один из них не соответствует вышеописанным требованиям.
task_struct или linux_binprm. Собственно говоря, это позволит нам узнать полный путь к исполняемому файлу процесса, но чтение информации из структур данных ядра делает нас зависимыми от версии ядра. Например, можно поместить точку трассировки в sched_process_exec и, воспользовавшись в eBPF-программе ограниченным циклом, обойти цепочку структур dentry [3] в bprm->file->f_path.dentry, отправляя в пользовательское пространство по одному фрагменту данных за раз с применением кольцевого буфера. Нашей eBPF-программе нужно знать смещения членов структуры. Поэтому её нужно компилировать для разных версий ядра с использованием соответствующих заголовков. Обычно подобные задачи решают, компилируя eBPF-программы во время выполнения кода, но у такого подхода есть свои проблемы, вроде требования наличия заголовков ядра на каждом компьютере, где планируется компилировать программы.cgroup ID, а это помогает в деле сопоставления процессов и контейнеров).LD_PRELOAD для каждого запускаемого исполняемого файла и перехват exec в libc. Сразу скажу, что делать этого я не рекомендую. Это решение не подойдёт для статически компилируемых исполняемых файлов, его использование снижает уровень защищённости системы от вредоносного кода, оно, к тому же, подразумевает довольно грубое вмешательство в работу системы.execve, fork/clone и chdir для того чтобы наблюдать не только за созданием процессов, но и получать сведения об их текущих рабочих директориях. Речь идёт о том, что для каждого вызова execve нужно будет находить рабочую директорию процесса и комбинировать эти данные с параметрами execve для получения полного пути. Если вы решите реализовать именно этот механизм — то позаботьтесь об использовании eBPF-мэппинга и поместите всю логику в eBPF-программу для того чтобы избежать состояния гонок в том случае, если события приходят в пользовательское пространство в неправильном порядке.ptrace. Эти решения слишком грубы для использования их в продакшн-коде. Но если вы воспользуетесь именно этими механизмами — применяйте ptrace вместе с seccomp и с флагом SECCOMP_RET_TRACE. Это позволит seccomp перехватывать все системные вызовы execve в ядре и передавать их в отладчик пользовательского пространства, который может логировать сведения о вызовах execve и после этого сообщать seccomp о возможности продолжения обычной работы с execve.Сразу скажу, что ни одно из этих решений не соответствует нашим требованиям, но, всё же, перечислю их:
ps. Этот инструмент просто обращается к /proc и, в результате, страдает от тех же проблем, что и прямое обращение к /proc.kprobe/kretprobe, поэтому оно так же зависит от версий ядра, как и подобные решения, описанные выше. Кроме того, execsnoop не даёт информации о полных путях к исполняемым файлам, а значит, применяя этот механизм, мы ничего не выигрываем.execsnoop, не поддерживающей eBPF. Это — обычный датчик kprobe, поэтому нам это не подходит.В будущем можно будет воспользоваться недоступной пока вспомогательной eBPF-функцией get_fd_path [5]. После того, как она будет добавлена в ядро, она пригодится для решения нашей задачи. Правда, полный путь к исполняемому файлу процесса придётся получать, используя способ, не предусматривающий чтение информации из структур данных ядра.
Ни один из рассмотренных API не идеален. Ниже я хочу дать несколько рекомендаций относительно того, какими именно подходами стоит пользоваться для получения информации о процессах, и о том, когда ими стоит пользоваться:
auditd или go-audit [2]. Это позволит получать сведения обо всех процессах, включая короткоживущие. Вы, кроме того, не прилагая никаких усилий, получите сведения о полных путях к исполняемым файлам процессов. Это решение, правда, не будет работать в том случае, если кто-то уже использует API аудита с помощью инструментов, работающих в пространстве пользователя, отличающихся от ваших. Если вы столкнулись с подобной проблемой — возможно, вам подойдут следующие рекомендации.execsnoop. Единственный минус этого подхода — необходимость в доступе к заголовкам ядра во время выполнения кода.perf. Рассказ обо всём этом достоин отдельной статьи. Самое главное, о чём стоит помнить, выбирая этот способ наблюдения за процессами, заключается в следующем. Если вы пользуетесь eBPF-программами — проконтролируйте возможность их статической компиляции, что позволит вам не зависеть от заголовков ядра. А ведь именно этой зависимости мы и пытаемся избежать, применяя этот метод. Применение этого метода, кроме того, означает невозможность работы со структурами данных ядра и невозможность использования фреймворков вроде BCC [6], компилирующих программы eBPF во время выполнения кода./proc.Как вы организуете наблюдение за работающими процессами в Linux?

Автор: ru_vds
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/linux/358415
Ссылки в тексте:
[1] Image: https://habr.com/ru/company/ruvds/blog/525830/
[2] go-audit: https://github.com/slackhq/go-audit
[3] цепочку структур dentry: https://github.com/iovisor/bcc/issues/237#issuecomment-547564661
[4] execsnoop: https://github.com/iovisor/bcc/blob/master/tools/execsnoop.py
[5] get_fd_path: https://patchwork.ozlabs.org/project/netdev/patch/7464919bd9c15f2496ca29dceb6a4048b3199774.1576629200.git.ethercflow@gmail.com/
[6] BCC: https://github.com/iovisor/bcc
[7] Image: http://ruvds.com/ru-rub?utm_source=habr&utm_medium=article&utm_campaign=perevod&utm_content=o_slozhnostyax_monitoringa_rabotayushhix_processov_v_linux#order
[8] Источник: https://habr.com/ru/post/525830/?utm_source=habrahabr&utm_medium=rss&utm_campaign=525830
Нажмите здесь для печати.