Администрирование → Контейнеры и безопасность: seccomp

AndreiYemelianov 17 февраля в 10:31 5,4k


Для работы с потенциально опасными, непроверенными или просто «сырыми» программами часто используются так называемые песочницы (sandboxes) — специально выделенные среды с жёсткими ограничениями. Для запускаемых в песочницах программ обычно сильно лимитированы доступ к сети, возможность взаимодействия с операционной системой на хост-машине и считывать информацию с устройств ввода-вывода.

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

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

Сначала мы разберём принципы работы seccomp, а затем продемонстрируем, как он используется в Docker.

Seccomp: первое знакомство


Seccomp (сокращение от secure computing) — механизм ядра Linuх, позволяющий процессам определять системные вызовы, которые они будут использовать. Если злоумышленник получит возможность выполнить произвольный код, seccomp не даст ему использовать системные вызовы, которые не были заранее объявлены.


Seccomp — разработка Google. Он используется, в частности, в браузере Google Chrome для запуска плагинов.


Для активации seccomp используется системный вызов prctl().


Посмотрим, как это работает, на примере простой программы:


#include <stdio.h>
#include <unistd.h>
#include <linux/seccomp.h>
#include <sys/prctl.h>

int main () {
  pid_t pid;

  printf("Step 1: no restrictions yet\n");

  prctl (PR_SET_SECCOMP, SECCOMP_MODE_STRICT);
  printf ("Step 2: entering the strict mode. Only read(), write(), exit() and sigreturn() syscalls    are allowed\n");

  pid = getpid ();
  printf ("!!YOU SHOULD NOT SEE THIS!! My PID = %d", pid);

  return 0;
}

Сохраним эту программу под именем seccomp1.c, скомпилируем и запустим:


$ gcc seccomp1.c -o seccomp1

$ ./seccomp1

Мы увидим на консоли следующий вывод:


Step 1: no restrictions yet
Step 2: entering the strict mode. Only read(), write(), exit() and sigreturn() syscalls are allowed
Killed

Чтобы понять, откуда взялся именно такой вывод, воспользуемся strace:


$ strace ./seccomp1


/приводим небольшой фрагмент вывода/

prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT) = 0
write(1, "Step 2: entering the strict mode"..., 100Step 2: entering the strict mode. Only read(), write(), exit() and sigreturn() syscalls are allowed
) = 100
+++ killed by SIGKILL +++
Killed

Итак, что же произошло? С помощью системного вызова prctl мы активировали seccomp и включили строгий режим. После этого наша программа попыталась узнать PID текущего процесса с помощью системного вызова getpid(), но наложенные ограничения не дали этого сделать: процесс получил сигнал SIGKILL и был тут же завершён.


Как видим, seccomp прекрасно справляется со своими задачами. Но строгий режим неудобен тем, что не даёт возможности выбирать, какие системные вызовы разрешать, а какие — нет. Для решения этой задачи мы можем воспользоваться механизмом BPF (Berkeley Packet Filters).


Seccomp и фильтры BPF


Механизм BPF (сокращение от Berkeley Packet Filters) был изначально создан для фильтрации сетевых пакетов, но впоследствии сфера его применения существенно расширилась. Сегодня BPF используется, например, для трассировки ядра Linux (вот интересная публикация на эту тему в блоге Брендана Грегга). В 2012 году он был интегрирован с seccomp; появилась расширенная версия, которая так и называется — seccomp-bpf.


Писать для BPF — дело очень сложное (кое-что об этом можно почитать, например, здесь). Мы же особенности синтаксиса BPF обсуждать не будем (эта тема выходит далеко за рамки нашей статьи) и воспользуемся библиотекой libseccomp, которая предоставляет простой и удобный API для фильтрации системных вызовов.

Устанавливается она при помощи стандартного менеджера пакетов:


$ sudo apt-get install libseccomp-dev

Попробуем теперь написать небольшую программу:



#include <stdio.h>
#include <seccomp.h>
#include <unistd.h>

int main() {
    pid_t pid;

    scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);

    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(sigreturn), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);


    printf ("No restrictions yet\n");

    seccomp_load(ctx);
    pid = getpid();
    printf("!! YOU SHOULD NOT SEE THIS!! My PID is%d\n", pid);

    return 0;
}

Прокомментируем этот код построчно.


scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);

Здесь мы инициализируем фильтр и указываем, какое действие нужно осуществлять по умолчанию — в нашем случае это SCMP_ACT_KILL, то есть немедленная остановка процесса, который выполнит запрещённый системный вызов.


Далее идут правила seccomp; в них мы указываем системные вызовы, которые будет разрешено выполнять нашему процессу:


 seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
 seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
 seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(sigreturn), 0);
 seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
 seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);

Далее мы активируем правила:


seccomp_load(ctx);

Как и в предыдущем примере, мы пытаемся вывести на консоль PID текущего процесса. Но получится ли у нас это сделать?


Скомпилируем и запустим программу:

$ gcc -o seccomp2 seccomp2.c  -lseccomp
$ ./seccomp2

Мы увидим следующий вывод:

No restrictions yet
Bad system call

Что произошло во время выполнения этой программы? Как и в предыдущем случае, ответить на этот вопрос нам поможет strace:


$ strace ./seccomp2

/приводим фрагмент вывода/
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, {len = 9, filter = 0x1ef5fe0}) = 0
+++ killed by SIGSYS +++

Мы видим, что сработал фильтр: процесс выполнил системный вызов getpid, запрещённый правилами, после чего был тут же остановлен.


Чтобы лучше понять, как работают фильтры seccomp, в качестве действия по умолчанию в коде полезно указать не SCMP_ACT_KILL, а SCMP_ACT_TRAP:


scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_TRAP);

Вывод strace будет куда более подробным:

$ strace ./seccomp2
/приводим фрагмент вывода/
syscall_18446744073709551615(0xffffffff, 0x7feb8c47ab28, 0, 0x22b, 0x130c0c0, 0) = 0x27
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0x7feb8c18366f, si_syscall=__NR_getpid, si_arch=AUDIT_ARCH_X86_64} ---
+++ killed by SIGSYS +++

В нашем случае (ОС Ubuntu 16.04, ядро 4.4.) в выводе прямо указывается запрещённый системный вызов, попытка выполнения которого повлекла за собой остановку процесса: si_syscall=__NR_getpid.


В других дистрибутивах и в других версиях ядра в выводе может быть приведено не имя системного вызова, а его номер из файла /asm/unistd.h.



Seccomp в Docker


В предыдущих разделах мы разобрали основные принципы работы seccomp. Рассмотрим теперь на примере Docker, как seccomp используется в конкретных инструментах контейнеризации.

Впервые профили seccomp для контейнеров появились в runc, о котором мы уже писали.


В Docker Engine они были добавлены начиная с версии 1.10.


По умолчанию во всех контейнерах Docker заблокированы 44 системных вызова (всего в современных 64-битных Linux-системах несколько сотен системных вызовов). К числу запрещённых относится, например, системный вызов reboot(): вряд ли можно представить себе ситуацию, когда требуется перезагрузить ОС на хост-машине из контейнера.


Ещё один хороший пример — системный вызов keyctl(), для которого не так давно была обнаружена уязвимость (CVE 2016-0728). Теперь в Docker он блокируется по умолчанию.


Профили seccomp по умолчанию — это хорошее нововведение, полезное уже тем, что ограничивает возможности для злоумышленников и снижает вероятность атак. Но этого явно недостаточно: у многих из незаблокированных вызовов есть уязвимости. Запретить все потенциально опасные вызовы по вполне понятным причинам просто-напросто невозможно!


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


Приведём простой пример:


{ 
   "defaultAction":"SCMP_ACT_KILL",
   "syscalls":[  
      {  
         "name":"chmod",
         "action":"SCMP_ACT_ERRNO"
      }
   ]
}

Как видим, здесь всё делается точно так же, как и в приводимых выше примерах кода. Сначала мы указываем, какое действие следует совершать по умолчанию. Далее мы перечисляем запрещённые вызовы, а также действия, которые нужно осуществить при выполнении одного их этих вызовов.


Сохраним этот файл под именем config.json и попытаемся запустить контейнер с прописанными выше настройками seccomp:


$ docker run --security-opt seccomp:chmod.json busybox chmod 400 /etc/hostname

chmod: /etc/hostname: Operation not permitted

Как видим, фильтр сработал в соответствии со сформулированными правилами: запрещённый системный вызов chmod был блокирован.


Заключение


В этой статье мы рассказали, как работает seccomp и как он используется в Docker. Если у вас есть вопросы, замечания и пожелания — добро пожаловать в комментарии.


В заключение мы, как обычно, приводим полезные ссылки для желающих узнать больше:


Проголосовать:
+31
Сохранить: