2) Сборка ядра. Выполняется командой от пользователя root
# make bzImage modules modules_install install
Перекомпиляция ядра происходит довольно долго. После установки ядра, как правило, автоматически настраивается и загрузчик. Поэтому после этого, следует перезагрузить систему уже от имени нового ядра.
3.2 Написание и внедрение модуля ядра
В программе перехват системных вызовов осуществлён механизмом загружаемого модуля ядра (LKM). В аналитической части были описаны преимущества, которые получает система при использовании данного механизма. Теперь стоит разобраться, как он реализуется.
Каждый LKM состоит минимум из двух основных функций:
· intinit_module(void) , которую вызывает insmod во время загрузки модуля
· void cleanup_module(void), которуювызывает rmmod
Функция init_module() выполняет регистрацию обработчика какого-либо события или замещает какую-либо функцию в ядре своим кодом (который, как правило, выполнив некие специфические действия, вызывает оригинальную версию функции в ядре). Вызывается при загрузке LKM в память.
Функция cleanup_module() является полной противоположностью, она производит "откат" изменений, сделанных функцией init_module(), что делает выгрузку модуля безопасной.
Любой загруженный модуль ядра заносится в список /proc/modules, поэтому при удачной загрузке можно убедиться, что модуль стал частью ядра.
В данном курсовом проекте функция init_module() изменяет соответствующий определённому системному вызову указатель в sys_call_table и сохраняет его первоначальное значение в переменной. Функция cleanup_module() восстанавливает указатель в sys_call_table, используя эту переменную.
В данном подходе кроются свои "подводные камни" из-за возможности существования двух модулей, перекрывающих один и тот же системный вызов. Например, имеется два модуля, А и B. Пусть модуль A перекрывает системный вызов open, своей функцией A_open, а модуль B -- функцией B_open. Первым загружается модуль A, он заменяет системный вызов open на A_open. Затем загружается модуль B, который заменит системный вызов A_open на B_open. Модуль B полагает, что он подменил оригинальный системный вызов, хотя на самом деле был подменен вызов A_open, если модуль B выгрузить первым, то ничего страшного не произойдет -- он просто восстановит запись в таблице sys_call_table в значение A_open, который в свою очередь вызывает оригинальную функцию sys_open. Однако, если первым будет выгружен модуль А, а затем B, то система "рухнет". Модуль А восстановит адрес в sys_call_table, указывающий на оригинальную функцию sys_open, "отсекая" таким образом модуль B от обработки действий по открытию файлов. Затем, когда будет выгружен модуль B, он восстановит адрес в sys_call_table на тот, который запомнил сам, потому что он считает его оригинальным. Т.е. вызовы будут направлены в функцию A_open, которой уже нет в памяти.
На первый взгляд, проблему можно решить, проверкой -- совпадает ли адрес в sys_call_table с адресом нашей функции open и если не совпадает, то не восстанавливать значение этого вызова (таким образом B не будет "восстанавливать" системный вызов), но это порождает другую проблему. Когда выгружается модуль А, он "видит", что системный вызов был изменен на B_open и "отказывается" от восстановления указателя на sys_open. Теперь, функция B_open будет по прежнему пытаться вызывать A_open, которой больше не существует в памяти, так что система "рухнет" еще раньше -- до удаления модуля B.
Подобные проблемы делают такую "подмену" системных вызовов неприменимой для широкого распространения. С целью предотвращения потенциальной опасности, связанной с подменой адресов системных вызовов, ядро более не экспортирует sys_call_table. Но всё же получить доступ к таблице системных вызовов можно, поставив соответствующую «заплату», что было описано в предыдущем параграфе.
3.3 Выбор языка программирования
Программа написана на языке С с использованием встроенного в
ОС Linux компилятора GCC. Выбор языка основан на том, что исходный код ядра, предоставляемый системой, написан на С, и использование другого языка программирования в данном случае было бы нецелесообразным. К тому же он является мощным средством для разработки практически любого приложения.
3.4 Структура программного обеспечения
Программа состоит из следующих объектов
1) Загружаемый модуль ядра Monitor.с
2) Модуль Start.с, который осуществляет подготовку к загружению модуля Monitor.с
3) Модуль Save.с, предназначенный для сохранения полученных данных и вывода их на экран.
4) Модуль Test.с, содержащий эмулятор системных вызовов для тестирования программного обеспечения.
Monitor.с представляет собой загружаемый модуль ядра (LKM), написание и принцип загрузки которого были описаны выше.
Помимо двух основных функций программа содержит новый обработчик системного вызова sys_ipc- функцию new_sys_ipc, которая, в зависимости от принимаемых данных вызывает функции:
my_shmget;
my_shmat;
my_shmctl;
my_shmdt;
представляющие собой обработчики системных вызовов
shmget(), shmat(), shmctl(), shmdt() соответственно.
Также в модуле находятся функции, отображающие полученные данные в системном файле /var/log/messages:
print_shmget_info();
print_shmat_info();
print_shmctl_info();
print_shmdt_info();
Модуль Test.c представляет собой программу, позволяющую пользователю
· Создать сегмент разделяемой памяти
· Записать в него данные
· Считать данные
· Узнать режим доступа сегмента
· Удалить сегмент
Для этого предназначены процедуры:
· void writeshm();
· void readshm();
· void removeshm();
· void seeuid();
Модуль создан для тестирования программы мониторинга.
Модули Start.c и Save.cпредназначены для отображения данных, полученных в результате выполнения программы мониторинга обращений к сегментам разделяемой памяти, на экран и для сохранения их в файл.
3.5 Структуры данных
Для каждого сегмента разделяемой памяти ядро хранит нижеследующую структуру, определённую в заголовочном файле <sys/shm.h>. Данный программный проект позволяет отследить процесс изменения содержимого полей этих структур при вызове системной функции sys_ctl.
struct shmid_ds
{
struct ipc_perm shm_perm;
int shm_segsz;
time_t shm_atime;
time_t shm_dtime;
time_t shm_ctime;
unsigned short shm_cpid;
unsigned short shm_lpid;
shortshm_nattch;
};
Значения полей структуры:
shm_perm – права на выполнение операции (структура определена в
файле <sys/ipc.h>)
shm_segsz – реальный размер сегмента (в байтах)
shm_atime – время последнего подключения
shm_dtime – время последнего отключения
shm_ctime – время последнего изменения
shm_cpid – идентификатор процесса создателя
shm_lpid – идентификатор процесса подключавшегося последним
shm_nattch – количество текущих подключений сегмента
struct ipc_perm
{
key_t key;
ushort uid;
ushort gid;
ushort cuid;
ushort cgid;
ushort mode;
ushort seq;
};
Со значением полей
key– уникальный ключ IPC
uid– идентификатор пользователя владельца
gid– идентификатор группы владельца
cuid– идентификатор пользователя создателя
cgid– идентификатор владельца создателя
mode– разрешения чтения/записи
seq– последовательный номер канала
Сохраняемая отдельно вместе с ключом IPC-объекта информация содержит данные о владельце и создателе этого объекта (они могут различаться) и режимы восьмеричного доступа.
Модуль Monitor.cсодержит следующие структуры данных для хранения информации о сегменте разделяемой памяти, полученной в результате выполнения программы
struct shmget_info
{
int cur_uid;
int key;
int shm_id;
int size;
int flag;
};
struct shmat_info
{
int cur_uid;
int shm_id;
unsigned long address;
};
struct shmctl_info
{
int cur_uid;
int shm_id;
int cmd;
struct shmid_ds *main;
int result;
};
struct shmdt_info
{
int cur_uid;
char *address;
intresult;
};
key – уникальный ключ для определения сегмента
shm_id – идентификатор сегмента
size – запрашиваемый размер сегмента
flag – флаг, определяющий, с какой целью был совершён системный
вызов sys_get:
· процесс создаёт новый сегмент разделяемой
памяти (flag = 0)
· процесс обращается к уже созданному сегменту
разделяемой памяти (flag !=0)
address – адрес начала сегмента в адресном пространстве пользователя
cmd – номер команды, указывающей на то, для каких действий был
произведён системный вызов sys_ctl:
· получить значения полей структуры shmid_ds (IPC_STAT)
· изменить значения полей в структуре shmid_ds (IPC_SET)
· удалить сегмент (IPC_RMID)
main – полученная копия структуры shmid_ds
result – результат выполнения системного вызова (success или failed)
3.6 Реализация мониторинга создания, управления и удаления сегментов разделяемой памяти
Как уже было отмечено, отследить обращения к сегментам разделяемой памяти можно, перехватив системный вызов sys_ipc, который помимо прочих аргумантов принимает параметр call, определяющий какую из системных функций sys_get, sys_shmat, sys_ctl, sys_dt следует вызвать.
Для перехвата выхова sys_ipc, надо сделать следующее:
· В функции init_module() сохранить указатель на оригинальный (исходный) вызов – orig_sys_ipc – и в таблице системных вызовов sys_call_table настроить соответствующий указатель на новый системный вызов.
· Создать функцию, реализующую новый системный вызов – new_sys_ipc.
· В функции cleanup_module() восстанавить оригинальный системный вызов, используя ранее сохраненный указатель.
В созданном обработчике системного вызова, в зависимости от аргумента call, вызывать функции обработки вызовов sys_get, sys_shmat, sys_ctl, sys_dt, которые сохраняют полученные данные в соответствующие структуры данных, затем отображаются в ядро и выводятся в системном файле /var/log/messages.
Компиляция созданного модуля ядра осуществляется с помощью команды
# make
Несмотря на все свои достоинства, утилита make ничего не знает о проекте, поэтому необходимо создать простой текстовый файл, который будет содержать все необходимые инструкции по сборке. Файл с инструкциями по сборке проекта называется Makefile.