Содержание статьи
Xакерская команда Immunity (известная своим клоном Ольги) в начале сентября
2008 выпустила руткит под Linux 2.6, который средства массовой дезинформации уже
окрестили принципиально новым и совершенно неуловимым. Общественность была
шокирована мрачными картинами надвигающейся схватки антивирусов с чудовищным
демоном, имя которому DRrootkit.
Но так ли все обстоит на деле?
Руткиты — это программы, которые прячут другие программы. Достигается такое,
как правило, перехватом определенных системных функций (с целью фальсификации
возвращаемого ими результата). Чаще всего перехват осуществляется путем правки
служебных структур данных (таблица прерываний; таблица системных вызовов) или же
модификацией кода самих системных функций. И то, и другое легко обнаруживается
проверкой целостности, а потому алгоритмы подобного типа уже лет пять как не
актуальны, за что и получили прозвище «классических миссионерских».
Более изощренные руткиты отказываются от модификации кода и перезаписывают
указатели на внутренние функции, хранящиеся в динамической памяти. Надежных
способов детекции таких извращенцев до сих пор не придумано, но сложность их
реализации, а также привязанность к конкретной версии операционной системы
делает их заложниками лабораторных экспериментов без всякой надежды на успешный
«промышленный» вариант.
Итак, задача: реализовать классический миссионерский алгоритм перехвата без
правки кода/данных операционной системы, с одной стороны обеспечив простоту
кодирования и совместимость с различными ядрами, а с другой — предотвратить
обнаружение факта вторжения.
Отладочные регистры на службе контрабандистов
Идея, лежащая в основе DR-руткита, на самом деле не нова. В чем разработчики
честно признаются и ссылаются на работы halfdead’а и Pierre Falda, jgbcsdf.o bt,
описывающие особенности перехвата управления под никсами, которые основаны на
установке аппаратных точек останова на системные функции, указатели и
прерывания. Windows-хакеры освоили эту технологию еще во времена MS-DOS, когда
термина «руткиты» не сущест вовало, а зловредные программы, скрывающие факт
своего присутствия,назывались Stealth-вирусами.
Однако многочисленные попытки создания «Голубой Пилюли» не увенчались
успехом. Руткиты либо палились без особых усилий, либо настолько глубоко
зарывались в операционную систему, что соглашались работать только со строго
определенной версией, опять же становясь непригодными для «промышленного»
применения.
Собственно говоря, разработчики DR-руткита реализовали лишь базовый
функционал, обнаруживаемый даже проще, чем классические миссионерские способы
перехвата, а остальное пообещали дописать в «коммерческой» версии. Только вот
сбыться этим планам не суждено. Почему? Да потому, что на определенном этапе
разработки возникнут непреодолимые технические проблемы, о которых мы
обязательно расскажем, но сначала разберемся с демонстрационной версией.
x86‑процессоры несут на своем борту четыре отладочных регистра DR0‑DR3,
содержащих линейные адреса (вектора прерываний) точек останова на исполнение
кода и/или доступ к памяти. Управляющий регистр DR7 специфицирует атрибуты точек
останова, а регистр статуса DR6 содержит информацию о текущей ситуации.
При срабатывании точки останова процессор генерирует исключение и передает
управление обработчику прерывания по вектору 1 (отладочное прерывание). Адреса
векторов прерываний содержатся в специальной таблице, хранящейся в оперативной
памяти и известной под именем IDT (Interrupt Description Table). Ее целостность
проверяется элементарно. На стерильной системе отладочное прерывание смотрит в
ядро, а если это не так — либо установлен нестандартный отладчик уровня ядра,
либо нас конкретно ломанули.
Выходит, что использование точек останова не освобождает от необходимости
правки самой IDT или системного обработчика, на который указывает отладочное
прерывание. В Linux-системах этот обработчик указывает на функцию do_debug
(реализованную в файле ./arch/i386/kernel/traps.c), которую и правит DR-руткит,
причем правит весьма криво. Отыскивает инструкцию, похожую на CALL, подменяя
оригинальный целевой адрес таким образом, чтобы он указывал на хакерский
обработчик. Примитив! И это они называют Stealth-руткитом! Ладно, спишем этот
недостаток на «демонстрационную» ориентацию текущей версии, хотя… как списать
концептуальные просчеты? Как ни крути, а без модификации отладочного прерывания
(или функции, на которую оно указывает) не обойтись, — те же яйца, только в
профиль.
Разумеется, никто не мешает нам поставить точку останова на модифицированный
код обработчика прерывания. Мы перехватываем все обращения к хакнутой ячейке
памяти и подсовываем чтецу фальсифицированный результат, а всех писцов
отправляем лесом (в противном случае защита может элементарно снять хакерский
обработчик, перезаписав содержимое первых байт функции). Но подобные меры
действуют только против пионеров. Начнем с того, что BIOS (точнее — чипсет)
может скидывать содержимое оперативной памяти на диск на аппаратном уровне в
обход процессора. Точки останова при этом не срабатывают. Достаточно просто
спроецировать банк физической памяти, где находится интересующий нас код, на
соседний регион адресного пространства (опять-таки, физического). Тогда точки
останова вновь не сработают. Наконец, современные процессоры обладают развитыми
средствами мониторинга производительности, позволяя отслеживать количество
выполненных переходов, машинных команд, обращений к памяти и т.д., а потому
любая маскировка тут же становится заметной. Но это мы уже полезли в дебри.
DR-руткит не предпринимает никаких попыток для маскировки факта перехвата
функции do_debug, что и подтверждается нижеследующим кодом:
Ключевой фрагмент DR-руткита, ответственный за модификацию кода функции ОС
do_debug():
static int __get_int_handler(int offset)
{
int idt_entry = 0;
// Загрузка содержимого IDT-таблицы посредством команды SIDT с последующим
определением линейного адреса отладочного прерывания
__asm__ __volatile__ ( "xorl %%ebx,%%ebx \n\t"
"pushl %%ebx \n\"
…
"popl %%ebx \n\t"
: "=a" (idt_entry)
: "r" (offset)
: "ebx", "esi" );
return idt_entry;
}
static int __get_and_set_do_debug_2_6(u_int handler,
u_int my_do_debug)
{
// Грязный поиск машинной команды CALL offset (опкод E8h xx xx xx xx),
потенциально небезопасный и в определенных ситуациях приводящий к ложным
срабатываниям, необратимо гробящим функцию do_debug и вгоняющим ядро в панику
while (p[0] != 0xe8)
{
p ++; // Если это не E8h, проверяем следующий байт
}
DEBUGLOG(("*** found call do_debug %X\n", (u_int)p));
…
// Замена старого оффсета на новый, указывающий на хакерский обработчик (на
многопроцессорных системах возможен крах, так как правка кода не атомарна)
p[1] = (offset & 0x000000ff);
p[2] = (offset & 0x0000ff00) >> 8;
p[3] = (offset & 0x00ff0000) >> 16;
p[4] = (offset & 0xff000000) >> 24;
return orig;
}
static int __init init_DR(void)
{
…
// Определение линейного адреса вектора прерывания INT
01h
h0x01 = __get_int_handler(0x1);
h0x01_global = h0x01;
// Правка системного обработчика отладочного прерывания путем прямой правки
системной функции в памяти
__orig_do_debug = (
void (*)())__get_and_set_do_debug_2_6(h0x01,
(u_int)__my_do_debug);
…
}
Ужас! Впрочем, для демонстрационной версии вполне сгодится. Углубляться в
дальнейший анализ кода руткита нет смысла.
Там все стандартно. Перехватывается диспетчер системных вызовов (на что
уходит две точки останова — одна на INT 80h [old gate], другая — на SYSENTER [new
gate]). Третья точка останова устанавливается динамически при срабатывании любой
из первых двух. Руткит анализирует, какая именно системная функция вызывается, и
загоняет ее адрес в DR3, а при генерации отладочного прерывания просто подменяет
регистровый контекст, перенаправляя EIP на код хакерского обработчика.
Ключевой фрагмент DR -руткита, отвечающий за установку точки останова на
вызываемую системную функцию:
// Определение адреса системного вызова для установки динамичной точки
останова
dr2 = sys_table_global +
(u_int)regs->eax * sizeof(void *);
// Задание параметров точки останова
s_control |= TRAP_GLOBAL_DR2;
s_control |= TRAP_LE;
s_control |= TRAP_GE;
s_control |= DR_RW_READ << DR2_RW;
s_control |= 3 << DR2_LEN;
// Копирование линейного адреса системного вызова в отладочный регистр DR2
__asm__ __volatile__ ("movl %0,%%dr2 \n\t"
:
: "r" (dr2) );
break;
Расстрел DR -руткита прямой наводкой
Истребители класса Stealth невидимы только для коротковолновых радаров
(используемых армией США). Длинноволновые радары (морально устаревшие, но все
еще не списанные) палят их без особых усилий. Поэтому для России «Стелсы»
большой угрозы не представляют.
Точно так же обстоят дела и с DR-руткитом. Он невидим для защит,
контролирующих целостность таблиц прерывания, и системных вызовов, которые
DR-руткит не изменяет. Но защиты рангом повыше (контролирующие целостность
обработчиков прерываний и системных функций) палят DR-руткит «в лет» — по
захаченной функции do_debug.
Поскольку DR-руткит использует аж три отладочных регистра (из четырех
имеющихся), отладчики уровня ядра с ним не работают и вызывают ужасные
конфликты, обусловленные борьбой за точки останова и отладочное прерывание.
Чисто теоретически, грамотно написанный руткит обходится всего одной точкой
останова и «делится» отладочным прерыванием с отладчиком. А еще лучше —
выгружает себя из памяти при активном ядерном отладчике, который позволяет
«запеленговать» руткит просмотром кода операционной системы и служебных структур
данных. Бороться с отладчиком, конечно, можно, но вот нужно ли? Сам факт борьбы
демаскирует руткита.
Не следует забывать, что отладочные регистры могут быть прочитаны из любого
ядерного модуля, состоящего всего лишь из нескольких строк кода. Создатели
DR-руткита обещают в следующей версии оказать этому способу проверки яростное
сопротивление и заставить процессор генерировать исключение при любом обращении
к отладочным регистрам. Процессор, действительно, может сделать это. А что
толку? Да, в теории все гладко и сладко. Защита читает DRx-регистр для контроля
его целостности. Процессор генерирует исключение, которое подхватывается
руткитом, возвращающим защите фиктивные данные.
Попытка практической реализации, однако, сталкивается с непреодолимым
препятствием в лице операционной системы. Допустим, ядро сохраняет DRx-регистры
для их последующего восстановления. Если руткит вернет фиктивные данные, то он и
получит фиктивные данные при восстановлении контекста. ОК, блокируем
восстановление контекста, прочно удерживая DRx-регистры от чтения/изменения. Но
при этом они неизбежно «вырываются» с уровня ядра на прикладной уровень и
вызывают непредвиденные исключения, которые руткиту придется как-то
обрабатывать. Все это усложняет реализацию, делая ее системно-зависимой.
Даже если руткит сумеет корректно обработать перехват отладочных регистров,
это не спасет его от расправы. Ведь тут действует правило: кто первый встал,
того и тапки. В смысле: кто первый захватил отладочные регистры, тому они и
принадлежат. Следовательно, защитный модуль, загруженный до запуска DR-руткита,
просто не позволит ему работать. Но даже если DR-руткит загрузится первым,
защита, реально использующая все четыре точки останова (а не просто читающая
содержимое DRx-регистров), поставит DR-руткит перед выбором: либо
дезактивировать все установленные им точки останова, совершив харакири, либо
обломать защиту с установкой, тем самым разоблачив себя!
Наконец, защита может и не проверять значение отладочного прерывания, а
просто создать новую таблицу прерываний и загрузить ее в процессор.
Воспрепятствовать перегрузке таблицы прерываний руткит не в силах.
Следовательно, защита может отобрать у него отладочное прерывание, без которого
руткит заглохнет, как двигатель от «Запора». Конечно, никто не мешает руткиту
перехватить одну или несколько функций операционной системы, передавая
управление процедуре самовосстановления при их вызове (методика, широко
использующаяся еще со времен MS-DOS). Однако при этом рухнет вся концепция — мы
же говорили о рутките нового поколения, нашедшем «волшебный способ» перехвата и
позволяющем отказаться от правки служебных таблиц и/или кода операционной
системы!
Оказывается, в рамках сей концепции построить жизнеспособный руткит
невозможно, и дело ограничивается демонстрацией принципиальной возможности.
Пустые обещания
Подведем итог. Нам обещали руткит, который не модифицирует код операционной
системы, используя для перехвата отладочные регистры.
В действительности, мы получили гибридный продукт, модифицирующий и
отладочные регистры, и код (данные). По другому никак не получится. Поскольку
захват отладочного прерывания (необходимого для поддержания жизнедеятельности
руткита) осуществляется по той же самой схеме, что и перехват прерывания INT 80h
(используемого в качестве гейта системных вызовов). Методика контроля
целостности хорошо отработана!
Обещали нам руткит, который нельзя обнаружить. На самом же деле он (как на
концептуальном уровне, так и на уровне отдельно взятой реализации)
обнаруживается элементарно, более того, требует, чтобы в системе отсутствовал
отладчик уровня ядра, с которым он жестоко конфликтует.
Нам обещали, что все вышеперечисленные недостатки будут устранены в следующей
версии, однако эти обещания не подкреплены никакими доводами и научно не
обоснованы. Говорить можно все, что угодно, а вот… слабо описать алгоритм
действий или дать ссылку на статьи или хоть какие-то работы в этой области?
Linux в отношении руткитов существенно отстает от Windows (в которой, как уже
говорилось, эксперименты с отладочными регистрами начали проводиться еще во
времена MS-DOS). До сих пор никому не удалось создать руткит, который
существенно превосходит своих коллег, использующих традиционные методики.
Короче говоря, умелое использование отладочных регистров действительно
улучшает качество руткита (хотя бы потому, что препятствует активной отладке, а,
значит, замедляет анализ), но не так радикально, как это утверждается. К чести
парней из Immunity, — они всего лишь создали первую минимально рабочую
реализацию руткита данного типа под Linux и выложили ее в открытый доступ вместе
с исходными текстами, снабженными подробными комментариями. Сенсацию из этого
сделали не они, так что не будем особо возмущаться.
|