19
0
0
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
Назад

Фильтрация системных вызовов в Linux

Время чтения 11 минут
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
19
0
0
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники

Привет, вАЙТИ. Меня зовут Егор Орлов, я более 25 лет в IT, преподаю в СПбПУ.

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

В этой статье подробно рассматривается один из них — seccomp (сокращение от Secure Computing), механизм ограничения системных вызовов, который выступает как фильтр с возможностью задать как белый, так и черный список.

Фильтрация системных вызовов в Linux

O seccomp

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

Если злоумышленник получит возможность выполнить произвольный код, seccomp не даст ему использовать системные вызовы, которые не были заранее объявлены и предусмотрены фильтром.

Изначально seccomp — это разработка компании Google. Сейчас фильтр является штатным механизмом ядра Linux.

Например, в Docker используется фильтр системных вызовов на основе seccomp, который по умолчанию блокирует более 40 системных вызовов из около 500 существующих в ядре Linux на текущий момент.

Seccomp позволяет указать, что делать с процессом-нарушителем: например, принудительно завершить работу процесса или просто вернуть ошибку в запрещенном системном вызове.

Почему фильтруем системные вызовы

Системные вызовы (syscall) — это обращение к ядру операционной системы от процесса, выполняющегося в пространстве пользователя.

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

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

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

Пример приложения

Рассмотрим в качестве примера простенькую программу на языке C (Си), выводящую версию ядра операционной системы. В ней используется системный вызов uname(2). Для получения того же результата можно воспользоваться стандартной командой:

$ uname -r 

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


#include <stdio.h>
#include <sys/utsname.h>
 
int main(void)
{
struct utsname name;
if (uname(&name)) return 1;
printf("Версия ядра: %sn", name.release);
return 0;
}

 

Собираем и запускаем:


$ make release
cc  release.c   -o release
$ ./release
Версия ядра: 6.12.68-6.12-alt1

 

Какие системные вызовы нужны процессу

Как выяснить, какие системные вызовы использует наше приложение?

Очевидно, что это явно используемый в коде системный вызов uname(2), возвращающий информацию о системе. Кроме того, очевидно, что это системные вызовы read(2) и write(2), используемые для организации ввода-вывода, вызовы openat(2) и close(2) — для работы со стандартными дескрипторами ввода-вывода.

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

Seccomp-фильтрация в systemd

Для работы с фильтрами seccomp командными средствами удобнее всего использовать инструменты SystemD.

Выполним запуск приложения средствами systemd-run, использовав явное перечисление фильтруемых системных вызовов в параметре property=“SystemCallFilter=…”:


$ systemd-run --user --pty --same-dir --wait --collect 
    --service-type=exec 
      --property="SystemCallFilter=uname read write openat close" 
  ./release
Running as unit: run-p123231-i123531.service; invocation ID: 07573b418714492eb7046cf6cb4dc8eb
Press ^] three times within 1s to disconnect TTY.
 
Finished with result: core-dump
Main processes terminated with: code=dumped, status=31/SYS
Service runtime: 143ms
CPU time consumed: 40ms
Memory peak: 952K (swap: 0B)

 

Видим, что процесс был завершен принудительно сигналом SIGSYS(31):

Finished with result: core-dump

Main processes terminated with: code=dumped, status=31/SYS

Это означает, что был использован неправильный (в данном случае неразрешенный seccomp) системный вызов. Определим, какой именно?

Проверка по журналу аудита причин завершения приложения

Ищем в журнале аудита (сервис аудита должен работать) сообщение о завершении процесса 31-м сигналом:


# journalctl -ekf -g 'sig=31'

фев 27 13:00:22 y520 kernel: audit: type=1326 audit(1772186422.360:2):
 auid=500 uid=500 gid=500 ses=2 pid=123232 comm="release" 
 exe=2F686F6D652F65676F722F626F6F6B732FD0B2D090D099D0A2D0982F736563636F6D702F72656C65617365 sig=31 
 arch=c000003e syscall=21 compat=0 ip=0x7f1d746b5bd7 code=0x80000000

 

Видим, что процесс был завершен при выполнении системного вызова 21 (syscall=21).

Ищем 21-й системный вызов в ранее приведенной таблице и видим, что это системный вызов access(2). Добавляем его в наш фильтр и заново запускаем приложение, но это может оказаться не самым коротким путем к цели.

Адаптация списка системных вызовов при использовании strace

Утилита strace(1) — это инструмент перехвата системных вызовов и сигналов, используемых приложением в процессе работы.

Получим список системных вызовов с помощью strace:


$ $ strace -qqqc -U name ./release
Версия ядра: 6.12.68-6.12-alt1
syscall
----------------
execve
mmap
openat
mprotect
brk
munmap
newfstatat
write
read
close
pread64
access
set_robust_list
uname
arch_prctl
set_tid_address
prlimit64
getrandom
rseq
----------------
total

 

Теперь мы имеем достаточно точный список системных вызовов процесса, используем его для определения фильтра.

Результат


$ systemd-run --user --pty --same-dir --wait --collect 
  --service-type=exec 
  --property="SystemCallFilter=execve mmap openat mprotect brk munmap 
 newfstatat write read close pread64 access set_robust_list uname arch_prctl set_tid_address 
 prlimit64 getrandom rseq" ./release
Running as unit: run-p126302-i126602.service; invocation ID: 
 9333d0df14f94e45834542f725182b61
Press ^] three times within 1s to disconnect TTY.
Версия ядра: 6.12.68-6.12-alt1
Finished with result: success
Main processes terminated with: code=exited, status=0/SUCCESS
Service runtime: 40ms
CPU time consumed: 28ms
Memory peak: 620K (swap: 0B)

 

Как мы видим, процесс успешно выполнился, наш фильтр на этот раз оказался достаточным.

Версия ядра: 6.12.68-6.12-alt1

Finished with result: success

Main processes terminated with: code=exited, status=0/SUCCESS

Если strace(1) не увидит что-либо из используемых системных вызовов, финальную адаптацию можно провести с применением сообщений в журнале аудита, как было показано выше.

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

Комментарии0
Тоже интересно
Комментировать
Поделиться
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники