Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
 iakovlev.org 
 Languages
 С
 GNU С Library 
 Qt 
 STL 
 Threads 
 C++ 
 Samples 
 stanford.edu 
 ANSI C
 Libs
 LD
 Socket
 Pusher
 Pipes
 Encryption
 Plugin
 Inter-Process
 Errors
 Deep C Secrets
 C + UNIX
 Linked Lists / Trees
 Asm
 Perl
 Python
 Shell
 Erlang
 Go
 Rust
NEWS
Последние статьи :
  Rust 07.11   
  Go 25.12   
  EXT4 10.11   
  FS benchmark 15.09   
  Сетунь 23.07   
  Trees 25.06   
  Apache 03.02   
  SQL 30.07   
  JFS 10.06   
  B-trees 01.06   
 
TOP 20
 P.Collins...1342 
 Steve Pate 1...791 
 QT->Qt3...509 
 Trees...433 
 Python...357 
 Steve Pate 3...340 
 TCP 2...316 
 Rodriguez 6...293 
 Rubni-Corbet -> Глав...291 
 Максвелл 3...290 
 TCP 3...289 
 Daniel Bovet 4...289 
 Mod_perl 2...279 
 Linux Inline Assembly...279 
 MySQL & PosgreSQL...273 
 Robert Love 2...265 
 UML 3...264 
 Rodriguez 2...264 
 Стивенс 6...262 
 Daniel Bovet 3...260 
 
  01.05.2017 : 2190164 посещений 

iakovlev.org

1. Системные вызовы

Системные вызовы - это набор функций, реализованных в ядре ОС. Любой запрос приложения пользователя в конечном итоге трансформируется в системный вызов, который выполняет запрашиваемое действие. Полный перечень системных вызовов ОС Linux находится в файле

 /usr/include/asm/unistd.h.

Давайте рассмотрим общий механизм выполнения системных вызовов на примере. Пусть в исходном тексте приложения вызывается функция creat() для создания нового файла. Компилятор, встретив вызов данной функции, преобразует его в ассемблерный код, обеспечивая загрузку номера системного вызова, соответствующего данной функции, и ее параметров в регистры процессора и последующий вызов прерывания 0x80.

В регистры процессора загружаются следующие значения:

* в регистр EAX - номер системного вызова. Так, для нашего случая номер системного вызова будет равен 8 (см. __NR_creat);

* в регистр EBX - первый параметр функции (для creat это указатель на строку, содержащую имя создаваемого файла);

* в регистр ECX - второй параметр (права доступа к файлу).

В регистр EDX загружается третий параметр, в данном случае он у нас отсутствует.

Для выполнения системного вызова в ОС Linux используется функция system_call, которая определена в файле arch/i386/kernel/entry.S. Эта функция - точка входа для всех системных вызовов. Ядро реагирует на прерывание 0x80 обращением к функции system_call, которая по сути представляет собой обработчик прерывания 0x80.

Чтобы убедиться, что мы на правильном пути, напишем небольшой тестовый фрагмент на ассемблере. В нем увидим, во что превращается функция creat() после компиляции. Файл назовем test.S. Вот его содержание:

	.globl _start
 	.text
 
 _start:
 
В регистр EAX загружаем номер системного вызова:
	movl $8, %eax
 
 
В регистр EBX - первый параметр, указатель на строку с именем файла:
	movl $filename, %ebx
 
В регистр ECX - второй параметр, права доступа:
	movl $0, %ecx
 
Вызываем прерывание:
	int $0x80
 
Выходим из программы. Для этого вызовем функцию exit(0):
	movl $1, %eax
 	movl $0, %ebx
 	int $0x80
 
В сегменте данных укажем имя создаваемого файла:
	.data
 
 filename: .string  "file.txt"
 
Компилируем:
	gcc -с test.S
 	ld -s -o test test.o
 

В текущем каталоге появится исполняемый файл test. Запустив его, мы создадим новый файл с именем file.txt.

Вернемся к рассмотрению механизма системных вызовов. Итак, ядро вызывает обработчик прерывания 0x80 - функцию system_call. System_call помещает копии регистров, содержащих параметры вызова, в стек при помощи макроса SAVE_ALL, и командой call вызывает нужную системную функцию. Таблица указателей на функции ядра, которые реализуют системные вызовы, расположена в массиве sys_call_table (см. файл arch/i386/kernel/entry.S). Номер системного вызова, который находится в регистре EAX, является индексом в этом массиве. Таким образом, если в EAX находится значение 8, будет вызвана функция ядра sys_creat().

Возвращаемое системным вызовом значение сохраняется в регистр EAX.

Перехватить системный вызов можно двумя способами - при помощи модуля ядра и путем прямой перезаписи адресного пространства ядра. Рассмотрим оба этих способа по очереди.

2. Перехват системных вызовов Linux при помощи загружаемых модулей ядра

Загружаемый модуль ядра (обозначим его LKM - Loadable Kernel Module) - это программный код, выполняемый в пространстве ядра. Главной особенностью LKM является возможность динамической загрузки и выгрузки, без необходимости перезагрузки всей системы или перекомпиляции ядра.

Каждый LKM состоит, как минимум, из двух функций:

- функция инициализации модуля. Вызывается при загрузке LKM в память:

int init_module(void)
 {
 ...
 }
 

- функция выгрузки модуля:

void cleanup_module(void)
 {
 ...
 }
 

Приведем пример простейшего модуля.

#define MODULE
 #include <linux/module.h>
 
 int init_module(void)
 {
 	printk("Hello World\n");
 	return 0;
 }
 
 void cleanup_module(void)
 {
 	printk("Bye\n");
 }
 
 
Компилируем и загружаем модуль. Загрузку модуля в память осуществляет команда insmod:
	gcc -O2 -Wall -c helloworld.c
 	insmod helloworld.o
 

Информация обо всех загруженных в данный момент в систему модулях находится в файле /proc/modules. Чтобы убедиться, что модуль загружен, введите команду cat /proc/modules либо lsmod.

Выгружает модуль команда rmmod:

	rmmod helloworld
 

3. Алгоритм перехвата системного вызова

Для реализации модуля, перехватывающего системный вызов, необходимо определить алгоритм перехвата. Алгоритм следующий:

* сохранить указатель на оригинальный (исходный) вызов, для возможности его восстановления;

* создать функцию, реализующую новый системный вызов;

* в таблице системных вызовов sys_call_table произвести замену вызовов, настроив соответствующий указатель на новый системный вызов;

* по окончании работы (при выгрузке модуля) восстановить оригинальный системный вызов, используя ранее сохраненный указатель.

Выяснить, какие системные вызовы задействуются при работе приложения пользователя, позволяет трассировка. Осуществив трассировку, можно определить, какой именно системный вызов следует перехватить, чтобы взять под контроль работу приложения. Пример использования программы трассировки будет рассмотрен ниже.

Теперь у нас достаточно информации, чтобы приступить к изучению примеров реализации модулей, осуществляющих перехват системных вызовов.

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

4.1. Запрет создания каталогов

При создание каталога вызывается функция ядра sys_mkdir. В качестве параметра задается строка, в которой содержится имя создаваемого каталога. Рассмотрим код, осуществляющий перехват соответствующего системного вызова.

#include <linux/module.h>
 
 #include <linux/kernel.h>
 #include <sys/syscall.h>
 
Экспортируем таблицу системных вызовов:
extern void *sys_call_table[];
 
Определим указатель для сохранения оригинального системного вызова:
int (*orig_mkdir)(const char *path);
 
Создадим собственный системный вызов. Наш вызов ничего не делает, просто возвращает нулевое значение:
int own_mkdir(const char *path)
 {
     return 0;
 }
 

Во время инициализации модуля сохраняем указатель на оригинальный вызов и производим замену системного вызова:

int init_module()
 {
     orig_mkdir=sys_call_table[SYS_mkdir];
     sys_call_table[SYS_mkdir]=own_mkdir;
     return 0;
 }
 

При выгрузке восстанавливаем оригинальный вызов:

void cleanup_module()
 {
     sys_call_table[SYS_mkdir]=orig_mkdir;
 }
 

Код сохраним в файле sys_mkdir_call.c. Для получения объектного модуля создадим Makefile следующего содержания:

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
 

Командой make создадим модуль ядра. Загрузив его, попытаемся создать каталог командой mkdir. Как вы можете убедиться, ничего при этом не происходит. Команда не работает. Для восстановления ее работоспособности достаточно выгрузить модуль.

4.2. Запрет чтения файла

Для того, чтобы прочитать файл, его необходимо вначале открыть при помощи функции open. Легко догадаться, что этой функции соответствует системный вызов sys_open. Перехватив его, мы можем защитить файл от прочтения. Рассмотрим реализацию модуля-перехватчика.

#include <linux/module.h>
 #include <linux/kernel.h>
 #include <sys/syscall.h>
 #include <linux/types.h>
 #include <linux/slab.h>
 
 #include <linux/string.h>
 #include <asm/uaccess.h>
 
 extern void *sys_call_table[];
 
Указатель для сохранения оригинального системного вызова:
int (*orig_open)(const char *pathname, int flag, int mode);
 

Первым параметром функции open является имя открываемого файла. Новый системный вызов должен сравнить этот параметр с именем файла, который мы хотим защитить. Если имена совпадут, будет сымитирована ошибка открытия файла.

Наш новый системный вызов имеет вид:

int own_open(const char *pathname, int flag, int mode)
 {
 
Сюда поместим имя открываемого файла:
    char *kernel_path;
 
Имя файла, который мы хотим защитить:
    char hide[]="test.txt";
 

Выделим память и скопируем туда имя открываемого файла:

    kernel_path=(char *)kmalloc(255,GFP_KERNEL);
     copy_from_user(kernel_path, pathname, 255);
 

Сравниваем:

    if(strstr(kernel_path,(char *)&hide) != NULL) {
 
 

Освобождаем память и возвращаем код ошибки при совпадении имен:

	kfree(kernel_path);
 	return -ENOENT;
     }
 
     else {
 

Если имена не совпали, вызываем оригинальный системный вызов для выполнения стандартной процедуры открытия файла:

	kfree(kernel_path);
 	return orig_open(pathname, flag, mode);
     }
 }
 
Далее смотрите комментарии к предыдущему примеру.
int init_module()
 {
     orig_open=sys_call_table[SYS_open];
     sys_call_table[SYS_open]=own_open;
     return 0;
 }
 
 void cleanup_module()
 {
     sys_call_table[SYS_open]=orig_open;
 }
 
Сохраним код в файле sys_open_call.c и создадим Makefile для получения объектного модуля:
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
 
 

В текущем каталоге создадим файл с именем test.txt, загрузим модуль и введем команду cat test.txt. Система сообщит об отсутствии файла с таким именем.

Честно говоря, такую защиту легко обойти. Достаточно командой mv переименовать файл, а затем прочесть его содержимое.

4.3. Сокрытие записи о файле в каталоге

Определим, какой системный вызов отвечает за чтение содержимого каталога. Для этого напишем еще один тестовый фрагмент, который занимается чтением текущей директории:

	/* Файл dir.c*/
 	#include 
 	#include 
 
 	int main ()
 	{
 		DIR *d;
 		struct dirent *dp;
 
 		d = opendir(".");
 		dp = readdir(d);
 
 		return 0;
 	}
 
 
Получим исполняемый модуль
	gcc -o dir dir.c
 
и выполним его трассировку:
	strace ./dir
 
Обратим внимание на предпоследнюю строку:
	getdents (6, /* 4 entries*/, 3933) = 72;
 

Содержимое каталога считывает функция getdents. Результат сохраняется в виде списка структур типа struct dirent. Второй параметр этой функции является указателем на этот список. Функция возвращает длину всех записей в каталоге.

В нашем примере функция getdents определила наличие в текущем каталоге четырех записей - ".", ".." и два наших файла, исполняемый модуль и исходный текст. Длина всех записей в каталоге составляет 72 байта. Информация о каждой записи сохраняется, как мы уже сказали, в структуре struct dirent. Для нас интерес представляют два поля данной структуры:

	d_reclen	- размер записи;
 	d_name	- имя файла.
 
 

Для того, чтобы спрятать запись о файле (другими словами, сделать его невидимым), необходимо перехватить системный вызов sys_getdents, найти в списке полученных структур соответствующую запись и удалить ее.

Рассмотрим код, выполняющий эту операцию (автор оригинального кода - Michal Zalewski):

#include <linux/module.h>
 #include <linux/kernel.h>
 #include <linux/types.h>
 #include <linux/dirent.h>
 #include <linux/slab.h>
 
 #include <linux/string.h>
 #include <sys/syscall.h>
 #include <asm/uaccess.h>
 
 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);
 	copy_from_user(dirp2,dirp,tmp);
 
Задействуем вторую структуру и сохраним значение длины записей в каталоге:
	dirp3=dirp2;
 	t=tmp;
 
 
Начнем искать наш файл:
	while(t>0) {
 
Считываем длину первой записи и определяем оставшуюся длину записей в каталоге:
		n = dirp3->d_reclen;
 		t -= n;
 
Проверяем, не совпало ли имя файла из текущей записи с искомым:
		if(strstr((char *)&(dirp3->d_name),(char *)&hide) != NULL) {
 
 
Если это так, затираем запись и вычисляем новое значение длины записей в каталоге:
			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;
 }
 
Сохраним исходный текст в файле sys_call_getd.c и создадим Makefile следующего содержания:
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
 

В текущем каталоге создадим файл our.file и загрузим модуль. Файл исчезает, что и требовалось доказать.

А теперь рассмотрим, как осуществить перехват системного вызова ОС Linux, используя метод прямого доступа к адресному пространству ядра.

5. Общая методика перехвата

Прямой доступ к адресному пространству ядра обеспечивает файл устройства /dev/kmem. В этом файле отображено все доступное виртуальное адресное пространство, включая раздел подкачки (swap-область). Для работы с файлом kmem используются стандартные системные функции - open(), read(), write(). Открыв стандартным способом /dev/kmem, мы можем обратиться к любому адресу в системе, задав его как смещение в этом файле. Данный метод был разработан Сильвио Чезаре (Silvio Cesare) (см. статью "Runtime kernel kmem patching", Silvio Cesare, http://www.sans.org/rr/threats/rootkits.php).

Вспомним (кратко) механизм функционирования системных вызовов в ОС Linux. Обращение к системной функции осуществляется посредством загрузки параметров функции в регистры процессора и последующим вызовом программного прерывания int $0x80. Обработчик этого прерывания, функция system_call, помещает параметры вызова в стек, извлекает из таблицы sys_call_table адрес вызываемой системной функции, и передает управление по этому адресу.

Имея полный доступ к адресному пространству ядра, мы можем получить все содержимое таблицы системных вызовов, т.е. адреса всех системных функций. Изменив адрес любого системного вызова, мы, тем самым, осуществим его перехват. Но для этого необходимо знать адрес таблицы, или, другими словами, смещение в файле /dev/kmem, по которому эта таблица расположена.

Чтобы определить адрес таблицы sys_call_table, предварительно необходимо вычислить адрес функции system_call. Поскольку данная функция является обработчиком прерывания, давайте рассмотрим, как обрабатываются прерывания в защищенном режиме.

В реальном режиме процессор при регистрации прерывания обращается к таблице векторов прерываний, находящейся всегда в самом начале памяти и содержащей двухсловные адреса программ обработки прерываний. В защищенном режиме аналогом таблицы векторов прерываний является таблица дескрипторов прерываний (IDT, Interrupt Descriptor Table), располагающаяся в операционной системе защищенного режима. Для того, чтобы процессор мог обратиться к этой таблице, ее адрес следует загрузить в регистр таблицы дескрипторов прерываний (IDTR, Interrupt Descriptor Table Register). Таблица IDT содержит дескрипторы обработчиков прерываний, в которые, в частности, входят их адреса. Эти дескрипторы называются шлюзами (вентилями). Процессор, зарегистрировав прерывание, по его номеру извлекает из IDT шлюз, определяет адрес обработчика и передает ему управление.

Для вычисления адреса функции system_call из таблицы IDT необходимо извлечь шлюз прерывания int $0x80, а из него - адрес соответствующего обработчика, т.е. адрес функции system_call. В функции system_call обращение к таблице sys_call_table выполняет команда call <адрес таблицы>(,%eax,4) (см. файл arch/i386/kernel/entry.S). Найдя опкод (сигнатуру) этой команды в файле /dev/kmem, мы найдем и адрес таблицы системных вызовов.

Для определения опкода воспользуемся отладчиком gdb. Загрузим в отладчик ядро:

	gdb -q /usr/src/linux/vmlinux
 

Файл vmlinux - это ядро в формате ELF, получаемое после компиляции.

Дизассемблируем функцию system_call:

	disass system_call
 
В ответ на экран будет выведен ассемблерный листинг. В этом листинге ищем строку типа:
	0xc010904d <system_call+45>: call *0xc0200520(,%eax,4)
 
 
Это и есть обращение к таблице sys_call_table. Значение 0xc0200520 - адрес таблицы (скорее всего, у вас значение адреса будет другим). Получим опкод этой команды:
	x/xw (system_call+45)
 
Результат:
	0xc010904d <system_call+45>: 0x208514ff
 
Мы нашли опкод команды обращения к таблице sys_call_table. Он равен \xff\x14\x85. Следующие за ним 4 байта - это адрес таблицы. Убедиться в этом можно, введя команду
	x/xw (system_call+45+3)
 
В ответ получим
	0xc0109050 : 0xc02000520
 
 

Таким образом, найдя в файле /dev/kmem последовательность \xff\x14\x85 и считав следующие за ней 4 байта, мы получим адрес таблицы системных вызовов sys_call_table. Зная ее адрес, мы можем получит содержимое этой таблицы (адреса всех системных функций) и изменить адрес любого системного вызова, перехватить его.

Рассмотрим псевдокод, выполняющий операцию перехвата:

	readaddr (&old_syscall, sct + SYS_CALL*4, 4);
 	writeaddr (new_syscall, sct + SYS_CALL*4, 4);
 

Функция readaddr считывает адрес системного вызова из таблицы системных вызовов и сохраняет его в переменной old_syscall. Каждая запись в таблице sys_call_table занимает 4 байта. Искомый адрес расположен по смещению sct+SYS_CALL*4 в файле /dev/kmem (здесь sct - адрес таблицы sys_call_table, SYS_CALL - порядковый номер системного вызова).

Функция writeaddr перезаписывает адрес системного вызова SYS_CALL адресом функции new_syscall, и все обращения к системному вызову SYS_CALL будут обслуживаться этой функцией.

Кажется, все просто и цель достигнута. Однако давайте вспомним, что мы работаем в адресном пространстве пользователя. Если разместить новую системную функцию в этом адресном пространстве, то при вызове этой функции мы получим красивое сообщение об ошибке. Отсюда вывод - новый системный вызов необходимо разместить в адресном пространстве ядра. Для этого необходимо:

- получить блок памяти в пространстве ядра;

- разместить в этом блоке новый системный вызов.

Выделить память в пространстве ядра можно при помощи функции kmalloc. Но вызвать напрямую функцию ядра из адресного пространства пользователя нельзя, поэтому воспользуемся следующим алгоритмом:

- зная адрес таблицы sys_call_table, получаем адрес некоторого системного вызова (например, sys_mkdir);

- определяем функцию, выполняющую обращение к функции kmalloc. Эта функция возвращает указатель на блок памяти в адресном пространстве ядра. Назовем эту функцию get_kmalloc;

- сохраняем первые N байт системного вызова sys_mkdir, где N - размер функции get_kmalloc;

- перезаписываем первые N байт вызова sys_mkdir функцией get_kmalloc;

- выполняем обращение с системному вызову sys_mkdir, тем самым запустив на выполнение функцию get_kmalloc;

- восстанавливаем первые N байт системного вызова sys_mkdir.

В результате в нашем распоряжении будет блок памяти, расположенный в пространстве ядра.

Функция get_kmalloc выглядит следующим образом:

struct kma_struc {
 	ulong (*kmalloc) (uint, int);
         int size;
         int flags;
         ulong mem;
 } __attribute__ ((packed));
 	    
 int get_kmalloc(struct kma_struc *k)
 {
 	k->mem = k->kmalloc(k->size, k->flags);
         return 0;
 }
 

Поля структуры struct kma_struc заполняются следующими значениями:

- поле size - требуемый размер блока памяти;

- поле флаг - спецификатор GFP_KERNEL. Для версий ядра 2.4.9 и выше это значение составляет 0x1f0;

- поле mem - в этом поле будет сохранен указатель на начало блока памяти длиной size, выделенного в адресном пространстве ядра (возвращаемое функцией kmalloc значение);

- поле kmalloc - адрес функции kmalloc.

Адрес функции kmalloc необходимо найти. Сделать это можно несколькими способами. Самый простой путь - считать этот адрес из файла System.map или определить с помощью отладчика gdb (print &kmalloc).

Если в ядре включена поддержка модулей, адрес kmalloc можно определить при помощи функции get_kernel_syms(). Этот вариант будет рассмотрен ниже.

Если же поддержка модулей ядра отсутствует, то адрес функции kmalloc придется искать по опкоду команды вызова kmalloc - аналогично тому, как было сделано для таблицы sys_call_table.

Функция kmalloc принимает два параметра - размер запрашиваемой памяти и спецификатор GFP. Вызов этой функции выглядит следующим образом:

	push GFP_KERNEL
 	push size
 	call kmalloc
 

Для поиска опкода воспользуемся отладчиком и дизассемблируем любую функцию ядра, в которой есть вызов функции kmalloc.

Загружаем ядро в отладчик:

	gdb -q /usr/src/linux/vmlinux
 

Дизассемблируем функцию inter_module_register. Неважно, что делает эта функция, главное, в ней есть то, что нам нужно - вызов функции kmalloc:

	disass inter_module_register
 

Сразу обращаем внимание на следующие строки:

	0xc0110de4 :	push $0x1f0
 	0xc0110de9 :	push $0x14
 	0xc0110deb :	call 0xc0121c38 
 

Это и есть вызов функции kmalloc. Сначала в стек загружаются параметры, а затем следует вызов функции. Значение 0xc0121c38 в вызове call является адресом функции kmalloc.

Первым в стек загружается спецификатор GFP (push $0x1f0). Как уже упоминалось, для версий ядра 2.4.9 и выше это значение составляет 0x1f0. Найдем опкод этой команды:

	x/xw (inter_module_register+4)
 
 

В результате получаем:

	0xc0110de4 : 0x0001f068
 

Если мы найдем этот опкод, то сможем вычислить адрес функции kmalloc. На первый взгляд, адрес этой функции является аргументом инструкции call, но это не совсем так. В отличии от функции system_call, здесь за инструкцией call стоит не адрес kmalloc, а смещение к нему относительно текущего адреса. Убедимся в этом, определив опкод команды call 0xc0121c38:

	x/xw (inter_module_register+11)
 

В ответ получаем:

	0xc0110deb : 0x010e48e8
 
 

Первый байт равен e8 - это опкод инструкции call. Найдем значение аргумента этой команды:

	x/xw (inter_module_register+12)
 

Получим

	0xc0110deс : 0x00010e48
 

Теперь если мы сложим текущий адрес 0xc0110deb, смещение 0x00010e48 и 5 байт команды (1 байт инструкции call и 4 байта смещения), то получим искомый адрес функции kmalloc:

	0xc0110deb + 0x00010e48 + 5 = 0xc0121c38
 

На этом завершим теоретические выкладки и, используя вышеприведенную методику, осуществим перехват системного вызова sys_mkdir.

6. Пример перехвата системного вызова

Заголовочные файлы:

#include <stdio.h>
 #include <string.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 
 #include <fcntl.h>
 #include <errno.h>
 #include <linux/module.h>
 #include <linux/unistd.h>
 
Определим имя файла устройства виртуальной памяти:
#define KMEM_FILE "/dev/kmem"
 
 int main() {
 
Структура, описывающая формат регистра IDTR:
struct {
     unsigned short limit;
     unsigned int base;
 } __attribute__ ((packed)) idtr;
 
 
Структура, описывающая формат шлюза прерывания таблицы IDT:
struct {
     unsigned short off1;
     unsigned short sel;
     unsigned char none, flags;
     unsigned short off2;
 } __attribute__ ((packed)) idt;
 
 
Номер вызова, который мы будем перехватывать:
#define _SYS_MKDIR_ 39
 
Номер 39 соответствует системному вызову sys_mkdir.

Переменные и их назначение:

    int kmem;
     ulong get_kmalloc_size;	// размер функции get_kmalloc
     ulong get_kmalloc_addr;	// адрес функции get_kmalloc
     ulong new_mkdir_size;	// размер функции-перехватчика
     ulong new_mkdir_addr;	// адрес функции-перехватчика вызова sys_mkdir
     ulong sys_mkdir_addr;	// адрес системного вызова sys_mkdir
     ulong page_offset;		// нижняя граница адресного пространства ядра
     ulong sct;			// адрес таблицы sys_call_table
     ulong kma;			// адрес функции kmalloc
     unsigned char tmp[1024];
 
Значения адресов функций get_kmalloc и new_mkdir_call и их размеры нам предстоит определить. Пока что оставим эти поля пустыми.
struct  kma_struc {
         ulong (*kmalloc) (uint, int);
         int size;
         int flags;
         ulong mem;
 } __attribute__ ((packed)) kmalloc;
 
 int get_kmalloc(struct kma_struc *k)
 {
 	k->mem = k->kmalloc(k->size, k->flags);
 	return 0;
 }
 
 
Структура struct kma_struc и функция get_kmalloc и их назначение уже были рассмотрены
int new_mkdir(const char *path)
 {
 	return 0;
 }
 
Функция new_mkdir перехватывает системный вызов sys_mkdir. Она ничего не делает, просто возвращает нулевое значение.

Определим несколько функций для работы с файлом устройства /dev/kmem.

Функция чтения данных из kmem:

static inline int rkm(int fd, uint offset, void *buf, uint size)
 {
         if (lseek(fd, offset, 0) != offset) return 0;
         if (read(fd, buf, size) != size) return 0;
         return size;
 }
 

Функция записи данных в kmem:

static inline int wkm(int fd, uint offset, void *buf, uint size)
 {
         if (lseek(fd, offset, 0) != offset) return 0;
         if (write(fd, buf, size) != size) return 0;
         return size;
 }
 

Функция чтения 4-х байтового значения (int, unsigned long) из kmem:

static inline int rkml(int fd, uint offset, ulong *buf)
 {
         return rkm(fd, offset, buf, sizeof(ulong));
 }
 
 

Функция записи 4-х байтового значения в kmem:

static inline int wkml(int fd, uint offset, ulong buf)
 {
         return wkm(fd, offset, &buf, sizeof(ulong));
 }
 

Следующая функция определяет адрес таблицы sys_call_table:

ulong get_sct()
 {
 	int kmem;
         ulong sys_call_off; // адрес обработчика прерывания int $0x80
 	                    // (функции system_call)
         char *p;
         char sc_asm[128];
 
         kmem = open("/dev/kmem", O_RDONLY);
         if (kmem < 0) return 0;
 

Командой SIDT получаем содержимое регистра таблицы дескрипторов прерываний. Результат команды поместим в структуру idtr:

        asm("sidt %0" : "=m" (idtr));
 
 

В поле base структуры idtr находиться адрес таблицы IDT. Зная этот адрес и размер шлюза в этой таблице (8 байт), получим содержимое шлюза прерывания int $0x80 и извлечем из него адрес обработчика:

        if (!rkm(kmem, idtr.base+(8*0x80), &idt, sizeof(idt))) return 0;
 

Содержимое шлюза поместим в структуру idt. Два поля этой структуры, off1 и off2, содержат адрес обработчика (функции system_call). В поле off1 находятся младшие 16 бит, а в поле off2 - старшие 16 бит адреса обработчика. Для получения адреса обработчика сложим содержимое этих полей следующим образом:

        sys_call_off = (idt.off2 << 16) | idt.off1;
 
Теперь нам известен адрес функции system_call (если точнее, это не адрес, а смещение в сегменте команд). Для получения адреса таблицы sys_call_table попытаемся найти опкод команды обращения к этой таблице.

Смещаемся по адресу функции system_call и считываем первые 128 байт обработчика в буфер sc_asm:

        if (!rkm(kmem, sys_call_off, &sc_asm, 128)) return 0;
         close(kmem);
 
 

В этом буфере ищем опкод обращения к таблице sys_call_table. Поиск выполняется при помощи функции memmem. Данная функция возвращает указатель на позицию в буфере, в которой была найдена эталонная строка. В нашем случае эталонной строкой является опкод команды обращения к таблице sys_call_table - \xff\x14\x85. Если этот опкод найден, то следующие за ним 4 байта будут содержать адрес таблицы:

        p = (char *)memmem(sc_asm, 128, "\xff\x14\x85", 3) + 3;
 

Если опкод удалось найти, возвращаем адрес таблицы системных вызовов:

        if (p) return *(ulong *)p;
 

В случае неудачи возвращаем нулевое значение:

        return 0;
 }
 

Функция для определения адреса функции kmalloc:

ulong get_kma(ulong pgoff)
 {
     uint i;
     unsigned char buf[0x10000], *p, *p1;
     int kmem;
     ulong ret;
 
Функция принимает один параметр - величину нижней граница адресного пространства ядра. Это значение составляет 0xc0000000.

Если в ядре включена поддержка модулей, то воспользуемся этим:

    ret = get_sym("kmalloc");
     if (ret) {
 	printf("\nZer gut!\n");
 	return ret;
     }
 
Если нет, будем искать адрес по опкоду.
    kmem = open("/dev/kmem", O_RDONLY);
     if (kmem < 0) return 0;
 
Для поиска нам придется просканировать все адресное пространство ядра. Для этого организуем цикл:
    for (i = pgoff+0x100000; i < (pgoff + 0x1000000); i += 0x10000)
         {
 
 
Считываем в буфер buf содержимое адресного пространства ядра:
            if (!rkm(kmem, i, buf, sizeof(buf))) return 0;
 
В этом буфере ищем опкод команды push $0x1f0, который, как нами установлено, равен \x68\xf0\x01\x00. Для поиска используем функцию memmem:
    	    p1=(char *)memmem(buf,sizeof(buf),"\x68\xf0\x01\x00",4);
 
 	    if(p1) {
 
Если последовательность \x68\xf0\x01\x00 найдена, ищем опкод инструкции call (\xe8). Сразу за ним будет находиться смещение к функции kmalloc относительно текущего адреса:
		p=(char *)memmem(p1+4,sizeof(buf),"\xe8",1)+1;
 		if (p) {
 
В этом месте указатель p в буфере buf будет позиционирован на смещении к функции kmalloc. Закрываем файл устройства и возвращаем адрес kmalloc:
		    close(kmem);
 		    return *(unsigned long *)p+i+(p-buf)+4;
 		}
 	    }
 	}
 
Если ничего найти не удалось, возвращаем нулевое значение:
	close(kmem);
 	return 0;
 }
 
 
 
Функция get_sym используется, если в ядре включена поддержка модулей. Данная функция принимает строку, содержащую имя функции ядра, и возвращает ее адрес:
#define MAX_SYMS 4096
 ulong get_sym(char *n) {
 	struct kernel_sym tab[MAX_SYMS];
         int numsyms;
         int i;
 
         numsyms = get_kernel_syms(NULL);
         if (numsyms > MAX_SYMS || numsyms < 0) return 0;
         get_kernel_syms(tab);
         for (i = 0; i < numsyms; i++) {
                 if (!strncmp(n, tab[i].name, strlen(n)))
 			return tab[i].value;
         }
         return 0;
 }
 

Итак, все необходимые функции определены. Теперь приступим непосредственно к перехвату системного вызова sys_mkdir. Определим адреса таблицы системных вызовов (sct), функции kmalloc (kma) и нижней границы адресного пространства ядра (page_offset):

    sct = get_sct();
     page_offset = sct & 0xF0000000;
     kma = get_kma(page_offset);
 
Отобразим полученные данные:
        printf("OK\n"
                 "page_offset\t\t:\t0x%08x\n"
                 "sys_call_table[]\t:\t0x%08x\n"
                 "kmalloc()\t\t:\t0x%08x\n",
                 page_offset, sct, kma);
 
         kmem = open(KMEM_FILE, O_RDWR, 0);
         if (kmem < 0) return 0;
 
Для размещения функции new_mkdir в адресном пространстве ядра выделим блок памяти. Для этого воспользуемся вышеприведенным алгоритмом вызова функции ядра из пространства пользователя.

Получим адрес системного вызова sys_mkdir:

        if (!rkml(kmem, sct+(_SYS_MKDIR_*4), &sys_mkdir_addr)) {
                 printf("Cannot get addr of %d syscall\n", _SYS_MKDIR_);
                 return 1;
         }
 
Сохраним первые N байт этого вызова в буфере tmp, где N=get_kmalloc_size (get_kmalloc_size - это размер функции get_kmalloc и его предстоит определить):
        if (!rkm(kmem, sys_mkdir_addr, tmp, get_kmalloc_size)) {
                 printf("Cannot save old %d syscall!\n", _SYS_MKDIR_);
                 return 1;
         }
 

Перезаписываем N сохраненных байт системного вызова sys_mkdir функцией get_kmalloc:

        if (!wkm(kmem, sys_mkdir_addr,(void *)get_kmalloc_addr, get_kmalloc_size)) {
                 printf("Can't overwrite our syscall %d!\n",_SYS_MKDIR_);
                 return 1;
         }
 
 

Адрес функции get_kmalloc (get_kmalloc_addr) также предстоит определить.

Заполняем поля структуры struct kma_struc:

        kmalloc.kmalloc = (void *) kma;	// адрес функции kmalloc
         kmalloc.size = new_mkdir_size;	// размер запрашиваемой памяти 
 	                                //(размер функции-перехватчика new_mkdir)
         kmalloc.flags = 0x1f0;		// спецификатор GFP
 

А теперь обращаемся к системному вызову sys_mkdir, запустив, тем самым, на выполнение функцию get_kmalloc:

	mkdir((char *)&kmalloc,0);
 

В результате в пространстве ядра будет выделен блок памяти, указатель на который будет записан в поле mem структуры struct kma_struc. В этом блоке памяти мы разместим функцию new_mkdir, которая будет обслуживать все обращения к системному вызову sys_mkdir.

Восстанавливаем системный вызов sys_mkdir:

        if (!wkm(kmem, sys_mkdir_addr, tmp, get_kmalloc_size)) {
                 printf("Can't restore syscall %d !\n",_SYS_MKDIR_);
                 return 1;
         }
 
 
Проверяем значение указателя на блок выделенной памяти. Он должен располагаться выше нижней границы адресного пространства ядра:
        if (kmalloc.mem < page_offset) {
                 printf("Allocated memory is too low (%08x < %08x)\n", 
 		kmalloc.mem, page_offset);
                 return 1;
         }
 
Отображаем результаты:
        printf(
                 "sys_mkdir_addr\t\t:\t0x%08x\n"
                 "get_kmalloc_size\t:\t0x%08x (%d bytes)\n\n"
                 "our kmem region\t\t:\t0x%08x\n"
                 "size of our kmem\t:\t0x%08x (%d bytes)\n\n",
                 sys_mkdir_addr, get_kmalloc_size, get_kmalloc_size,
                 kmalloc.mem, kmalloc.size, kmalloc.size);
 
Размещаем в пространстве ядра функцию new_mkdir:
	if(!wkm(kmem, kmalloc.mem, (void *)new_mkdir_addr, new_mkdir_size)) {
 		printf("Unable to locate new system call !\n");
 		return 1;
 	}
 
и в таблице системных вызовов заменяем адрес функции sys_mkdir адресом new_mkdir:
	if(!wkml(kmem, sct+(_SYS_MKDIR_*4), kmalloc.mem)) {
 		printf("...");
 		return 1;
 	}
 
     return 1;
 }
 
Сохраним вышеприведенный код в файле new_mkdir.c и получим исполняемый модуль командой:
	gcc -o new_mkdir new_mkdir.c
 

Но запускать на выполнение полученный модуль пока рано. Нам еще необходимо определить адреса и размеры функций get_kmalloc и new_mkdir. Для этого воспользуемся утилитой objdump. Введем команду:

	objdump -x ./new_mkdir > dump
 
Вывод перенаправим в файл dump. Откроем этот файл и найдем в нем следующие строки:
	08048630 l	F .text	00000038	get_kmalloc.39
 	08048668 l	F .text	00000011	new_mkdir.43
 
 
Итак, адрес функции get_kmalloc - 0x08048630, размер - 56 байт (0x38), адрес функции new_mkdir - 0x08048668, размер - 17 байт (0x11).

Открываем файл new_mkdir.c и в разделе переменных заполняем полученными значениями соответствующие поля:

    ulong get_kmalloc_size = 56;		// размер функции get_kmalloc
     ulong get_kmalloc_addr = 0x08048630;	// адрес функции get_kmalloc
     ulong new_mkdir_size = 17;		// размер функции new_mkdir
     ulong new_mkdir_addr = 0x08048668;	// адрес функции new_mkdir
 

После этого перекомпилируем модуль.

Запустив его на выполнение, мы осуществим перехват системного вызова sys_mkdir. Все обращения к вызову sys_mkdir будут обслуживаться функцией new_mkdir.

Чтобы убедиться, что вызов перехвачен, введем команду mkdir <имя каталога>. При этом ничего не произойдет, так как функция-перехватчик new_mkdir просто вернет нулевое значение.

Работоспособность вышеприведенного кода была проверена для ядер версий 2.4.17 и 2.4.20.

При подготовке статьи были использованы материалы сайтов phrack.org, thehackerschoice.com

Впервые статья была опубликована в журнале "Системный администратор"
Оставьте свой комментарий !

Ваше имя:
Комментарий:
Оба поля являются обязательными

 Автор  Комментарий к данной статье