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"
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/.