Почему загрузка фильтра seccomp влияет на набор разрешенных и эффективных возможностей?

Недавно я пишу программы с libcap и libseccomp и заметил проблему при их совместном использовании.

В следующем минимальном воспроизводимом примере я сначала установил для текущего процесса возможность только P(inheritable) = CAP_NET_RAW, а остальные наборы возможностей были очищены. Затем я инициализирую фильтр seccomp с действием SCMP_ACT_ALLOW (по умолчанию разрешены все системные вызовы), загружаю его и очищаю.

Наконец, эта программа выводит свои текущие возможности и выполняет capsh --print, чтобы показать свои возможности после выполнения execve().

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

#define CAPSH "/usr/sbin/capsh"

int main(void) {
    cap_value_t net_raw = CAP_NET_RAW;

    cap_t caps = cap_init();
    cap_set_flag(caps, CAP_INHERITABLE, 1, &net_raw, CAP_SET);
    if (cap_set_proc(caps)) {
        perror("cap_set_proc");
    }
    cap_free(caps);

    scmp_filter_ctx ctx;
    if ((ctx = seccomp_init(SCMP_ACT_ALLOW)) == NULL) {
        perror("seccomp_init");
    }

    int rc = 0;
    rc = seccomp_load(ctx); // comment this line later
    if (rc < 0)
        perror("seccomp_load");
    seccomp_release(ctx);

    ssize_t y = 0;
    printf("Process capabilities: %s\n", cap_to_text(cap_get_proc(), &y));
    
    char *argv[] = {
        CAPSH,
        "--print",
        NULL
    };
    execve(CAPSH, argv, NULL);
    return -1;

}

Скомпилируйте с -lcap и -lseccomp, запустите под пользователем root (UID=EUID=0) и получите следующее:

Process capabilities: = cap_net_raw+i
Current: = cap_net_raw+i
Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read
Securebits: 00/0x0/1'b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
uid=0(root)
gid=0(root)
groups=0(root)

Это указывает на то, что текущий процесс и выполняемые capsh все имеют наследуемый набор, а не только пустой. Однако, если я прокомментирую строку rc = seccomp_load(ctx);, все будет по-другому:

Process capabilities: = cap_net_raw+i
Current: = cap_net_raw+eip cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read+ep
Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read
Securebits: 00/0x0/1'b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
uid=0(root)
gid=0(root)
groups=0(root)

До execve() результат такой же, как и выше. Но после этого все остальные возможности возвращаются в разрешенные и эффективные наборы.

Я просмотрел capabilities(7) и обнаружил следующее: в руководстве:

Capabilities and execution of programs by root
       In order to mirror traditional UNIX semantics, the kernel performs
       special treatment of file capabilities when a process with UID 0
       (root) executes a program and when a set-user-ID-root program is exe‐
       cuted.

       After having performed any changes to the process effective ID that
       were triggered by the set-user-ID mode bit of the binary—e.g.,
       switching the effective user ID to 0 (root) because a set-user-ID-
       root program was executed—the kernel calculates the file capability
       sets as follows:

       1. If the real or effective user ID of the process is 0 (root), then
          the file inheritable and permitted sets are ignored; instead they
          are notionally considered to be all ones (i.e., all capabilities
          enabled).  (There is one exception to this behavior, described
          below in Set-user-ID-root programs that have file capabilities.)

       2. If the effective user ID of the process is 0 (root) or the file
          effective bit is in fact enabled, then the file effective bit is
          notionally defined to be one (enabled).

       These notional values for the file's capability sets are then used as
       described above to calculate the transformation of the process's
       capabilities during execve(2).

       Thus, when a process with nonzero UIDs execve(2)s a set-user-ID-root
       program that does not have capabilities attached, or when a process
       whose real and effective UIDs are zero execve(2)s a program, the cal‐
       culation of the process's new permitted capabilities simplifies to:

           P'(permitted)   = P(inheritable) | P(bounding)

           P'(effective)   = P'(permitted)

       Consequently, the process gains all capabilities in its permitted and
       effective capability sets, except those masked out by the capability
       bounding set.  (In the calculation of P'(permitted), the P'(ambient)
       term can be simplified away because it is by definition a proper sub‐
       set of P(inheritable).)

       The special treatments of user ID 0 (root) described in this subsec‐
       tion can be disabled using the securebits mechanism described below.

И вот что меня смущает: наследуемое множество не пусто, и по упрощенному правилу разрешенные и эффективные множества не должны быть пустыми. Однако загрузка фильтра seccomp, похоже, нарушает это правило.


person taoky    schedule 16.07.2020    source источник


Ответы (1)


Сам Seccomp этого не делает, но libseccomp делает.

Используя strace, вы можете видеть, что seccomp_load фактически выполняет три системных вызова:

prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)  = 0
seccomp(SECCOMP_SET_MODE_STRICT, 1, NULL) = -1 EINVAL (Invalid argument)
seccomp(SECCOMP_SET_MODE_FILTER, 0, {len=7, filter=0x5572a6213930}) = 0

Обратите внимание, как подозрительно выглядит первый.

Из документации ядра на no_new_privs:

При установленном no_new_privs execve обещает не предоставлять привилегию делать все, что невозможно было бы сделать без вызова execve.

И из capabilities(7), которое вы процитировали:

Если реальный или эффективный идентификатор пользователя процесса равен 0 (root), то наследуемый и разрешенный наборы файлов игнорируются; вместо этого они условно считаются всеми единицами (т. е. все возможности включены).

Ваш код создает пустой набор возможностей (cap_t caps = cap_init()) и добавляет только CAP_NET_RAW как наследуемый без разрешенных возможностей (как в = cap_net_raw+i). Затем, поскольку для этого потока установлено значение NO_NEW_PRIVS, при вызове execve разрешенный набор не восстанавливается до полного набора, как это обычно делается для корневого процесса (UID = 0 или EUID = 0). Это объясняет то, что вы видите от capsh --print до и после использования seccomp_load().

После установки флаг NO_NEW_PRIVS нельзя сбросить (prctl(2)), и есть причина seccomp_load() устанавливает его по умолчанию.

Чтобы запретить seccomp_load() устанавливать NO_NEW_PRIVS, добавьте следующий код перед загрузкой контекста:

seccomp_attr_set(ctx, SCMP_FLTATR_CTL_NNP, 0);

Дополнительные сведения см. в разделе seccomp_attr_set(3).

Однако вам, вероятно, следует поступить правильно, добавив желаемые возможности в разрешенный набор.

cap_set_flag(caps, CAP_PERMITTED, 1, &net_raw, CAP_SET);
person iBug    schedule 16.07.2020