Терминологический казус
То, о чём сейчас пойдёт речь, по-английски называется 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;
void *stack;
atomic_t usage;
unsigned int flags;
unsigned int ptrace;
...
const struct cred __rcu *real_cred;
const struct cred __rcu *cred;
char comm[TASK_COMM_LEN];
...
};
Далее include/linux/cred.h:
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers;
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid;
kgid_t gid;
kuid_t suid;
kgid_t sgid;
kuid_t euid;
kgid_t egid;
kuid_t fsuid;
kgid_t fsgid;
unsigned securebits;
kernel_cap_t cap_inheritable;
kernel_cap_t cap_permitted;
kernel_cap_t cap_effective;
kernel_cap_t cap_bset;
#ifdef CONFIG_KEYS
unsigned char jit_keyring;
struct key __rcu *session_keyring;
struct key *process_keyring;
struct key *thread_keyring;
struct key *request_key_auth;
#endif
#ifdef CONFIG_SECURITY
void *security;
#endif
struct user_struct *user;
struct user_namespace *user_ns;
struct group_info *group_info;
struct rcu_head rcu;
};
И наконец 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
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)
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;
}
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;
}
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 (has_cap && !uid_eq(new->uid, root_uid) && uid_eq(new->euid, root_uid)) {
warn_setuid_and_fcaps_mixed(bprm->filename);
goto skip;
}
if (uid_eq(new->euid, root_uid) || uid_eq(new->uid, root_uid)) {
new->cap_permitted = cap_combine(old->cap_bset,
old->cap_inheritable);
}
if (uid_eq(new->euid, root_uid))
effective = true;
}
skip:
if (!cap_issubset(new->cap_permitted, old->cap_permitted))
bprm->per_clear |= PER_CLEAR_ON_SETID;
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) {
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;
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():
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;
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];
__u32 inheritable = caps->inheritable.cap[i];
new->cap_permitted.cap[i] =
(new->cap_bset.cap[i] & permitted) |
(new->cap_inheritable.cap[i] & inheritable);
if (permitted & ~new->cap_permitted.cap[i])
ret = -EPERM;
}
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():
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);
...
Инициализация множеств разрешений нового процесса:
int prepare_bprm_creds(struct linux_binprm *bprm)
{
if (mutex_lock_interruptible(¤t->signal->cred_guard_mutex))
return -ERESTARTNOINTR;
bprm->cred = prepare_exec_creds();
if (likely(bprm->cred))
return 0;
mutex_unlock(¤t->signal->cred_guard_mutex);
return -ENOMEM;
}
struct cred *prepare_exec_creds(void)
{
struct cred *new;
new = prepare_creds();
if (!new)
return new;
#ifdef CONFIG_KEYS
key_put(new->thread_keyring);
new->thread_keyring = NULL;
key_put(new->process_keyring);
new->process_keyring = NULL;
#endif
return new;
}
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
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;
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)) {
if (mode & S_ISUID) {
bprm->per_clear |= PER_CLEAR_ON_SETID;
bprm->cred->euid = inode->i_uid;
}
if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) {
bprm->per_clear |= PER_CLEAR_ON_SETID;
bprm->cred->egid = inode->i_gid;
}
}
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
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
bool capable(int cap)
{
return ns_capable(&init_user_ns, cap);
}
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
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
int cap_capable(const struct cred *cred, struct user_namespace *targ_ns,
int cap, int audit)
{
struct user_namespace *ns = targ_ns;
for (;;) {
if (ns == cred->user_ns)
return cap_raised(cred->cap_effective, cap) ? 0 : -EPERM;
if (ns == &init_user_ns)
return -EPERM;
if ((ns->parent == cred->user_ns) && uid_eq(ns->owner, cred->euid))
return 0;
ns = ns->parent;
}
}
В общем-то, не то, чтобы всё это было очень сложно. Идея довольно проста. Запутанность привносит модульная модель безопасности. Но за стенами кода скрыт достаточно простой механизм. В нашем примере 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