Нестандартные сценарии использования модулей ядра

Часть 42. Подмена системного вызова

Comments

Серия контента:

Этот контент является частью # из серии # статей: Нестандартные сценарии использования модулей ядра

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Нестандартные сценарии использования модулей ядра

Следите за выходом новых статей этой серии.

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

Подмена системных вызовов

Все системные вызовы из пользовательских процессов, как говорилось в предыдущих статьях, проходят через таблицу sys_call_table (некий case-селектор, который передаёт управление в обработчик требуемого запроса). Индекс адреса обработчика каждого системного вызова, содержащегося в этом массиве, определяется номером вызова, как показано ниже:

$ cat /usr/include/asm/unistd_32.h
...
#define __NR_restart_syscall   0 
#define __NR_exit		  1 
#define __NR_fork		  2 
...

Эта таблица используется для связи системных вызовов пространства пользователя с обработчиками этих вызовов в пространстве ядра (в случае с 64-х битной системой эта информация содержится в файле unistd_64.h).

Иногда требуется подменить существующий или добавить новый адрес обработчика в эту таблицу. Подобная техника, известная ещё со времён MS-DOS, применяется в различных операционных системах. Ниже приведен далеко не полный список ситуаций, когда необходимо выполнить подобную операцию:

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

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

При cтатическом подходе необходимо добавить свой файл реализации arch/i386/kernel/new_calls.c в дерево исходных кодов ядра Linux и соответствующую строку-запись в таблицу системных вызовов arch/i386/kernel/syscall_table.S, а также включить свою реализацию в сборку ядра, дописав в arch/i386/kernel/Makefile строку вида:

  • obj-y += new_calls.o
  • После потребуется собрать ядро заново, чтобы получить его модифицированную версию, в которой реализован требуемый новый системный вызов в пространстве ядра. Однако этот подход выходит за рамки нашего цикла и поэтому дальше рассматриваться не будет.
  • При динамическом подходе во время выполнения в таблицу sys_call_table[] добавляется ссылка на код собственного модуля, который и реализует новый системный вызов (и сделать это действие в пространстве ядра может, естественно, только код модуля ядра).

До версии 2.6 ядро экспортировало адрес таблицы системных вызовов sys_call_table[]. В текущих версиях этот символ может присутствовать в таблице имён ядра (/proc/kallsyms), но он уже не экспортируется для использования модулями:

$ cat /proc/kallsyms | grep 'sys_call'
c052476b t proc_sys_call_handler
c07ab3d8 R sys_call_table

Однако ядро всегда экспортирует символ sys_close, находящийся в начальных позициях таблицы sys_call_table[]:

$ cat /proc/kallsyms | grep sys_close
c04dea99 T sys_close

Специальные программы, обсуждаемые на профильных форумах, разыскивают это известное значение в сегменте кода ядра, обратным отсчётом (__NR_close) и определяют по нему местоположение таблицы sys_call_table[], после чего могут динамически добавлять новые или подменять существующие системные вызовы.

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

$ sudo insmod mod_wrchg_1.ko 
...
Message from syslogd@notebook at Dec 31 01:56:41 ... 
 kernel:CR2: 00000000c07ab3e8 
$ dmesg | tail -n100 | grep -v audit 
! адрес sys_call_table = c07ab3d8 
! адрес в позиции 4[__NR_write] = c04e12fc 
! адрес sys_write = c04e12fc 
! адрес нового sys_write = fe1f1024 
! CR0 = 8005003b 
BUG: unable to handle kernel paging request at c07ab3e8 
IP: [<fe1f40b6>] wrchg_init+0xb6/0xd4 [mod_wrchg_1] 
*pdpt = 0000000000a8c001 *pde = 0000000036881063 *pte = 00000000007ab161 
Oops: 0003 [#1] SMP 
...

В результате получился аварийно установленный модуль, который невозможно удалить без перезагрузки системы. Ошибка связана с тем, что таблица адресов системных вызовов находится в сегменте только для чтения, и попытка записи в неё приводит к ошибке защиты памяти (обращаем внимание на символ R):

$ cat /proc/kallsyms | grep sys_call_table 
c07ab3d8 R sys_call_table

Это обусловлено нововведениями аппаратной архитектуры процессора Intel x86, которые используются в относительно свежих версиях ядра. Это ограничение можно обойти, так как мы выполняем код модуля в режиме супервизора, в нулевом кольце защиты процессора x86, где всё допустимо. Для этого на время перезаписи точки входа в таблице sys_call_table отменим контроль записи в сегмент, объявленный доступным только для чтения, а затем восстановим его обратно. Стоит отметить, что этот пример будет работать только для архитектуры x86, так как защита записи присутствует именно в x86, а для других платформ существуют решения аналогичного плана. Так, в архитектуре ia-32 за контроль записи отвечает 16-й бит в управляющем регистре процессора CR0 (называемый WP бит), а в архитектуре ia-64 применяется другой подход.

Рассмотрим код примера, выполняющего необходимые действия и приведенного в архиве new_sys.tgz, который можно найти в разделе "Материалы для скачивания". Так как приведенная функциональность будет использоваться и в других модулях, то вынесем её в отдельные включаемые файлы. В листинге 1 приведен первый из таких файлов — CR0.c.

Листинг 1. Управление защитой записи.
// 16 бит WP:          | 
//                     V 
//   3    2    2    1    1    1    0    0  0 
//   1    7    3    9    5    1    7    3  0 
//   0000 0000 0000 0001 0000 0000 0000 0000 => 0x00010000 
//   1111 1111 1111 1110 1111 1111 1111 1111 => 0xfffeffff 

// показать управляющий регистр CR0 
#define show_cr0()                        \ 
{  register unsigned r_eax asm ( "eax" ); \ 
   asm( "pushl %eax" );                   \ 
   asm( "movl %cr0, %eax" );              \ 
   printk( "! CR0 = %x\n", r_eax );       \ 
   asm( "popl %eax");                     \ 
} 
 
//код выключения защиты записи: 
#define rw_enable()              \ 
asm( "pushl %eax \n"             \ 
     "movl %cr0, %eax \n"        \ 
     "andl $0xfffeffff, %eax \n" \ 
     "movl %eax, %cr0 \n"        \ 
     "popl %eax" ); 

//код включения защиты записи: 
#define rw_disable()             \ 
asm( "pushl %eax \n"             \ 
     "movl %cr0, %eax \n"        \ 
     "orl $0x00010000, %eax \n"  \ 
     "movl %eax, %cr0 \n"        \ 
     "popl %eax" );

В комментарии в начале листинга 2 показан формат управляющего регистра cr0 для 32-разрядных процессоров и несколько макросов, оперирующих с этим регистром. Макросы rw_enable() (разрешающий запись в сегмент для чтения) и rw_disable() (восстанавливающий контроль записи) реализованы как инлайновые ассемблерные вставки, причём без параметров, а поэтому регистры в них можно указать и как %eax и %cr0, (с одним префиксом %, а не двойным).

Этот пример будет работать только на 32-разрядной платформе, для 64-разрядной платформы всё принципиально останется так же, но

  1. должны использоваться 64-разрядные операции (суффикс q в записи мнемоник команд AT&T),
  2. вместо регистра %eax должен определяться регистр %rax,
  3. за защиту записи отвечает бит CR другого формата:
    asm( "andq $0xfffffffffffeffff, %rax" ); 
    ...
    asm( "orq $0x0000000000001000, %rax" );

В листинге 3 приведен включаемый файл find.c, содержащий реализацию функции

find_sym()

, которая выполняет поиск символа ядра, переданного в параметре функции, и возвращает его адрес (или не возвращает, если такого символа в ядре не существует).

Листинг 2. Функция для поиска символа в ядре.
static void* find_sym( const char *sym ) { 
   static unsigned long faddr = 0; // static!!! 
   // ----------- вложенная функция - расширение GCC --------- 
   int symb_fn( void* data, const char* sym, struct module* mod, unsigned long addr ) {
      if( 0 == strcmp( (char*)data, sym ) ) { 
         faddr = addr; 
         return 1; 
      } 
      else return 0; 
   }; 
   // -------------------------------------------------------- 
   kallsyms_on_each_symbol( symb_fn, (void*)sym ); 
   return (void*)faddr; 
}

Мы уже рассматривали подобные функции, но в коде функции find_sym() используется такое синтаксическое расширение gcc (не допускаемое стандартами языка С, но часто встречающееся в языках группы PASCAL), как определение вложенной функции symb_fn(), локальной по отношению к вызывающей. Несмотря на свою «вычурность», такой подход позволяет описать возврат адреса любого имени sym из таблицы /proc/kallsyms, не прибегая ни к каким глобальным переменным для общего использования.

В листинге 3 рассматривается код самого модуля, который можно найти в файле mod_wrchg.c в архиве new_sys.tgz.

Листинг 3. Модуль, подменяющий обработчик sys_write.
#include <linux/module.h> 
#include <linux/kallsyms.h> 
#include <linux/uaccess.h> 
#include <linux/unistd.h> 

#include "../find.c" 
#include "../CR0.c" 

asmlinkage long (*old_sys_write) ( 
       unsigned int fd, const char __user *buf, size_t count ); 

asmlinkage long new_sys_write ( 
       unsigned int fd, const char __user *buf, size_t count ) { 
   int n; 
   if( 1 == fd ) { 
      static const char prefix[] = ":-) "; 
      mm_segment_t fs = get_fs(); 
      set_fs( get_ds() ); 
      n = old_sys_write( 1, prefix, strlen( prefix ) ); 
      set_fs(fs); 
   } 
   n = old_sys_write( fd, buf, count ); 
   return n; 
}; 
EXPORT_SYMBOL( new_sys_write ); 

static void **taddr; // адрес таблицы sys_call_table 

static int __init wrchg_init( void ) { 
   void *waddr; 
   if( ( taddr = find_sym( "sys_call_table" ) ) != NULL ) 
      printk( "! адрес sys_call_table = %p\n", taddr ); 
   else { 
      printk( "! sys_call_table не найден\n" ); 
      return -EINVAL; 
   } 
   old_sys_write = (void*)taddr[ __NR_write ]; 
   printk( "! адрес в позиции %d[__NR_write] = %p\n", __NR_write, old_sys_write );
   if( ( waddr = find_sym( "sys_write" ) ) != NULL ) 
      printk( "! адрес sys_write = %p\n", waddr ); 
   else { 
      printk( "! sys_write не найден\n" ); 
      return -EINVAL; 
   } 
   if( old_sys_write != waddr ) { 
      printk( "! непонятно! : адреса не совпадают\n" ); 
      return -EINVAL; 
   } 
   printk( "! адрес нового sys_write = %p\n", &new_sys_write ); 
   show_cr0(); 
   rw_enable(); 
   taddr[ __NR_write ] = new_sys_write; 
   show_cr0(); 
   rw_disable(); 
   show_cr0(); 
   return 0; 
} 

static void __exit wrchg_exit( void ) { 
   printk( "! адрес sys_write при выгрузке = %p\n", (void*)taddr[ __NR_write ] );
   rw_enable(); 
   taddr[ __NR_write ] = old_sys_write; 
   rw_disable(); 
   return; 
} 

module_init( wrchg_init ); 
module_exit( wrchg_exit ); 

MODULE_LICENSE( "GPL" ); 
MODULE_AUTHOR( "Oleg Tsiliuric <olej@front.ru>" );

Это самый большой и сложный пример из встречавшихся ранее, поэтому его стоит рассмотреть во всех подробностях. Из элементов, заслуживающих детального рассмотрения, можно выделить:

  • новый обработчик new_sys_write() для системного вызова sys_write при выводе на SYSOUT делает предшествующий выводимой строке вывод собственного префикса (строки ":-) "), причём для этого ему необходимо сначала считать сегмент данных в адресном пространстве ядра (взять данные строки вывода из области ядра), после чего, восстановить контроль принадлежности адреса сегменту данных пространства пользователя (для вывода оригинальной переданной строки);
  • при выгрузке модуля обязательно требуется восстановить прежнюю функцию обработчик old_sys_write();
  • при таком восстановлении возможно возникновение критической ошибки для ядра, если некоторый другой модуль ещё раз подменит адрес обработчика после нашей замены; поэтому показанный пример можно использовать только в качестве иллюстрации, но не в реальных задачах;
  • работа в ядре (и с таблицей системных вызовов) – крайне рискованное занятие, поэтому в коде выполняется двойная перепроверка: адрес обработчика вызова, найденный как символ ядра sys_write, сравнивается с адресом в позиции __NR_write в таблице sys_call_table;

Тем не менее почти все эти элементы встречались ранее, только теперь мы используем их совместно. Выполним этот пример и изучим полученный результат:

$ sudo insmod mod_wrchg.ko 
:-) $ :-) e:-) c:-) h:-) o:-)  :-) с:-) т:-) р:-) о:-) к:-) а:-) 
:-) строка 
:-) $ lsmod | head -n4 
:-) :-) Module                  Size  Used by 
:-) mod_wrchg               1382  0 
:-) fuse                   48375  2 
:-) ip6table_filter         2227  0 
:-) $ sudo rmmod mod_wrchg 
$

Так как мы подменили один из самых используемых при работе с терминалом системных вызовов Linux, то можно ожидать, что работа с системой в командной строке несколько усложнится. Но после удаления модуля всё должно вернуться в норму. Теперь можно посмотреть на действия модуля с точки зрения системного журнала.

$ dmesg | tail -n120 | grep -v audit 
! адрес sys_call_table = c07ab3d8 
! адрес в позиции 4[__NR_write] = c04e12fc 
! адрес sys_write = c04e12fc 
! адрес нового sys_write = fd8ae024 
! CR0 = 8005003b 
! CR0 = 8004003b 
! CR0 = 8005003b 
! адрес sys_write при выгрузке = fd8ae024

В этом примере специально показывается отладочный вывод содержимого управляющего регистра cr0 процессора.

Заключение

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


Ресурсы для скачивания


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=856080
ArticleTitle=Нестандартные сценарии использования модулей ядра: Часть 42. Подмена системного вызова
publish-date=01242013