ОБЗОР
#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)), связанной с сигналом:
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,
который используется во время работы для выполнения следующих проверок:
[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); }