Перейти к тексту

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

Профиль создается, когда вы в первый раз заходите в developerWorks. Выберите данные в своем профиле (имя, страна/регион, компания) которые будут общедоступными и будут отображаться, когда вы публикуете какую-либо информацию. Вы можете изменить данные вашего ИБМ аккаунта в любое время.

Вся введенная информация защищена.

  • Закрыть [x]

При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

Вся введенная информация защищена.

  • Закрыть [x]

Нестандартные сценарии использования модулей ядра: Часть 44. Скрытие системного вызова

Олег Цилюрик, преподаватель тренингового отделения, Global Logic
Фото автора
Олег Иванович Цилюрик, много лет был разработчиком программного обеспечения в крупных центрах разработки: ВНИИ РТ, НПО "Дельта", КБ ПМ. Последние годы работал над проектами в области промышленной автоматики, IP телефонии и коммуникаций. Автор нескольких книг. Преподаватель тренингового отделения международной софтверной компании Global Logic.

Описание:  Статья является частью миницикла, посвященного использованию модулей ядра для решения различных прикладных задач. В статье рассказывается, как добавить в ядро «скрытый» обработчик системного вызова, который не будет виден при диагностике системы.

Дата:  07.02.2013
Уровень сложности:  средний
Активность:  1178 просмотров
Комментарии:  


Введение

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


Скрытые обработчики системных вызовов

Вопрос заключается в том, может ли модуль установить обработчик (подменив существующий или добавив новый) таким образом, чтобы ядро системы «не знало» об этом (т.е. чтобы такой модуль нельзя было выявить средствами диагностики lsmod или в файловой системе /proc). Ценность этого вопроса в том, что ответ на него определяет, могут ли вредоносные программы произвести подобные изменения. И ответ на него — положительный! Чтобы скрытно установить обработчик, модуль должен:

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

Идея состоит в том, что блоки памяти, динамически выделяемые запросами к kmalloc() и другим подобным API ядра, продолжают существовать, даже если создавший их модуль прекратил существование (если при этом он «забудет» выполнить парный вызов kfree()). В этом случае в памяти возникают объекты, все пути доступа к которым утрачены, и которые не могут быть никаким образом уничтожены. Подобные действия (сознательно или по ошибке) приведут к постепенной деградации операционной системы.

Рассмотрим это на примере модулей из архива hidden.tgz в разделе "Материалы для скачивания", который включает файлы определений из предыдущей статьи и очень похожий тестовый процесс пространства пользователя. В этом проекте для тестирования собирается сразу несколько альтернативных вариантов модуля.


Листинг 1. Общие определения.

#include <linux/module.h> 
#include <linux/kallsyms.h> 
#include <linux/uaccess.h> 
#include <linux/unistd.h> 
#include <../arch/x86/include/asm/cacheflush.h> 

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

// описания функций обработчиков для разных вариантов: 
// asmlinkage long new_sys_call( const char __user *buf, size_t count ) 
#define VARNUM 5 
#ifndef VARIANT 
#define VARIANT 0 
#endif 
#if VARIANT>VARNUM 
#undef VARIANT 
#define VARIANT 0 
#endif 

static long shift; // величина сдвига тела функции системного обработчика 
#if VARIANT == 0 
   #include "null.c" 
#elif VARIANT == 1 
   #include "local.c" 
#elif VARIANT == 2 
   #include "getpid.c" 
#elif VARIANT == 3 
   #include "strlen_1.c" 
#elif VARIANT == 4 
   #include "strlen_2.c" 
#elif VARIANT == 5 
   #include "write.c" 
#else 
   #include "null.c" 
#endif 

static int __init new_sys_init( void ) { 
   void *waddr, *move_sys_call, 
       **taddr;                   // адрес таблицы sys_call_table 
   size_t sys_size; 
   printk( "!... SYSCALL=%d, VARIANT=%d\n", NR, VARIANT ); 
   if( ( taddr = find_sym( "sys_call_table" ) ) != NULL ) 
      printk( "! адрес sys_call_table = %p\n", taddr ); 
   else 
      return -EINVAL | printk( "! sys_call_table не найден\n" ); 
   if( NULL == ( waddr = find_sym( "sys_ni_syscall" ) ) ) 
      return -EINVAL | printk( "! sys_ni_syscall не найден\n" ); 
   if( taddr[ NR ] != waddr ) 
      return -EINVAL | printk( "! системный вызов %d занят\n", NR ); 
   {  unsigned long end; 
      asm( "movl $L2, %%eax \n" 
           :"=a"(end):: 
         ); 
      sys_size = end - (long)new_sys_call; 
      printk( "! статическая функция: начало= %p, конец=%lx, длина=%d \n", 
              new_sys_call, end, sys_size ); 
  } 
   // выделяем блок памяти под функцию обработчик 
   move_sys_call = kmalloc( sys_size, GFP_KERNEL ); 
   if( !move_sys_call ) return -EINVAL | printk( "! memory allocation error!\n" ); 
   printk( "! выделен блок %d байт с адреса %p\n", sys_size, move_sys_call ); 
   // копируем резидентный код нового обработчика в выделенный блок памяти 
   memcpy( move_sys_call, new_sys_call, sys_size ); 
   shift = move_sys_call - (void*)new_sys_call; 
   printk( "! сдвиг кода = %lx байт\n", shift ); 
   // снять бит NX-защиты с страницы 
   //int set_memory_x(unsigned long addr, int numpages); 
   set_memory_x( (long unsigned)move_sys_call, sys_size / PAGE_SIZE + 1 ); 
   printk( "! адрес нового sys_call = %p\n", move_sys_call ); 
   rw_enable(); 
   taddr[ NR ] = move_sys_call; 
   rw_disable(); 
   return -EPERM; 
} 
module_init( new_sys_init ); 

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

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

asmlinkage long new_sys_call( const char __user *buf, size_t count );

Прототип этого нового системного вызова был выбран произвольно. Простейший обработчик null.c показывает только возможность осуществления данной операции, поэтому устанавливаемый им обработчик системного вызова с номером NR (определяется параметром сборки SYSCALL и по умолчанию равен 223) ничего не делает, а только возвращает признак нормального завершения, как показано в листинге 2.


Листинг 2. Простейший вариант скрытия системного вызова.

asmlinkage long new_sys_call( const char __user *buf, size_t count ) { 
   asm( "movl $0, %%eax\n"  // эквивалент return 0;
        "popl %%ebp    \n" 
        "ret           \n" 
        "L2: nop       \n"  // нам нужна метка L2 после return
        ::: "%eax" 
      ); 
   return 0;                // только для синтаксиса
}; 

Поскольку код такого модуля портит таблицу sys_call_table и не может её восстановить, а при многократном выполнении будет оставлять после себя блоки навсегда потерянной памяти, то потребуется ещё один модуль для восстановления первичного состояния таблицы.


Листинг 3. Модуль для восстановления таблицы системных вызовов.

#include <linux/module.h> 
#include <linux/kallsyms.h> 
#include <linux/uaccess.h> 
#include <linux/unistd.h> 

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

static int __init new_sys_init( void ) { 
   void **taddr;                   // адрес таблицы sys_call_table 
   void *waddr, *old_sys_addr; 
   if( ( taddr = find_sym( "sys_call_table" ) ) != NULL ) 
      printk( "! адрес sys_call_table = %p\n", taddr ); 
   else return -EINVAL | printk( "! sys_call_table не найден\n" ); 
   if( ( waddr = find_sym( "sys_ni_syscall" ) ) != NULL ) 
      printk( "! адрес sys_ni_syscall = %p\n", waddr ); 
   else return -EINVAL | printk( "! sys_ni_syscall не найден\n" ); 
   old_sys_addr = (void*)taddr[ NR ]; 
   printk( "! адрес в позиции %d = %p\n", NR, old_sys_addr ); 
  if( old_sys_addr != waddr ) { 
      kfree( old_sys_addr ); 
      rw_enable(); 
      taddr[ NR ] = waddr;  // восстановить sys_ni_syscall 
      rw_disable(); 
      printk( "! итоговый адрес обработчика %p\n", taddr[ NR ] ); 
   } 
   else 
      printk( "! итоговый адрес обработчика не изменяется\n" ); 
   return -EPERM; 
} 
module_init( new_sys_init ); 

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

Теперь мы можем проверить, как это работает.

$ make VARIANT=0
...
$ ./syscall 
syscall return -38 [ffffffda], reason: Function not implemented 
$ sudo insmod hidden.ko 
insmod: error inserting 'hidden.ko': -1 Operation not permitted 
$ lsmod | grep hid 
$
$ ./syscall 1 23 456 
syscall return 0 [00000000], reason: Success 
syscall return 0 [00000000], reason: Success 
syscall return 0 [00000000], reason: Success 
$ dmesg | tail -n60 | grep ! 
!... SYSCALL=223, VARIANT=0 
! адрес sys_call_table = c07ab3d8 
! статическая функция: начало= f7ecd000, конец=f7ecd00f, длина=15 
! выделен блок 15 байт с адреса d9322030 
! сдвиг кода = e1455030 байт 
! адрес нового sys_call = d9322030 
$ sudo insmod restore.ko 
insmod: error inserting 'restore.ko': -1 Operation not permitted 
$ ./syscall 
syscall return -38 [ffffffda], reason: Function not implemented 

Как можно видеть, хотя модуль hidden.ko не установлен в системе, но новый системный вызов с номером NR отрабатывается. Единственный новый фрагмент в коде hidden.c, требующий дополнительных комментариев, это строка, в которой производится вызов:

int set_memory_x( unsigned long addr, int numpages ); 

В старших моделях x86, работающих в 64-битном или расширенном режиме PAE, введен NX-бит защиты страницы памяти от выполнения (старший бит в записи таблицы страниц). В ядре Linux этот аппаратный механизм защиты применяется, начиная с версии 2.6.30. Вызов set_memory_x() снимает ограничение выполнения для numpages последовательных страниц (размером PAGESIZE каждая), начиная со страницы адреса addr. Аналогично вызов восстанавливает защиту страницы от выполнения. Для 32-битного ядра на платформе x86 (не PAE!) этот механизм, как уже было сказано, не используется.

Ассемблерная вставка в функции обработчика представляет полный эквивалент оператора return 0; (в соответствии с соглашениями языка С - с выталкиванием регистра %ebp из стека). Этот эквивалент понадобился только потому, что нам необходима метка L2 (её адрес) после оператора return, а компилятор gcc такие метки после завершения кода «оптимизирует».

Писать код для таких обработчиков довольно сложно, фактически при этом предстоит вручную, без помощи компилятора gcc (опция -fPIC), реализовать нечто подобное PIC-кодированию (Position Independed Code - позиционно независимое кодирование). Сложности при разработке перемещаемой функции обработчика связаны с тем, что:

  1. функция не может использовать никакие внешние переменные, описанные вне её тела, так как после завершения функции инициализации модуля все такие области памяти будут освобождены;
  2. функция может использовать только локальные переменные в стеке;
  3. тоже самое относится и к другим (локальным) функциям, описанным в модуле.

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

  1. Следующая сложность состоит в том, что в таком коде нельзя, как обычно, по имени вызывать функции API ядра, неважно, экспортируемые они или нет. Адрес такого вызова разрешится в момент статической линковки (смещение адреса запишется в поле команды), а при вызове приведёт к вызову со смещением и краху выполнения. Адрес нужной функции можно взять статически из /proc/kallsym или найти и динамически, как было показано в предыдущих статьях. В листинге 5 приведен пример модуля, в котором показано, как обойти оба этих ограничения.

Листинг 5. Пример обработчика системного вызова с вложенной функцией и обращением к API ядра.

asmlinkage long new_sys_call( const char __user *buf, size_t count ) { 
   long res = 0; 
   size_t own_strlen( const char* ps ) { // вложенная функция 
      long res = 0; 
      asm( 
        "movl    8(%%ebp), %%eax \n"     // ps -> call parameter 
        "movl    %%eax, (%%esp)  \n" 
        "call   *%%ebx           \n" 
        :"=a"(res):"b"((long)(&strlen)): // strlen() из API ядра
      ); 
      return res; 
   } 
   res = own_strlen( buf ); 
   asm( "leave            \n" 
        "ret              \n"            // эквивалент return res; 
        "L2: nop          \n"            // нам нужна метка L2 после return 
        ::"a"(res): 
      ); 
   return 0;                             // только для синтаксиса 
};

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

$ make VARIANT=4
...
$ sudo insmod hidden.ko 
insmod: error inserting 'hidden.ko': -1 Operation not permitted 
$ lsmod | grep hid 
$
$ ./syscall 1 23 456 'новая строка' 
syscall return 24 [00000018], reason: Success 
syscall return 4 [00000004], reason: Success 
syscall return 3 [00000003], reason: Success 
syscall return 2 [00000002], reason: Success 
$ dmesg | tail -n60 | grep ! 
!... SYSCALL=223, VARIANT=4 
! адрес sys_call_table = c07ab3d8 
! статическая функция: начало= f7ede000, конец=f7ede018, длина=24 
! выделен блок 24 байт с адреса d9085940 
! сдвиг кода = e11a7940 байт 
! адрес нового sys_call = d9085940 

В результате запуска этого примера можно увидеть очередную проблему, связанную с подобными функциями, где мы смогли успешно воспользоваться скалярными (int, long) локальными переменными, объявленными внутри функции. Но это не относится к массивам, в частности, к строкам, размещённым как локальные переменные в теле функции.

В архиве hidden.tgz можно найти ещё ряд вариантов реализации подобного обработчика для самостоятельного изучения.


Заключение

В данной статье мы рассмотрели достаточно сложную, но, в принципе, реализуемую технику, которая потенциально может быть использована для создания эксплойтов. Однако напомню, что для запуска любого модуля ядра требуются права root. Это один из самых мощных защитных инструментов платформы UNIX (и Linux), и об этом никогда не следует забывать, когда речь касается защищённости системы. Именно благодаря этому обстоятельству, можно увидеть, что за более 20 лет своей истории для Linux не появилось практически ни одного серьёзного вредоносного вируса.

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



Загрузка

ИмяРазмерМетод загрузки
hidden.tgz6KBHTTP

Информация о методах загрузки


Ресурсы

Об авторе

Фото автора

Олег Иванович Цилюрик, много лет был разработчиком программного обеспечения в крупных центрах разработки: ВНИИ РТ, НПО "Дельта", КБ ПМ. Последние годы работал над проектами в области промышленной автоматики, IP телефонии и коммуникаций. Автор нескольких книг. Преподаватель тренингового отделения международной софтверной компании Global Logic.

Помощь по сообщениям о нарушениях

Сообщение о нарушениях

Спасибо. Эта запись была помечена для модератора.


Помощь по сообщениям о нарушениях

Сообщение о нарушениях

Сообщение о нарушении не было отправлено. Попробуйте, пожалуйста, позже.


developerWorks: вход


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


При первом входе в developerWorks для Вас будет создан профиль. Выберите информацию отображаемую в Вашем профиле — скрыть или отобразить поля можно в любой момент.

Выберите ваше отображаемое имя

При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

(Должно содержать от 3 до 31 символа.)


Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Оценить эту статью

Комментарии

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