Рост популяции руткитов, оккупировавших никсы, продолжается ударными темпами.
Они поражают системы, не обремененные антивирусами и прочими защитными
механизмами, которые уже давно стали привычными средствами обороны в мире
Windows. Поэтому приходится выдумывать что-то концептуальное.
Введение
Согласно общепринятой классификации, руткитами называют программы
(обычно безвредные), предназначенные для сокрытия сетевых соединений, процессов
и дисковых файлов, а также других программ, чаще всего довольно агрессивных по
натуре (чего им тогда шифроваться, спрашивается). Классификация – это прекрасно,
но на практике нам приходится бороться не с руткитами в чистом виде (тоже мне,
понимаешь, сферические кони в вакууме), а с различными механизмами маскировки.
Огромное количество червей (и прочей малвари) имеет встроенные руткиты с
полиморфным движком. Поэтому условимся понимать под руткитами любую нечисть,
занимающуюся сокрытием системных объектов (файлов, процессов, сетевых
соединений). Своих или чужих — неважно. Попробуем разобраться — как же работает
эта шапка-невидимка, и какие способы обнаружения руткитов существуют.
Кочевые племена против оседлых форм жизни
Существуют два типа руткитов: первые, внедряясь в систему, создают
новые файлы или модифицируют уже существующие, получая управление при каждой
загрузке операционной системы. Другие же — вообще не прикасаются к диску, не
создают новых процессов, ограничиваясь модификацией оперативной памяти.
Естественно, руткиты такого типа умирают при перезагрузке и выглядят не
слишком-то жизнеспособными, однако до тех пор, пока дыра, через которую
проникает руткит, остается не залатанной, он будет приходить вновь и вновь.
Закрытие дыры мало чего изменит, ведь там, где есть одна дыра, найдутся и другие
— создателю руткита достаточно переписать несколько десятков строк кода,
ответственных за внедрение первичного загрузчика в целевую систему, – и дело
сделано.
В распределенных сетях (ботнетах) перезагрузка одного или нескольких узлов —
вообще не проблема, к тому же после перезагрузки узел будет инфицирован вновь.
Этот факт очень трудно обнаружить, ведь никаких изменений на диске нет! А
сетевые соединения современные руткиты скрывают весьма эффективно. Прошли те
времена, когда открытые порты обнаруживались тривиальным сканированием с
соседней машины. Продвинутые руткиты не открывают никаких портов. Они садятся на
сетевой интерфейс, контролируя трафик и модифицируя определенные поля в
заголовках TCP/IP-пакетов, значения которых согласно RFC выбираются случайным
образом. Скремблер скроет факт модификации (независимо от передаваемых руткитом
данных мы получим такое же хаотичное распределение, как и на незараженной
машине), а несимметричный шифратор предотвратит декодирование перехваченной
информации. Даже если мы заведомо знаем, что руткит есть!
Откуда мы узнаем, что он есть? Объем трафика в норме, никаких изменений на
диске не наблюдается (что кардинальным образом отличается от руткитов первого
типа, которые обнаруживаются настолько тривиально, насколько это можно себе
представить). Загружаемся с LiveCD и проверяем контрольные суммы всех файлов
(или просто осуществляем побайтовое сравнение с дистрибутивом). Конечно, для
серверов такой способ не очень-то пригоден — их вообще лучше не перезагружать,
но сервера, критичные к перезагрузкам, обычно оснащены RAID-массивами с
hot-plug'ом. Так что просто вытаскиваем один набор дисков из матрицы, ставим его
на другую машину, проверяем контрольную сумму и делаем оргвыводы.
Короче говоря, руткиты, вносящие изменения в файловую систему, нам
неинтересны, и дальше мы будем говорить исключительно о заразе, обитающей
непосредственно в оперативной памяти и получающей управление путем модификации
ядра (поскольку на прикладном уровне нормальному руткиту делать нечего).
Методы борьбы, или была б катана — сделал бы харакири
Прежде чем продвигаться вглубь, сразу выбросим на помойку несколько
популярных, но безнадежно устаревших способов борьбы с руткитами. Чтение памяти
ядра через /dev/[k]mem (при активом рутките!) — это курам на смех. Поиск следов
компрометации при помощи GDB – из той же оперы. Руткиту ничего не стоит
отследить обращение к любому файлу/устройству, «вычистив» следы своего
пребывания или совершить «харакири» при запуске GDB. Чуть сложнее — ввести в
заблуждение GDB, оставаясь при этом активным, живым и здоровым.
Достойных отладчиков ядерного уровня под никсы не существует. Ну, не то,
чтобы совсем нет, но в штатный комплект поставки уж точно ни один не входит.
Хорошо еще, если установка отладчика не требует перекомпиляции ядра, не говоря
уже о перезагрузке. Самих же отладчиков довольно много: NLKD, KDB,
LinIce, DDB, и ни один из них не обладает неоспоримыми
преимуществами перед остальными. Кстати, для ловли руткитов иметь готовый к
употреблению отладчик необязательно. Достаточно написать загружаемый модуль
ядра, считывающий и передающий на прикладной уровень все критичные к перехвату
структуры данных вместе с машинным кодом (естественно, ядро должно быть
скомпилировано с поддержкой модульности). Что это за данные — мы сейчас выясним.
Магические аббревиатуры — GDT, LDT, IDT
Сокрытие чего бы там ни было базируется на перехвате/модификации ядерных
структур данных/системного кода. Способов перехвата придумано множество, и
каждый день появляются все новые. Однако количество самих системных структур
ограничено, что существенно упрощает борьбу с заразой.
Начнем с простого. С таблиц глобальных/локальных дескрипторов (Global/Local
Description Table или, сокращенно, GDT/LDT), хранящих базовые адреса,
лимиты и атрибуты селекторов. Чем они могут помочь руткиту? Ну, кое-чем могут.
Linux/xBSD используют плоскую модель памяти, при которой селекторы CS (код), DS
(данные) и SS (стек) «распахнуты» на все адресное пространство: от нуля до самых
верхних его окраин. Создание нового селектора с базой, отличной от нуля, с
последующей его загрузкой в один из сегментных регистров существенно затрудняет
дизассемблирование руткита, особенно тех экземпляров, что выдраны из памяти
чужой машины. Таблицы дескрипторов в распоряжении реверсера нет и не будет (руткит
умер). Грубо говоря, мы вообще не можем определить, к каким данным
осуществляется обращение, ведь база селектора неизвестна! Реверсеров и
сотрудников антивирусных компаний такие руткиты просто доводят до бешенства,
затягивая анализ, а вместе с ним и приготовление «вакцины».
Побочным эффектом этого антиотладочного приема становится появление новых
селекторов в таблице дескрипторов, которых там никогда не наблюдалось ранее.
Отладчики ядерного уровня позволяют просматривать таблицы дескрипторов в
удобочитаемом виде, но при активном рутките пользоваться отладчиком не
рекомендуется. Лучше написать свой загружаемый модуль ядра, считывающий
содержимое таблицы дескрипторов командами SGDT/SLDT, описанными (вместе с
форматами самих таблиц) в документации на процессоры Intel и AMD.
Огромное количество руткитов модифицирует таблицу дескрипторов прерываний (Interrupt
Description Table или, сокращенно, IDT), позволяющую им перехватывать
любые прерывания и исключения, в том числе и системные вызовы, реализованные на
некоторых системах именно как прерывания. Но о сисколлах мы еще поговорим, а
пока лишь отметим, что модификация IDT позволяет руткиту перехватывать обращения
к страницам, вытесненным на диск (при обращении к ним возникает исключение
Page Fault). А также перехватывать другие исключения, например, общее
исключение защиты (General Protection Fault), отладочное и пошаговое
исключение (отличный способ борьбы с отладчиками), не говоря уже о прерываниях,
поступающих от аппаратных устройств — клавиатуры, сетевой карты и прочего
оборудования, прямое обращение к которому очень полезно для сокрытия
«преступной» деятельности.
Таблица прерываний, отображаемая отладчиками в удобочитаемом виде, может быть
прочитана процессорной командой SIDT, что намного надежнее, поскольку
перехватить ее выполнение руткит уже не в состоянии.
Практическая магия системных вызовов
Системные вызовы — основной механизм взаимодействия ядра с прикладными
процессами, обеспечивающий базовый функционал и абстрагирующий приложения от
особенностей конкретного оборудования. В частности, системный вызов sys_read
обеспечивает унифицированный способ чтения данных из файлов, устройств и
псевдоустройств. Соответственно, перехват sys_read позволяет руткиту
контролировать обращения ко всем файлам и (псевдо)устройствам. Даже если руткит
не создает и не скрывает никаких файлов, ему необходимо заблокировать
возможность чтения памяти ядра, иначе любой, даже самый примитивный антивирус
тут же его обнаружит.
В зависимости от типа и версии ОС системные вызовы реализуются по-разному.
Самый древний механизм — это далекий вызов по селектору семь, смещение ноль —
CALL FAR 0007h:00000000h (или, то же самое, но в AT&T синтаксисе — lcall $7,$0).
Он работает практически на всех x86-клонах UNIX'а, однако практического значения
не имеет, поскольку им пользуются только некоторые ассемблерные программы в
стиле «hello, world!», ну и… вирусы, также написанные на ассемблере.
Стандартом де-факто стал программный вызов прерывания 80h (INT 80h),
работающий как в Linux, так и во FreeBSD. Как руткит его может перехватить?
Посредством модификации таблицы дескрипторов прерываний, переназначая вектор 80h
на свой собственный код. Однако это не единственный вариант. Стандартно INT 80h
передает управление на функцию system_call, адрес которой можно определить по
файлу System.map, если он, конечно, не удален администратором по соображениям
безопасности, — тогда руткит либо читает вектор 80h через SIDT, либо находит
system_call эвристическим путем, поскольку она, как и любой другой обработчик
прерывания, содержит довольно характерный код. Вставив в начало (или середину)
этой функции команду перехода на свое тело, руткит будет получать управление при
всяком системном вызове. Следовательно, мы должны считать код функции
system_call из памяти, сравнив его с оригиналом, который можно позаимствовать из
неупакованного ядра, выдернутого из дистрибутивного диска (как это сделать, мы
уже неоднократно рассказывали).
После выполнения системного вызова управление получает другая интересная
функция — ret_from_sys_call, идущая следом за system_call и также, как и
system_call, присутствующая в System.map. Ее перехватывают многие руткиты, что
вполне логично, поскольку «вычистить» следы своего пребывания лучше всего после
отработки системного вызова (а не до него). Популярные руководства по поиску
руткитов об этом почему-то забывают, а зря! Функцию ret_from_sys_call следует
проверять в первую очередь, сравнивая ее код с кодом оригинальной
ret_from_sys_call, ну или просто дизассемблируя его на предмет наличия
посторонних переходов.
Начиная с версии 2.5, ядро Linux поддерживает механизм быстрых системных
вызовов, реализуемый командами SYSENTER/SYSEXIT (Intel) и SYSCALL/SYSRET (AMD).
Он существенно облегчает перехват и делает его трудно заметным. Команда SYSENTER
передает управление с 3-го кольца прикладного уровня на ядерный уровень,
используя специальные MSR-регистры, а конкретно: IA32_SYSENTER_CS содержит
селектор целевого сегмента, IA32_SYSENTER_EIP — целевой адрес перехода,
IA32_SYSENTER_ESP — новое значение регистра ESP при переходе на ядерный уровень.
При этом селектор стека равняется (IA32_SYSENTER_CS + 08h). SYSCALL работает
практически аналогичным образом, только MSR регистры другие: STAR, LSTAR и CSTAR
(подробнее об этом можно прочитать в описании самой команды SYSCALL в
спецификации от AMD, ну или от Intel, с учетом, что она поддерживает эту команду
в той же манере, в какой AMD поддерживает SYSENTER).
Суть в том, что целостность MSR регистров долгое время никто не проверял –
чем руткиты с успехом и воспользовались, изменяя MSR-регистры таким образом,
чтобы управление получал не системный обработчик, а код руткита со всеми
вытекающими отсюда последствиями. Далеко не все отладчики отображают содержание
MSR регистров. Но это легко осуществить с ядерного уровня командой RDMSR,
которую руткит также не может перехватить, а потому все его махинации с MSR
регистрами будут немедленного разоблачены. Естественно, помимо проверки
MSR-регистров (они должны указывать на тот же самый системный обработчик, что и
в заведомо неинфицированной системе с той же самой версией ядра), мы должны
проверить код самого обработчика. Он может быть изменен руткитом для перехвата
управления без модификации MSR (впрочем, одно другому не мешает, и многие
руткиты используют гибридный вариант).
Поддержка SYSENTER/SYSCALL не отменяет INT 80h, по-прежнему присутствующую в
ядре и вызываемую из старых прикладных библиотек некоторых ассемблерных
программ, ну и, конечно, вирусов, работающих на прикладном уровне! Так что
руткитам теперь приходится перехватывать и то, и другое, хотя перехват SYSENTER/SYSCALL
намного более перспективен (INT 80h используется все реже и реже).
А вот разработчики FreeBSD от INT 80h отказываться пока не собираются, и хотя
существует патч от David'а Xu, написанный в конце 2002 года и переводящий
систему на SYSENTER/SYSCALL (people.freebsd.org/~davidxu/sysenter/),
по умолчанию он не включен в стабильный релиз. Впрочем, сторонние составители
дистрибутивов его активно используют (взять, к примеру, DragonFlyBSD).
Официальный дистрибутив FreeBSD 7.0 на 3-х DVD для архитектуры x86.
Модификация таблицы системных вызовов
Указатели на системные вызовы перечислены в таблице sys_call_table, адрес
которой можно найти все в том же System.map или вычислить эвристическим путем
(удаление System.map'а не слишком-то усиливает безопасность).
Подмена указателя на оригинальный системный вызов указателем на код руткита —
это классика перехвата. Элементарно обнаруживается путем сравнения оригинальной
таблицы системных вызовов, выдернутой из неупакованного ядра дизассемблером, с
ее «живой» сестрицей, прочитать которую можно либо отладчиком, либо «руками» –
командой mov, вызываемой из загружаемого модуля ядра. Оба способа абсолютно
ненадежны и выявляют только пионерские руткиты. «Зверюшки» посерьезнее
сбрасывают страницы, принадлежащие таблице системных вызовов, в NO_ACCESS. В
результате, при обращении к ним процессор выбрасывает исключение, подхватываемое
руткитом, который смотрит, откуда пришел вызов на чтение — если это функция
system_call, то все ОК, если же нет, то руткит возвращает подложные данные, и
таблица системных вызовов выглядит, как сама невинность. Конечно, перед чтением
можно проверить атрибуты страницы, но весь фокус в том, что функция определения
атрибутов страниц реализована как системный вызов, находящийся в той же самой
таблице, контролируемой руткитом. Упс! Приехали! Ладно, перед чтением мы
назначим свой собственный обработчик исключений, который выручит нас только в
том случае, если руткит не модифицировал IDT. Решение заключается в «ручном»
разборе страничного каталога, формат которого описан в руководствах по
системному программированию на процессоры Intel/AMD и представляет собой намного
более простую задачу, чем это кажется поначалу.
Естественно, кроме таблицы системных вызовов необходимо проверить и
целостность самих системных вызовов, помня о том, что руткиты могут внедрять
команду перехода на свое тело не только в начало функции системного вызова, но
также в ее конец или середину, хотя для этого им придется тащить за собой
дизассемблер длин инструкций. Тем не менее, «серединный перехват» — стандарт
де-факто для всех серьезных руткитов.
Дайте мне мыло, веревку или… дропер!
Сотрудники антивирусных компаний получают вирусы/руткиты из трех основных
источников. Первый – свои собственные HoneyPot'ы, второй – малварь, присланная
коллегами (другими антивирусными компаниями), третий (самый плодотворный) —
файлы, полученные от пользователей.
В какой-то момент вирусописателям надоело, что их творения разносят в пух и
прах в считанные дни, когда на создание руткита и его отладку уходят многие
недели — обидно, да? Вот они и стали искать пути, как затруднить анализ малвари,
и ведь нашли! Специальная программа, называемая дропером (от английского to drop
– бросать), «сбрасывает» основное тело малвари на целевой компьютер, при этом
оно шифруется ключом, сгенерированным на основе данных о конфигурации системы, и
наружу «торчит» только расшифровщик (зачастую, полиморфный). Как нетрудно
догадаться, малварь такого вида работает только на том компьютере, на который
она попала «естественным» путем, а всякая попытка запуска ее на другой машине
ничего не дает! Ничего, абсолютно!
Чтобы проанализировать малварь, необходимо вместе с ней получить данные о
конфигурации, что довольно затруднительно. Поэтому остается только искать дропер,
то есть ждать, пока малварь не влипнет в HoneyPot, принадлежащий реверсеру или
одному из его партнеров.
WWW
Тема сокрытия трафика подробно изложена Жанной Рутковской, так что не будем
повторять уже сказанное, а просто откроем ее блог и почитаем:
theinvisiblethings.blogspot.com.
|