Подтвердить что ты не робот

Seccomp --- как EXIT_SUCCESS?

Ηow до EXIT_SUCCESS после установки строгого режима seccomp. Правильно ли это, вызывать syscall(SYS_exit, EXIT_SUCCESS); в конце основного?

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

int main(int argc, char **argv) {
  prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);

  //return EXIT_SUCCESS; // does not work
  //_exit(EXIT_SUCCESS); // does not work
  // syscall(__NR_exit, EXIT_SUCCESS); // (EDIT) This works! Is this the ultimate answer and the right way to exit success from seccomp-ed programs?
  syscall(SYS_exit, EXIT_SUCCESS); // (EDIT) works; SYS_exit equals __NR_exit
}

// gcc seccomp.c -o seccomp && ./seccomp; echo "${?}" # I want 0
4b9b3361

Ответ 1

Как объяснено в eigenstate.org и в SECCOMP (2 ):

Единственный системный вызов, которому разрешающий поток разрешен               make читаются (2), пишите (2), _exit (2) (, но не exit_group (2)),               и sigreturn (2). Другие системные вызовы приводят к доставке               сигнала SIGKILL.

В результате можно было бы ожидать, что _exit() будет работать, но это функция-оболочка, которая вызывает exit_group(2), которая не допускается в строгом режиме ([1], [2]), таким образом, процесс убивается.

Он даже сообщил в exit (2) - Linux man page:

В glibc до версии 2.3 функция обертки _exit() вызывала системный вызов ядра с тем же именем. Начиная с glibc 2.3, функция wrapper вызывает exit_group (2), чтобы завершить все потоки в процессе.

То же самое происходит с оператором return, который должен закончиться тем, что вы убили свой процесс, очень похожим образом с _exit().

Упрощение процесса обеспечит дополнительное подтверждение (чтобы это отображалось, вам не нужно устанавливать PR_SET_SECCOMP, просто комментарий prctl()), и я получил аналогичный вывод для обоих нерабочих случаев:

linux12:/home/users/grad1459>gcc seccomp.c -o seccomp
linux12:/home/users/grad1459>strace ./seccomp
execve("./seccomp", ["./seccomp"], [/* 24 vars */]) = 0
brk(0)                                  = 0x8784000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb775f000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=97472, ...}) = 0
mmap2(NULL, 97472, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7747000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\220\226\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1730024, ...}) = 0
mmap2(NULL, 1739484, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xdd0000
mmap2(0xf73000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a3) = 0xf73000
mmap2(0xf76000, 10972, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xf76000
close(3)                                = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7746000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb7746900, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0xf73000, 8192, PROT_READ)     = 0
mprotect(0x8049000, 4096, PROT_READ)    = 0
mprotect(0x16e000, 4096, PROT_READ)     = 0
munmap(0xb7747000, 97472)               = 0
exit_group(0)                           = ?
linux12:/home/users/grad1459>

Как вы можете видеть, вызывается exit_group(), объясняя все!


Теперь, как вы правильно сказали, "SYS_exit equals __NR_exit"; например, он определен в mit.syscall.h:

#define SYS_exit __NR_exit

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

linux12:/home/users/grad1459>gcc seccomp.c -o seccomp && ./seccomp ; echo "${?}" 
0

PS

Вы можете, конечно, определить filter самостоятельно и использовать:

prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, filter);

как объяснено в собственной связи, чтобы позволить _exit() (или, строго говоря, exit_group(2)), но делать это только в том случае, если вам действительно нужно и знать, что вы делаете.

Ответ 2

Проблема возникает, потому что библиотека GNU C использует syscall, если она доступна, в Linux вместо exit для функции _exit() (см. sysdeps/unix/sysv/linux/_exit.c для проверки), и как описано в man 2 prctl, syscall exit_group не разрешается строгим фильтром seccomp.

Так как вызов функции _exit() происходит внутри библиотеки C, мы не можем вставлять его с нашей собственной версией (которая просто выполняет syscall exit). (Нормальная очистка процесса выполняется в другом месте, в Linux функция _exit() выполняет только окончательный syscall, который завершает процесс.)

Мы могли бы попросить разработчиков библиотеки 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, чтобы просмотреть сделанные вызовы syscalls и библиотеки;

strace ./example

Программа BPF strict_filter действительно тривиальна. Первый код операции загружает номер системного вызова в аккумулятор. Следующие пять кодов операций сравнивают его с приемлемым номером системного вызова, и если он найден, перейдите к окончательному коду операции, который позволяет использовать системный вызов. В противном случае код операции "второй-последний" убивает процесс.

Обратите внимание, что хотя документация относится к sigreturn как разрешенный syscall, фактическое имя syscall в Linux составляет rt_sigreturn. (sigreturn устарел в пользу rt_sigreturn лет назад.)

Кроме того, когда фильтр установлен, коды операций копируются в память ядра (см. kernel/seccomp.c в источниках ядра Linux), поэтому это никак не влияет на фильтр, если данные будут изменены позже. Другими словами, структура структур static const имеет нулевой эффект безопасности.

Я использовал static, так как нет необходимости, чтобы символы были видимыми вне этого блока компиляции (или в разделенном двоичном файле) и const, чтобы поместить данные в секцию данных только для чтения двоичного файла ELF.

Форма a BPF_JUMP(BPF_JMP | BPF_JEQ, nr, equals, differs) проста: аккумулятор (номер системы) сравнивается с nr. Если они равны, то следующие коды кода equals пропускаются. В противном случае следующие коды кода differs пропускаются.

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

Обратите внимание, что printf() не будет работать после установки фильтра seccomp, потому что внутри библиотеки C требуется выполнить syscall fstat (на стандартном выходе) и syscall brk для выделения некоторой памяти для буфер.