From: Bob <ubob at mail.ru>
Newsgroups: email
Date: Mon, 14 Sep 2004 14:31:37 +0000 (UTC)
Subject: Процессы в Linux
ПРОЦЕССЫ В LINUX
----------------
Характерной чертой современных операционных систем является поддержка
многозадачности - параллельного выполнения нескольких задач (task), что
обеспечивается, главным образом, аппаратными возможностями центрального
процессора.
Под задачей понимают экземпляр программы, которая находится в стадии выполнения.
Синонимом термина "задача" является термин "процесс", который впервые начали
применять разработчики системы MULTICS в 60-х годах прошлого века.
1. Общие сведения о процессах.
------------------------------
Процесс состоит из адресного пространства и набора структур данных, содержащихся
внутри ядра операционной системы. Адресное пространство представляет собой
совокупность страниц памяти, которое ядро выделило для выполнения процесса.
Адресное пространство содержит сегменты кода для программы, которую выполняет
процесс, используемые процессом переменные, стек процесса и различную
вспомогательную информацию, необходимую ядру во время работы процесса.
В структурах данных ядра хранится различная информация о каждом процессе. К
наиболее важным сведениям относятся:
- таблица распределения памяти процесса;
- текущий статус процесса;
- приоритет выполнения процесса;
- информация о ресурсах, которые использует процесс;
- владелец процесса.
Совокупность всех процессов, выполняющихся в системе, образует иерархическую
структуру, подобную дереву каталогов файловой системы. На вершине дерева
процессов находится управляющий процесс init, являющийся предком все системных и
пользовательских процессов.
С каждым процессом связан набор атрибутов, которые помогают системе управлять
выполнением и планированием процессов. С точки зрения системного
администрирования интерес представляют следующие атрибуты:
1) Идентификатор процесса (PID).
Каждому новому процессу ядро присваивает уникальный идентификационный номер. В
любой момент времени идентификатор процесса является уникальным, хотя после
завершения процесса он может использоваться снова для другого процесса.
Некоторые идентификаторы зарезервированы системой для особых процессов. Так,
процесс с идентификатором 1 - это процесс инициализации init, являющийся предком
всех других процессов в системе.
2) Идентификатор родительского процесса (PPID).
Новый процесс создается путем клонирования одного из уже существующих процессов.
Исходный процесс в терминологии UNIX называется родительским, а его клон -
порожденным. Помимо собственного идентификатора, каждый процесс имеет атрибут
PPID, т.е. идентификатор своего родительского процесса.
3) Идентификатор владельца (UID) и эффективный идентификатор владельца.
UID - это идентификационный номер пользователя, который создал данный процесс.
Вносить изменения в процесс могут только его создатель и привилегированный
пользователь. EUID - это "эффективный" UID процесса. EUID используется для того,
чтобы определить, к каким ресурсам и файлам у процесса есть право доступа. У
большинства процессов UID и EUID будут одинаковыми. Исключение составляют
программы, у которых установлен бит смены идентификатора пользователя.
4) Идентификатор группы GID и эффективный идентификатор группы (EGID)
GID - это идентификационный номер группы данного процесса. EGID связан с GID
также, как EUID с UID.
2. Жизненный цикл процесса
--------------------------
Все процессы, кроме init, создаются при помощи системного вызова fork (процесс
init создается во время начальной загрузки системы). Вызывая функцию fork,
процесс создает свой дубликат, называемый дочерним процессом. Дочерний процесс
является практически точной копией родительского, но имеет следующие отличия:
- у дочернего процесса свой PID;
- PPID дочернего процесса равен PID родителя;
После выполнения fork родительский процесс может посредством системного вызова
wait или waitpid приостановить свое выполнение до завершения порожденного
(дочернего) процесса, или продолжать свое выполнение независимо от дочернего, а
дочерний процесс, в свою очередь, может запустить на выполнение новую программу
при помощи одного из системных вызовов семейства exec.
Совместное применение системных вызовов fork и exec представляют мощный
инструмент для программиста. Благодаря ветвлению при использовании вызова exec в
дочернем процессе может выполнятся другая программа. Таким образом, один процесс
может создавать несколько других процессов для параллельного выполнения
нескольких программ, и поскольку каждый порожденный процесс выполняется в
собственном адресном пространстве, статус его выполнения не влияет на
родительский процесс.
Процесс завершает выполнение при помощи системного вызова exit. Аргументом этого
вызова является код статуса завершения процесса. Младшие восемь бит статуса
доступны родительскому процессу при условии, что он выполнил системный вызов
wait. По соглашению, нулевой код статуса завершения означает, что процесс
завершил выполнение успешно, а ненулевой свидетельствует о неудаче.
Следующий пример демонстрирует работу вызова fork и порядок обработки кода
статуса завершения процесса:
int main()
{
pid_t pid; // идентификатор процесса
int status; // код статуса завершения процесса
/* При помощи fork cоздаем дочерний процесс */
switch(pid = fork()) {
case -1:
perror("fork");
return -1;
case 0:
printf("Выполняется дочерний процессn");
/* Код статуса завершения равен 4 */
exit(4);
}
printf("Выполняется родительский процессn");
printf("Идентификатор дочернего процесса - %dn", pid);
/*
* Ждем завершения дочернего процесса
* и обрабатываем код статуса завершения
*/
if((pid = waitpid(pid, &status, 0)) && WIFEXITED(status)) {
printf("Дочерний процесс с PID = %d завершил выполнениеn", pid);
printf("Код статуса завершения равен %dn", WEXITSTATUS(status));
}
return 0;
}
-------------------------------
Функция waitpid приостанавливает выполнение родительского процесса, пока не
завершится порожденный процесс. Первый аргумент этой функции (pid) указывает,
завершения какого именно порожденного процесса следует ожидать.
Первый аргумент может принимать следующие значения:
- > 0 - ждать завершения процесса с данным идентификатором
- 0 - ждать завершения любого порожденного процесса, принадлежащего к той же
группе, что и родительский
- -1 - ждать завершения любого порожденного процесса
- < -1 - любого порожденного процесса, идентификатор группы (GID) которого
является абсолютным значением pid
Второй аргумент будет содержать код статуса завершения процесса, поэтому он
передается по ссылке. Возвращаемым значением функции будет PID порожденного
процесса.
В нашем примере значение первого аргумента равно идентификатору дочернего
процесса.
Для обработки код статуса завершения процесса используются два макроса,
WIFEXITED и WEXITSTATUS, которые определены в файле <sys/wait.h>.
Макрос WIFEXITED возвращает ненулевое значение, если порожденный процесс был
завершен посредством вызова exit. Макрос WEXITSTATUS возвращает код завершения
порожденного процесса, присвоенного вызовом exit. Оба этих макроса обрабатывают
значение второго аргумента функции waitpid - переменной status. Эта переменная
имеет следующий формат:
- биты 0 - 6 - содержат нуль, если порожденный процесс был завершен с помощью
функции exit, или номер сигнала, завершившего процесс.
- бит 7 - равен 1, если из-за прерывания порожденного процесса сигналом был
создан дамп образа процесса (файл core). В противном случае равен 0
- биты 8 - 15 - содержат код завершения порожденного процесса, переданным
посредством exit.
А теперь немного изменим приведенный выше пример для демонстрации возможности
запуска в дочернем процессе новой программы:
В дочернем процессе при помощи системного вызова exec запускается на выполнение
программа gzip для сжатия файл test.txt.
3. Получение информации о процессе при помощи proc
--------------------------------------------------
Главным источником информации о процессах на пользовательском уровне является
файловая система proc. Для доступа к этой информации достаточно перейти в
каталог /proc. Информация о каждом процессе собрана в отдельном подкаталоге, имя
которого совпадает с идентификационным номером процесса. Так, например, информация о
процессе init находится в подкаталоге 1, т.к. идентификационный номер этого
процесса зарезервирован и равен 1.
На примере процесса init рассмотрим, какие файлы присутствуют в каталоге
процесса и какую информацию о процессе они содержат (какая информация в них
содержится):
Каждый файл содержит определенную информацию о процессе:
- cmdline - список аргументов процесса
- cwd - символическая ссылка на текущий рабочий каталог процесса
- environ - переменные среды процесса
- exe - символическая ссылка на исполняемый файл процесса
- fd - подкаталог, содержащий ссылки на файлы, открытые процессом
- maps - адресное пространство, выделенное процессу
- root - символическая ссылка на корневой каталог процесса
- mounts - информация о точках монтирования и типах файловых систем
- status - статистическая информация о процессе (имя процесса, идентификационный
номер, состояние процесса, идентификатор владельца, группы, статистика
использования памяти и т.д.)
Таким образом, при помощи /proc можно получить исчерпывающую информацию об
интересующем процессе, используя имеющийся в нашем распоряжении инструментарий -
команды shell либо средства языка программирования.
4. Представление процессов в ядре
---------------------------------
Совокупность процессов в ядре Linux представляет собой кольцевой двусвязный
список структур struct task_struct. Структура struct task_struct определена в
файле <linux/sched.h> и содержит полную информацию о выполняемом процессе. Для
нас интерес представляют следующие поля этой структуры:
1) volatile long state - статус выполняемого процесса.
Может принимать следующие значения:
- TASK_RUNNING - процесс находится в очереди запущенных на выполнение задач;
- TASK_INTERRUPTIBLE - процесс в состоянии "сна", но может быть "разбужена" по
сигналу или по истечении таймера;
- TASK_UNINTERRUPTIBLE - состояние процесса схоже с TASK_INTERRUPTIBLE, только
он не может быть разбужена;
- TASK_ZOMBIE - процесс-"зомби". Процесс завершил свою работу до того, как
родительский процесс выполнил системный вызов wait;
- TASK_STOPPED - выполнение процесса остановлено.
Все эти значения определены в файле <linux/sched.h>:
В состав структуры struct mm_struct входит структура struct vm_area_struct *
mmap, в которой находятся данные об областях памяти, выделенных процессу. Два
поля этой структуры, vm_start и vm_end, содержат адреса памяти, которую
использует процесс.
Детальное рассмотрение структуры struct vm_area_struct выходит за рамки данной
статьи, для дальнейшей работы нам достаточно и этой информации.
3) pid_t pid - идентификационный номер процесса
4) uid_t uid, euid, suid, fsuid - идентификаторы владельца процесса
5) gid_t gid, egid, sgid, fsgid - идентификаторы группы, к которой принадлежит
данный процесс
6) char comm[16] - символьное имя процесса
7) struct fs_struct *fs - информация о файловой системе.
Сама структура struct fs_struct определена в файле <linux/fs_struct.h>. Вот как
она выглядит:
Информация о точках монтирования корневого каталога и о текущем каталоге
процесса находится в полях struct dentry *root и *pwd.
8) struct files_struct *files - информация о файлах, открытых процессом.
Состав структуры struct files_struct (см.<linux/sched.h>:
/* Open file table structure */
struct files_struct {
atomic_t count;
rwlock_t file_lock; /* Protects all the below members. Nests insidetsk->alloc_lock */
int max_fds;
int max_fdset;
int next_fd;
struct file ** fd; /* current fd array */
....
struct file * fd_array[NR_OPEN_DEFAULT];
};
В поле next_fd находится число открытых процессом файлов, а в массиве структур
struct file ** fd собрана информация об этих файлах. Структура struct file
определена в <linux/fs.h>.
9) struct signal_struct *sig - указатели на обработчики сигналов.
Определение struct signal_struct находится в <linux/signal.h>:
В массиве структур struct k_sigaction action[_NSIG] находятся указатели на
функции, которые вызывает процесс при получении сигналов. Структура struct
k_sigaction определена в <asm-i386/signal>:
struct k_sigaction {
struct sigaction sa;
};
Структура struct sigaction определена в этом же файле:
struct sigaction {
__sighandler_t sa_handler;
unsigned long sa_flags;
void (*sa_restorer)(void);
sigset_t sa_mask; /* mask last for extensibility */
};
/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);
Адрес обработчика сигнала находится в поле __sighandler_t sa_handler структуры
struct sigaction. Это поле может принимать следующие значения, определенные в
<asm-i386/signal.h>:
#define SIG_DFL ((__sighandler_t)0) /* default signal handling */
#define SIG_IGN ((__sighandler_t)1) /* ignore signal */
Значение SIG_DFL требует выполнения стандартного действия. Отметим, что SIG_DFL
эквивалентен NULL. Значение SIG_IGN означает, что сигнал будет игнорироваться.
Также в этом поле может находиться адрес функции, которая будет вызвана по
приходу сигнала.
Поле sigset_t sa_mask представляет собой набор сигналов, которые должны быть
заблокированы в течении обработки данного сигнала. Например, если для процесса
необходимо заблокировать сигналы SIGHUP и SIGINT, пока обрабатывается сигнал
SIGCHLD, тогда относящаяся к SIGCHLD sa_mask для процесса устанавливает разряды,
соответствующие SIGHUP и SIGINT.
Определение sigset_t находится в <asm-i386/signal.h>:
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t;
Единственный компонент в sigset_t - это массив из unsigned long, каждый разряд
которого соответствует одному сигналу. Номера всех сигналов перечислены в
<asm-i386/signal.h>.
10) sigset_t blocked - маска сигналов, заблокированных процессом. Для
блокирования сигнала соответствующий бит устанавливается в 1.
11) struct sigpending pending - содержит номера сигналов, посылаемых процессу.
Эта структура определена в <linux/sched.h> следующим образом:
sigset_t signal, как мы уже рассмотрели, является простой последовательностью
бит, и посылка сигнала процессу означает установку бита в соответствующей
позиции в 1.
Для более детального ознакомления с вышеперечисленными полями разработаем модуль
ядра, который при загрузке будет отображать информацию об определенном процессе,
подобно тому, как это делает /proc (см. "Получение информации о процессе при
помощи proc").
Для решения этой задачи нам понадобится какой-нибудь процесс. Лучше всего, если
он будет функционировать в фоновом режиме (в режиме демона, daemon). Этот
процесс после запуска будет выполнять следующие действия:
- перехватывать все сигналы, а для сигнала SIGUSR1 определять новый обработчик
- открывать исполняемый файл программы, находящийся в текущем каталоге
Создадим такой процесс при помощи следующего кода:
static pid_t pid; // идентификатор создаваемого процесса
int main ()
{
pid = fork();
if (pid < 0) {
perror("fork");
return -1;
}
if (pid == 0) { // дочерний процесс
setsid(); // отсоединяемся от терминала
start_daemon();
}
return 0;
}
-------------------------------
Функция start_daemon() выполняет следующие задачи:
- перехватывает все сигналы
- для сигнала SIGUSR1 определяет новый обработчик
- открывает исполняемый файл программы, находящийся в текущем каталоге
- запускает на выполнение бесконечный цикл
-------------------------------
void start_daemon()
{
int i, out;
sigset_t mask;
static struct sigaction act;
sigfillset(&mask);
sigdelset(&mask, SIGUSR1);
/* Блокируем все сигналы */
sigprocmask(SIG_SETMASK, &mask, NULL);
/* Определяем новый обработчик для SIGUSR1 */
act.sa_handler = stop_daemon;
sigaction(SIGUSR1, &act, NULL);
/* Открываем исполняемый файл программы */
out = open("./sfc", O_RDONLY);
if(out < 0) perror("open");
for(;;);
}
-------------------------------
Новый обработчик сигнала SIGUSR1 просто завершит выполнение демона, вызвав
функцию exit:
void stop_daemon()
{
exit(0);
}
Получаем исполняемый файл и запускаем его на выполнение:
# gcc -o sfc sfc.c
# ./sfc
Процесс начинает свое выполнение в фоновом режиме. Найдем его в списке
процессов:
# ps -ax | grep sfc
903 ? R 0:02 ./sfc
Итак, процесс ./sfc успешно выполняется, и его PID равен 903. Теперь приступим к
реализации модуля ядра.
Первое, что должен сделать модуль после загрузки - найти в списке структур
struct task_struct структуру, соответствующую процессу sfc. Поиск выполняется
при помощи функции find_task_by_name(). Аргументом функции является имя искомого
процесса, возвращаемое значение - указатель на структуру процесса struct
task_struct.
Функция find_task_by_name выглядит следующим образом:
Функция find_task_by_name будет вызвана во время процедуры инициализации модуля:
static int __init task_on(void)
{
struct task_struct *p;
int pid = 0;
int i;
Ищем структуру, которая описывает процесс sfc:
p = find_task_by_name("sfc");
Если поиск завершился удачно, отобразим PID процесса (результаты работы модуля
будут сохранены в файле /var/log/messages):
if(p) printk(KERN_INFO "PID - %dn", p->pid);
else {
printk(KERN_INFO "No such taskn");
return 0;
}
Можно также выполнить поиск процесса по заданному PID при помощи функции
find_task_by_pid(int pid) (см. <linux/sched.h>). Повторим поиск процесса,
используя найденный PID:
pid = p->pid;
p = find_task_by_pid(pid);
Процесс найден. Отобразим результаты поиска:
- имя процесса и его состояние:
printk(KERN_INFO "NAME - %sn", p->comm);
printk(KERN_INFO "STATE - %un", (u32)p->state);
Процесс sfc уже запущен на выполнение. После загрузки модуля информация об этом
процессе будет собрана в файле /var/log/messages. Cравните результаты работы
модуля с данными о процессе, которые находятся в /proc.
Остановимся подробнее на информации, касающейся адресов обработчиков сигналов:
Как мы видим, все адреса обработчиков сигналов имеют значение SIG_DFL, т.е.
NULL. Это значит, что при поступлении любого из этих сигналов процессу система
выполнит стандартные действия.
Исключение составляет сигнал под номером 9 (10-й в списке). Согласно перечню,
приведенному в <asm-i386/signal.h>, это сигнал SIGUSR1:
#define SIGUSR1 10
Именно для этого сигнала мы ввели новый обработчик, функцию stop_daemon.
Извлечем адрес этой функции из исполняемого файла sfc и сравним его с
результатом, полученным при работе модуля:
# objdump -x ./sfc | grep stop_daemon
080484D8 g F .text 00000010 stop_daemon
Адрес фунции stop_daemon - нового обработчика сигнала SIGUSR1 - равен
0x080484D8. Точно такое же значение выдал и модуль.
Согласитесь, что просто смотреть на процесс - неинтересно. Давайте им управлять.
Пошлем процессу sfc из модуля сигнал SIGUSR1, при получении которого процесс
завершить свое выполнение.
Для того, чтобы из ядра послать процессу сигнал, необходимо установить в
структуре struct sigpending pending бит, соответствующий порядковому номеру
посылаемого сигнала, в единицу, а также присвоить единичное значение полю
sigpending, которое служит индикатором того, что процесс получил сигнал и его
надо обработать.
Перепишем функцию инициализации модуля - вместо отображения информации о
процессе sfc, модуль будет посылать ему сигнал SIGUSR1:
Функция инициализации модуля будет выглядеть следующим образом:
static int __init task_on(void)
{
struct task_struct *p;
Ищем процесс sfc:
p = find_task_by_name("sfc");
if(p) printk(KERN_INFO "PID - %dn", p->pid);
else {
printk(KERN_INFO "No such taskn");
return 0;
}
В структуре struct sigpending pending устанавливаем бит, соответствующий сигналу
SIGUSR1:
sigaddset(&p->pending.signal, SIGUSR1);
p->sigpending = 1; // индикатор прихода сигнала
return 0;
}
Функция sigaddset устанавливает в 1 бит с указанным номером. Номер бита
передается как параметр функции. Эта функция (платформенно-зависимый вариант)
определена в файле <asm-i386/signal.h>:
Загрузив модуль, мы тем самым отправим процессу sfc сигнал SIGUSR1 и остановим
его выполнение.
Кстати, совсем не обязательно переопределять обработчик сигнала в самом процессе
- это можно сделать в модуле, вписав адрес нового обработчика непосредственно в
поле sa_handler.
Посмотрим, как это делается. Перепишем функцию start_daemon процесса, убрав из
нее переопределение обработчика сигнала SIGUSR1:
/* Блокируем все сигналы */
sigprocmask(SIG_SETMASK, &mask, NULL);
for(;;);
}
В функции start_daemon() заблокированы все сигналы, новые обработчики не
определены. Функцию stop_daemon оставим без изменений.
Получаем исполняемый файл и определяем адрес функции stop_daemon:
# gcc -o sfc sfc.c
# objdump -x ./sfc | grep stop_daemon
08048434 g F .text 00000010 stop_daemon
Адрес функции stop_daemon равен 0x08048434. Этот адрес будет указан в качестве
нового обработчика сигнала SIGUSR2 для процесса sfc.
Переопределение обработчика сигнала выполняет непосредственно модуль, в
следствии чего функция инициализации модуля принимает следующий вид:
static int __init task_on(void)
{
struct task_struct *p;
Ищем структуру, соответствующую процессу sfc:
p = find_task_by_name("sfc");
if(p) printk(KERN_INFO "PID - %dn", p->pid);
else {
printk(KERN_INFO "No such taskn");
return 0;
}
Устанавливаем адрес нового обработчика сигнала SIGURS2 - вписываем адрес функции
stop_daemon в поле адреса обработчика sa_handler. Порядковый номер сигнала
SIGUSR2 известен и равен 12 (см. <asm-i386/signal.h>):
Если мы сейчас же пошлем сигнал процессу, то он его не воспримет. Почему? Дело в
том, что все сигналы на данный момент заблокированы в функции start_daemon.
Чтобы процесс воспринял приход сигнала SIGUSR2, нужно его разблокировать. Для
этого необходимо сбросить соответствующий бит в маске заблокированных сигналов -
в поле sigset_t blocked структуры task_struct:
Подведем предварительные итоги - мы выяснили, как при помощи модуля ядра можно
получить информацию о выполняющемся процессе, и как из ядра послать процессу
сигнал.
Рассмотрим еще один пример работы с содержимым структуры task_struct.
Предположим, что в системе зарегистрирован пользователь play. Идентификатор
этого пользователя (UID) равен 1000, и принадлежит он к группе users (GID=100).
От имени этого пользователя в фоновом режиме выполняется процесс, который по
приходу сигнала SIGUSR2 пытается добавить в файл /etc/passwd новую учетную
запись для пользователя play1, обладающего правами root:
play1::0:0:,,,:/home/play1:/bin/bash
Очевидно, что попытка записи в файл /etc/passwd какой-либо информации будет
безуспешной, если процесс не обладает достаточным уровнем привилегий. Значит,
необходимо выдать этому процессу соответствующие полномочия - права
суперпользователя (root).
Заботу об этом берет на себя модуль ядра, который после загрузки находит
структуру, описывающую процесс, назначает ему права суперпользователя путем
установки полей uid/gid, euid/egid, suid/sgid, fsuid/fsgid структуры процесса в
0, и после этого посылает процессу уведомление о том, что тот получил права root.
Уведомление представляет собой сигнал SIGUSR2, при получении которого процесс
выполняет запись информации в файл /etc/passwd, уже имея для этого
соответствующие полномочия.
Итак, нам необходим процесс, который по сигналу SIGUSR2 будет выполнять запись в
/etc/passwd. Модифицируем уже имеющийся в нашем распоряжении процесс sfc.
Изменениям подвергнутся только функции start_daemon и stop_daemon.
В функции start_daemon определим новый обработчик для сигнала SIGUSR2:
Компилируем и запускаем процесс sfc от имени пользователя play. После этого
загружаем модуль и пробуем зайти в систему под именем play1.
Кроме присвоения привилегий процессу sfc, модуль отображает также информацию о
текущем выполняющемся процессе:
kernel: Current - insmod
Именно с помощью команды insmod мы загружаем модуль. Вопрос - каким образом
можно воздействовать на текущий процесс? Ведь по сравнению с фоновым он надолго
в памяти не задерживается. Например, тот же insmod - загрузил модуль и сразу на
выход.
По этому поводу очень интересный пример был приведен в 59-м выпуске электронного
журнала PHRACK (www.phrack.com), автор которого kad (реальное имя не было
указано, только e-mail - kadamyse@altern.org) показал, как можно присвоить права
root текущему процессу пользователя, используя для этой цели исключения (1).
Основная идея заключается в перехвате исключения номер 3, Breakpoint
(мнемоническое обозначение #BP), которое возникает при работе отладчика.
Перехват представляет собой простую замену адреса обработчика исключения #BP в
таблице дескрипторов прерываний (IDT, Interrupt Descriptor Table) адресом нового
обработчика. При возникновении #BP управление передается новому обработчику,
который вернет управление старому, но не сразу - сначала он проверит, кем было
сгенерировано исключение #BP, т.е. какой процесс является текущим, current.
Проверка выполняется путем сравнения значения, находящегося в поле comm
структуры процесса, с шаблонным значением, которое хранится в новом обработчике
#BP. Короче говоря, сравниваются две строки, при совпадении которых текущий
процесс (назовем его "test") получает привилегии root:
Для того, чтобы процесс вызвал исключение #BP, его необходимо запустить в
отладчике и установить где-нибудь точку останова. Как только эта точка будет достигнута,
будет сгенерировано исключение #BP и управление получит новый обработчик.
// Прототип нового обработчика исключения #BP
extern void my_stub();
__u32 idt_addr = 0; // адрес таблицы IDT
__u32 old_handler = 0; // адрес старого обработчика исключения #BP
__u32 new_handler = 0; // адрес функции, которая будет вызвана перед обработчиком
// исключения #BP.
Формат дескриптора IDT определяет следующая структура:
Функция get_idt_addr() считывает содержимое регистра IDTR в структуру idtr при
помощи инструкции SIDT. В результате в поле base этой структуры будет находиться
искомый базовый адрес IDT.
После того, как найден базовый адрес IDT, создаем новую таблицу
дескрипторов прерываний, а затем в ней производим подмену адреса исключения #BP:
-------------------------------
void set_new_idt()
{
unsigned long flags;
/* Выделяем память для новой таблицы IDT и копируем в нее
* содержимое старой таблицы:
*/
idt = (idt_t *)kmalloc(255 * sizeof(idt_t), GFP_KERNEL);
memcpy((void *)idt, (void *)idt_addr, 255 * sizeof(idt_t));
/* В поле base структуры idtr заносим новое значение
* базового адреса таблицы и загружаем его в регистр IDTR
* инструкцией LIDT.
*/
idtr.base = (__u32)idt;
Следующая функция выполняет непосредственно то, ради чего все затевалось -
назначает права root процессу test. Эта функция должна быть вызвана перед
обработчиком исключения #BP, для проверки имени текущего процесса:
При совпадении имен текущего процесса и шаблона функция назначает процессу
привилегии root.
Осталось рассмотреть, чем мы заменим стандартный обработчик исключения #BP.
Следующая функция содержит в себе определение вызова нового обработчика
исключения #BP - my_stub():
Сначала команда call вызвает функцию my_handler, адрес которой находится в
переменной new_handler, а после возврата из этой функции команда jmp передает
управление по адресу старого обработчика #BP, который сохранен в переменной
old_handler.
Все функции, которые мы рассмотрели, будут вызваны во время загрузки модуля
ядра:
-------------------------------
int init_module()
{
int i = 3; // номер исключения - Breakpoint (#BP)
__u32 new_addr; // адрес нового обработчика исключения #BP
/* Формируем новую таблицу IDT */
set_new_idt();
printk(KERN_INFO "New IDT address - 0x%08xn",(__u32)(idtr.base));
new_handler = (__u32)&my_handler; // адрес ф-ии my_handler
/* Сохраняем адрес старого обработчика #BP, определяем адрес
* нового и производим замену адресов в новой таблице IDT
*/
old_handler = get_handler(i);
if(old_handler < PAGE_OFFSET) return 0;
printk(KERN_INFO "Old handler - 0x%08xn",old_handler);
new_addr = (__u32)&my_stub;
set_handler(i, new_addr);
printk(KERN_INFO "New address - 0x%08xn",new_addr);
return 0;
}
-------------------------------
Во время выгрузки модуля необходимо восстановить старую таблицу IDT. Ее адрес
сохранен в переменной idt_addr. Адрес обработчика #BP восстанавливать не надо,
так как в старой таблице он остался без изменений.
-------------------------------
void cleanup_module()
{
unsigned long flags;
/* Заносим адрес таблицы IDT в поле base структуры idtr
* а затем командой LIDT загружаем его в регистр IDTR
*/
idtr.base = (__u32)idt_addr;
__save_flags(flags);
__cli();
__asm__("lidt %0"::"m" (idtr));
__restore_flags(flags);
Дошли до точки останова #1. Продолжим выполнение процесса:
(gdb) cont
Continuing.
bash-2.05b$
Запущен новый shell. Проверяем, с какими правами:
bash-2.05b$ id
uid=1000(play) gid=100(users) groups=100(users)
bash-2.05b$
В итоге мы получили в свое распоряжение еще один shell с правами play.
Интересного в этом мало. Другое дело, если запустить shell с правами root.
Завершим работу отладчика:
bash-2.05b$ exit
exit
Program exited normally
(gdb) quit
play@darkstar:~$
Теперь загрузим модуль командой insmod и опять запустим процесс в отладчике:
play@darkstar:~$ gdb -q ./test
(gdb) break main
Breakpoint 1 at 0x804832e
(gdb) run
Starting program: /home/play/test
Breakpoint 1, 0x0804832e in main()
(gdb) cont
Continuing.
bash-2.05b#
Смотрим, какими правами обладает новый shell:
bash-2.05b# id
uid=0(root) gid=0(root) groups=100(users)
bash-2.05b#
На этот раз все получилось так, как мы и предполагали - модуль перехватил
исключение #BP, которое возникло в момент остановки выполнения процесса на
функции main, проверил имя процесса и установил его идентификаторы в 0, повысив,
тем самым, уровень привилегий процесса до root. После этого управление было
передано "родному" обработчику исключения #BP и процесс продолжил свое
выполнение, но уже с другими правами, которые и унаследовал новый shell.
-----------------------------------------------------------------------------
(1) Исключения (exception) - это внутренние прерывания процессора,
сигнализирующие ему о том, что произошла исключительная ситуация, требующая
немедленного вмешательства. Яркий пример исключения - ошибка деления на 0,
Divide Error, мнемоническое обозначение #DE.
Подробная информация об исключениях и полный их перечень приведен в IA-32 Intel
Architecture Software Developer's Manual, Volume 3: System Programming Guide,
Chapter 5 "Interrupt and Exception Handling" (http://www.intel.com).
Cтатья была опубликована в журнале "Системный администратор", 6/2004 г.,
http://www.samag.ru
597 Прочтений • [Процессы в Linux (linux proccess ipc kernel proc)] [08.05.2012] [Комментариев: 0]