- Процессы в Linux
- Жизненный цикл процесса linux
- Содержание
- Введение
- Атрибуты процесса
- Жизненный цикл процесса
- Рождение процесса
- Состояние «готов»
- Состояние «выполняется»
- Перерождение в другую программу
- Состояние «ожидает»
- Состояние «остановлен»
- Завершение процесса
- Состояние «зомби»
- Забытье
- Благодарности
- Linux: process life cycle and access control
- Жизненный цикл процессов ОС
- Контроль создания и выполнения процессов в СЗИ НСД
- Заключение
- Литература
Процессы в Linux
Данной теме посвящено много статей, но в Сети мало сугубо практических статей. О какой именно практике идет речь, вы узнаете прочитав эту статью. Правда, одной только практикой нам не обойтись — вдруг вы не читали всю эту серую массу теории, которую можно найти в Сети.
Термин «процесс» впервые появился при разработке операционной системы Multix и имеет несколько определений, которые используются в зависимости от контекста. Процесс — это:
- программа на стадии выполнения
- «объект», которому выделено процессорное время
- асинхронная работа
Для описания состояний процессов используется несколько моделей. Самая простая модель — это модель трех состояний. Модель состоит из:
- состояния выполнения
- состояния ожидания
- состояния готовности
Выполнение — это активное состояние, во время которого процесс обладает всеми необходимыми ему ресурсами. В этом состоянии процесс непосредственно выполняется процессором.
Ожидание — это пассивное состояние, во время которого процесс заблокирован, он не может быть выполнен, потому что ожидает какое-то событие, например, ввода данных или освобождения нужного ему устройства.
Готовность — это тоже пассивное состояние, процесс тоже заблокирован, но в отличие от состояния ожидания, он заблокирован не по внутренним причинам (ведь ожидание ввода данных — это внутренняя, «личная» проблема процесса — он может ведь и не ожидать ввода данных и свободно выполняться — никто ему не мешает), а по внешним, независящим от процесса, причинам. Когда процесс может перейти в состояние готовности? Предположим, что наш процесс выполнялся до ввода данных. До этого момента он был в состоянии выполнения, потом перешел в состояние ожидания — ему нужно подождать, пока мы введем нужную для работы процесса информацию. Затем процесс хотел уже перейти в состояние выполнения, так как все необходимые ему данные уже введены, но не тут-то было: так как он не единственный процесс в системе, пока он был в состоянии ожидания, его «место под солнцем» занято — процессор выполняет другой процесс. Тогда нашему процессу ничего не остается как перейти в состояние готовности: ждать ему нечего, а выполняться он тоже не может.
Из состояния готовности процесс может перейти только в состояние выполнения. В состоянии выполнения может находится только один процесс на один процессор. Если у вас n-процессорная машина, у вас одновременно в состоянии выполнения могут быть n процессов.
Из состояния выполнения процесс может перейти либо в состояние ожидания или состояние готовности. Почему процесс может оказаться в состоянии ожидания, мы уже знаем — ему просто нужны дополнительные данные или он ожидает освобождения какого-нибудь ресурса, например, устройства или файла. В состояние готовности процесс может перейти, если во время его выполнения, квант времени выполнения «вышел». Другими словами, в операционной системе есть специальная программа — планировщик, которая следит за тем, чтобы все процессы выполнялись отведенное им время. Например, у нас есть три процесса. Один из них находится в состоянии выполнения. Два других — в состоянии готовности. Планировщик следит за временем выполнения первого процесса, если «время вышло», планировщик переводит процесс 1 в состояние готовности, а процесс 2 — в состояние выполнения. Затем, когда, время отведенное, на выполнение процесса 2, закончится, процесс 2 перейдет в состояние готовности, а процесс 3 — в состояние выполнения.
Диаграмма модели трех состояний представлена на рисунке 1.
Рисунок 1. Модель трех состояний
Более сложная модель — это модель, состоящая из пяти состояний. В этой модели появилось два дополнительных состояния: рождение процесса и смерть процесса. Рождение процесса — это пассивное состояние, когда самого процесса еще нет, но уже готова структура для появления процесса. Как говорится в афоризме: «Мало найти хорошее место, надо его еще застолбить», так вот во время рождения как раз и происходит «застолбление» этого места. Смерть процесса — самого процесса уже нет, но может случиться, что его «место», то есть структура, осталась в списке процессов. Такие процессы называются зобми и о них мы еще поговорим в этой статье.
Диаграмма модели пяти состояний представлена на рисунке 2.
Рисунок 2. Модель пяти состояний
Над процессами можно производить следующие операции:
- Создание процесса — это переход из состояния рождения в состояние готовности
- Уничтожение процесса — это переход из состояния выполнения в состояние смерти
- Восстановление процесса — переход из состояния готовности в состояние выполнения
- Изменение приоритета процесса — переход из выполнения в готовность
- Блокирование процесса — переход в состояние ожидания из состояния выполнения
- Пробуждение процесса — переход из состояния ожидания в состояние готовности
- Запуск процесса (или его выбор) — переход из состояния готовности в состояние выполнения
Для создания процесса операционной системе нужно:
- Присвоить процессу имя
- Добавить информацию о процессе в список процессов
- Определить приоритет процесса
- Сформировать блок управления процессом
- Предоставить процессу нужные ему ресурсы
Подробнее о списке процессов, приоритете и обо всем остальном мы еще поговорим, а сейчас нужно сказать пару слов об иерархии процессов. Процесс не может взяться из ниоткуда: его обязательно должен запустить какой-то процесс. Процесс, запущенный другим процессом, называется дочерним (child) процессом или потомком. Процесс, который запустил процесс называется родительским (parent), родителем или просто — предком. У каждого процесса есть два атрибута — PID (Process ID) — идентификатор процесса и PPID (Parent Process ID) — идентификатор родительского процесса.
Процессы создают иерархию в виде дерева. Самым «главным» предком, то есть процессом, стоящим на вершине этого дерева, является процесс init (PID=1).
На мой взгляд, приведенной теории вполне достаточно, чтобы перейти к практике, а именно — «пощупать» все состояния процессов. Конечно, мы не рассмотрели системные вызовы fork(), exec(), exit(), kill() и многие другие, но в Сети предостаточно информации об этом. Тем более, что про эти вызовы вы можете прочитать в справочной системе Linux, введя команду man fork. Правда, там написано на всеми любимом English, так что за переводом (если он вам нужен) все-таки придется обратиться за помощью к WWW.
Для наблюдения за процессами мы будем использовать программу top.
Полный вывод программы я по понятным причинам урезал. Рассмотрим по порядку весь вывод программы. В первой строке программа сообщает текущее время, время работы системы ( 58 min), количество зарегистрированных (login) пользователей (4 users), общая средняя загрузка системы (load average).
Примечание. Общей средней загрузкой системы называется среднее число процессов, находящихся в состоянии выполнения (R) или в состоянии ожидания (D). Общая средняя загрузка измеряется каждые 1, 5 и 15 минут.
Во второй строке вывода программы top сообщается, что в списке процессов находятся 52 процесса, из них 51 спит (состояние готовности или ожидания), 1 выполняется (у меня только 1 процессор), 0 процессов зомби и 0 остановленных процессов.
В третьей-пятой строках приводится информация о загрузке процессора, использования памяти и файла подкачки. Нас данная информация не очень интересует, поэтому переходим сразу к таблице процессов.
В таблице отображается различная информация о процессе. Нас сейчас интересуют колонки PID (идентификатор процесса), USER (пользователь, запустивший процесс), STAT (состояние процесса) и COMMAND (команда, которая была введена для запуска процесса).
Колонка STAT может содержать следующие значения:
- R — процесс выполняется или готов к выполнению (состояние готовности)
- D — процесс в «беспробудном сне» — ожидает дискового ввода/вывода
- T — процесс остановлен (stopped) или трассируется отладчиком
- S — процесс в состоянии ожидания (sleeping)
- Z — процесс-зобми
- < — процесс с отрицательным значением nice
- N — процесс с положительным значением nice (о команде nice мы поговорим позже)
Давайте просмотрим, когда же процесс находится в каждом состоянии. Создайте файл process — это обыкновенный bash-сценарий
Сделайте этот файл исполнимым chmod +x ./process и запустите его ./process. Теперь перейдите на другую консоль (ALT + Fn) и введите команду ps -a | grep process. Вы увидите следующий вывод команды ps:
Данный вывод означает, что нашему процессу присвоен идентификатор процесса 4035. Теперь введите команду top -p 4035
Обратите внимание на колонку состояния нашего процесса. Она содержит значение R, которое означает, что в данный момент выполняется процесс с номером 4035.
Теперь приостановим наш процесс — состояние T. Перейдите на консоль, на которой запущен ./process и нажмите Ctrl + Z. Вы увидите сообщение Stopped.
Теперь попробуем «усыпить» наш процесс. Для этого нужно сначала «убить» его: kill 4035. Затем добавить перед циклом while в сценарии ./process строку sleep 10m, которая означает, что процесс будет спать 10 минут. После этого опять запустите команду ps -a | grep process, чтобы узнать PID процесса, а затем — команду top -p PID. Вы увидите в колонке состояния букву S, что означает, что процесс находится в состоянии ожидания или готовности — попросту говоря «спит».
Мы вплотную подошли к самому интересному — созданию процесса-зомби. Во многих статьях, посвященных процессам, пишется «зомби = не жив, не мертв». А что это означает на самом деле? При завершении процесса должна удаляться его структура из списка процессов. Иногда процесс уже завершился, но его имя еще не удалено из списка процессов. В этом случае процесс становится зомби — его уже нет, но мы его видим в таблице команды top. Такое может произойти, если процесс-потомок (дочерний процесс) завершился раньше, чем этого ожидал процесс-родитель. Сейчас мы напишем программу, порождающую зомби, который будет существовать 8 секунд. Процесс-родитель будет ожидать завершения процесса-потомка через 10 секунд, а процесс-потомок завершить через 2 секунды.
Для компиляции данной программы нам нужен компилятор gcc:
Для тех, у кого не установлен компилятор, скомпилированная программа доступна отсюда.
После того, как программа будет откомпилирована, запустите ее: ./zombie. Программа выведет следующую информацию:
Запомните последний номер и быстро переключайтесь на другую консоль. Затем введите команду top -p 1148
Мы видим, что в списке процессов появился 1 зомби (STAT=Z), который проживет аж 10 секунд.
Мы уже рассмотрели все возможные состояния процессов. Осталось только рассмотреть команду для повышения приоритета процесса — это команда nice. Повысить приоритет команды может только пользователь root, указав соответствующий коэффициент понижения. Для увеличения приоритета нужно указать отрицательный коэффициент, например, nice -5 process
Источник
Жизненный цикл процесса linux
В этой статье я хотел бы рассказать о том, какой жизненный путь проходят процессы в семействе ОС Linux. В теории и на примерах я рассмотрю как процессы рождаются и умирают, немного расскажу о механике системных вызовов и сигналов.
Данная статья в большей мере рассчитана на новичков в системном программировании и тех, кто просто хочет узнать немного больше о том, как работают процессы в Linux.
Всё написанное ниже справедливо к Debian Linux с ядром 4.15.0.
Содержание
- Введение
- Атрибуты процесса
- Жизненный цикл процесса
- Рождение процесса
- Состояние «готов»
- Состояние «выполняется»
- Перерождение в другую программу
- Состояние «ожидает»
- Состояние «остановлен»
- Завершение процесса
- Состояние «зомби»
- Забытье
- Благодарности
Введение
Системное программное обеспечение взаимодействует с ядром системы посредством специальных функций — системных вызовов. В редких случаях существует альтернативный API, например, procfs или sysfs, выполненные в виде виртуальных файловых систем.
Атрибуты процесса
Процесс в ядре представляется просто как структура с множеством полей (определение структуры можно прочитать здесь).
Но так как статья посвящена системному программированию, а не разработке ядра, то несколько абстрагируемся и просто акцентируем внимание на важных для нас полях процесса:
- Идентификатор процесса (pid)
- Открытые файловые дескрипторы (fd)
- Обработчики сигналов (signal handler)
- Текущий рабочий каталог (cwd)
- Переменные окружения (environ)
- Код возврата
Жизненный цикл процесса
Рождение процесса
Только один процесс в системе рождается особенным способом — init — он порождается непосредственно ядром. Все остальные процессы появляются путём дублирования текущего процесса с помощью системного вызова fork(2). После выполнения fork(2) получаем два практически идентичных процесса за исключением следующих пунктов:
- fork(2) возвращает родителю PID ребёнка, ребёнку возвращается 0;
- У ребёнка меняется PPID (Parent Process Id) на PID родителя.
После выполнения fork(2) все ресурсы дочернего процесса — это копия ресурсов родителя. Копировать процесс со всеми выделенными страницами памяти — дело дорогое, поэтому в ядре Linux используется технология Copy-On-Write.
Все страницы памяти родителя помечаются как read-only и становятся доступны и родителю, и ребёнку. Как только один из процессов изменяет данные на определённой странице, эта страница не изменяется, а копируется и изменяется уже копия. Оригинал при этом «отвязывается» от данного процесса. Как только read-only оригинал остаётся «привязанным» к одному процессу, странице вновь назначается статус read-write.
Пример простой бесполезной программы с fork(2)
int main() <
int pid = fork();
switch(pid) <
case -1:
perror(“fork”);
return -1;
case 0:
// Child
printf(“my pid = %i, returned pid = %in”, getpid(), pid);
break;
default:
// Parent
printf(“my pid = %i, returned pid = %in”, getpid(), pid);
break;
>
return 0;
>
$ gcc test.c && ./a.out
my pid = 15594, returned pid = 15595
my pid = 15595, returned pid = 0
Состояние «готов»
Сразу после выполнения fork(2) переходит в состояние «готов».
Фактически, процесс стоит в очереди и ждёт, когда планировщик (scheduler) в ядре даст процессу выполняться на процессоре.
Состояние «выполняется»
Как только планировщик поставил процесс на выполнение, началось состояние «выполняется». Процесс может выполняться весь предложенный промежуток (квант) времени, а может уступить место другим процессам, воспользовавшись системным вывозом sched_yield.
Перерождение в другую программу
В некоторых программах реализована логика, в которой родительский процесс создает дочерний для решения какой-либо задачи. Ребёнок в данном случае решает какую-то конкретную проблему, а родитель лишь делегирует своим детям задачи. Например, веб-сервер при входящем подключении создаёт ребёнка и передаёт обработку подключения ему.
Однако, если нужно запустить другую программу, то необходимо прибегнуть к системному вызову execve(2):
int execve(const char *filename, char *const argv[], char *const envp[]);
или библиотечным вызовам execl(3), execlp(3), execle(3), execv(3), execvp(3), execvpe(3):
int execl(const char *path, const char *arg, … /* (char *) NULL */);
int execlp(const char *file, const char *arg, … /* (char *) NULL */);
int execle(const char *path, const char *arg, …
/*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
Все из перечисленных вызовов выполняют программу, путь до которой указан в первом аргументе. В случае успеха управление передаётся загруженной программе и в исходную уже не возвращается. При этом у загруженной программы остаются все поля структуры процесса, кроме файловых дескрипторов, помеченных как O_CLOEXEC, они закроются.
Как не путаться во всех этих вызовах и выбирать нужный? Достаточно постичь логику именования:
- Все вызовы начинаются с exec
- Пятая буква определяет вид передачи аргументов:
- l обозначает list, все параметры передаются как arg1, arg2, …, NULL
- v обозначает vector, все параметры передаются в нуль-терминированном массиве;
- Далее может следовать буква p, которая обозначает path. Если аргумент file начинается с символа, отличного от “/”, то указанный file ищется в каталогах, перечисленных в переменной окружения PATH
- Последней может быть буква e, обозначающая environ. В таких вызовах последним аргументом идёт нуль-терминированный массив нуль-терминированных строк вида key=value — переменные окружения, которые будут переданы новой программе.
Пример вызова /bin/cat –help через execve
int main() <
char* args[] = < “/bin/cat”, “–help”, NULL >;
execve(“/bin/cat”, args, environ);
// Unreachable
return 1;
>
$ gcc test.c && ./a.out
Usage: /bin/cat [OPTION]… [FILE]…
Concatenate FILE(s) to standard output.
Семейство вызовов exec* позволяет запускать скрипты с правами на исполнение и начинающиеся с последовательности шебанг (#!).
Пример запуска скрипта с подмененным PATH c помощью execle
int main() <
char* e[] = <“PATH=/habr:/rulez”, NULL>;
execle(“/tmp/test.sh”, “test.sh”, NULL, e);
// Unreachable
return 1;
>
$ cat test.sh
#!/bin/bash
echo $0
echo $PATH
$ gcc test.c && ./a.out
/tmp/test.sh
/habr:/rulez
Есть соглашение, которое подразумевает, что argv[0] совпадает с нулевым аргументов для функций семейства exec*. Однако, это можно нарушить.
Пример, когда cat становится dog с помощью execlp
int main() <
execlp(“cat”, “dog”, “–help”, NULL);
// Unreachable
return 1;
>
$ gcc test.c && ./a.out
Usage: dog [OPTION]… [FILE]…
Любопытный читатель может заметить, что в сигнатуре функции int main(int argc, char* argv[]) есть число — количество аргументов, но в семействе функций exec* ничего такого не передаётся. Почему? Потому что при запуске программы управление передаётся не сразу в main. Перед этим выполняются некоторые действия, определённые glibc, в том числе подсчёт argc.
Состояние «ожидает»
Некоторые системные вызовы могут выполняться долго, например, ввод-вывод. В таких случаях процесс переходит в состояние «ожидает». Как только системный вызов будет выполнен, ядро переведёт процесс в состояние «готов».
В Linux так же существует состояние «ожидает», в котором процесс не реагирует на сигналы прерывания. В этом состоянии процесс становится «неубиваемым», а все пришедшие сигналы встают в очередь до тех пор, пока процесс не выйдет из этого состояния.
Ядро само выбирает, в какое из состояний перевести процесс. Чаще всего в состояние «ожидает (без прерываний)» попадают процессы, которые запрашивают ввод-вывод. Особенно заметно это при использовании удалённого диска (NFS) с не очень быстрым интернетом.
Состояние «остановлен»
В любой момент можно приостановить выполнение процесса, отправив ему сигнал SIGSTOP. Процесс перейдёт в состояние «остановлен» и будет находиться там до тех пор, пока ему не придёт сигнал продолжать работу (SIGCONT) или умереть (SIGKILL). Остальные сигналы будут поставлены в очередь.
Завершение процесса
Ни одна программа не умеет завершаться сама. Они могут лишь попросить систему об этом с помощью системного вызова _exit или быть завершенными системой из-за ошибки. Даже когда возвращаешь число из main(), всё равно неявно вызывается _exit.
Хотя аргумент системного вызова принимает значение типа int, в качестве кода возврата берется лишь младший байт числа.
Состояние «зомби»
Сразу после того, как процесс завершился (неважно, корректно или нет), ядро записывает информацию о том, как завершился процесс и переводит его в состояние «зомби». Иными словами, зомби — это завершившийся процесс, но память о нём всё ещё хранится в ядре.
Более того, это второе состояние, в котором процесс может смело игнорировать сигнал SIGKILL, ведь что мертво не может умереть ещё раз.
Забытье
Код возврата и причина завершения процесса всё ещё хранится в ядре и её нужно оттуда забрать. Для этого можно воспользоваться соответствующими системными вызовами:
pid_t wait(int *wstatus); /* Аналогично waitpid(-1, wstatus, 0) */
pid_t waitpid(pid_t pid, int *wstatus, int options);
Вся информация о завершении процесса влезает в тип данных int. Для получения кода возврата и причины завершения программы используются макросы, описанные в man-странице waitpid(2).
Пример корректного завершения и получения кода возврата
int main() <
int pid = fork();
switch(pid) <
case -1:
perror(“fork”);
return -1;
case 0:
// Child
return 13;
default: <
// Parent
int status;
waitpid(pid, &status, 0);
printf(“exit normally? %sn”, (WIFEXITED(status) ? “true” : “false”));
printf(“child exitcode = %in”, WEXITSTATUS(status));
break;
>
>
return 0;
>
$ gcc test.c && ./a.out
exit normally? true
child exitcode = 13
Пример некорректного завершения
Передача argv[0] как NULL приводит к падению.
#include
#include
#include
#include
#include
int main() <
int pid = fork();
switch(pid) <
case -1:
perror(“fork”);
return -1;
case 0:
// Child
execl(“/bin/cat”, NULL);
return 13;
default: <
// Parent
int status;
waitpid(pid, &status, 0);
if(WIFEXITED(status)) <
printf(“Exit normally with code %in”, WEXITSTATUS(status));
>
if(WIFSIGNALED(status)) <
printf(“killed with signal %in”, WTERMSIG(status));
>
break;
>
>
return 0;
>
$ gcc test.c && ./a.out
killed with signal 6
Бывают случаи, при которых родитель завершается раньше, чем ребёнок. В таких случаях родителем ребёнка станет init и он применит вызов wait(2), когда придёт время.
После того, как родитель забрал информацию о смерти ребёнка, ядро стирает всю информацию о ребёнке, чтобы на его место вскоре пришёл другой процесс.
Благодарности
Спасибо Саше «Al» за редактуру и помощь в оформлении;
Спасибо Саше «Reisse» за понятные ответы на сложные вопросы.
Они стойко перенесли напавшее на меня вдохновение и напавший на них шквал моих вопросов.
В статье описан жизненный цикл процессов и приведены
особенности, возникающие при разработке средств разграничения доступа в ОС
Linux. Предлагается способ «отслеживания» процессов ОС для однозначного и
точного определения субъекта доступа в момент получения доступа к объектам ОС.
Linux, разграничение доступа, fork, exec, setuid
Linux: process life cycle and access control
Author describes life cycle of processes and
singularities arising during development of tools for access control in Linux
operating systems. Article provides method of «tracking»
processes for unambiguous and precise definition of subject which gains access
to objects in operating system.
Linux, access
control, fork, exec, setuid
В общем случае
для корректной реализации системы защиты информации от несанкционированного
доступа (СЗИ НСД) в Linux вне зависимости от реализуемой политики разграничения
доступа (дискреционная, мандатная или любая другая) ко всему прочему,
свойственному только ОС на базе ядра Linux [1, 2, 3, 4], необходимо для любого
типа доступа к объектам ОС ответить на главный вопрос — какой именно субъект
доступа в данный момент осуществляет доступ? На первый взгляд этот вопрос может
показаться не столь сложным, однако тут есть множество ньюансов.
Итак, субъектом
доступа всегда является процесс ОС, который выполняется от имени некоторого
субъекта доступа — пользователя ОС, которому соответствует некоторый уникальный
идентификатор (UID, Useridentifier). У каждого процесса на уровне ядра ОС
существует описывающая его структура task_struct, в которой содержатся
такие значения как реальный и эффективный UID (для версий ядра Linux >=
2.6.29 эти значения перенесены в структуру cred внутри структуры task_struct).
Не вдаваясь в отличия этих двух UID-ов, отметим только то, что в зависимости от
их значений процесс выполняется от имени того или иного пользователя ОС.
Таким образом, в
общем случае присутствие определенного пользователя в ОС (т. е.
пользователя, прошедшего процедуру идентификации и аутентификации и имеющего
незавершенные задачи или запущенный интерактивный шелл) означает наличие хотя бы одного процесса с UID, соответствующим UID-у этого пользователя.
Жизненный цикл процессов ОС
Какой UID имеют
процессы изначально? Откуда возникают процессы с различными UID?
На самом раннем
этапе загрузки ОС Linux, а именно, в момент загрузки ядра создается самый
первый процесс — swapper или sched (так называемый
процесс 0, процесс имеющий Process ID/PID = 0). Этот процесс, вообще говоря,
нельзя считать реальным процессом, скорее это функция ядра ОС, ответственная за
организацию управления памятью и некоторые другие возможности. В ходе
дальнейшей загрузки ядра ОС (start_kernel()) запускается по сути первый процесс
пользовательского режима init, который имеет PID = 1. Этот процесс в дальнейшем
является родителем всех процессов, запускаемых в ОС (PID-ы всех остальных
процессов нефиксированы), в т. ч. и процессов реальных пользователей. Само собой
по умолчанию и процесс swapper/sched, и init имеют UID = 0
(т. е. выполняются от имени пользователя root в ОС Linux).
Сами процессы в ходе своей жизни
могут пребывать в следующих состояниях (рис. 1): состояние выполнения
(running), состояние сна (uninterruptible/interruptible sleep), состояние остановки
выполнения (stopped), zombie-состояние.
Рисунок 1: жизненный цикл процесса
При этом в процессе выполнения
каждый процесс может создавать новые процессы (child process), по отношению к
которым он будет предком-родителем, т. е. parent process (рис. 2). Эти новые
процессы могут запускать на выполнение другие задачи (fork()&exec()) или выполняться дальше (fork()), с
возможностью порождения других процессов в обоих случаях.
Рисунок 2: создание новых процессов (fork() и fork()&exec())
Таким образом, все процессы ОС можно
представить в виде дерева, корнем которого является процесс init. Необходимо отметить, что при
создании child process наследуют большинство параметров от своего
предка-родителя, в т. ч. и UID. Однако, далеко не все процессы даже во время
загрузки ОС Linux будут постоянно иметь UID = 0, наследую его от процесса init.
Существует
возможность изменить текущий UID процесса с помощью системного вызова setuid(). Так, в ходе загрузки ОС Linux,
некоторые процессы будут иметь UID, соответствующий некоторым сервисам/демонам
для которых существуют учетные записи в ОС (например, apache, mysql и другие
учетные записи, у которых обычно в /etc/passwd вместо реального шелла прописаны
/sbin/nologin или /bin/false). Также в ходе штатной процедуры идентификации и
аутентификации в ОС (login, gdm и т. п.) UID некоторого процесса, который в
дальнейшем запустит шелл или сессию пользователя, будет заменен на UID
реального пользователя.
Контроль создания и выполнения процессов в СЗИ НСД
Чтобы в любой
момент времени иметь возможность однозначно ответить на вопрос «Какой именно
субъект доступа осуществляет доступ?» и при этом реализовывать некоторое внешнее
по отношению к ОС СЗИ НСД в ОКБ САПР в рамках продукта ПАК СЗИ НСД «Аккорд-Х»
было решено реализовать следующее:
1. Начиная
с момента загрузки СЗИ НСД на раннем этапе загрузки ОС помечать все
существующие и вновь создаваемые (во время операций fork() и/или exec())
процессы собственными дескрипторами безопасности. В таком случае в каждый
момент времени в ОС не будет существовать процессов, которые не помечены
каким-либо дескриптором безопасности;
2. Для
всех уже существующих процессов ОС на момент загрузки ставить в соответствие
дескрипторы с субъектом доступа типа shadow с именем root (это специальный
субъект доступа, от которого выполняется процесс init и большинство процессов
во время загрузки ОС);
3. Во
время выполнения setuid() можно
изменять субъект доступа в дескрипторе безопасности. При этом необходимо
придерживаться определенных правил смены субъектов доступа, которые описаны
ниже.
В продукте ПАК
СЗИ НСД «Аккорд-Х» на данный момент используется 2 типа субъектов доступа:
- user
— реальные пользователи ОС, которые могут проходить процедуру идентификации и
аутентификации; - shadow
— псевдо-пользователи, от имени которых могут выполняться определенные процессы
в ОС, но которые не могут проходить процедуру идентификации и аутентификации
(примером таких пользователей являются, например, различные сервисы/демоны).
В связи с этим
логика условий и правил для смены субъекта доступа в рамках setuid() будет разной в зависимости от
того, какой тип субъекта доступа соответствует текущему процессу:
- Изменять субъект доступа типа user на другой субъект
доступа типа user можно только в случае успешного прохождения процедуры
идентификации/аутентификации в СЗИ НСД одним из процессов-предков текущего
процесса и при условии, что UID в setuid() совпадает с UID из процедуры идентификации/аутентификации. Т. к. в разных
дистрибутивах ОС Linux процедуры идентификации/аутентификации и, собственно,
смены UID-а могут призводить разные, даже достаточно далекие «в родстве»,
процессы — необходимо вместе с дескриптором безопасности наследовать (при
операциях fork() и fork()&exec()) от процесса-родителя в т. ч.
и информацию о пользователе, который осуществил успешную процедуру
идентификации / аутентификации.; - Изменять субъект доступа типа shadow после
идентификации/аутентификации в СЗИ НСД:
- можно на субъект доступа типа user, если UID в setuid() совпадает с UID из процедуры
идентификации и аутентификации (такая замена является следствием обычного
логина пользователя в ОС); - необходимона другой субъект доступа типа shadow, если текущий и
заменяющий UID оба равны 0 и UID в setuid() не совпадает с UID из процедуры идентификации и аутентификации.
- Изменять субъект доступа типа shadow без
идентификации/аутентификации в СЗИ НСД:- на субъект доступа типаuserнельзя;
- можно на другой субъект доступа типаshadow, если такой
субъект доступа shadow существует и у
текущего субъекта доступа есть разрешение делать setuid().
В соответствии с
приведенными правилами выполнения setuid() в системе при
использовании СЗИ НСД должно присутствовать некоторое количество пользователей
типа shadow. Для корректного создания таких пользователей shadow (сервисов, демонов и т. п.) необходимо предусмотреть
режим работы СЗИ НСД, в котором все переключения на пользователей типа shadow будут фиксироваться и заноситься в журнал (т.н. лог
работы СЗИ НСД в «мягком» режиме). В дальнейшем из журнала необходимо заносить
таких пользователей типа shadow в список
пользователей СЗИ НСД.
«Мягкий» режим
работы является средством обучения СЗИ НСД, поэтому все субъекты доступа типа shadow, которые не были отображены в журнале и,
соответственно, не были созданы в самом СЗИ НСД, не смогут корректно работать в
обычном режиме работы с включенными политиками разграничения доступа. В связи с
этим в «мягком» режиме работы СЗИ НСД желательно достаточно точно эмулировать
работу пользователей ОС с целью создания как можно более точного
соответствия/наличия в системе пользователей типа shadow.
В противном случае при возникновении каких-либо ошибок придется повторно
«дообучать» СЗИ НСД при дальнейшем использовании.
Как было
отмечено выше, желательно для всех пользователей типа shadow
завести параметры с разрешением/запрещением делать операции setuid() (т.н. setuid_ability) и делать setuid() на UID = 0 (т.н. setuid_root_ability). Эти параметры
призваны исключить возможность эскалации привилегий и должны запрещать все
действия setiud(), если
выбранному субъекту доступа типа shadow в своей работе
не требуется переключения на другие учетные записи (как правило, это требуется
всего нескольким shadow, например при
входе пользователей для открытия /etc/passwd и т. п.).
Заключение
В заключении
хотелось бы отметить, что описанный выше способ является не единственным
способом для корректной «пометки» процессов соответствующими дескрипторами безопасности.
Вторым, более простым вариантом можно считать аналогичную маркировку процессов
в начале работы СЗИ НСД и при всех операциях fork() и/или exec(), но со сменой субъекта доступа на этапе
успешного прохождения процедуры идентификации и аутентификации (т. е. по сигналу
от специального PAM-модуля), без
учета каких-либо событий setuid(). Этот способ
является вполне корректным, но в меньшей степени соответствует реальному
поведению процессов, т. к., например, после прохождения идентификации и
аутентификации, процессы, помеченные дескриптором зашедшего пользователя, будут
производить множество системных действий, на которые пользователю, вообще
говоря, нельзя выдавать права доступа (доступ к /etc/passwd, различным сервисам, активирующимся при старте
графической пользовательской сессии и т. д.).
Рассмотренный же
в данной статье способ «отслеживания» процессов ОС является по сути практически
полноценной эмуляцией реального поведения процессов и не требует назначения
пользователю каких-либо лишних прав в системе. Т. к. часто для защиты ОС
используются внешние по отношению к ОС СЗИ НСД — такая эмуляция необходима,
т. к. без неё нельзя с достаточной степенью уверенности сказать, что
определенный доступ осуществил в действительности тот или иной пользователь, а
не другой процесс/сервис/демон ОС.
Литература
- Каннер А. М. Linux: о доверенной
загрузке загрузчика ОС // Безопасность информационных технологий. М., 2013. N
2. С. 41–46. - Каннер А. М. Linux: объекты контроля целостности // Информационная безопасность. Материалы XIII Международной конференции. Таганрог., 2013. Часть 1. С. 112–118. 14.
- Каннер А. М. Linux: к вопросу о построении системы защиты на основе абсолютных путей к объектам доступа // Информационная безопасность. Материалы XIII Международной конференции. Таганрог., 2013. Часть 1. С. 118–121. 15.
- Каннер А. М., Ухлинов Л. М. Управление доступом
в ОС GNU/Linux // Вопросы защиты информации. Научно-практический журнал. М.,
2012. № 3. С. 35–38.
Автор: Каннер А. М.
Дата публикации: 01.01.2014
Выходные данные: Вопросы защиты информации: Научно-практический журнал/ФГУП «ВИМИ», 2014. Вып. 4 (107). С. 37-40
Источник