Настольная книга компьютерного исследователя


Программирование на уровне ядра для Solaris.

Часть 1.

Итак, что из себя представляет kernel-модуль, для какой бы ОС он не писался? Говоря простыми словами, kernel-модуль — это кусок кода, подгружаемый «извне»(т.е. не являющийся частью ядра), но функционирующий в адресном пространстве ядра операционной системы. Работая в одном адресном пространстве с ядром, модуль получает возможность обращаться к внутренним структурам ядра, что бывает важно при написании драйверов и ещё в ряде других случаев. К примеру, если вам нужно ограничить возможность работы с сетью некоторым пользователям, то одним из решений данной проблемы является перехват при помощи kernel-модуля системного вызова типа socket() и замена его на другой, задача которого — выдача сообщения об ошибке всем юзерам, которым требовалось запретить обращаться к сети. Или же вам нужно закрепиться в захваченной системе — опять перехватываете системный вызов типа accept() и открываете рутовый шелл (это только один из способов) при запросе соединения с определённого IP-адреса. Самое интересное, что написание подобного софта, укрепляющего безопасность, или же, наоборот, атакующего и «заражающего» операционку практически никак не отличается — принципы работы обоих программ очень похожи.

Из чего же должен состоять самый простой модуль ядра Solaris? Если обычная программа имеет только одну точку входа — функцию main(), то здесь у нас их 3 — _init(), _fini() и _info().
_init() — запускается при запуске самого модуля;
_fini() — при выгрузке модуля из памяти;
_info() — при запросе информации о модуле.

Функция _init() — это, по сути дела, аналог функции main(). В ней происходит инициализация и установка модуля в системе. Она обязательно должна вызывать функцию mod_install(), которая, как следует из её названия, инсталирует модуль в системе.
Так выглядит её прототип: int mod_install (struct modlinkage *modlinkage).

В качестве аргумента принимается указатель на структуру modlinkage, в которой указываются характеристики модуля. Для правильной загрузки модуля в память нужно объявить несколько структур, одна из которых и есть modlinkage. Объявления этих структур находятся в заголовочном файле /usr/include/sysmodctl.h

Структура modlinkage достаточно проста и выглядит примерно так:

struct modlinkage {
int ml_rev; /* rev of loadable modules system */
#ifdef _LP64
void *ml_linkage[7]; /* more space in 64-bit OS */
#else
void *ml_linkage[4]; /* NULL terminated list of */
/* linkage structures */
#endif
};

В переменной ml_rev содержится версия подсистемы, ответственной за работу модулей. Почти всегда этой переменной присваивается значение MODREV_1.

ml_linkage представляет собой массив указателей на структуры, собственно и описывающие конкретный модуль. Данный массив должен завершаться нулевым указателем (NULL). Если модуль представляет собой драйвер устройства, то ml_linkage указывает на структуру modldrv; если модуль вводит новый системный вызов, то используется структура modlsys и т.д. Вот список всех возможных структур из семейства modl*, характеризующие различные типы модулей:

modldrv
modlsys
modlfs
modlmisc
modlstrmod
modlsched
modlexec
modldacf

За более подробной информацией об этих структурах можно обратиться к sysmodctl.h. В качестве примера я рассмотрю структуру modlmisc, которая, в отличие от остальных, позволяет создавать «просто модули» (то есть не драйвер и т.д., а просто программу, которая сможет функционировать в адресном пространстве ядра). Вот она:

struct modlmisc {
struct mod_ops *misc_modops;
char *misc_linkinfo;
};

В её состав входит указатель на структуру mod_ops:

struct mod_ops {
int (*modm_install)(); /* install module in kernel */
int (*modm_remove)(); /* remove from kernel */
int (*modm_info)(); /* module info */
};

В mod_ops содержатся указатели на функции, ответственные за загрузку, выгрузку модуля и получение информации о нём. Например, функцию, адресуемую указателем modm_install, использует впоследствии mod_install(). Так как Solaris поддерживает 8 различных структур modl* (см. выше) и, соответственно, 8 разных типов модулей, каждый тип имеет в ядре свои, специфичные только для него функции инициализации, деинициализации и получения информации. Структуры mod_ops нам формировать не надо — мы просто не сможем этого сделать, т.к. только одному ядру известны адреса функций. Данные структуры обхявляются и заполняются в самом ядре, в чём нетрудно убедиться, имея в руках исходные коды ОС Solaris (всё это происходит в modctl.c). Вполне логично, что ядро должно проинициализировать 8 структур mod_ops — для каждого типа модулей. В modctl.с они имеют такие имена:

struct mod_ops mod_driverops;
struct mod_ops mod_execops;
struct mod_ops mod_fsops;
struct mod_ops mod_miscops;
struct mod_ops mod_schedops;
struct mod_ops mod_strmodops;
struct mod_ops mod_syscallops;
struct mod_ops mod_dacfops;

а в modctl.h они объявлены как внешние (extern), так что мы без проблем можем их использовать. Для примера, при заполнении структуры modlmisc, мы должны в неё просто вставить указатель на структуру mod_miscops (&mod_miscops).

И, наконец, второй и последний член структуры modlmisc — misc_linkinfo. Здесь просто указывается имя модуля.
Итак, подведу итог. В простейшем случае, создавая «просто модуль» нам нужно заполнить структуры modlmisc и modlinkage и передать последнюю в качестве параметра функции mod_install():

struct modlmisc modlmisc =
{ &mod_miscops;
«Our first module»;
}

struct modlinkage modlinkage
{ MODREV_1;
&modlmisc;
}

int _init ()
{
mod_install (&modlinkage);
}

Теперь перейдём ко второй функции, необходимой в каждом модуле — _fini(), отвечающей за отключение модуля. По аналогии с _init() и mod_install(), здесь есть mod_remove(), «убивающая» модуль. Как и mod_install(), она в качестве своего единственного аргумента принимает структуру modlinkage.

И, наконец, третья точка входа — _info. Она единственная из трёх, которая имеет аргумент. Аргумент здесь — это указатель на структуру modinfo, посредством которой операционная система получает информацию о модуле. В самом простом случае _info должна вызывать mod_info(), имеющую 2 аргумента: указатели на modlinkage и modinfo. После вызова этой функции мы получаем заполненную структуру modinfo, которую и надо возвратить операционной системе.

Всё… теперь мы в состоянии написать простенький kernel-модуль. Вот пример того, как это может выглядеть:

#include <sys/types.h>
#include <sys/errno.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/modctl.h>

struct modlmisc modlmisc =
{ &mod_miscops;
«Our first module»;
}

struct modlinkage modlinkage
{ MODREV_1;
&modlmisc;
}

int _init ()
{
return (mod_install (&modlinkage));
cmn_err (CE_NOTE, «Hello! I’m your first module!\n»);
}

int _info (struct modinfo *modinfo)
{
return (mod_info (&modlinkage, modinfo));
}

int _fini ()
{
return (mod_remove (&modlinkage));
}

Единственное, что делает данный модуль — это подгружается в память ядра и выдаёт сообщение на консоль при помощи функции cmn_err().

Теперь хотелось бы рассмотреть использование модулей со стороны укрепления безопасности системы. В самом начале, была дана идея перехвата системного вызова socket() с целью запрета некоторым пользователям общаться с сетью. Реализуется это достаточно просто. Как и во FreeBSD, в Solaris существует массив sysent[], состоящий из структур sysent (см. sys/systm.h). Каждая структура sysent описывает тот или иной системный вызов:

struct sysent {
char sy_narg; /* total number of arguments */
#ifdef _LP64
unsigned short sy_flags; /* various flags as defined below */
#else
unsigned char sy_flags; /* various flags as defined below */
#endif
int (*sy_call)(); /* argp, rvalp-style handler */
krwlock_t *sy_lock; /* lock for loadable system calls */
int64_t (*sy_callc)(); /* C-style call hander or wrapper */
};

Не буду заострять внимание на каждом элементе данной структуры, скажу только, что её последний элемент sy_callc — указатель на функцию-обработчик системного вызова. Всё, что теперь требуется — выбрать из массива структуру sysent, соответствующую перехватываемому системному вызову, и заменить значение sy_callc на указатель на свою функцию.
Теперь о том, в каких ситуациях и как это можно использовать. Представьте, есть в системе один или несколько подозрительных пользователей, которым надо запретить доступ к сети. Пусть это будет юзер с UID’ом 1234. Вот один из способов, как это можно реализовать:

#include <sys/types.h>
#include <sys/errno.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/modctl.h>
#include <sys/syscall.h>
#include <sys/systm.h>

struct modlmisc modlmisc =
{ &mod_miscops;
«Net stoper»;
}

struct modlinkage modlinkage
{ MODREV_1;
&modlmisc;
}

int (*socktemp)(int, int, int); /* Переменная, в которой будет сохранён старый
обработчик системного вызова socket() */

int sock (int domain, int type, int protocol) /* функция, которая должна заменить
{ системный вызов socket() */
if (getuid() == 1234)
return (EPROTOTYPE); /* Возвращаем ошибку, если данный
системный вызов сделал юзер с
UID’ом 1234 */
else /* В противном случае запускаем
старый обработчик */
return (socktemp (domain, type, protocol));
}

int _init ()
{
int t;

t = mod_install (&modlinkage);
socktemp = sysent[SYS_so_socket].sy_callc; /* сохраняем старый обработчик */
sysent[SYS_so_socket].sy_callc = sock; /* назначаем новый обработчик
системному вызову socket() */
return (t);
}

int _info (struct modinfo *modinfo)
{
return (mod_info (&modlinkage, modinfo));
}

int _fini ()
{
int t;

t = mod_remove (&modlinkage);
sysent[SYS_so_socket].sy_callc = socktemp; /* при завершении работы модуля
восстанавливаем старый обработчик
системного вызова socket() */
return (t);
}

————-

Вот в принципе и всё…. Для введения достаточно :-) Конечно, рассмотренные выше модули обладают очень маленьким количеством функций и в большинстве случаев просто бесполезны. Хотите разработать что-либо стоящее? Вперёд! — ядро Solaris неплохо документировано фирмой-разработчиком, т.е. Sun’ом. Плюс к этому, по заявлениям этой же фирмы, исходники операционной системы являются свободно распространяемыми и доступными для изучения, хотя по собственному опыту могу сказать, что получить их не так уж и просто (но это уже другой разговор). Кроме того, как вы наверное заметили по заглавию, это не всего лишь первая часть материалов, посвящённых ядру Solaris, так что ждите ещё статей на эту тему в будущих выпусках журнала.



©2015 Журнал Хакера Entries (RSS) and Comments (RSS)