Владимир мешков



Скачать 81.09 Kb.
Pdf просмотр
Дата21.11.2016
Размер81.09 Kb.
Просмотров215
Скачиваний0

40
программирование программирование программирование программирование программирование
ВЛАДИМИР МЕШКОВ
ПЕРЕХВАТ
СИСТЕМНЫХ
ВЫЗОВОВ В
ОС LINUX
ПЕРЕХВАТ
СИСТЕМНЫХ
ВЫЗОВОВ В
ОС LINUX
За последние годы операционная система
Linux прочно заняла лидирующее положение
в качестве серверной платформы, опережая
многие коммерческие разработки. Тем не
менее вопросы защиты информационных
систем, построенных на базе этой ОС, не
перестают быть актуальными. Существует
большое количество технических средств,
как программных, так и аппаратных, которые
позволяют обеспечить безопасность
системы. Это средства шифрования данных
и сетевого трафика, разграничения прав
доступа к информационным ресурсам,
защиты электронной почты, веб-серверов,
антивирусной защиты, и т. д. Список, как вы
понимаете, достаточно длинный.
В данной статье предлагаем вам рассмотреть
механизм защиты, основанный на перехвате
системных вызовов операционной системы
Linux. Данный механизм позволяет взять под
контроль работу любого приложения и тем
самым предотвратить возможные
деструктивные действия, которые оно может
выполнить.
За последние годы операционная система
Linux прочно заняла лидирующее положение
в качестве серверной платформы, опережая
многие коммерческие разработки. Тем не
менее вопросы защиты информационных
систем, построенных на базе этой ОС, не
перестают быть актуальными. Существует
большое количество технических средств,
как программных, так и аппаратных, которые
позволяют обеспечить безопасность
системы. Это средства шифрования данных
и сетевого трафика, разграничения прав
доступа к информационным ресурсам,
защиты электронной почты, веб-серверов,
антивирусной защиты, и т. д. Список, как вы
понимаете, достаточно длинный.
В данной статье предлагаем вам рассмотреть
механизм защиты, основанный на перехвате
системных вызовов операционной системы
Linux. Данный механизм позволяет взять под
контроль работу любого приложения и тем
самым предотвратить возможные
деструктивные действия, которые оно может
выполнить.

41
№3(4), март 2003
программирование программирование программирование программирование программирование movl $1, %eax movl $0, %ebx int $0x80
gcc -ñ test.S
ld -s -o test test.o int init_module(void) { ... }
void cleanup_module(void) { ... }
int $0x80
movl $0, %ecx movl $8, %eax
.globl _start
.text
_start:
Системные вызовы
Начнем с определения. Системные вызовы – это набор функций, реализованных в ядре ОС. Любой запрос при- ложения пользователя в конечном итоге трансформиру- ется в системный вызов, который выполняет запрашива- емое действие. Полный перечень системных вызовов ОС
Linux находится в файле /usr/include/asm/unistd.h. Давай- те рассмотрим общий механизм выполнения системных вызовов на примере. Пусть в исходном тексте приложе- ния вызывается функция creat() для создания нового фай- ла. Компилятор, встретив вызов данной функции, преоб- разует его в ассемблерный код, обеспечивая загрузку номера системного вызова, соответствующего данной функции, и ее параметров в регистры процессора и пос- ледующий вызов прерывания 0x80. В регистры процессо- ра загружаются следующие значения:
в регистр EAX – номер системного вызова. Так, для на- шего случая номер системного вызова будет равен 8
(см. __NR_creat);
в регистр EBX – первый параметр функции (для creat это указатель на строку, содержащую имя создавае- мого файла);
в регистр ECX – второй параметр (права доступа к файлу).
В регистр EDX загружается третий параметр, в данном случае он у нас отсутствует. Для выполнения системного вызова в ОС Linux используется функция system_call, ко- торая определена в файле /usr/src/liux/arch/i386/kernel/
entry.S. Эта функция – точка входа для всех системных вызовов. Ядро реагирует на прерывание 0x80 обращени- ем к функции system_call, которая, по сути, представляет собой обработчик прерывания 0x80.
Чтобы убедиться, что мы на правильном пути, напи- шем небольшой тестовый фрагмент на ассемблере. В нем увидим, во что превращается функция creat() после ком- пиляции. Файл назовем test.S. Вот его содержание:
В регистр EAX загружаем номер системного вызова:
В регистр EBX – первый параметр, указатель на стро- ку с именем файла:
В регистр ECX – второй параметр, права доступа:
Вызываем прерывание:
Выходим из программы. Для этого вызовем функцию exit(0):
В сегменте данных укажем имя создаваемого файла:
Компилируем:
В текущем каталоге появится исполняемый файл test.
Запустив его, мы создадим новый файл с именем file.txt.
А теперь давайте вернемся к рассмотрению меха- низма системных вызовов. Итак, ядро вызывает обра- ботчик прерывания 0x80 – функцию system_call.
System_call помещает копии регистров, содержащих параметры вызова, в с тек при помощи макроса
SAVE_ALL и командой call вызывает нужную системную функцию. Таблица указателей на функции ядра, кото- рые реализуют системные вызовы, расположена в мас- сиве sys_call_table (см. файл arch/i386/kernel/entry.S).
Номер системного вызова, который находится в регис- тре EAX, является индексом в этом массиве. Таким об- разом, если в EAX находится значение 8, будет вызва- на функция ядра sys_creat(). Зачем нужен макрос
SAVE_ALL? Объяснение тут очень простое. Так как практически все системные функции ядра написаны на
С, то свои параметры они ищут в стеке. А параметры помещаются в стек при помощи макроса SAVE_ALL!
Возвращаемое системным вызовом значение сохраня- ется в регистр EAX.
Теперь давайте выясним, как перехватить систем- ный вызов. Поможет нам в этом механизм загружае- мых модулей ядра. Хотя ранее мы уже рассматривали вопросы разработки и применения модулей ядра, в ин- тересах последовательности изложения материала рас- смотрим кратко, что такое модуль ядра, из чего он со- стоит и как взаимодействует с системой.
Загружаемый модуль ядра
Загру жаемый модуль ядра (обозначим его LKM –
Loadable Kernel Module) – это программный код, выпол- няемый в пространстве ядра. Главной особенностью LKM
является возможность динамической загрузки и выгруз- ки без необходимости перезагрузки всей системы или перекомпиляции ядра.
Каждый LKM состоит из двух основных функций (ми- нимум):
функция инициализации модуля. Вызывается при загрузке LKM в память:
функция выгрузки модуля:
Приведем пример простейшего модуля:
movl $filename, %ebx
.data filename:
.string "file.txt"

42
программирование программирование программирование программирование программирование
#define MODULE
#include
int init_module(void)
{
printk("Hello World\n");
return 0;
}
void cleanup_module(void)
{
printk("Bye\n");
}
gcc -c -O3 helloworld.c insmod helloworld.o rmmod helloworld
CC = gcc
CFLAGS = -O3 -Wall -fomit-frame-pointer
MODFLAGS = -D__KERNEL__ -DMODULE -I/usr/src/linux/include sys_mkdir_call.o: sys_mkdir_call.c
$(CC) -c $(CFLAGS) $(MODFLAGS) sys_mkdir_call.c void cleanup_module()
{
sys_call_table[SYS_mkdir]=orig_mkdir;
}
int init_module()
{
orig_mkdir=sys_call_table[SYS_mkdir];
sys_call_table[SYS_mkdir]=own_mkdir; return 0;
}
int own_mkdir(const char *path)
{
return 0;
}
int (*orig_mkdir)(const char *path);
extern void *sys_call_table[];
#include
#include
#include
Компилируем и загружаем модуль. Загрузку модуля в память осуществляет команда insmod:
Информация обо всех загруженных в данный момент в систему модулях находится в файле /proc/modules. Что- бы убедиться, что модуль загружен, введите команду cat
/proc/modules либо lsmod. Выгружает модуль команда rmmod:
Алгоритм перехвата системного вызова
Для реализации модуля, перехватывающего системный вызов, необходимо определить алгоритм перехвата. Ал- горитм следующий:
сохранить указатель на оригинальный (исходный) вы- зов для возможности его восстановления;
создать функцию, реализующую новый системный вы- зов;
в таблице системных вызовов sys_call_table произве- сти замену вызовов, т.е. настроить соответствующий указатель на новый системный вызов;
по окончании работы (при выгрузке модуля) восстано- вить оригинальный системный вызов, используя ранее сохраненный указатель.
Выяснить, какие системные вызовы задействуются при работе приложения пользователя, позволяет трас- сировка. Осуществив трассировку, можно определить,
какой именно системный вызов следует перехватить,
чтобы взять под контроль работу приложения. Пример использования программы трассировки будет рассмот- рен ниже.
Теперь у нас достаточно информации, чтобы присту- пить к изучению примеров реализации модулей, осуще- ствляющих перехват системных вызовов.
Примеры перехвата системных вызовов
Запрет создания каталогов
При создании каталога вызывается функция ядра sys_mkdir. В качестве параметра задается строка, в кото- рой содержится имя создаваемого каталога. Рассмотрим код, осуществляющий перехват соответствующего сис- темного вызова.
Экспортируем таблицу системных вызовов:
Определим указатель для сохранения оригинального системного вызова:
Создадим собственный системный вызов. Наш вызов ничего не делает, просто возвращает нулевое значение:
Во время инициализации модуля сохраняем указатель на оригинальный вызов и производим замену системного вызова:
При выгрузке восстанавливаем оригинальный вызов:
Код сохраним в файле sys_mkdir_call.c. Для получе- ния объектного модуля создадим Makefile следующего содержания:
Командой make создадим модуль ядра. Загрузив его,
попытаемся создать каталог командой mkdir. Как вы мо- жете убедиться, ничего при этом не происходит. Команда не работает. Для восстановления ее работоспособности достаточно выгрузить модуль.
Запрет чтения файла
Для того чтобы прочитать файл, его необходимо вначале открыть при помощи функции open. Легко догадаться, что этой функции соответствует системный вызов sys_open.
Перехватив его, мы можем защитить файл от прочтения.
Рассмотрим реализацию модуля-перехватчика.

43
№3(4), март 2003
программирование программирование программирование программирование программирование gcc -o dir dir.c strace ./dir getdents (6, /* 4 entries*/, 3933) = 72;
CC = gcc
CFLAGS = -O2 -Wall -fomit-frame-pointer
MODFLAGS = -D__KERNEL__ -DMODULE -I/usr/src/linux/include sys_open_call.o: sys_open_call.c
$(CC) -c $(CFLAGS) $(MODFLAGS) sys_open_call.c int own_open(const char *pathname, int flag, int mode)
{
return 0;
}
void cleanup_module()
{
sys_call_table[SYS_open]=orig_open;
}
int init_module()
{
orig_open=sys_call_table[SYS_open];
sys_call_table[SYS_open]=own_open;
kfree(kernel_path);
return orig_open(pathname, flag, mode);
}
}
kernel_path=(char *)kmalloc(255,GFP_KERNEL);
copy_from_user(kernel_path, pathname, 255);
char hide[]="test.txt"
char *kernel_path;
#include
#include
#include
#include
#include
#include
#include
extern void *sys_call_table[];
Указатель для сохранения оригинального системного вызова:
Первым параметром функции open является имя откры- ваемого файла. Новый системный вызов должен сравнить этот параметр с именем файла, который мы хотим защи- тить. Если имена совпадут, будет сымитирована ошибка открытия файла. Наш новый системный вызов имеет вид:
Сюда поместим имя открываемого файла:
Имя файла, который мы хотим защитить:
Выделим память и скопируем туда имя открываемого файла:
Сравниваем:
Освобождаем память и возвращаем код ошибки при совпадении имен:
Если имена не совпали, вызываем оригинальный сис- темный вызов для выполнения стандартной процедуры от- крытия файла:
Далее смотрите комментарии к предыдущему примеру.
Сохраним код в файле sys_open_call.c и создадим
Makefile для получения объектного модуля:
В текущем каталоге создадим файл с именем test.txt,
загрузим модуль и введем команду cat test.txt. Система сообщит об отсутствии файла с таким именем.
Честно говоря, такую защиту легко обойти. Достаточ- но командой mv переименовать файл, а затем прочесть его содержимое.
Сокрытие записи о файле в каталоге
Определим, какой системный вызов отвечает за чтение содержимого каталога. Для этого напишем еще один тес- товый фрагмент, который занимается чтением текущей директории:
Получим исполняемый модуль:
и выполним его трассировку:
Обратим внимание на предпоследнюю строку:
Содержимое каталога считывает функция getdents.
Результат сохраняется в виде списка структур типа struct dirent. Второй параметр этой функции является указате- лем на этот список. Функция возвращает длину всех за- писей в каталоге. В нашем примере функция getdents оп- ределила наличие в текущем каталоге четырех записей –
«.», «..» и два наших файла, исполняемый модуль и ис- ходный текст. Длина всех записей в каталоге составляет
72 байта. Информация о каждой записи сохраняется, как int (*orig_open)(const char *pathname, int flag, int mode);
if(strstr(kernel_path,(char *)&hide) != NULL) {
/* Ôàéë dir.c*/
#include
#include
int main ()
{
DIR *d;
struct dirent *dp;
d = opendir(«.»);
dp = readdir(d);
return 0;
}
kfree(kernel_path);
return -ENOENT;
}
else {

44
программирование программирование программирование программирование программирование memcpy(dirp3,(char *)dirp3+dirp3->d_reclen,t);
tmp-=n;
}
dirp3=(struct dirent *)((char *)dirp3+dirp3->d_reclen);
}
copy_to_user(dirp,dirp2,tmp);
kfree(dirp2);
}
return tmp;
}
int init_module(void)
{
orig_getdents=sys_call_table[SYS_getdents];
sys_call_table[SYS_getdents]=own_getdents;
return 0;
}
void cleanup_module()
{
sys_call_table[SYS_getdents]=orig_getdents;
}
CC = gcc module = sys_call_getd.o
CFLAGS = -O3 -Wall
LINUX = /usr/src/linux
MODFLAGS = -D__KERNEL__ -DMODULE -I$(LINUX)/include sys_call_getd.o: sys_call_getd.c $(CC) -c
$(CFLAGS) $(MODFLAGS) sys_call_getd.c if(strstr((char *)&(dirp3->d_name),(char *)&hide) != NULL) {
#include
#include
#include
#include
#include
#include
#include
#include
extern void *sys_call_table[];
int (*orig_getdents)(u_int, struct dirent *, u_int);
int own_getdents(u_int fd, struct dirent *dirp, u_int count)
{
unsigned int tmp, n;
int t;
struct dirent *dirp2, *dirp3;
char hide[]=»our.file»;
tmp=(*orig_getdents)(fd,dirp,count);
if(tmp>0){
dirp2=(struct dirent *)kmalloc(tmp,GFP_KERNEL);
ñopy_from_user(dirp2,dirp,tmp);
dirp3=dirp2;
t=tmp;
while(t>0) {
n=dirp3->d_reclen;
t-=n;
мы уже сказали, в структуре struct dirent. Для нас интерес представляют два поля данной структуры:
d_reclen – размер записи;
d_name – имя файла.
Для того чтобы спрятать запись о файле (другими сло- вами, сделать его невидимым), необходимо перехватить системный вызов sys_getdents, найти в списке получен- ных структур соответствующую запись и удалить ее. Рас- смотрим код, выполняющий эту операцию (автор ориги- нального кода – Michal Zalewski):
Определим свой системный вызов.
Назначение переменных будет показано ниже. Допол- нительно нам понадобятся структуры:
Имя файла, который мы хотим спрятать:
Определим длину записей в каталоге:
Выделим память для структуры в пространстве ядра и скопируем в нее содержимое каталога:
Задействуем вторую структуру и сохраним значение длины записей в каталоге:
Начнем искать наш файл:
Считываем длину первой записи и определяем остав- шуюся длину записей в каталоге:
Проверяем, не совпало ли имя файла из текущей за- писи с искомым:
Если это так, затираем запись и вычисляем новое зна- чение длины записей в каталоге:
Позиционируем указатель на следующую запись и продолжаем поиск:
Возвращаем результат и освобождаем память:
Возвращаем значение длины записей в каталоге:
Функции инициализации и выгрузки модуля имеют стандартный вид:
Сохраним исходный текст в файле sys_call_getd.c и создадим Makefile следующего содержания:
В текущем каталоге создадим файл our.file и загру- зим модуль. Файл исчезает, что и требовалось доказать.
Как вы понимаете, рассмотреть в рамках одной ста- тьи пример перехвата каждого системного вызова не представляется возможным. Поэтому тем, кто заинтере- совался данным вопросом, рекомендую посетить сайты:
www.phrack.com www.atstake.com www.thehackerschoice.com.
Там вы сможете найти более сложные и интересные примеры перехвата системных вызовов. Обо всех заме- чаниях и предложениях пишите на форум журнала.
При подготовке статьи были использованы материа- лы сайта http://www.thehackerschoice.com/.


Поделитесь с Вашими друзьями:


База данных защищена авторским правом ©nethash.ru 2017
обратиться к администрации

войти | регистрация
    Главная страница


загрузить материал