seccomp(2) переводит процесс в состояние безопасных вычислений

ОБЗОР

#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/audit.h>
#include <linux/signal.h>
#include <sys/ptrace.h>
int seccomp(unsigned int operation, unsigned int flags, void *args);

ОПИСАНИЕ

Системный вызов seccomp() переводит вызвавший процесс в состояние безопасных вычислений (Secure Computing, seccomp).

В настоящее время в Linux поддерживаются следующие значения operation:

SECCOMP_SET_MODE_STRICT
Вызвавшей нити доступны только системные вызовы read(2), write(2), _exit(2) (но не exit_group(2)) и sigreturn(2). При запуске других системных вызовов генерируется сигнал SIGKILL. Строгий режим безопасных вычислений полезен для вычислительных приложений, которым может потребоваться выполнить недоверительный байт-код, возможно полученный при чтении из канала или сокета.

Заметим, что хотя вызывающая нить больше не вызывает sigprocmask(2), она может использовать sigreturn(2) для блокировки всех сигналов (кроме SIGKILL и SIGSTOP). Это означает, что alarm(2) (например) недостаточно для ограничения времени выполнения процесса. Вместо него для надёжного завершения процесса нужно использовать SIGKILL. Это можно сделать с помощью timer_create(2) с SIGEV_SIGNAL и sigev_signo равным SIGKILL, или используя setrlimit(2) для задания жёсткого ограничения по RLIMIT_CPU.

Эта операция доступна только, если в ядре включён параметр CONFIG_SECCOMP.

Значение flags должно быть равно 0, а args — NULL.

Эта операция функционально идентична вызову:


    prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);

SECCOMP_SET_MODE_FILTER
Разрешённые системные вызовы определяются указателем на Berkeley Packet Filter (BPF), передаваемый через args. Данный аргумент является указателем на struct sock_fprog; эту структуру можно использовать для отбора произвольных системных вызовов и их аргументов. Если фильтр некорректен, то seccomp() завершается с ошибкой EINVAL в errno.

Если фильтром разрешён fork(2) или clone(2), то все потомки будут ограничены тем же фильтром системных вызовов что и родитель. Если разрешён execve(2), то существующий фильтр сохраняется и после вызова execve(2).

Чтобы использовать операцию SECCOMP_SET_MODE_FILTER вызывающий должен иметь мандат CAP_SYS_ADMIN или у нити уже должен быть установлен бит no_new_privs. Если этот бит не установлен предком этой нить, то в нити можно сделать следующий вызов:


    prctl(PR_SET_NO_NEW_PRIVS, 1);

В противном случае операция SECCOMP_SET_MODE_FILTER завершится с ошибкой и вернёт EACCES в errno. Данное требование гарантирует, что непривилегированный процесс не сможет применить вредоносный фильтр и вызвать программу с set-user-ID или другую привилегированную программу с помощью execve(2), то есть потенциально подвергнуть эту программу опасности (такой вредоносный фильтр может, например, заставить попытаться использовать setuid(2) для установки ID вызывающего пользователя в ненулевые значения вместо возврата 0 без действительного запуска системного вызова. Таким образом, программа может быть обманута и остаться с правами суперпользователя в окружении, где возможно заставить её сделать что-то опасное, так как в действительности она не отказалась от своих прав).

Если prctl(2) или seccomp(2) разрешены присоединённым фильтром, то могут быть добавлены дополнительные фильтры. Это увеличит время вычисления, но в дальнейшем позволит сократить область атаки при выполнении нити.

Операция SECCOMP_SET_MODE_FILTER доступна только, если в ядре включён параметр CONFIG_SECCOMP_FILTER.

Если значение flags равно 0, то эта операция функционально идентична вызову:


    prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, args);

Возможные значения flags:

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

Если в какой-то нити невозможна синхронизация с единым деревом фильтров, то вызов не присоединит новый фильтр seccomp, и завершится с ошибкой, вернув ID первой обнаруженной нити, для которой синхронизация невозможна. Синхронизации не получится, если другая нить того же процесса находится в SECCOMP_MODE_STRICT, или если она присоединила новые фильтры seccomp к самой себе, отличающиеся от дерева фильтров вызывающей нити.

Фильтры

При добавлении фильтров посредством SECCOMP_SET_MODE_FILTER, значение args указывает на программу фильтрации:

struct sock_fprog {
    unsigned short      len;    /* количество инструкций BPF */
    struct sock_filter *filter; /* указатель на массив
                                   инструкций BPF */
};

В каждой программе должно быть не менее одной инструкции BPF:

struct sock_filter {            /* блок фильтрации */
    __u16 code;                 /* действительный код фильтра */
    __u8  jt;                   /* переход при совпадении */
    __u8  jf;                   /* переход при несовпадении */
    __u32 k;                    /* общее поле для различных целей */
};

При выполнении инструкций информация о системном вызове (когда используется режим адресации BPF_ABS) программе BPF доступна из буфера (только для чтения) в виде:

struct seccomp_data {
    int   nr;                   /* номер системного вызова */
    __u32 arch;                 /* значение AUDIT_ARCH_*
                                   (смотрите <linux/audit.h>) */
    __u64 instruction_pointer;  /* указатель на инструкцию ЦП */
    __u64 args[6];              /* до 6 аргументов системного вызова */
};

Так как количество системных вызовов различно на разных архитектурах и некоторые архитектуры (например, x86-64) позволяют коду в пользовательском пространстве использовать соглашения о вызовах нескольких архитектур, то обычно необходимо проверять значение поля arch.

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

Поле arch не уникально для всех соглашений о вызовах. В x86-64 ABI и x32 ABI в arch используется AUDIT_ARCH_X86_64, и они запускаются на одних и тех же процессорах. Чтобы отличать один ABI от другого используется маска __X32_SYSCALL_BIT с номером системного вызова.

Это означает, что для создания чёрного списка системных вызовов на основе seccomp, выполняемых через x86-64 ABI, необходимо не только проверять что arch равно AUDIT_ARCH_X86_64, но также явно отвергать все системные вызовы, которые содержат __X32_SYSCALL_BIT в nr.

В поле instruction_pointer содержится адрес инструкции машинного языка, который запускает системный вызов. Это может быть полезно вместе с /proc/[pid]/maps для выполнения проверок из какой области (отображение) программы делается системный вызов (вероятно, стоит блокировать системные вызовы mmap(2) и mprotect(2) для запрета программе удалять такие проверки).

При проверке значений из args по чёрному списку имейте в виду, что часто аргументы просто обрезаются до обработки, но после проверки seccomp. Например, это случается, если на ядре x86-64 используется i386 ABI: хотя ядро, обычно, не смотрит дальше 32 младших бит аргументов, в данные seccomp попадут значения полных 64-битных регистров. Менее удивительный пример: если для выполнения системного вызова с аргументом типа int используется x86-64 ABI, то старшая половина регистра аргумента игнорируется системным вызовом, но видима в данных seccomp.

Фильтр seccomp возвращает 32-битное значение, состоящее из двух частей: в старших 16 битах (соответствует маске, определяемой константой SECCOMP_RET_ACTION) содержится одно из значений «действие», перечисленных далее; в младших 16 битах (определяется константой SECCOMP_RET_DATA) содержатся «данные», связанные с возвращаемым значением.

Если существует несколько фильтров, то все они выполняются в обратном порядке их добавления в дерево фильтров — то есть последние добавленные выполняются первыми (заметим, что все фильтры будут вызваны даже, если ранее выполнявшиеся фильтры вернули SECCOMP_RET_KILL. Это сделано для простоты кода ядра и предоставления крошечного ускорения выполнения набора фильтров, так как не выполняется проверка этого редкого случая). Возвращаемое значение для вычисления данного системного вызова —первое встреченного значение SECCOMP_RET_ACTION наивысшего приоритета (вместе с сопутствующими ему данными), возвращаемое выполнением всех фильтров.

Возвращаемые значения фильтром seccomp (в порядке уменьшения приоритета):

SECCOMP_RET_KILL
Это значение приводит к немедленному завершению процесса без выполнения системного вызова. Процесс завершается как от сигнала SIGSYS (не SIGKILL).
SECCOMP_RET_TRAP
Это значение приводит к отправке ядром сигнала SIGSYS возбудившему процессу без выполнения системного вызова. Заполняются некоторые поля структуры siginfo_t (смотрите sigaction(2)), связанной с сигналом:
*
В si_signo будет содержаться значение SIGSYS.
*
В si_call_addr будет показан адрес инструкции системного вызова.
*
В si_syscall и si_arch будет указываться какой системный вызов была попытка запустить.
*
В si_code будет содержаться значение SYS_SECCOMP.
*
В si_errno будет содержаться часть SECCOMP_RET_DATA из возвращаемого значения фильтра.
Программный счётчик будет таким же как при системном вызове (т. е., он не будет указывать на инструкцию системного вызова). В регистре возвращаемого значения будет содержаться значение, зависящее от архитектуры; если выполнение продолжится, оно равно чему-нибудь подходящему для системного вызова (зависимость от архитектуры возникает из-за того, что при замене его на ENOSYS может перезаписаться какая-нибудь полезная информация).
SECCOMP_RET_ERRNO
Это значение приводит к тому, что часть SECCOMP_RET_DATA возвращаемого значения фильтра передаётся в пространство пользователя в виде значения errno без выполнения системного вызова.
SECCOMP_RET_TRACE
При возврате это значение заставит ядро попытаться уведомить трассировщика, использующего ptrace(2), до выполнения системного вызова. Если трассировщика нет, то системный вызов не выполняется и возвращается состояние ошибки со значением errno равным ENOSYS.

Трассировщик будет уведомлён, если он запросил PTRACE_O_TRACESECCOMP посредством ptrace(PTRACE_SETOPTIONS). Трассировщик будет уведомлён оPTRACE_EVENT_SECCOMP, а часть SECCOMP_RET_DATA возвращаемого значения фильтра будет доступна через PTRACE_GETEVENTMSG.

Трассировщик может пропустить системный вызов, изменив номер системного вызова на -1. Или же он может изменить запрашиваемый системный вызов на системный вызов с другим номером. Если трассировщик просит пропустить системный вызов, то системный вызов появится в возвращаемом значении, которое трассировщик помещает в регистр возвращаемого значения.

Проверка seccomp не будет запущена ещё раз после уведомления трассировщика (это означает, что ограниченные окружения (sandbox) на основе seccomp не должны позволять использовать ptrace(2) — даже другим процессам в окружении — без максимальной предосторожности; ptracer-ы могут использовать этот механизм для выхода из окружения seccomp).

SECCOMP_RET_ALLOW
Это значение приводит к выполнению системного вызова.

ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ

При успешном выполнении seccomp() возвращает 0. При ошибке, если был использован SECCOMP_FILTER_FLAG_TSYNC, то возвращается ID нити, которая была причиной ошибки синхронизации (данный ID — идентификатор нити ядра с типом, возвращаемом clone(2) и gettid(2)). При других ошибках возвращается -1 и в errno записывается причина ошибки.

ОШИБКИ

Функция seccomp() может завершиться с ошибкой по следующим причинам:
EACCESS
У вызывающего нет мандата CAP_SYS_ADMIN или не установлен no_new_privs до использования SECCOMP_SET_MODE_FILTER.
EFAULT
Аргумент args не содержит допустимого адреса.
EINVAL
Значение operation неизвестно; или допустимое значение flags для указанного operation.
EINVAL
Значение operation включает BPF_ABS, но указанное смещение не выровнено по 32-битной границе или превышает sizeof(struct seccomp_data).
EINVAL
Режим безопасных вычислений уже включён, и значение operation отличается от существующей настройки.
EINVAL
В operation указано SECCOMP_SET_MODE_FILTER, но ядро не собрано с параметром CONFIG_SECCOMP_FILTER.
EINVAL
В operation указано SECCOMP_SET_MODE_FILTER, но фильтрующая программа, задаваемая в args, некорректна или её длина равна 0 или превышает BPF_MAXINSNS (4096) инструкций.
ENOMEM
Не хватает памяти.
ENOMEM
Общая длина всех фильтрующих программ, присоединённых к вызывающей нити, превысила бы MAX_INSNS_PER_PATH (32768) инструкций. Заметим, что для вычисления этого предела на каждую уже существующую фильтрующую программу прибавляются ещё 4 инструкции.
ESRCH
Во время синхронизации нити произошла ошибка в другой нити, но её ID невозможно определить.

ВЕРСИИ

Системный вызов seccomp() впервые появился в Linux 3.17.

СООТВЕТСТВИЕ СТАНДАРТАМ

Системный вызов seccomp() является нестандартным расширением Linux.

ЗАМЕЧАНИЯ

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

В поле Seccomp файла /proc/[pid]/status отображается метод просмотра режима seccomp в процессе; смотрите proc(5).

Вызов seccomp() предоставляет больше возможностей по сравнению с операцией PR_SET_SECCOMP prctl(2) (которая не поддерживает flags).

Особенности seccomp в BPF

Заметим, что следующие особенности BPF относятся только к фильтрам seccomp:
*
Модификаторы размера BPF_H и BPF_B не поддерживаются: все операции должны загружать и сохранять слова (4-байта) (BPF_W).
*
Для доступа к содержимому буфера seccomp_data используйте модификатор режима адресации BPF_ABS.
*
Модификатор режима адресации BPF_LEN выдаёт непосредственный операнд режима, чьё значение равно размеру буфера seccomp_data.

ПРИМЕР

Программа, показанная далее, обрабатывает четыре и более аргументов. Первые три аргумента — номер системного вызова, числовой идентификатор архитектуры и номер ошибки. Программа использует эти значения для создания фильтра BPF, который используется во время работы для выполнения следующих проверок:
[1]
Если программа не запущена на определённой архитектуре, то фильтр BPF заставляет системные вызовы завершаться с ошибкой ENOSYS.
[2]
Если программа попытается выполнить системный вызов с заданным номером, то фильтр BPF заставит системный вызов завершиться с ошибкой, а в errno будет записан указанный номер ошибки.

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

Сначала мы выведем имя архитектуры, на которой работаем (x86-64), а затем создадим функцию оболочки, которая выдаёт список номеров системных вызовов этой архитектуры:

$ uname -m
x86_64
$ syscall_nr() {
    cat /usr/src/linux/arch/x86/syscalls/syscall_64.tbl | \
    awk '$2 != "x32" && $3 == "'$1'" { print $1 }'
}

Когда фильтр BPF отклоняет системный вызов (случай [2] выше), системный вызов завершается с номером ошибки, указанной в командной строке. В наших экспериментах используется номер ошибки 99:

$ errno 99
EADDRNOTAVAIL 99 Cannot assign requested address

В следующем примере мы пытаемся выполнить команду whoami(1), но фильтр BPF отклоняет системный вызов execve(2), и поэтому команда даже не начнёт выполняться:

$ syscall_nr execve
59
$ ./a.out
Использование: ./a.out <syscall_nr> <arch> <errno> <prog> [<args>]
Подсказка для <arch>: AUDIT_ARCH_I386: 0x40000003
                 AUDIT_ARCH_X86_64: 0xC000003E
$ ./a.out 59 0xC000003E 99 /bin/whoami
execv: Cannot assign requested address

В следующем примере фильтр BPF отклоняет системный вызов write(2), и хотя выполнение началось, команда whoami(1) не может записать в стандартный вывод:

$ syscall_nr write
1
$ ./a.out 1 0xC000003E 99 /bin/whoami

В последнем примере фильтр BPF отклоняет системный вызов, который не используется в команде whoami(1), и поэтому она выполняется без ошибок и выводит:

$ syscall_nr preadv
295
$ ./a.out 295 0xC000003E 99 /bin/whoami
cecilia

Исходный код программы

#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/audit.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <sys/prctl.h>
#define X32_SYSCALL_BIT 0x40000000
static int
install_filter(int syscall_nr, int t_arch, int f_errno)
{
    unsigned int upper_nr_limit = 0xffffffff;
    /* предполагается, что AUDIT_ARCH_X86_64 означает обычный x86-64 ABI */
    if (t_arch == AUDIT_ARCH_X86_64)
        upper_nr_limit = X32_SYSCALL_BIT - 1;
    struct sock_filter filter[] = {
        /* [0] загружаем архитектуру из буфера «seccomp_data» в
               аккумулятор */
        BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
                 (offsetof(struct seccomp_data, arch))),
        /* [1] прыгаем вперёд на 5 инструкции, если архитектура не совпадает
               с «t_arch» */
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, t_arch, 0, 4),
        /* [2] загружаем номер системного вызова из буфера «seccomp_data» в
               аккумулятор */
        BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
                 (offsetof(struct seccomp_data, nr))),
        /* [3] проверяем ABI — нужно только для чёрного списка на x86-64.
               Используем JGT вместо проверки битовой маски,
               чтобы избежать перезагрузки номера syscall. */
        BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, upper_nr_limit, 3, 0),
        /* [4] прыгаем вперёд на 1 инструкцию, если номер системного вызова
               не совпадает с «syscall_nr» */
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, syscall_nr, 0, 1),
        /* [5] совпала архитектура и системный вызов: не выполняем
               системный вызов и возвращаем «f_errno» в «errno» */
        BPF_STMT(BPF_RET | BPF_K,
                 SECCOMP_RET_ERRNO | (f_errno & SECCOMP_RET_DATA)),
        /* [6] не совпал номер системного вызова: разрешаем
               работу других системных вызовов */
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
        /* [7] не совпала архитектура: прерываем процесс */
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    };
    struct sock_fprog prog = {
        .len = (unsigned short) (sizeof(filter) / sizeof(filter[0])),
        .filter = filter,
    };
    if (seccomp(SECCOMP_SET_MODE_FILTER, 0, &prog)) {
        perror("seccomp");
        return 1;
    }
    return 0;
}
int
main(int argc, char **argv)
{
    if (argc < 5) {
        fprintf(stderr, "Использование: "
                "%s <syscall_nr> <arch> <errno> <prog> [<args>]\n"
                "Подсказка для <arch>: AUDIT_ARCH_I386: 0x%X\n"
                "                 AUDIT_ARCH_X86_64: 0x%X\n"
                "\n", argv[0], AUDIT_ARCH_I386, AUDIT_ARCH_X86_64);
        exit(EXIT_FAILURE);
    }
    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
        perror("prctl");
        exit(EXIT_FAILURE);
    }
    if (install_filter(strtol(argv[1], NULL, 0),
                       strtol(argv[2], NULL, 0),
                       strtol(argv[3], NULL, 0)))
        exit(EXIT_FAILURE);
    execv(argv[4], &argv[4]);
    perror("execv");
    exit(EXIT_FAILURE);
}