Перехват системных вызовов в Linux традиционно использовался для написания различных руткитов и других программ, используемых хакерами для сбора информации и скрытого контроля над удаленной системой. В данной же статье я расскажу о том, как использовать данную технику для мониторинга и защиты системы.
Перехват системных вызовов в 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 с целью мониторинга и защиты информации. Это,
естественно, далеко не все, что можно осуществить, и вы наверняка сможете
придумать еще большее количество применений этой техники.
"Лаборатория Касперского"
- международная компания-разработчик
программного обеспечения для защиты от вирусов,
хакеров и спама. Продукты компании предназначены
для широкого круга клиентов - от домашних
пользователей до крупных корпораций. В активе
"Лаборатории Касперского" 16-летний опыт
непрерывного противостояния вирусным угрозам,
позволивший компании накопить уникальные знания
и навыки и стать признанным экспертом в области
создания систем антивирусной зашиты.
Компания SoftKey – это уникальный сервис для покупателей, разработчиков, дилеров и аффилиат–партнеров. Кроме того, это один из лучших Интернет-магазинов ПО в России, Украине, Казахстане, который предлагает покупателям широкий ассортимент, множество способов оплаты, оперативную (часто мгновенную) обработку заказа, отслеживание процесса выполнения заказа в персональном разделе, различные скидки от
Академия Информационных
Систем (АИС) создана в 1996 году и за время работы обучила свыше 7000
специалистов различного профиля. АИС предлагает своим партнерам десятки
образовательных программ, курсов, тренингов и выездных семинаров. Сегодня АИС
представлена направлениями: «Информационные технологии», «Дистанционное обучение
в области ИТ», «Информационная безопасность, «Управление проектами»,
«Бизнес-образование», «Семинары и тренинги», «Экологические промышленные
системы», «Конференции», «Консалтинг» и «Конкурентная разведка на основе
Интернет».
В Матрице безопасности выбор очевиден