Powered By Blogger

Sunday, June 8, 2014

Разрешения процессов в Linux

Терминологический казус



То, о чём сейчас пойдёт речь, по-английски называется capabilities. Дословно "возможности", "способности". Семантически, это то, что называется привилегиями. Как ни странно, хоть тема и не нова, устоявшегося русского перевода этого термина я не видел. Точнее даже так, я никакого русского перевода не видел. И этот момент несколько смущает. Но ничего не поделаешь, придётся выкручиваться своими силами. Мне кажется, что наиболее адекватный перевод в данном случае - "разрешения", хотя по сути речь как раз о возможностях. Но раз обсуждаемый предмет - это то, что процессу разрешено делать, а не какие-то сугубо процесс-специфические возможности, им самим и создаваемые, я счёл более корректной приведённую версию перевода: разрешения процессов или привилегии. В дальнейшем я буду применять эти два понятия, как синонимы. Ещё один казуистический момент, так как с точки зрения ядра поток и процесс описываются единым дескриптором, дескриптором задачи (thread - это аппаратный контекст, являющийся частью дескриптора задачи), я не делаю разницы между потоком и процессом, так как здесь она не имеет особого значения. Важно лишь уточнить, что разрешения являются такой же частью дескриптора задачи, как и аппаратный контекст выполнения. И раз уж речь зашла о ядре (впрочем она и дальше пойдёт во многом именно о ядре), то сразу определимся, что речь идёт о версии 3.12. Система разделения власти суперпользователя на множество привилегий появилась в доисторической с точки зрения сегодняшнего дня ветке ядра 2.2. За эти годы многое изменилось самым кардинальным образом. Поэтому даже между ветками 2.6 и 3 будут расхождения в коде. Но общие принципы остаются в целом теми же.

Постановка задачи



В общем и в целом, всё просто. Кто пользуется таким замечательным инструментом, как wireshark, наверняка сталкивался уже с разрешениями процессов. Суть идеи такова: есть масса различных операций, результаты которых имеют критическое значение для всей системы, или же могут сказаться на отдельных пользователях этой самой системы, если оных много. Дабы свести к минимуму риск от потенциально деструктивных операций, система не даёт их выполнять обычным пользователям. А для административных задач есть всемогущий root и подразумевается, что человек, знающий пароль root'а на данной системе знает, что делает. Всё просто и вроде бы даже эффективно. Противопоставление "root vs обычный пользователь" простое, понятное, как чёрное и белое, и прозрачное. В идеале, пользователи имеют лишь минимально необходимый набор прав, они не могут навредить друг другу, так как доступ к данным защищён правами владения и доступа, не могут нанести вред системе и всё ещё могут добровольно делиться друг с другом доступом к данным. Идиллия. Но не всё так просто. Нынче Unix-подобные ОС уже совсем не обязательно работают лишь на системах, обслуживающих уйму пользователей. И тем не менее, вхождение в массы не изменило одного обстоятельства. Сидеть под root'ом всё так же моветон и категорически не рекомендуется без настоятельной нужды. А что делать, если пользователь на своей системе хочет изменить время? И пусть даже он единственный живой пользователь системы и никому навредить не сможет, кроме самого себя, операция изменения системного времени всё ещё требует привилегий. Что же делать? Тем более, если наш пользователь, скажем, путешествует туда-сюда по всему земному шару и меняет время чуть ли не каждый день. Прикажете ему сидеть под root'ом? Есть sudo, а вместе с этим и проблема. Когда наш пользователь сделает sudo date, процесс сможет не только изменять системное время, но делать всё то, что может сделать root. А что если злобные хацкеры подменили образ date на диске, пока наш пользователь плевал вниз с Эйфелевой башни, например? Вот тут-то и появилось осознание, что чего-то не хватает. Этот пробел призваны восполнить разрешения процессов в Linux. Если кратко, то существует целый класс операций, требующих привилегий. Однако специализированной программе, устанавливающей системное время, требуется право лишь на одну такую операцию. Чтобы избежать вероятности злоупотребления привилегиями, которые получает процесс с EUID=0, мы наделим нашу программу разрешением ровно на одну операцию. Всё остальное будет запрещено и наши злобные горе-хакеры останутся ни с чем. И хотя не стоит воспринимать приведённый выше пример всерьёз, проблема гранулярности традиционной модели безопасности Unix назрела давно. Более удачный и жизненный пример всё тот же wireshark - программа с графическим интерфейсом пользователя, которая позволяет следить за сетевым трафиком. Для того, чтобы успешно справляться со своей задачей, wireshark должен иметь право на создание "сырых" сокетов (raw sockets). При этом, GUI такие полномочия совершенно ни к чему и напротив, чем меньше кода выполняется с EUID=0, тем лучше. Ещё один пример из этой же области - ping. Раньше это решалось либо установкой suid-бита либо с помощью sudo. Решения, прямо скажем, далёкие от идеала и гибкости. Проблемы, связанные с подходами вроде sudo или suid-бита, были осознаны многими и в каждом потомке Unix они решались по-своему. В Solaris, к примеру, есть RBAC - разделение привилегий на основе ролей. Хотя это не то же самое, что разрешения процессов, но с RBAC тесно связана идея привилегий. В Linux же у нас есть process capabilities. И появились они, кстати, уже довольно давно, но по разным причинам широкого применения не нашли. Кажется, ситуация начинает медленно меняться, так что разберёмся с этим механизмом получше.

Кровавые детали

Контроль привилегий осуществляет ядро. В ядре Linux привилегии являются ничем иным, как битовыми картами. Чтобы убедиться в этом, проследите за корнями соответствующих структур в ядре Linux (интересующие нас поля выделены жирным курсивом, дескриптор задачи - task_struct, здесь сильно сокращён по понятным причинам):

Начнём с include/linux/sched.h:

struct task_struct {
        volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
        void *stack;
        atomic_t usage;
        unsigned int flags;     /* per process flags, defined below */
        unsigned int ptrace;

 ...

/* process credentials */
        const struct cred __rcu *real_cred; /* objective and real subjective task
                                         * credentials (COW) */
        const struct cred __rcu *cred;  /* effective (overridable) subjective task
                                         * credentials (COW) */
        char comm[TASK_COMM_LEN]; /* executable name excluding path
                                     - access with [gs]et_task_comm (which lock
                                       it with task_lock())
                                     - initialized normally by setup_new_exec */
 ...
};

Далее include/linux/cred.h:

struct cred {
        atomic_t        usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
        atomic_t        subscribers;    /* number of processes subscribed */
        void            *put_addr;
        unsigned        magic;
#define CRED_MAGIC      0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
        kuid_t          uid;            /* real UID of the task */
        kgid_t          gid;            /* real GID of the task */
        kuid_t          suid;           /* saved UID of the task */
        kgid_t          sgid;           /* saved GID of the task */
        kuid_t          euid;           /* effective UID of the task */
        kgid_t          egid;           /* effective GID of the task */
        kuid_t          fsuid;          /* UID for VFS ops */
        kgid_t          fsgid;          /* GID for VFS ops */
        unsigned        securebits;     /* SUID-less security management */
        kernel_cap_t    cap_inheritable; /* caps our children can inherit */
        kernel_cap_t    cap_permitted;  /* caps we're permitted */
        kernel_cap_t    cap_effective;  /* caps we can actually use */
        kernel_cap_t    cap_bset;       /* capability bounding set */
#ifdef CONFIG_KEYS
        unsigned char   jit_keyring;    /* default keyring to attach requested
                                         * keys to */
        struct key __rcu *session_keyring; /* keyring inherited over fork */
        struct key      *process_keyring; /* keyring private to this process */
        struct key      *thread_keyring; /* keyring private to this thread */
        struct key      *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
        void            *security;      /* subjective LSM security */
#endif
        struct user_struct *user;       /* real user ID subscription */
        struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
        struct group_info *group_info;  /* supplementary groups for euid/fsgid */
        struct rcu_head rcu;            /* RCU deletion hook */
};

И наконец include/linux/capability.h:

 typedef struct kernel_cap_struct {
         __u32 cap[_KERNEL_CAPABILITY_U32S];
 } kernel_cap_t;

Т.е., в текущей версии реализации одно множество разрешений представлено двумя 32-битными словами или 64 битами (строка "#define _LINUX_CAPABILITY_U32S_3 2" в файле include/uapi/linux/capability.h). Всего таких множеств 3 (см. выше определение структуры struct cred): разрешённые (permitted), эффективные (effective), наследуемые (inheritable). В общем-то названия этих множеств и краткие комментарии в коде говорят сами за себя. Множество разрешённых привилегий - это те разрешения, которые наш процесс может активировать в принципе, но которые не обязательно действуют в данный момент времени. Если процесс исключает привилегию из множества доступных (privilege drop), то он больше не может ею воспользоваться, в том числе, он не сможет вернуть себе право на данную привилегию (reacquire privilege) Множество эффективных привилегий - это те элементы множества разрешённых привилегий, которые были активированы (запрошены) процессом и действуют в данный момент. Именно это поле используется ядром для проверки права на данную операцию. И, наконец, множество наследуемых привилегий - это те разрешения, с которыми может работать дочерний процесс, порождённый нашим процессом, т.е., множество привилегий, которые присваиваются процессу, порождённому с помощью exec*() (все они в конечном счёте сходятся на execve()). Процесс, созданный в результате fork(), получает полные копии множеств привилегий родительского процесса. Эти три множества привилегий свойственны и выполняющимся процессам, и файлам (о файлах и привилегиях чуть ниже). Ещё одно множество - ограничивающее (bounding set). Если с 3 другими множествами всё ясно, то на этом стоит остановиться чуть подробнее. Во-первых, это множество в отличие от уже рассмотренных свойственно только процессам, но не файлам. Во-вторых, это ещё один ограничительный механизм, но в отличие от множества доступных процессу (permitted) привилегий, устанавливаемого при запуске исполняемого образа или при посредничестве другого процесса, это множество устанавливается для процессов-потомков, порождённых вызовом execve(). Происходит это следующим образом: во время выполнения exec*() к привилегиям из ограничивающего множества (bounding set) процесса, вызывающего exec*(), и множества доступных привилегий (permitted), ассоциированного с файлом исполняемого образа на диске, применяется логическое И, а результат заносится в множество разрешённых привилегий дочернего процесса. Таким образом, потомку будут доступны лишь те операции, которые есть и в ограничивающем множестве, и во множестве разрешённых привилегий, прочитанном с диска. Начиная с версии Linux 2.6.25 процесс не может добавить привилегию в множество наследуемых привилегий (inheritable), если она не входит в ограничивающее множество, даже не смотря на то, что данная привилегия доступна вызывающему потоку (т.е., она может быть в его permitted-множестве). Ограничивающее множество является маской разрешённых привилегий, определённых для файла, но не для множества уже унаследованных привилегий. Таким образом, это механизм контроля наследования разрешений, а не их использования уже существующим процессом. Следует отметить важный технический момент: операция ограничения привилегий в соответствии с маской ограничивающего множества не выполняется непосредственно над атрибутами файла, а над разрешениями свежесозданного процесса. Так что хоть ограничивающее множество действует в первую очередь на те разрешения, которые ассоциированы с файлом, вычисления происходят на множествах разрешений процесса, не файла. Создаваемый процесс получит в точности те разрешения, которые свойственны файлу исполняемого образа, но позже они будут откорректированы в соответствии с маской родительского процесса. Звучит запутанно, но надеюсь, всё станет понятнее далее, когда мы дойдём до файловых атрибутов. В зависимости от версии ядра ограничивающее множество - это либо системный атрибут, либо свойство конкретного процесса (http://lwn.net/Articles/251666/). До версии 2.6.25 маска задаётся через файл /proc/sys/kernel/cap-bound в виде целого со знаком в десятичной системе. Эту маску может установить только init. Процессы c EUID=0, имеющие разрешение CAP_SYS_MODULE, могут удалять отдельные привилегии из этой маски. Обычно на таких системах маска не включает разрешение CAP_SETPCAP. Чтобы снять это ограничение, достаточно изменить определение CAP_INIT_EFF_SET в файле include/linux/capability.h и пересобрать ядро. Начиная с версии 2.6.25 ограничивающее множество больше не является глобальным общесистемным атрибутом. Оно наследуется при вызове fork() и exec*(). Поток может удалять привилегии из своего ограничивающего множества. Детали см. в capabilities(2)

Итак, отдельно взятая привилегия - это ни много, ни мало бит в битовой карте, представляющей множество разрешённых, эффективных или наследуемых привилегий. Приведём наконец некоторые примеры конкретных привилегий:

CAP_SETPCAP Манипуляции привилегиями и, соответственно, разрешение использовать вызов capset() в ядрах, где поддержка файловых разрешений не включена.
Если же файловые разрешения поддерживаются, эта привилегия позволяет добавлять разрешения в ограничивающее множество потока, а также удалять их оттуда (prctl(2)); манипуляции с флагами безопасности (http://lwn.net/Articles/368600/)
CAP_NET_ADMIN Различные операции, относящиеся к сетевому администрированию, как то: конфигурирование сетевых интерфейсов, администрирование файрволла, изменение таблицы маршрутизации, включение "неразборчивого" режима на интерфейсе и тому подобное
CAP_CHOWN Манипуляции с идентификаторами пользователя/группы объекта файловой системы (chown(2))
CAP_DAC_OVERRIDE Обход проверок безопасности при операциях ввода-вывода
CAP_KILL Обход проверок безопасности при посылке сигналов (kill(2))
CAP_SETFCAP (начиная с Linux 2.6.24) Манипуляции с файловыми разрешениями
CAP_SYSLOG (начиная с Linux 2.6.37) Привилегированные операции с системным логом; отображение адресов ядра в /proc, если атрибут /proc/sys/kernel/kptr_restrict установлен в 1
CAP_SYS_TIME Изменение системного времени
CAP_SYS_BOOT Использование вызовов reboot() и kexec_load() - перезагрузка системы и "горячая" загрузка ядра, соответственно (reboot(2) kexec_load(2))
CAP_SYS_ADMIN Широкий спектр административных операций, как то: управление дисковыми квотами, активация и деактивация устройств своппинга и много чего ещё
CAP_SETGID
CAP_SETUID
Манипуляции с идентификаторами пользователя/группы процесса - вызовы setuid(2), setreuid(2), setresuid(2), setfsuid(2)

Данный список далеко не полный, т.к. я не ставил целью переписывание соответствующей man-страницы, и лишь демонстрирует некоторые более или менее типичные привилегии. Все доступные для данного ядра привилегии можно посмотреть в заголовочном файле include/uapi/linux/capability.h или в man-странице capabilities(2).

О флагах безопасности (securebits). Это флаги, управляющие наследованием и использованием привилегий. Этими флагами манипулируют сами потоки. Для этого, впрочем, им нужно иметь привилегию CAP_SETPCAP, как уже было упомянуто.

SECBIT_KEEP_CAPS Поток, имеющий UID 0, сохраняет все разрешения при смене идентификатора пользователя (не root)

По умолчанию этот флаг очищен и даже если он установлен потоком, он принудительно очищается при вызове exec*() с целью предотвращения утечки пивилегий
SECBIT_NO_SETUID_FIXUP Множества разрешеий не будут корректироваться ядром при смене UID между 0 и любым другим значением
SECBIT_NOROOT Программа с suid-битом не получает автоматически все привилегии, которые имеет root

Управление флагами возможно с помощью prctl(2) и "операторов" PR_SET_SECUREBITS / PR_GET_SECUREBITS.

Так в общих чертах выглядит объект нашего интереса. Но встаёт вопрос, а кто и как назначает привилегии конкретным процессам? Когда поддержки файловых разрешений ещё не было, единственным способом использования привилегий был некий вспомогательный процесс, который мог бы раздавать их другим процессам. Этакий сервер безопасности. Либо сама программа, запущенная изначально с правами суперпользователя, могла добровольно отказаться от ненужных ей привилегий. Оба способа явно довольно вычурны. Вероятно это в немалой степени послужило фактором, сдерживающим внедрение использования привилегий.

Файловые разрешения

Вот мы и подошли, пожалуй, к самому интересному. Действительно, без файловых разрешений реализация привилегий в Linux так и осталась бы ущербной. Говоря выше о множествах привилегий, мы узнали, что множества доступных (permitted), наследуемых (inheritable) и эффективных (effective) привилегий свойственны как файлам, так и процессам. Что это значит? Ровно то, что значит. Привилегии могут быть ассоциированы не только с выполняющимся процессом, но и с файлом, в котором хранится исполняемый образ. Какой-то особой поддержки привилегий со стороны файловой системы не требуется. Всё делом в том, что разрешения хранятся в расширенных атрибутах файлов, а это значит, что любая файловая система, которая поддерживает расширенные атрибуты (а это, в общем-то, все современные файловые системы, которые широко используются в Linux - ext, reiserfs, xfs, например), автоматически поддерживает и файловые разрешения. Linux разделяет расширенные атрибуты файлов по пространствам имён. В частности, например, определены такие пространства, как "user" - для пользовательских атрибутов, "security" - для атрибутов безопасности. Нас интересует именно последнее. Разрешения хранятся в атрибуте capability. Имя атрибута определено в заголовке include/uapi/linux/xattr.h таким образом:

#define XATTR_SECURITY_PREFIX   "security."
...
#define XATTR_CAPS_SUFFIX "capability"
#define XATTR_NAME_CAPS XATTR_SECURITY_PREFIX XATTR_CAPS_SUFFIX

Проведём небольшой эксперимент:

$ attr -l /usr/bin/dumpcap
Attribute "capability" has a 20 byte value for /usr/bin/dumpcap

# getcap /usr/bin/dumpcap
/usr/bin/dumpcap = cap_dac_read_search,cap_net_admin,cap_net_raw+ep

Что и требовалось продемонстрировать. dumpcap является частью пакета wireshark. Именно этот исполняемый файл "хватает" пакеты с сетевого интерфейса. Для того, чтобы мы могли использовать wireshark из-под обычного пользовательского аккаунта, этому файлу присвоены соответствующие разрешения: операции сетевого администрирования, право на создание символьных ("сырых", raw) сокетов во множествах доступных и эффективных привилегий. Каким образом всё это облечено в код ядра? Вся чёрная магия скрыта в файле security/commoncap.c

/*
 * Extract the on-exec-apply capability sets for an executable file.
 */
int get_vfs_caps_from_disk(const struct dentry *dentry, struct cpu_vfs_cap_data *cpu_caps)
{
        struct inode *inode = dentry->d_inode;
        __u32 magic_etc;
        unsigned tocopy, i;
        int size;
        struct vfs_cap_data caps;

        memset(cpu_caps, 0, sizeof(struct cpu_vfs_cap_data));

        if (!inode || !inode->i_op->getxattr)
                return -ENODATA;

        size = inode->i_op->getxattr((struct dentry *)dentry, XATTR_NAME_CAPS, &caps,
                                   XATTR_CAPS_SZ);
        if (size == -ENODATA || size == -EOPNOTSUPP)
                /* no data, that's ok */
                return -ENODATA;
        if (size < 0)
                return size;

        if (size < sizeof(magic_etc))
                return -EINVAL;

        cpu_caps->magic_etc = magic_etc = le32_to_cpu(caps.magic_etc);

        switch (magic_etc & VFS_CAP_REVISION_MASK) {
        case VFS_CAP_REVISION_1:
                if (size != XATTR_CAPS_SZ_1)
                        return -EINVAL;
                tocopy = VFS_CAP_U32_1;
                break;
        case VFS_CAP_REVISION_2:
                if (size != XATTR_CAPS_SZ_2)
                        return -EINVAL;
                tocopy = VFS_CAP_U32_2;
                break;
        default:
                return -EINVAL;
        }

        CAP_FOR_EACH_U32(i) {
                if (i >= tocopy)
                        break;
                cpu_caps->permitted.cap[i] = le32_to_cpu(caps.data[i].permitted);
                cpu_caps->inheritable.cap[i] = le32_to_cpu(caps.data[i].inheritable);
        }

        return 0;
}

/*
 * Attempt to get the on-exec apply capability sets for an executable file from
 * its xattrs and, if present, apply them to the proposed credentials being
 * constructed by execve().
 */
static int get_file_caps(struct linux_binprm *bprm, bool *effective, bool *has_cap)
{
        struct dentry *dentry;
        int rc = 0;
        struct cpu_vfs_cap_data vcaps;

        bprm_clear_caps(bprm);

        if (!file_caps_enabled)
                return 0;

        if (bprm->file->f_path.mnt->mnt_flags & MNT_NOSUID)
                return 0;

        dentry = dget(bprm->file->f_dentry);

        rc = get_vfs_caps_from_disk(dentry, &vcaps);
        if (rc < 0) {
                if (rc == -EINVAL)
                        printk(KERN_NOTICE "%s: get_vfs_caps_from_disk returned %d for %s\n",
                                __func__, rc, bprm->filename);
                else if (rc == -ENODATA)
                        rc = 0;
                goto out;
        }

        rc = bprm_caps_from_vfs_caps(&vcaps, bprm, effective, has_cap);
        if (rc == -EINVAL)
                printk(KERN_NOTICE "%s: cap_from_disk returned %d for %s\n",
                       __func__, rc, bprm->filename);

out:
        dput(dentry);
        if (rc)
                bprm_clear_caps(bprm);

        return rc;
}

/**
 * cap_bprm_set_creds - Set up the proposed credentials for execve().
 * @bprm: The execution parameters, including the proposed creds
 *
 * Set up the proposed credentials for a new execution context being
 * constructed by execve().  The proposed creds in @bprm->cred is altered,
 * which won't take effect immediately.  Returns 0 if successful, -ve on error.
 */
int cap_bprm_set_creds(struct linux_binprm *bprm)
{
        const struct cred *old = current_cred();
        struct cred *new = bprm->cred;
        bool effective, has_cap = false;
        int ret;
        kuid_t root_uid;

        effective = false;
        ret = get_file_caps(bprm, &effective, &has_cap);
        if (ret < 0)
                return ret;

        root_uid = make_kuid(new->user_ns, 0);

        if (!issecure(SECURE_NOROOT)) {
                /*
                 * If the legacy file capability is set, then don't set privs
                 * for a setuid root binary run by a non-root user.  Do set it
                 * for a root user just to cause least surprise to an admin.
                 */
                if (has_cap && !uid_eq(new->uid, root_uid) && uid_eq(new->euid, root_uid)) {
                        warn_setuid_and_fcaps_mixed(bprm->filename);
                        goto skip;
                }
                /*
                 * To support inheritance of root-permissions and suid-root
                 * executables under compatibility mode, we override the
                 * capability sets for the file.
                 *
                 * If only the real uid is 0, we do not set the effective bit.
                 */
                if (uid_eq(new->euid, root_uid) || uid_eq(new->uid, root_uid)) {
                        /* pP' = (cap_bset & ~0) | (pI & ~0) */
                        new->cap_permitted = cap_combine(old->cap_bset,
                                                         old->cap_inheritable);
                }
                if (uid_eq(new->euid, root_uid))
                        effective = true;
        }
skip:

        /* if we have fs caps, clear dangerous personality flags */
        if (!cap_issubset(new->cap_permitted, old->cap_permitted))
                bprm->per_clear |= PER_CLEAR_ON_SETID;


        /* Don't let someone trace a set[ug]id/setpcap binary with the revised
         * credentials unless they have the appropriate permit.
         *
         * In addition, if NO_NEW_PRIVS, then ensure we get no new privs.
         */
        if ((!uid_eq(new->euid, old->uid) ||
             !gid_eq(new->egid, old->gid) ||
             !cap_issubset(new->cap_permitted, old->cap_permitted)) &&
            bprm->unsafe & ~LSM_UNSAFE_PTRACE_CAP) {
                /* downgrade; they get no more than they had, and maybe less */
                if (!capable(CAP_SETUID) ||
                    (bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS)) {
                        new->euid = new->uid;
                        new->egid = new->gid;
                }
                new->cap_permitted = cap_intersect(new->cap_permitted,
                                                   old->cap_permitted);
        }

        new->suid = new->fsuid = new->euid;
        new->sgid = new->fsgid = new->egid;

        if (effective)
                new->cap_effective = new->cap_permitted;
        else
                cap_clear(new->cap_effective);
        bprm->cap_effective = effective;

        /*
         * Audit candidate if current->cap_effective is set
         *
         * We do not bother to audit if 3 things are true:
         *   1) cap_effective has all caps
         *   2) we are root
         *   3) root is supposed to have all caps (SECURE_NOROOT)
         * Since this is just a normal root execing a process.
         *
         * Number 1 above might fail if you don't have a full bset, but I think
         * that is interesting information to audit.
         */
        if (!cap_isclear(new->cap_effective)) {
                if (!cap_issubset(CAP_FULL_SET, new->cap_effective) ||
                    !uid_eq(new->euid, root_uid) || !uid_eq(new->uid, root_uid) ||
                    issecure(SECURE_NOROOT)) {
                        ret = audit_log_bprm_fcaps(bprm, new, old);
                        if (ret < 0)
                                return ret;
                }
        }

        new->securebits &= ~issecure_mask(SECURE_KEEP_CAPS);
        return 0;
}

Ключевой здесь является функция get_vfs_caps_from_disk(const struct dentry *dentry, struct cpu_vfs_cap_data *cpu_caps), принимающая в качестве аргументов указатель на элемент каталога, связанный с файлом исполняемого образа и указатель на структуру, которая принимает прочитанные из атрибута разрешения. struct cpu_vfs_cap_data - то же самое, что struct vfs_cap_data из include/uapi/linux/capability.h, но при этом числа представлены в специфичном для процессора порядке следования байтов (endianness), в то время, как для struct vfs_cap_data представление little endian. Атрибуты непосредственно считываются с диска при помощи метода getxattr() объекта, представляющего индексный узел:

size = inode->i_op->getxattr((struct dentry *)dentry, XATTR_NAME_CAPS, &caps, XATTR_CAPS_SZ);
Если таковой метод реализован для индексного узла в данной файловой системе, конечно же. Эта функция непосредственно читает данные с диска и проверяет их валидность. Прежде всего, ревизия реализации разрешений должна соответствовать размеру дисковых данных для этой реализации. В свою очередь get_vfs_caps_from_disk() используется функцией get_file_caps(), которая вычисляет множество доступных процессу разрешений основываясь на данных, прочитанных с диска из расширенных атрибутов файла и том, что мы наследуем от родителя через execve(). Собственно последняя часть задачи ложится на функцию bprm_caps_from_vfs_caps():

/*
 * Calculate the new process capability sets from the capability sets attached
 * to a file.
 */
static inline int bprm_caps_from_vfs_caps(struct cpu_vfs_cap_data *caps,
                                          struct linux_binprm *bprm,
                                          bool *effective,
                                          bool *has_cap)
{
        struct cred *new = bprm->cred;          /* Creds we inherit from the parent and simultaneously container for new creds */
        unsigned i;
        int ret = 0;

        if (caps->magic_etc & VFS_CAP_FLAGS_EFFECTIVE)
                *effective = true;

        if (caps->magic_etc & VFS_CAP_REVISION_MASK)
                *has_cap = true;

        CAP_FOR_EACH_U32(i) {
                __u32 permitted = caps->permitted.cap[i];          /* Those we have read from disk */
                __u32 inheritable = caps->inheritable.cap[i];

                /*
                 * pP' = (X & fP) | (pI & fI)
                 */
                new->cap_permitted.cap[i] =
                        (new->cap_bset.cap[i] & permitted) |
                        (new->cap_inheritable.cap[i] & inheritable);

                if (permitted & ~new->cap_permitted.cap[i])
                        /* insufficient to execute correctly */
                        ret = -EPERM;
        }

        /*
         * For legacy apps, with no internal support for recognizing they
         * do not have enough capabilities, we return an error if they are
         * missing some "forced" (aka file-permitted) capabilities.
         */
        return *effective ? ret : 0;
}}

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

P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & cap_bset)
P'(effective) = F(effective) ? P'(permitted) : 0
P'(inheritable) = P(inheritable)    [не изменяются]
Здесь F - файловые разрешения, P - привилегии родительского процесса, P' - результирующие привилегии. Ещё раз посмотрите на код функции bprm_caps_from_vfs_caps() и без труда поймёте, что к чему. Через один из своих аргументов, effective, bprm_caps_from_vfs_caps() также возвращает флаг, уведомляющий о наличии среди файловых разрешений таких, которые должны быть активированы - эффективных файловых разрешений. Вопреки ожиданию наличие этих разрешений определяется не их фактическим присутствием, а специальным флагом, VFS_CAP_FLAGS_EFFECTIVE. Старые разрешения передаются через блок параметров исполняемого файла - переменную типа struct linux_binprm. Поднимаясь снизу вверх, мы обнаружим, что get_file_caps() - это ещё отнюдь не верхушка айсберга. Эта функция в свою очередь вызывается из cap_bprm_set_creds(). Последняя не объявлена, как статичная, что указывает на простой факт - она используется где-то за пределами данной единицы компиляции. Проявив такие чудеса дедукции и порывшись вокруг, обнаруживаем, что на cap_bprm_set_creds() "ссылается" static int selinux_bprm_set_creds(struct linux_binprm *bprm) из security/selinux/hooks.c, напрямую последняя не вызывается. Что же касается cap_bprm_set_creds(), то именно она и является основным хабом, где происходит вся обработка, связанная с разрешениями. Говоря об этой функции, как о хабе, я имею в виду, что её вызывает execve() с проинициализированным блоком параметров исполняемого образа, отсюда же тракт управления идёт к считыванию данных с диска, и частичной обработке полученных данных и здесь же потом происходит окончательная обработка множеств разрешений. Результаты своей работы эта функция предоставляет в распоряжение execve() опять же через блок параметров файла. Переход к этому коду происходит через указатель на операции безопасности (security ops).

Чуть ранее, прежде чем управление будет передано bprm_caps_from_vfs_caps(), в локальной переменной new, куда bprm_caps_from_vfs_caps() запишет вычисленные разрешения, кто-то должен был уже что-то записать. Это понятно уже из самого кода bprm_caps_from_vfs_caps(), ибо в самом деле, довольно странный способ вычисления разрешений, применяя их к чему-то неопределённому. Разрешения передаются между разными участками кода через параметры исполняемого образа, struct linux_binprm. Эти параметры заполняются опять же, в do_execve_common().

Так как в процесс загрузки и запуска образа на исполнение участвует очень много кода, лишь в самых общих чертах окинем взглядом всю цепь событий. На этот раз пойдём в обратном направлении - сверху вниз. Всё начинается с execve():

/*
 * sys_execve() executes a new program.
 */
static int do_execve_common(const char *filename,
                                struct user_arg_ptr argv,
                                struct user_arg_ptr envp)
{
...
        retval = prepare_bprm_creds(bprm);
        if (retval)
                goto out_free;
...
        retval = prepare_binprm(bprm);
...
Инициализация множеств разрешений нового процесса:
/*
 * Prepare credentials and lock ->cred_guard_mutex.
 * install_exec_creds() commits the new creds and drops the lock.
 * Or, if exec fails before, free_bprm() should release ->cred and
 * and unlock.
 */
int prepare_bprm_creds(struct linux_binprm *bprm)
{
        if (mutex_lock_interruptible(&current->signal->cred_guard_mutex))
                return -ERESTARTNOINTR;

        bprm->cred = prepare_exec_creds();
        if (likely(bprm->cred))
                return 0;

        mutex_unlock(&current->signal->cred_guard_mutex);
        return -ENOMEM;
}
/*
 * Prepare credentials for current to perform an execve()
 * - The caller must hold ->cred_guard_mutex
 */
struct cred *prepare_exec_creds(void)
{
        struct cred *new;

        new = prepare_creds();
        if (!new)
                return new;

#ifdef CONFIG_KEYS
        /* newly exec'd tasks don't get a thread keyring */
        key_put(new->thread_keyring);
        new->thread_keyring = NULL;

        /* inherit the session keyring; new process keyring */
        key_put(new->process_keyring);
        new->process_keyring = NULL;
#endif

        return new;
}
/**
 * prepare_creds - Prepare a new set of credentials for modification
 *
 * Prepare a new set of task credentials for modification.  A task's creds
 * shouldn't generally be modified directly, therefore this function is used to
 * prepare a new copy, which the caller then modifies and then commits by
 * calling commit_creds().
 *
 * Preparation involves making a copy of the objective creds for modification.
 *
 * Returns a pointer to the new creds-to-be if successful, NULL otherwise.
 *
 * Call commit_creds() or abort_creds() to clean up.
 */
struct cred *prepare_creds(void)
{
        struct task_struct *task = current;
        const struct cred *old;
        struct cred *new;

        validate_process_creds();

        new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
        if (!new)
                return NULL;

        kdebug("prepare_creds() alloc %p", new);

        old = task->cred;
        memcpy(new, old, sizeof(struct cred));

        atomic_set(&new->usage, 1);
        set_cred_subscribers(new, 0);
        get_group_info(new->group_info);
        get_uid(new->user);
        get_user_ns(new->user_ns);

#ifdef CONFIG_KEYS
        key_get(new->session_keyring);
        key_get(new->process_keyring);
        key_get(new->thread_keyring);
        key_get(new->request_key_auth);
#endif

#ifdef CONFIG_SECURITY
        new->security = NULL;
#endif

        if (security_prepare_creds(new, old, GFP_KERNEL) < 0)
                goto error;
        validate_creds(new);
        return new;

error:
        abort_creds(new);
        return NULL;
}
EXPORT_SYMBOL(prepare_creds);
Данные просто копируются из дескриптора текущей задачи, т.е. вызвавшей exec(), в блок параметров образа, который будет запущен. Теперь весь остальной код может сделать свою работу.

Далее, считывание разрешений с диска и вычисление новых разрешений, там же в fs/exec.c

/* 
 * Fill the binprm structure from the inode. 
 * Check permissions, then read the first 128 (BINPRM_BUF_SIZE) bytes
 *
 * This may be called multiple times for binary chains (scripts for example).
 */
int prepare_binprm(struct linux_binprm *bprm)
{
        umode_t mode;
        struct inode * inode = file_inode(bprm->file);
        int retval;

        mode = inode->i_mode;
        if (bprm->file->f_op == NULL)
                return -EACCES;

        /* clear any previous set[ug]id data from a previous binary */
        bprm->cred->euid = current_euid();
        bprm->cred->egid = current_egid();

        if (!(bprm->file->f_path.mnt->mnt_flags & MNT_NOSUID) &&
            !current->no_new_privs &&
            kuid_has_mapping(bprm->cred->user_ns, inode->i_uid) &&
            kgid_has_mapping(bprm->cred->user_ns, inode->i_gid)) {
                /* Set-uid? */
                if (mode & S_ISUID) {
                        bprm->per_clear |= PER_CLEAR_ON_SETID;
                        bprm->cred->euid = inode->i_uid;
                }

                /* Set-gid? */
                /*
                 * If setgid is set but no group execute bit then this
                 * is a candidate for mandatory locking, not a setgid
                 * executable.
                 */
                if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) {
                        bprm->per_clear |= PER_CLEAR_ON_SETID;
                        bprm->cred->egid = inode->i_gid;
                }
        }

        /* fill in binprm security blob */
        retval = security_bprm_set_creds(bprm);
        if (retval)
                return retval;
        bprm->cred_prepared = 1;

        memset(bprm->buf, 0, BINPRM_BUF_SIZE);
        return kernel_read(bprm->file, 0, bprm->buf, BINPRM_BUF_SIZE);
}
Далее, в security/security.c
int security_bprm_set_creds(struct linux_binprm *bprm)
{
        return security_ops->bprm_set_creds(bprm);
}
Структура, содержащая указатели на операции безопасности, инициализируется в security/selinux/hooks.c. Здесь без труда находим наш selinux_bprm_set_creds()
static struct security_operations selinux_ops = {
        .name =                         "selinux",
...
        .bprm_set_creds =               selinux_bprm_set_creds,
...
};
Резюмируя, получим такой вот флоучарт:

Надеюсь, это головокружительное путешествие помогло понять, каким образом реализованы привилегии Linux и что делает их привлекательными, надеюсь, вскользь коснувшись кода, вы поймёте, какую роль в реализации поддержки привилегий играет VFS. К сожалению, написано уже и так больше, чем я предполагал изначально, а во все детали заглянуть всё равно не представляется возможным в заметке сколь-нибудь вменяемого объёма. Поэтому напоследок только взглянем краем глаза, как на практике реализуется контроль привилегий и на этом остановимся. Рассмотрим самый простой пример, с которого мы и начали, с настройкой системных часов. Наша программа вызывает stime(). Так как представить себе использование этого системного вызова совсем не сложно, текст самой программы не приведён:

kernel/time.c
 /*
  * sys_stime() can be implemented in user-level using
  * sys_settimeofday().  Is this for backwards compatibility?  If so,
  * why not move it into the appropriate arch directory (for those
  * architectures that need it).
  */
 
 SYSCALL_DEFINE1(stime, time_t __user *, tptr)
 {
         struct timespec tv;
         int err;
 
         if (get_user(tv.tv_sec, tptr))
                 return -EFAULT;
 
         tv.tv_nsec = 0;
 
         err = security_settime(&tv, NULL);
         if (err)
                 return err;
 
         do_settimeofday(&tv);
         return 0;
 }
include/linux/security.h
static inline int security_settime(const struct timespec *ts,
                                   const struct timezone *tz)
{
        return cap_settime(ts, tz);
}
security/commoncap.c
int cap_settime(const struct timespec *ts, const struct timezone *tz)
{
        if (!capable(CAP_SYS_TIME))
                return -EPERM;
        return 0;
}
kernel/capability.c
/**
 * capable - Determine if the current task has a superior capability in effect
 * @cap: The capability to be tested for
 *
 * Return true if the current task has the given superior capability currently
 * available for use, false if not.
 *
 * This sets PF_SUPERPRIV on the task if the capability is available on the
 * assumption that it's about to be used.
 */
bool capable(int cap)
{
        return ns_capable(&init_user_ns, cap);
}
/**
 * ns_capable - Determine if the current task has a superior capability in effect
 * @ns:  The usernamespace we want the capability in
 * @cap: The capability to be tested for
 *
 * Return true if the current task has the given superior capability currently
 * available for use, false if not.
 *
 * This sets PF_SUPERPRIV on the task if the capability is available on the
 * assumption that it's about to be used.
 */
bool ns_capable(struct user_namespace *ns, int cap)
{
        if (unlikely(!cap_valid(cap))) {
                printk(KERN_CRIT "capable() called with invalid cap=%u\n", cap);
                BUG();
        }

        if (security_capable(current_cred(), ns, cap) == 0) {
                current->flags |= PF_SUPERPRIV;
                return true;
        }
        return false;
}
EXPORT_SYMBOL(ns_capable);
security/selinux/hooks.c
/*
* (This comment used to live with the selinux_task_setuid hook,
* which was removed).
*
* Since setuid only affects the current process, and since the SELinux
* controls are not based on the Linux identity attributes, SELinux does not
* need to control this operation.  However, SELinux does control the use of
* the CAP_SETUID and CAP_SETGID capabilities using the capable hook.
*/

static int selinux_capable(const struct cred *cred, struct user_namespace *ns,
                          int cap, int audit)
{
       int rc;

       rc = cap_capable(cred, ns, cap, audit);
       if (rc)
               return rc;

       return cred_has_capability(cred, cap, audit);
}
security/commoncap.c
 /**
  * cap_capable - Determine whether a task has a particular effective capability
  * @cred: The credentials to use
  * @ns:  The user namespace in which we need the capability
  * @cap: The capability to check for
  * @audit: Whether to write an audit message or not
  *
  * Determine whether the nominated task has the specified capability amongst
  * its effective set, returning 0 if it does, -ve if it does not.
  *
  * NOTE WELL: cap_has_capability() cannot be used like the kernel's capable()
  * and has_capability() functions.  That is, it has the reverse semantics:
  * cap_has_capability() returns 0 when a task has a capability, but the
  * kernel's capable() and has_capability() returns 1 for this case.
  */
int cap_capable(const struct cred *cred, struct user_namespace *targ_ns,
                 int cap, int audit)
{
         struct user_namespace *ns = targ_ns;

         /* See if cred has the capability in the target user namespace
          * by examining the target user namespace and all of the target
          * user namespace's parents.
          */
         for (;;) {
                 /* Do we have the necessary capabilities? */
                 if (ns == cred->user_ns)
                         return cap_raised(cred->cap_effective, cap) ? 0 : -EPERM;

                 /* Have we tried all of the parent namespaces? */
                 if (ns == &init_user_ns)
                         return -EPERM;

                 /* 
                  * The owner of the user namespace in the parent of the
                  * user namespace has all caps.
                  */
                 if ((ns->parent == cred->user_ns) && uid_eq(ns->owner, cred->euid))
                         return 0;

                /*
                 * If you have a capability in a parent user ns, then you have
                 * it over all children user namespaces as well.
                 */
                ns = ns->parent;
        }

        /* We never get here */
}

В общем-то, не то, чтобы всё это было очень сложно. Идея довольно проста. Запутанность привносит модульная модель безопасности. Но за стенами кода скрыт достаточно простой механизм. В нашем примере security_settime() не имеет никакого отношения к реальной настройке часов. Всё, что делает эта функция, проверяет наличие соответствующей привилегии. Через хуки SELinux управляющий тракт в конечном итоге приводит нас в код, находящийся в файле security/commoncap.c, где макрос cap_raised() производит тривиальную проверку (логическое И между имеющимся у процесса разрешением и требуемой привелегией - соответствующая привилегия из эффективного множества должна быть установлена в единицу). В качестве упражнения оставляю вам удовольствие найти, что за security_settime() и как мы выходим на описанный выше тракт :)

Почти живой пример

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

#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 main(int argc, char ** argv)
{
 printf ("cap_setuid and cap_setgid: %d\n", prctl (PR_CAPBSET_READ, CAP_SETUID | CAP_SETGID, 0, 0, 0));
 printf (" %s\n", cap_to_text (cap_get_file (argv [0]), NULL));
 printf (" %s\n", cap_to_text (cap_get_proc (), NULL));

 if (setresuid (0, 0, 0));
  printf ("setresuid(): %s\n", strerror (errno));

 execve ("/bin/sh", NULL, NULL);
}
Эта программка должна изменить свой UID и дать нам шелл суперпользователя. Что ж, попытаем счастья:
$ gcc -o cap_test cap_test.c -lcap
$ ./cap_test
cap_setuid and cap_setgid: 1
 =
 =
setresuid(): Operation not permitted
Не повезло :( Но мы-то уже знаем, дело вовсе не в везении, а в том, что наш файл не имеет соответствующих разрешений. Попробуем теперь вот так:
# setcap cap_setuid,cap_setgid+ep ./cap_test
$ ./cap_test
cap_setuid and cap_setgid: 1
 = cap_setgid,cap_setuid+ep
 = cap_setgid,cap_setuid+ep
setresuid(): Success
# exit
exit
Успех! Да, кстати, на всякий случай, не забывайте выходить из шеллов, т.к. даже при безуспешном вызове setresuid() эта программа очевидным образом спавнит новый шелл каждый раз, только без привилегий :) В первой строке она показывает файловые разрешения, во второй - разрешения выполняющегося процесса. Так что при желании можете поэкспериментировать с этим примером больше. Информацию о разрешениях выполняющегося процесса можно посмотреть через /proc:
$ grep ^Cap /proc/$$/status
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000001fffffffff
Причём, не имея прав суперпользователя, вы ничего интересного не увидите, кроме маски. Вот так - уже интереснее:
# grep ^Cap /proc/$$/status
CapInh: 0000000000000000
CapPrm: 0000001fffffffff
CapEff: 0000001fffffffff
CapBnd: 0000001fffffffff
Значения можно декодировать с помощью capsh:
$ capsh --decode=0000001fffffffff
Также обратите внимание на утиль pscap, который показывает разрешения выполняющихся процессов. Всё сказанное наталкивает на мысль, что теоретически root, как таковой, уже не сильно нужен и вместо него можно ввести пользователя с полным набором привилегий. Кроме того, такая модель позволяет создавать промежуточных, недо-root'ов, с усечёнными полномочиями. Правда это требует обязательного включения поддержки некоторых параметров ядра при сборке, которые могут быть и отключены в пользовательских ядрах, так что модель root'а всё же пока актуальна, так как остаётся универсальным запасным вариантом для всех вариантов сборки ядра Linux.

PS: в заметке довольно много кода. Изначально я хотел спрятать всё это безобразие под ковриккат, но он работал не совсем так, как мне бы хотелось и я решил, что пусть уж лучше будет так, чем сломанный функционал. Надеюсь, к следующему разу я найду какой-нибудь рабочий вариант.

Источники

[1] How Linux Capability Works in 2.6.25
[2] Linux Capabilities: making them work

No comments:

ПОСЕТИТЕЛИ

free counters