В прошлом посте я уже сделал небольшой модуль ядра, который при инициализации находит адреса обработчиков системных вызовов setuid(2), setgid(2) и setgroups(2). Пора в него добавить средства коммуникации с usersapce окружением. Выбор механизмов достаточно широк, но я решил решил остановиться на старом и добром ioctl(2). Он позволяет передавать адрес буфера с данными при вызове и возвращать статус операции в виде int’а. Назову char-device соответственно — /dev/ugidctl.
Теперь пора придумать протокол общения userspace-окружения с ядром. Разделю его на 2 части. Первые несколько вызовов будут работать когда вызывающий процесс обладает правами рута, т.е. в моем случае «взведены» CAP_SETUID и CAP_SETGID. Эти вызовы будут инициализировать список uid’ов и gid’ов, между которыми процесс сможет свободно переключаться, будучи непривилегированным. Остальная часть вызовов будет непосредственно проверять права и обращаться к sys_setuid, sys_setgid и sys_setgroups. В качестве дополнительной защиты введу случайный ключ, который процесс может получить только во время стадии инициализации ugidctl и будет обязан предоставлять в процессе переключения при каждом вызове. Также ограничу процессы, которые могут совершать переключения проверкой PID’а (идентификатора процесса), PGID’а (идентификатора группы процессов) или SID’а (идентификатора сессии), потому как открытый файловый дескриптор моего устройства процесс может намеренно или случайно передать другому.
Опишу протокол здесь:
Получение ключа для доступа к вызовам в непривелигерованном режиме
1 2 3 4 5 6 |
ioctl(fd, UGIDCTLIO_GETKEY, struct ugidctl_key_rq *request); struct ugidctl_key_rq { __u8 key[32]; /* access key */ }; |
Функция возвращает ключ, ассоциированный с файловым дескриптором.
Требует CAP_SETUID и CAP_SETGID, т.е. root-привилегии.
Возвращает 0 и заполненную структуру ugidctl_key_rq в случае успеха. При ошибке возвращает -1, а в errno номер ошибки:
EPERM — у вызывающего процесса недостаточно прав.
EFAULT — в запросе указан недоступный участок памяти.
Запрос и установка типа проверки идентификатора запроса
1 2 |
ioctl(fd, UGIDCTLIO_GETPIDCHKTYPE); ioctl(fd, UGIDCTLIO_SETPIDCHKTYPE, pid_check_type); |
Обе функции возвращают тип идентификатора процесса, который будет проверяться в последующих непривилегерованных вызовах, т.е. UGIDCTLIO_SETUID, UGIDCTLIO_SETGID и UGIDCTLIO_SETGROUPS. Функция UGIDCTLIO_SETPIDCHKTYPE также устанавливает новый тип, а именно:
UGIDCTL_PIDTYPE_PID — будет проверяться идентификатор процесса (PID)
UGIDCTL_PIDTYPE_PGID — будет проверяться идентификатор группы процессов (PGID)
UGIDCTL_PIDTYPE_SID — будет проверяться идентификатор сессии (SID)
Требует CAP_SETUID и CAP_SETGID, т.е. root-привилегии.
В случае ошибки возвращает -1, а errno будет сожержать номер ошибки:
EPERM — у вызывающего процесса недостаточно прав.
EINVAL — указан неверный pid_check_type
Добавление UID’ов и GID’ов в список разрешенных
1 2 3 4 5 6 7 8 9 10 11 |
ioctl(fd, UGIDCTLIO_ADDUIDLIST, struct ugidctl_add_rq *request); ioctl(fd, UGIDCTLIO_ADDGIDLIST, struct ugidctl_add_rq *request); struct ugidctl_add_rq { __u32 count; /* list elements count */ union { uid_t uid_list[]; gid_t gid_list[]; }; }; |
Обе функции добавляют в список uid’ов и gid’ов, ассоциированный с файловым дескриптором, соответствующие значения.
Требуют CAP_SETUID и CAP_SETGID, т.е. root-привилегии.
При успешном выполнении возвращают 0, а в случае ошибки возвращают -1. errno будет содержать номер ошибки:
EPERM — у вызывающего процесса нет привилегий CAP_SETUID и CAP_SETGID.
EINVAL — поле count содежит значение, превышающее UGIDCTL_LISTMAX
ENOMEM — недостаточно памяти для обработки запроса.
EFAULT — в запросе указан недоступный участок памяти.
Установка UID’а / GID’а
1 2 3 4 5 6 7 8 9 10 11 |
ioctl(fd, UGIDCTLIO_SETUID, struct ugidctl_setid_rq *request); ioctl(fd, UGIDCTLIO_SETGID, struct ugidctl_setid_rq *request); struct ugidctl_setid_rq { __u8 key[32]; /* access key */ union { uid_t uid; gid_t gid; }; }; |
Команда UGIDCTLIO_SETUID устанавливает фактический, действительный и сохраненный идентификаторы владельца текущего процесса. Если у вызывающего процесса нет CAP_SETUID, ключ, иденификатор процесса (или группы процессов / сессии) и uid проверяются. Запрошенный uid должен быть в списке uid’ов, разрешенных предыдущими вызовами команды UGIDCTLIO_ADDUIDLIST.
Абсолютно аналогично, команда UGIDCTLIO_SETGID устанавливает фактический, действительный и сохраненный идентификаторы группы текущего процесса, и все, описанное для UGIDCTLIO_SETUID, остается в силе с заменой «uid» на «gid».
При успешном выполнении возвращают 0, а в случае ошибки возвращают -1. errno будет содержать номер ошибки:
EPERM — процесс непривилегован, а ключ, pid, pgid, sid или uid/gid не прошли проверку.
EAGAIN — uid относится к процессу, который превышает свой RLIMIT_NPROC лимит ресурсов (только для UGIDCTLIO_SETUID).
ENOMEM — недостаточно памяти для обработки запроса.
EFAULT — в запросе указан недоступный участок памяти.
Установка списка дополнительных идентификаторов групп
1 2 3 4 5 6 7 |
ioctl(fd, UGIDCTLIO_SETGRUPS, struct ugidctl_setgroups_rq *request); struct ugidctl_setgroups_rq { __u8 key[32]; __u32 count; gid_t list[0]; }; |
Устанавливает идентификаторы дополнительных групп для текущего процесса. Если у вызывающего процесса нет CAP_SETGID, ключ, иденификатор процесса (или группы процессов / сессии) и список gid’ов проверяются. Запрошенные gid’ы должены быть в списке gid’ов, разрешенных предыдущими вызовами команды UGIDCTLIO_ADDGIDLIST.
При успешном выполнении возвращает 0, а в случае ошибки возвращает -1. errno будет содержать номер ошибки:
EPERM — процесс непривилегован, а ключ, pid, pgid, sid или gid-list не прошли проверку.
EINVAL — gid_count больше чем NGROUPS_MAX
ENOMEM — недостаточно памяти для обработки запроса.
EFAULT — в запросе указан недоступный участок памяти.
Для протокола пригодиться header-файл ugidctl.h, который описывает все структуры данных и номера команд ioctl(2). Примерно такой:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
/* * This file is released under the GPL. */ #ifndef _UGIDCTL_H #define _UGIDCTL_H #include <linux/types.h> #include <sys/types.h> struct ugidctl_key_rq { __u8 key[32]; }; struct ugidctl_add_rq { __u32 count; union { uid_t uid_list[0]; gid_t gid_list[0]; }; }; struct ugidctl_setid_rq { __u8 key[32]; union { uid_t uid; gid_t gid; }; }; struct ugidctl_setgroups_rq { __u8 key[32]; __u32 count; gid_t list[0]; }; /* limits */ #define UGIDCTL_UIDSMAX 1048576 #define UGIDCTL_GIDSMAX 1048576 /* pid check type */ #define UGIDCTL_PIDTYPE_PID 0 #define UGIDCTL_PIDTYPE_PGID 1 #define UGIDCTL_PIDTYPE_SID 2 /* ioctl interface */ #define UGIDCTLIO_GETKEY _IOR ('S', 0x01, struct ugidctl_key_rq) #define UGIDCTLIO_GETPIDCHKTYPE _IO ('S', 0x02) #define UGIDCTLIO_SETPIDCHKTYPE _IOW ('S', 0x03, unsigned long) #define UGIDCTLIO_ADDUIDLIST _IOW ('S', 0x04, struct ugidctl_add_rq) #define UGIDCTLIO_ADDGIDLIST _IOW ('S', 0x05, struct ugidctl_add_rq) #define UGIDCTLIO_SETUID _IOW ('S', 0x06, struct ugidctl_setid_rq) #define UGIDCTLIO_SETGID _IOW ('S', 0x07, struct ugidctl_setid_rq) #define UGIDCTLIO_SETGROUPS _IOWR('S', 0x08, struct ugidctl_setgroups_rq) #endif |
Отлично, теперь можно приступить к описанию всего этого в модуле ядра. В сети полно документации, как написать создание char-device’а, поэтому подробно останавливаться на этом не буду. Вот собственно отправная точка:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
/* * This file is released under the GPL. */ #include <linux/module.h> #include <linux/kernel.h> #include <linux/device.h> #include <linux/cdev.h> #include <linux/fs.h> #include "ugidctl.h" MODULE_LICENSE("GPL"); MODULE_AUTHOR("Rommer <rommer@ibuffed.com>"); #define DEV_NAME "ugidctl" #define MOD_NAME "ugidctl" /* log prefix */ /* char device */ static struct cdev ugidctl_cdev; static unsigned int ugidctl_major; static struct class *ugidctl_class; static int ugidctl_open(struct inode *inode, struct file *filp) { printk(KERN_INFO "%s: device open stub\n", MOD_NAME); return 0; } static int ugidctl_release(struct inode *inode, struct file *filp) { printk(KERN_INFO "%s: device close stub\n", MOD_NAME); return 0; } static long ugidctl_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { printk(KERN_INFO "%s: device ioctl stub\n", MOD_NAME); return -ENOIOCTLCMD; } static struct file_operations ugidctl_fops = { .owner = THIS_MODULE, .open = ugidctl_open, .release = ugidctl_release, .unlocked_ioctl = ugidctl_ioctl, }; static int __init init_ugidctl(void) { dev_t dev; int rc; rc = alloc_chrdev_region(&dev, 0, 1, DEV_NAME); if (rc) { printk(KERN_ERR "%s: failed to register chrdev region\n", MOD_NAME); goto out; } ugidctl_major = MAJOR(dev); ugidctl_class = class_create(THIS_MODULE, DEV_NAME); if (IS_ERR(ugidctl_class)) { printk(KERN_ERR "%s: failed to register with sysfs\n", MOD_NAME); rc = PTR_ERR(ugidctl_class); goto out_region; } cdev_init(&ugidctl_cdev, &ugidctl_fops); rc = cdev_add(&ugidctl_cdev, dev, 1); if (rc) { printk(KERN_ERR "%s: failed to add char device\n", MOD_NAME); goto out_class; } device_create(ugidctl_class, NULL, dev, NULL, DEV_NAME); return 0; out_class: class_destroy(ugidctl_class); out_region: unregister_chrdev_region(dev, 1); out: return rc; } static void __exit exit_ugidctl(void) { dev_t dev = MKDEV(ugidctl_major, 0); device_destroy(ugidctl_class, dev); class_destroy(ugidctl_class); cdev_del(&ugidctl_cdev); unregister_chrdev_region(dev, 1); } module_init(init_ugidctl); module_exit(exit_ugidctl); |
Тут функция ugidctl_open будет вызвана когда кто-то откроет (open(2)) /dev/ugidctl, ugidctl_release — когда закроет (close(2)) и ugidctl_ioctl, соответственно, когда вызовет ioctl(2) на открытом устройстве. Проверим:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$ make make -C /lib/modules/3.10.0-229.14.1.el7.x86_64/build M=/home/user/tmp modules make[1]: Entering directory `/usr/src/kernels/3.10.0-229.14.1.el7.x86_64' CC [M] /home/user/tmp/ugidctl.o Building modules, stage 2. MODPOST 1 modules CC /home/user/tmp/ugidctl.mod.o LD [M] /home/user/tmp/ugidctl.ko make[1]: Leaving directory `/usr/src/kernels/3.10.0-229.14.1.el7.x86_64' $ sudo insmod ugidctl.ko $ ls -la /dev/ugidctl crw------- 1 root root 249, 0 Oct 23 23:08 /dev/ugidctl $ sudo perl -e 'open(my $fd,"<","/dev/ugidctl");' $ dmesg | tail -3 [417044.623598] ugidctl: device open stub [417044.623606] ugidctl: device ioctl stub [417044.623620] ugidctl: device close stub $ sudo rmmod ugidctl $ |
Теперь я не только имею доступ к системным вызовам sys_setuid, sys_setgid и sys_setgroups, но и создал устройство и заглушки для будующих функций модуля. В следующем посте надо будет придумать, как хранить списки uid’ов и gid’ов внури ядра и эффективно по ним искать.