Linux - статьи


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


До сих пор, все что мы делали, это использовали ранее определенные механизмы ядра для регистрации файлов в файловой системе /proc и файлов устройств. Это все замечательно, но это годится только для создания драйверов устройств. А что если вы хотите сделать действительно что-то необычное, например изменить реакцию системы на какое либо событие?

Мы как раз вступаем в ту область, где программирование действительно становится опасным. При разработке примера, приведенного ниже, я "уничтожил" системный вызов open. В результате система потеряла возможность открывать любые файлы, что равносильно отказу системы выполнять любые программы. Я не мог даже остановить систему командой shutdown. Из-за этого пришлось прибегнуть к "помощи" кнопки выключения питания. К счастью, ни один файл не был уничтожен. Чтобы обезопасить себя от потери данных, в аналогичных ситуациях, перед загрузкой подобных модулей всегда выполняйте резервное копирование.

Забудьте про /proc, забудьте и про файлы устройств. Реальный механизм взаимодействия процессов с ядром -- это системные вызовы. Когда процесс запрашивает какую-либо услугу ядра (например, открытие файла, запуск нового процесса или выделение дополнительной памяти), используется механизм системных вызовов. Если вы хотите изменить поведение ядра, то системные вызовы -- это как раз то место, куда можно приложить свои знания и умения. Между прочим, если вы захотите увидеть -- какие системные вызовы используются той или иной программой, запустите: strace <command> <arguments>.

Строго говоря, процесс не имеет доступа в пространство ядра. Он не может обращаться к памяти ядра и не может вызывать функции в ядре. Микропроцессор ограничивает такого рода доступ на аппаратном уровне (вот почему режим исполнения ядра называется защищенным, или привилегированным).

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


Точка перехода, в ядре, называется system_call. Процедура, которая там находится, проверяет номер системного вызова, который сообщает ядру -- какую именно услугу запрашивает процесс. Затем, она просматривает таблицу системных вызовов (sys_call_table), отыскивает адрес функции ядра, которую следует вызвать, после чего вызывается нужная функция. По окончании работы системного вызова, выполняется ряд дополнительных проверок и лишь после этого управление возвращается вызывающему процессу (или другому процессу, если вызывающий процесс исчерпал свой квант времени). Код, выполняющий все вышеперечисленные действия, вы найдете в файле arch/<architecture>/kernel/entry.S, после строки ENTRY(system_call).
Итак, если вас одолевает желание изменить поведение некоторого системного вызова, то первое, что необходимо сделать -- это написать вашу собственную функцию, которая выполняла бы требуемые действия (обычно, после выполнения своих действий, в подобных случаях, вызывается первоначальная функция, реализующая системный вызов), затем -- изменить указатель в sys_call_table так, чтобы он указывал на вашу функцию. Поскольку ваш модуль впоследствии может быть выгружен, то следует предусмотреть восстановление системы в ее первоначальное состояние, чтобы не оставлять ее в нестабильном состоянии. Это делается в пределах функции cleanup_module.
Ниже приводится исходный текст такого модуля. Он "шпионит" за выбранным пользователем, и посылать через printk сообщение всякий раз, когда данный пользователь открывает какой-либо файл. Для этого, системный вызов open(), подменяется функцией с именем our_sys_open. Она проверяет UID (User ID) текущего процесса, и если он равен заданному, то вызывает printk, чтобы сообщить имя открываемого файла, и в заключение вызывает оригинальную функцию open() с теми же параметрами, которая открывает требуемый файл.
Функция init_module изменяет соответствующий указатель в sys_call_table и сохраняет его первоначальное значение в переменной. Функция cleanup_module восстанавливает указатель в sys_call_table, используя эту переменную. В данном подходе кроются свои "подводные камни" из-за возможности существования двух модулей, перекрывающих один и тот же системный вызов. Представьте себе: имеется два модуля, А и B. Пусть модуль A перекрывает системный вызов open, своей функцией A_open, а модуль B -- функцией B_open. Первым загружается модуль A, он заменяет системный вызов open на A_open. Затем загружается модуль B, который заменит системный вызов A_open на B_open. Модуль B полагает, что он подменил оригинальный системный вызов, хотя на самом деле был подменен вызов A_open.


Теперь, если модуль B выгрузить первым, то ничего страшного не произойдет -- он просто восстановит запись в таблице sys_call_table в значение A_open, который в свою очередь вызывает оригинальную функцию sys_open. Однако, если первым будет выгружен модуль А, а затем B, то система "рухнет". Модуль А восстановит адрес в sys_call_table, указывающий на оригинальную функцию sys_open, "отсекая" таким образом модуль B от обработки действий по открытию файлов. Затем, когда будет выгружен модуль B, он восстановит адрес в sys_call_table на тот, который запомнил сам, потому что он считает его оригинальным. Т.е. вызовы будут направлены в функцию A_open, которой уже нет в памяти. На первый взгляд, проблему можно решить, проверкой -- совпадает ли адрес в sys_call_table с адресом нашей функции open и если не совпадает, то не восстанавливать значение этого вызова (таким образом B не будет "восстанавливать" системный вызов), но это порождает другую проблему. Когда выгружается модуль А, он "видит", что системный вызов был изменен на B_open и "отказывается" от восстановления указателя на sys_open. Теперь, функция B_open будет по прежнему пытаться вызывать A_open, которой больше не существует в памяти, так что система "рухнет" еще раньше -- до удаления модуля B.
Обратите внимание: подобные проблемы делают такую "подмену" системных вызовов неприменимой для широкого распространения.С целью предотвращения потенциальной опасности, связанной с подменой адресов системных вызовов, ядро более не экспортирует sys_call_table. Поэтому, если вы желаете сделать нечто большее, чем просто пробежать глазами по тексту данного примера, вам надлежит наложить "заплату" на ядро. В каталоге с примерами вы найдете файл README и "заплату". Как вы наверняка понимаете, подобные модификации сопряжены с определенными трудностями, поэтому я не рекомендую производить их на системах, владельцем которых вы не являетесь или не в состоянии быстро восстановить. Если вас одолевают сомнения, то лучшим выбором будет отказ от прогона этого примера.




Пример 7-1. syscall.c
/* * syscall.c * * Пример "перехвата" системного вызова. */
/* * Copyright (C) 2001 by Peter Jay Salzman */
/* * Необходимые заголовочные файлы */
#include <linux/kernel.h> /* Все-таки мы работаем с ядром! */ #include <linux/module.h> /* Необходимо для любого модуля */ #include <linux/moduleparam.h> /* для передачи параметров модулю */ #include <linux/unistd.h> /* Список системных вызовов */
/* * Необходимо, чтобы уметь определять * user id вызвавшего процесса. */ #include <linux/sched.h> #include <asm/uaccess.h>
/* * Таблица системных вызовов (таблица адресов функций). * Просто определим ее как ссылку на внешнюю таблицу. * * sys_call_table больше не экспортируется ядрами 2.6.x. * Если вы намереваетесь опробовать этот ОПАСНЫЙ модуль, * вам следует наложить "заплату" на ядро и пересобрать его */ extern void *sys_call_table[];
/* * UID пользователя, за которым "шпионим", * принимается из командной строки */ static int uid; module_param(uid, int, 0644);
/* * Указатель на оригинальную функцию, выполняющую системный вызов. * Мы сохраняем ее, вместо того, чтобы напрямую вызывать оригинальную * функцию, для того, чтобы имелась возможность вызова * обработчиков, вставленных до нас * Это не гарантирует 100% безопасность, поскольку другой модуль, * замещающий sys_open может быть выгружен раньше нашего модуля. * * Другая причина -- мы не можем получить адрес оригинальной sys_open. * Этот адрес не экспортируется ядром. */ asmlinkage int (*original_call) (const char *, int, int);
/* * Функция, замещающая sys_open (вызывается * всякий раз, когда делается обращение к системному вызову open). * Прототип функции, количество аргументов и их тип * вы найдете в fs/open.c. * * Теоретически - мы "привязаны" к данной конкретной * версии ядра. Практически -- системные вызовы * очень редко подвергаются кардинальному изменению, * поскольку это сделало бы огромное количество программного обеспечения * несовместимым с ядром и потребовало бы их пересборки. */ asmlinkage int our_sys_open(const char *filename, int flags, int mode) { int i = 0; char ch;


/* * Проверить -- это искомый пользователь? */ if (uid == current->uid) { /* * Зафиксировать */ printk("Opened file by %d: ", uid); do { get_user(ch, filename + i); i++; printk("%c", ch); } while (ch != 0); printk("\n"); }
/* * Вызвать оригинальную версию системного вызова sys_open - иначе * система потеряет возможность открывать файлы */ return original_call(filename, flags, mode); }
/* * Инициализация модуля - подмена системного вызова */ int init_module() { /* * Внимание - предупреждение запоздало, но * может быть в следующий раз... */ printk("I'm dangerous. I hope you did a "); printk("sync before you insmod'ed me.\n"); printk("My counterpart, cleanup_module(), is even"); printk("more dangerous. If\n"); printk("you value your file system, it will "); printk("be \"sync; rmmod\" \n"); printk("when you remove this module.\n");
/* * Сохранить указатель на оригинальную функцию * в переменной original_call, и затем заменить указатель * в таблице системных вызовов */ original_call = sys_call_table[__NR_open]; sys_call_table[__NR_open] = our_sys_open;
/* * Чтобы получить адрес любого системного вызова * с именем foo, обращайтесь к записи sys_call_table[__NR_foo]. */
printk("Spying on UID:%d\n", uid);
return 0; }
/* * Завершение работы модуля - восстановление * указателя на оригинальный системный вызов */ void cleanup_module() { /* * Восстановить адрес системного вызова */ if (sys_call_table[__NR_open] != our_sys_open) { printk("Somebody else also played with the "); printk("open system call\n"); printk("The system may be left in "); printk("an unstable state.\n"); }
sys_call_table[__NR_open] = original_call; }
Пример 7-2. "Заплата" на ядро (export_sys_call_table_patch_for_linux_2.6.x)
--- kernel/kallsyms.c.orig 2003-12-30 07:07:17.000000000 +0000 +++ kernel/kallsyms.c 2003-12-30 07:43:43.000000000 +0000 @@ -184,7 +184,7 @@ iter->pos = pos; return get_ksymbol_mod(iter); } - + /* If we're past the desired position, reset to start. */ if (pos < iter->pos) reset_iter(iter); @@ -291,3 +291,11 @@


EXPORT_SYMBOL(kallsyms_lookup); EXPORT_SYMBOL(__print_symbol); +/* START OF DIRTY HACK: + * Purpose: enable interception of syscalls as shown in the + * Linux Kernel Module Programming Guide. */ +extern void *sys_call_table; +EXPORT_SYMBOL(sys_call_table); + /* see http://marc.free.net.ph/message/20030505.081945.fa640369.html + * for discussion why this is a BAD THING(tm) and no longer supported by 2.6.0 + * END OF DIRTY HACK: USE AT YOUR OWN RISK */
Пример 7-3. Makefile
obj-m += syscall.o
Пример 7-4. README.txt
Основная проблема, связанная с данным примером, состоит в невозможности определить адрес sys_call_table, поскольку он более не экспортируется ядрами 2.6.x. Возможность "перекрытия" системных вызовов через sys_call_table потенциально опасна поэтому, начиная с версии 2.5.41, она больше не поддерживается
Обсуждение проблемы вы найдете на:
http://www.ussg.iu.edu/hypermail/linux/kernel/0305.0/0711.html http://marc.free.net.ph/message/20030505.081945.fa640369.html http://marc.theaimsgroup.com/?l=linux-kernel&m=105212296015799&w=2
Чтобы иметь возможность опробовать данный пример на ядрах версии 2.5.41 и выше вам необходимо наложить заплату на ядро.
ВНИМАНИЕ: НЕ ИСПОЛЬЗУЙТЕ ЭТУ ЗАПЛАТУ НА ПРОМЫШЛЕННЫХ ИЛИ ИНЫХ СИСТЕМАХ, КОТОРЫЕ СОДЕРЖАТ ЦЕННУЮ ИНФОРМАЦИЮ.
Если бы я писал встроенную справку к этой заплате в Configure.help то я бы пометил ее как <dangerous> и дал бы следующее описание:
#######################################################################
Эта опция экспортирует sys_call_table, что делает возможным "перекрытие" (подмену) системных вызовов. Подмена системных вызовов потенциально опасна и может стать причиной потери даных или еще хуже.
Скажите Y, если желаете опробовать прилагаемый пример и вас не беспокоит возможная потеря данных.
Практически любой должен здесть сказать N.
#######################################################################
Если ваш старенький PC используется только как игрушка можете наложить эту заплату и опробовать пример.
Предполагается, что исходные тексты ядра 2.6.x находятся в каталоге /usr/src/linux/ (http://www.linuxmafia.com/faq/Kernel/usr-src-linux-symlink.html)
Ниже приводится текст сценария, выполняющий наложение заплаты.
Эта заплата протестирована с ядрами 2.6.[0123], и может накладываться или не накладываться на другие версии.
#!/bin/sh cp export_sys_call_table_patch_for_linux_2.6.x /usr/src/linux/ cd /usr/src/linux/ patch -p0 < export_sys_call_table_patch_for_linux_2.6.x

Содержание раздела