Как реализовать защиту процессов. Про встроенные в микропроцессоры Intel механизмы защиты задач говорить здесь не будем – об этом уже много написано в различных источниках. Поговорим о программной защите процессов. В ядре для каждого процесса хранится определённая информация: идентификаторы процесса, пользователя, группы, приоритеты выполнения и др. Все эти данные содержатся в специальной структуре task_struct (include/linux/sched.h). Для нас в этой структуре наибольший интерес представляет поле void *security. Этот указатель очень полезен для хранения каких-либо данных о процессе, касающихся его уровня допуска к секретной информации или, например, степени секретности самого процесса и используемых им данных. Этот указатель можно использовать для адресации некоторой структуры или просто целочисленной переменной. Когда создается процесс, то есть новая структура task_struct, - это поле устанавливается сначала в NULL. Механизм LSM для инициализации и уничтожения этого указателя предусматривает следующие функции
(include/linux/security.h):

………
struct security_operations {
……….
int (*task_alloc_security) (struct task_struct *p);
void (*task_free_security) (struct task_struct *p);
……….
};
……….
static inline int security_task_alloc (struct task_struct *p) {
return security_ops->task_alloc_security();
}
static inline void security_task_free (struct task_struct *p) { 
security_ops->task_free_security();
}

А поля структуры security_ops инициализируются указателями на наши процедуры обработки при загрузке нашего модуля безопасности. Круто!

Обращение к процедурам security_task_alloc и security_task_free производится при создании процесса и при его уничтожении. Мы же хорошо помним, как в *nix создаются процессы – «копируется» родительский процесс. Заглянем в файл kernel/fork.c – найдём там функции struct task_struct *copy_process(……….) и void __put_task_struct(……). Ну и что мы там встретили? Правильно security_task_alloc и security_task_free соответственно. Сделаем замечание, что в разных ядрах даже одной ветки функции могут отличаться, хотя для директории kernel это мало вероятно.
Когда мы придумали, что нам делать с указателем void *security из task_struct, можно добавить некоторый код в уже позабытый нами файл нашего модуля безопасности – по-моему, мы его назвали «security/super_secure_system.c». Во-первых, объявим наши функции:

static int super_secure_task_alloc_security (struct task_struct * p) {
// здесь можно воткнуть инициализацию p->security
// !!!! память не забудьте выделять – kmalloc(…)
return 0; // success result
}
static void sids_task_free_security (struct task_struct * p) {
if(p->security != NULL) {
kfree(p->security); // скорее всего, что-то в этом роде
p->security = NULL;
}
return;
}

Теперь добавим жару в нашу структуру
………….
static struct security_operations super_secure_oper = {
……………………………….
Адреса вывозов процедур защиты данных
……………………………….
.task_alloc_security = super_secure__task_alloc_security,
.task_free_security = super_secure_free_security,
………………………………..
Адреса вывозов процедур защиты данных
………………………………..
}
………….

В этом же духе к нашим услугам и другие процедуры:
.task_create – Возвращаем что-то вроде кода -13 и процесс не создастся – ГАРАНТИЯ!!! Если вернём 0 – то типа всё ОК, мы не против.
.task_kill – По каждому «запросу на убийство» процесса вернём код -13 и процесс умрёт не раньше, чем система выключится. Но я не думаю, что так следует поступать!
Поручаю Вам самим выяснить, что такое код -13, а также назначение и смысл других кодов ошибок .
.task_setuid – Можно ограничить смену идентификатора пользователя, от имени которого запущен данный процесс. Если вы используете эту функцию, то вам скорее всего пригодится и post_setuid, вызов которой происходит по успешному выполнению task_setuid. Функция post_setuid, например, обновить данные, хранимые в
p->security.

Есть ещё с десяток подобных (полное описание в include/linux/security.h). Ещё очень интересная функция task_to_inode – нам надо позаботиться о данных, хранимых в /proc. К процессам мы ещё вернёмся.
Защита программ при их загрузке на исполнение. Если у вас ядро благополучно собирается и загружается, то идём дальше. Рассмотрим защиту того момента, когда начинает исполняться новая программа. Напоминаю, что в *nix для запуска на выполнение некоторой программы, родительский процесс сначала клонирует себя (fork), затем дочерний клон выполняет системный вызов sys_execve (arch/i386/kernel/process.c), который пытается загрузить указанный исполняемый бинарный файл на место процесса, запросившего sys_execve. Если бы не execve, то и кроме процесса init больше ничего бы не удалось «запустить», да и сам процесс init бы скоропостижно скончался и послал бы ядро куда подальше в panic. Механизм LSM предоставляет набор процедур для управления загрузкой процессами исполняемых кодов программ – binprm_security_ops. Что такое binprm? Познакомимся поближе.

Системный вызов sys_execve получает следующие параметры (помним, что передача параметров осуществляется через регисты: eax, ebx, ecx, edx, esi, edi, а также, что если передаются адреса – то они обязательно адресуют пространство пользователя): имя файла на исполнение; адресс заканчивающегося на NULL массива указателей на строки, содержащие аргументы командной строки; адрес заканчивающегося на NULL массива указателей на строки, содержащие переменные окружения в формате ПЕРЕМЕННАЯ=значение.

asmlinkage int sys_execve(struct pt_regs regs) {
int error;
char * filename;

filename = getname((char __user *) regs.ebx);
error = PTR_ERR(filename);
if (IS_ERR(filename))
goto out;
error = do_execve(filename,
(char __user * __user *) regs.ecx,
(char __user * __user *) regs.edx,
®s);
if (error == 0) {
current->ptrace &= ~PT_DTRACE;
/* Make sure we don't return using sysenter.. */
set_thread_flag(TIF_IRET);
}
putname(filename);
out:
return error;
}

Одно замечание: многие разработчики считают использование меток перехода плохим стилем программирования – это действительно не очень хорошо, но и не плохо. Метки – такая же конструкция языка, как for или while. Надо использовать всю мощь языка, тем более что в некоторых ситуациях (как, например, в данной) это может быть весьма полезным.
Внимание. Содержимое struct pt_regs regs хранится в режиме ядра ОС.

Мы, наконец, дошли до структуры linux_binprm («include/linux/binfmts.h»), – её использует функция do_execve, которая и выполняет всю основную работу при «запуске программ». Именно эта структура содержит все данные необходимые (в том числе и аргументы, переданные через командную строку) для загрузки бинарника на исполнение. Становится ясно, почему важно, чтобы эти данные были надёжно защищены. К тому же в некоторых случаях может быть разрешено клонирование процесса, но запрещено выполнение некоторого другого кода. Для этих целей и нужны процедуры из блока security_bprm. Например, в уже упомянутом нами файле security/root_plug.c процедура rootplug_bprm_check_security не позволит исполнять ни какой посторонний код от имени пользователя root, если в разъем usb не вставлено устройство с определёнными идентифицирующими данными.

static int rootplug_bprm_check_security (struct linux_binprm *bprm) {
struct usb_device *dev;
root_dbg("file %s, e_uid = %d, e_gid = %d\n",
bprm->filename, bprm->e_uid, bprm->e_gid);
if (bprm->e_gid == 0) {
dev = usb_find_device(vendor_id, product_id);
if (!dev) {
root_dbg("e_gid = 0, and device not found, "
"task not allowed to run...\n");
return -EPERM;
}
usb_put_dev(dev);
}
return 0;
}

Читателю, безусловно, известно, что любая уважающаяся себя система безопасности базируется не только на программной защите, но и АППАРАТНОЙ.

(Продолжение следует)

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии