PDA

Просмотр полной версии : ЗАЩИТА LINUX С ПОМОЩЬЮ LKM



SDA
17.07.2005, 16:05
Перехват системных вызовов в Linux традиционно использовался для написания различных руткитов и других программ, используемых хакерами для сбора информации и скрытого контроля над удаленной системой. В данной же статье я расскажу о том, как использовать данную технику для мониторинга и защиты системы.

Перехват системных вызовов

Многим известно, что исполнение системных вызовов сводится к выбору нужного слота в массиве sys_call_table, чтению адреса функции из этого слота и исполнению кода, записанного по этому адресу. Перехват в таком случае является заменой адреса оригинальной функции в sys_call_table на адрес нового системного вызова. Но ядрами Linux 2.6.x адрес sys_call_table более не экспортируется. Поэтому его придется искать вручную. Я воспользуюсь методикой, предложенной dev0id из UkR Security Team. Для начала следует узнать, какие системные вызовы экспортируются ядром. Для примера был взят вызов sys_close. Адрес же таблицы sys_call_table находится между концом секции кода и концом секции данных, поэтому не составляет большого труда простым перебором найти адрес sys_close и вычислить точное местоположение sys_call_table. Для подробного описания см. статью "Защита от исполнения в стеке (ОС Линукс)."
Стоит обратить внимание на перехват вызова execve(). По стандартной схеме перехватить его не получится, и самым легким выходом из этой ситуации является перемещение оригинального вызова в свободный слот sys_call_table.

Мониторинг

Для ведения журнала действий пользователя(и, возможно, хакера) нам также потребуется перехват системных вызовов. Какие это вызовы - зависит только от того, что именно вы хотите записывать в журнал. В моем примере я перехватываю системные вызовы execve(), open(), и socketcall(). Возможен, например, еще мониторинг содержимого файлов с помощью перехвата вызова write().
Прежде всего мне понадобилось журналировать запущенные программы. Это делается легче всего - в перехваченном execve() следует просто записать в журнал аргумент filename.

char* temp;
/* выделим память под буфер */
temp = (char*)kmalloc(strlen(filename)+1, GFP_KERNEL);
copy_from_user(temp, filename, strlen(filename)+1);
/* запишем в журнал */
do_log(temp, EXECVE);
kfree(temp);

Также будет полезным мониторинг каких-либо файлов, не предназначенных для просмотра/редактирования обычными пользователями. Для этой цели мною был перехвачен вызов open(). Здесь все происходит по той же схеме, только добавлена проверка открываемого файла на присутствие в списке файлов, предназначенных для мониторинга.

Для отслеживания сетевых подключений потребуется перехват socketcall(). Данный вызов имеет два параметра - команду и специфический для нее набор аргументов. Для отслеживания, к примеру, исходящих подключений требуется проверить команду на SYS_CONNECT, а затем привести второй элемент в массиве args к типу sockaddr и узнать нужный IP и порт из поля sa_data.

Функция do_log служит для добавления записи в журнал. Для этого используются оригинальные системные вызовы open(), write(), close(), brk(). Их адреса копируются из sys_call_table при инициализации модуля. Сохранение адресов оригинальных функций потребуется и для всех остальных перехваченных системных вызовов.

o_open = sys_call_table[__NR_open];
o_write = sys_call_table[__NR_write];
o_close = sys_call_table[__NR_close];
o_brk = sys_call_table[__NR_brk];

При исполнении оригинальных системных вызовов в пространстве ядра необходимо скопировать аргументы в адресное пространство пользователя. Это достигается за счет следующей схемы. В указателе на текущий процесс current имеется подструктура mm, отвечающая за менеджмент памяти данного процесса. В ней же имеется указатель на конец сегмента данных brk. Для изменения размера сегмента данных используется функция brk(). Мы расширим сегмент данных и с помощью функции copy_to_user скопируем наши аргументы в адресное пространство пользователя:

unsigned long end;
end = mm->brk;
sys_brk((void *)(end+strlen(pathname)+2));
copy_to_user(end+2, pathname, strlen(pathname+1));

После этого можно исполнить системный вызов в адресном пространстве ядра. С помощью системного вызова write() следует записать в файл журнала сведения о произошедшем событии. Файл журнала, естесственно, должен быть скрыт с помощью перехвата функции getdents(). Но журналирование - далеко не все, что можно сделать с помощью LKM. Данную технику можно применять и для защиты.

Ограничение действий пользователей

Всем известно, что права root в Unix отличаются от прав администратора, скажем, в Windows XP. Пользователь root может выполнять любые действия. Именно поэтому нулевой UID есть самая главная цель хакера. Я обьясню, как, используя LKM, можно ограничить в правах любого пользователя.

Запрет на запуск программ/открытие файлов

Запрет на запуск программ осуществляется с помощью перехвата execve(), а запрет на открытие файлов с помощью перехвата open(). Создадим специальную структуру, описывающую режим доступа к программе или файлу:

struct {
char* filename; /* путь к файлу */
struct { /* структура доступа */
int id; /* id пользователя */
int pid; /* pid задачи */
char* prog; /* имя задачи */
int type; /* тип ограничения(пользователь|pid|имя задачи) */
int mode; /* режим(запрет|открытие|запуск|открытие и запуск) */
}* list;
}* access;

Поле filename указывает на полный путь к файлу. Структура доступа поддерживает запрет по id пользователя, по pid и имени задачи. Тут же устанавливается и режим доступа - полный запрет, только открытие, только запуск, открытие и запуск. По желанию этот список можно расширить, добавив чтение/редактирование и т.д. При запуске execve() или open() следует выполнить поиск данного имени файла в списке, а затем сверить разрешения. В случае запрета вернуть -EACCESS. Можно и расширить возможности данного примера, не запрещая доступ, а введя аутентификацию с помощью запуска псевдопрограммы. Для ограничения доступа к файлам простых пользователей проблемы может решить chmod, но для ограничений действий суперпользователя можно воспользоваться предложенной схемой.

Ограничение доступа к информации

Как уже было сказано, права root в Linux позволяют выполнять любые действия. Но запрет на выполнение для пользователя root дает понять о скрытой защите. Я предлагаю еще одну схему защиты информации - защита с помощью редиректа.
Допустим, имеется какой-либо файл с важной информацией. Эту информацию используют какие-либо программы, но для остальных этот файл должен быть недоступен. Мы подменим системный вызов open(), чтобы вызывался оригинальный вызов, открывающий другой, скрытый файл, в котором важная информация будет отсутствовать. Всё это в случае открытия файла не специализированной программой. Иначе будет вызван оригинальный вызов с правильным параметром. Точно так же следует поступить и с вызовом execve() - запускать безопасные бинарные файлы вместо информационно важных. Для осуществления этого можно использовать модифицированную структуру из предыдущего примера, добавив в нее путь к подменяющему файлу.

Контроль с помощью псевдопрограмм

Любая защитная система должна обеспечивать режим администратора. Режим, в котором можно производить настройки и где сняты ограничения. Так как у нас урезаны права суперпользователя, то следует предусмотреть режим настройки и режим полного доступа. Я предлагаю осуществить это с помощью псевдопрограммы с именем, например, /bin/control. Псевдопрограмма не является бинарным исполняемым файлом, и она не хранится на жестком диске. В перехваченном вызове execve() мы проверим, если параметр filename равен /bin/control. В этом случае мы исполняем код, находящийся в нашем модуле ядра. Естесственно, здесь необходима авторизация, хотя бы вида логин/пароль. Для вывода на экран используется вызов write() с параметром 1(stdout), а для ввода - read() с параметром 0(stdin). Я же приведу пример с передачей логина/пароля через опции командной строки:

/* исполнение псевдопрограммы */
if(!strcmp(filename, "/bin/control")) {
/* проверка логина и пароля */
if(!strcmp(argv[1], "auth")) if(auth(argv[2], argv[3])) control = 1;
if(control == 0) return -ENOENT;
/* получение опций командной строки и настройка
* редиректов, уровней доступа и т.д.
* ...
*/
/* закрытие сессии */
if(!strcmp(argv[1], "close") control = 0;
/* псевдопрограмма не существует, вернем ENOENT */
return -ENOENT;
}

В данный код следует еще вставить обработку команды переключения в режим без ограничений, тот режим, в котором снимаются все запреты и редиректы. Следует ввести переменную safe - если она, допустим, равна нулю, то все проверки в перехваченных вызовах будут игнорироваться. Данная псевдопрограмма не существует, поэтому следует вернуть -ENOENT, если какой-нибудь хакер все же додумается ее запустить.

Защита против шеллкода

Очень большое количество эксплоитов используют шеллкод, то есть скомпилированный набор ассемблерных инструкций. Чаще всего, конечно, это вызов /bin/sh с правами эксплуатированной программы. Многие создатели эксплоитов не утруждают себя написанием собственного шеллкода и просто берут готовый. Я предлагаю защиту от распространенных шеллкодов с помощью перехвата системных вызовов. Для защиты от шеллкодов от удаленных эксплоитов следует перехватить socketcall(), а для локальных, например, read() и execve(). В первом случае надо проверить команду на получение данных, скопировать второй аргумент в адресное пространство ядра и сверить находящиеся там данные с наиболее известными шеллкодами, такими как запуск /bin/sh, предоставление удаленного доступа, и т.д. С функцией read(), похожей на recv(), поступить следует так же - вызвать оригинальную функцию и проверить буфер на наличие шеллкода. Еще ошибки с переполнением буфера встречаются в аргументах, передаваемых в программу. В таком случае надо проверять параметр argv[] в вызове execve(). В случае обнаружения данной последовательности байт,я считаю, не нужно скрывать данную последовательность, потому что ее появление может быть чистой случайностью. Но если после этого программа, вызвавшая read() или socketcall(), запустит /bin/sh, можно запретить запуск, вернув -EACCESS. Такая примитивная техника позволит защититься от многих распространенных шеллкодов.

Заключение

В данной статье были рассмотрены некоторые способы применения перехвата системных вызовов в Linux с целью мониторинга и защиты информации. Это, естественно, далеко не все, что можно осуществить, и вы наверняка сможете придумать еще большее количество применений этой техники.
http://www.securitylab.ru/

Muromec
18.07.2005, 13:23
класс! мониторить вызовы правда можно и strace`ом, но заменить execve() и open() это круто (-;