Powered By Blogger

Monday, June 9, 2014

Пользовательские привилегии в Linux

В свете уже поднятой и рассмотренной здесь темы встаёт закономерный вопрос: всё это конечно хорошо и замечательно, но как можно наделить конкретными привилегиями не отдельный процесс, а пользователя, чтобы можно было создавать своих root'ов и при этом ограничить множество доступных пользователю привилегий? Ответ на этот вопрос - Pluggable Authentication Modules, также известные, как подключаемые модули аутентификации или попросту PAM, а именно, модуль pam_cap.so. Вместо того, чтобы ассоциировать разрешения с конкретными файлами, мы могли бы наделить конкретного пользователя нужной привилегией. При успешной авторизации шелл пользователя получит необходимое разрешение в множество наследуемых разрешений, которое будет унаследовано всеми его дочерними процессами, а это значит всеми процессами данного пользователя.

Сперва добавим модуль авторизации:

# cat /etc/pam.d/system-login
auth     required     pam_cap.so

Теперь определим, что и кому мы разрешаем:

# cat /etc/security/capability.conf
cap_kill,cap_setpcap luser
none *

Пользователь luser получает право на привилегии cap_kill - обход проверок безопасности при посылке сигналов другим процессам, не принадлежащим данному пользователю, и разрешение на манипуляции привилегиями. По умолчанию модуль pam_cap.so обычно не подключается, но если бы подключался, то обычный "бесправный" пользователь описывался бы строкой "none *".

Итак, luser получает свои разрешения cap_kill и cap_setpcap, а все остальные не будут иметь никаких разрешённых привилегий. Кстати, особенность состоит в том, что присваиваемые таким образом привилегии не являются активными. Они лишь наследуются и могут быть разрешены - далее мы увидим, как, а пользовательский процесс, требующий, допустим, привилегию cap_net_raw, сможет её активировать. Изначально же она будет присутствовать лишь во множестве наследуемых (inheritable) привилегий. Чем-то это напоминает Windows, где пользователи из группы администраторов могут иметь в токене безопасности привилегию отладки процесса (т.е., практически полного доступа к нему, включая операции над содержимым его памяти), что ещё не означает автоматически возможность каждого администраторского процесса воспользоваться ею, ведь её ещё надо активировать (что обычно происходит после принятия консента на системах с UAC - т.е. всей линейки Vista+). Однако здесь у нас всё чуть-чуть сложнее.

Проверим, что у нас получилось:

$ grep ^Cap /proc/$$/status
CapInh: 0000000000000120
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000001fffffffff

$ capsh --decode=0000000000000120
0x0000000000000120=cap_kill,cap_setpcap

Вроде бы всё работает. Следует заметить, что само по себе это ещё мало что значит, ведь разрешённые привилегии только лишь наследуются, но они даже не входят в множество доступных привилегий (CapPrm). Рассмотрим небольшой самодельный пример:

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/capability.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <unistd.h>

int cap_enable (cap_value_t cap)
{
        cap_t _caps;

        _caps = cap_get_proc ();

        if (_caps == NULL)
                return -1;

        if (cap_set_flag (_caps, CAP_EFFECTIVE, 1, &cap, CAP_SET) != 0)
                return -1;

        if (cap_set_proc (_caps) != 0)
                return -1;

        return 0;
}

void dump_caps (const char* img_name, const char* m)
{
        if (m)
                printf (m);

        printf (" file caps %s\n", cap_to_text (cap_get_file (img_name), NULL));
        printf (" process caps %s\n", cap_to_text (cap_get_proc (), NULL));

        return;
}

int main(int argc, char ** argv)
{
        dump_caps (argv [0], "before enabling...\n");
        sleep (30);
        cap_enable (CAP_KILL);
        dump_caps (argv [0], "after enabling...\n");
        sleep (60);
}
Его сборка и выполнение должны дать такие результаты:
$ ./cap_test       
before enabling...
 file caps (null)
 process caps = cap_kill,cap_setpcap+i
after enabling...
 file caps (null)
 process caps = cap_kill,cap_setpcap+i
То есть, привилегии недоступны. Чтобы понять, в чём дело, вспомним уже упоминавшиеся формулы, по которым вычисляются разрешения:
P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & cap_bset)
P'(effective) = F(effective) ? P'(permitted) : 0
P'(inheritable) = P(inheritable)
где F - разрешения, ассоциированные с файлом, а P - с процессом. Штрихом обозначены разрешения после exec(). Иными словами, для того, чтобы наша программа получила привилегию в свой набор доступных разрешений, необходимо либо чтобы для файла было установлено наследование сообветствующей привилегии, либо, чтобы она изначально входила в множество доступных привилегий файла. Т.е., либо cap_kill+i, либо cap_kill+p. Остановимся на первом варианте:
# setcap cap_kill+i ./cap_test
$ ./cap_test
before enabling...
 file caps = cap_kill+i
 process caps = cap_kill+ip cap_setpcap+i
after enabling...
 file caps = cap_kill+i
 process caps = cap_kill+eip cap_setpcap+i
 
[ в другом терминале между паузами ]

$ grep ^Cap /proc/`pgrep cap_test`/status
CapInh: 0000000000000120
CapPrm: 0000000000000020
CapEff: 0000000000000000
CapBnd: 0000001fffffffff
$ grep ^Cap /proc/`pgrep cap_test`/status
CapInh: 0000000000000120
CapPrm: 0000000000000020
CapEff: 0000000000000020
CapBnd: 0000001fffffffff
На первый взгляд ненамного удобнее. Более того, мы всё ещё зависим от разрешений, ассоциированных с файлами. Однако именно в этом и состоит гибкость. Во-первых, при распределении привилегий среди пользователей, мы наделяем их лишь некоторыми полномочиями. И именно пользователей. Кто-то сможет посылать сигналы чужим процессам, а кто-то нет. Во-вторых, с помощью дисковых разрешений, мы определяем, что пользователям можно запускать с данными привилегиями, а что нет, потому что позволять пользователю запускать вообще всё, что попало при данном повышенном уровне привилегий - это тоже не лучшее решение. Так что наделив пользователя небольшим кусочком власти, мы всё ещё контролируем, как он сможет этой властью воспользоваться. Однако приведённый пример имеет одну небольшую особенность. Программа в курсе относительно привилегий и сама изменяет свои разрешения на основе заданных значений. Для того, чтобы применять привилегии к любым программам, в том числе таким, которые могут ничего не знать о привилегиях, необходимо снова использовать форсированные разрешения, которые ассоциированы с файлами на диске. Например, так setcap cap_kill+ei ./cap_test (при логине пользователь получает унаследованное разрешение cap_kill, для файла cap_test cap_kill тоже помечено, как наследуемое разрешение - в соответствие с правилом, приведённом выше, при запуске cap_test разрешение cap_kill будет занесено в его множество доступных привилегий - P'(permitted) = P(inheritable) & F(inheritable); если для данного файла данное разрешение входит в множество эффективных разрешений, то оно заносится и в множество эффективных, или действительных, разрешений выполняемого процесса: P'(effective) = F(effective) ? P'(permitted) : 0). Допустим, что наш cap_test имеет такие разрешения:
$ getcap ./cap_test
./cap_test = cap_kill,cap_net_admin+ei
Однако, процесс не получит привилегию cap_net_admin в какое-либо из своих множеств, если эта привилегия не была предоставлена пользователю через PAM:
$ ./cap_test
before enabling...
 file caps = cap_kill,cap_net_admin+ei
 process caps = cap_kill+eip cap_setpcap+i
after enabling...
 file caps = cap_kill,cap_net_admin+ei
 process caps = cap_kill+eip cap_setpcap+i
Таким образом, это скорее механизм ограничения и разграничения привилегий, чем наделения ими :) На первый взгляд всё несколько запутанно, но на деле, привилегии не так страшны. Напоследок, если вдруг по какой-то причине вы не можете воспользоваться библиотекой libcap, активировать привилегию можно вот так, напрямую используя сисколлы (не совсем напрямую, ибо libc, но уже без посредничества libcap):
#include <sys/capability.h>

int main (int argc, char **argv)
{
        struct __user_cap_header_struct hdr;
        struct __user_cap_data_struct data;

        memset (&hdr, 0, sizeof(hdr));
        hdr.version = _LINUX_CAPABILITY_VERSION;

        if (capget (&hdr, &data) < 0)
                return 1;

        data.effective |= CAP_TO_MASK(CAP_KILL);
        data.effective |= CAP_TO_MASK(CAP_SETPCAP);

        if (capset (&hdr, &data) < 0)
                return 2;

        return 0;
}
Но разумеется, лучше пользоваться libcap, как более портабельным API.

No comments:

ПОСЕТИТЕЛИ

free counters