Проблема возникает из-за того, что библиотека GNU C использует системный вызов exit_group
, если он доступен, в Linux вместо exit
для функции _exit()
(см. sysdeps/unix/sysv/linux/_exit.c
для проверки), и как описано в man 2 prctl
, системный вызов exit_group
не разрешен строгим фильтром seccomp.
Поскольку вызов функции _exit()
происходит внутри библиотеки C, мы не можем вставить его в нашу собственную версию (это просто вызовет системный вызов exit
). (Обычная очистка процесса выполняется в другом месте; в Linux функция _exit()
выполняет только последний системный вызов, завершающий процесс.)
Мы могли бы попросить разработчиков библиотеки GNU C использовать системный вызов exit_group
в Linux только тогда, когда в текущем процессе более одного потока, но, к сожалению, это было бы непросто, и даже если бы его добавили прямо сейчас, потребовалось бы довольно много времени для эта функция будет доступна в большинстве дистрибутивов Linux.
К счастью, мы можем отказаться от строгого фильтра по умолчанию и вместо этого определить свой собственный. Есть небольшая разница в поведении: видимый сигнал, убивающий процесс, изменится с SIGKILL
на SIGSYS
. (Сигнал на самом деле не доставляется, так как ядро убивает процесс; изменяется только кажущийся номер сигнала, вызвавший смерть процесса.)
Более того, это даже не так сложно. Я потратил немного времени на изучение некоторых трюков с макросами GCC, которые упростили бы управление списком разрешенных системных вызовов, но я решил, что это не будет хорошим подходом: список разрешенных системных вызовов должен быть тщательно продуман — мы только добавить exit_group()
по сравнению со строгим фильтром, здесь! -- так что если сделать его немного сложным, это нормально.
Следующий код, например example.c
, проверен для работы с ядром 4.4 (должен работать с ядром 3.5 или более поздней версии) на платформе x86-64 (как для x86, так и для x86-64, т. е. 32-разрядные и 64-битные двоичные файлы). Однако он должен работать на всех архитектурах Linux и не требует и не использует библиотеку libseccomp.
#define _GNU_SOURCE
#include <stdlib.h>
#include <stddef.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <stdio.h>
static const struct sock_filter strict_filter[] = {
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof (struct seccomp_data, nr))),
BPF_JUMP(BPF_JMP | BPF_JEQ, SYS_rt_sigreturn, 5, 0),
BPF_JUMP(BPF_JMP | BPF_JEQ, SYS_read, 4, 0),
BPF_JUMP(BPF_JMP | BPF_JEQ, SYS_write, 3, 0),
BPF_JUMP(BPF_JMP | BPF_JEQ, SYS_exit, 2, 0),
BPF_JUMP(BPF_JMP | BPF_JEQ, SYS_exit_group, 1, 0),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)
};
static const struct sock_fprog strict = {
.len = (unsigned short)( sizeof strict_filter / sizeof strict_filter[0] ),
.filter = (struct sock_filter *)strict_filter
};
int main(void)
{
/* To be able to set a custom filter, we need to set the "no new privs" flag.
The Documentation/prctl/no_new_privs.txt file in the Linux kernel
recommends this exact form: */
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
fprintf(stderr, "Cannot set no_new_privs: %m.\n");
return EXIT_FAILURE;
}
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &strict)) {
fprintf(stderr, "Cannot install seccomp filter: %m.\n");
return EXIT_FAILURE;
}
/* The seccomp filter is now active.
It differs from SECCOMP_SET_MODE_STRICT in two ways:
1. exit_group syscall is allowed; it just terminates the
process
2. Parent/reaper sees SIGSYS as the killing signal instead of
SIGKILL, if the process tries to do a syscall not in the
explicitly allowed list
*/
return EXIT_SUCCESS;
}
Скомпилируйте, используя, например.
gcc -Wall -O2 example.c -o example
и запустить с помощью
./example
или под strace
, чтобы увидеть выполненные системные и библиотечные вызовы;
strace ./example
Программа strict_filter
BPF действительно тривиальна. Первый код операции загружает номер системного вызова в аккумулятор. Следующие пять кодов операций сравнивают его с приемлемым номером системного вызова и, если он найден, выполняют переход к последнему коду операции, разрешающему системный вызов. В противном случае предпоследний код операции убивает процесс.
Обратите внимание, что хотя в документации указано, что sigreturn
является разрешенным системным вызовом, фактическое имя системного вызова в Linux — rt_sigreturn
. (sigreturn
было объявлено устаревшим в пользу rt_sigreturn
веков назад.)
Кроме того, при установке фильтра коды операций копируются в память ядра (см. kernel/seccomp.c
в исходных кодах ядра Linux), поэтому он никак не повлияет на фильтр, если данные будут изменены позже. Другими словами, наличие структур static const
не оказывает никакого влияния на безопасность.
Я использовал static
, поскольку нет необходимости, чтобы символы были видны за пределами этой единицы компиляции (или в удаленном двоичном файле), и const
, чтобы поместить данные в раздел данных только для чтения двоичного файла ELF.
Форма BPF_JUMP(BPF_JMP | BPF_JEQ, nr, equals, differs)
проста: аккумулятор (номер системного вызова) сравнивается с nr
. Если они равны, то следующие equals
опкодов пропускаются. В противном случае следующие коды операций differs
пропускаются.
Поскольку случаи равенства переходят к самому последнему коду операции, вы можете добавлять новые коды операций вверху (то есть сразу после начального кода операции), увеличивая счетчик пропусков равных для каждого из них.
Обратите внимание, что printf()
не будет работать после установки фильтра seccomp, поскольку внутри библиотеки C требуется выполнить системный вызов fstat
(на стандартный вывод) и системный вызов brk
, чтобы выделить часть памяти для буфера.
person
Nominal Animal
schedule
08.11.2016
_exit(EXIT_SUCCESS)
не работает, так как на странице руководства четко указано, что в строгом режиме seccomp единственные системные вызовы, которые разрешено делать вызывающему потоку, это read(2), write(2), _exit(2) ( но не exit_group(2)), а sigreturn(2). (где числа в скобках, конечно же, являются ручными разделами). - person   schedule 06.11.2016