Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР 120, ДЕКАБРЬ 2008 г.

Энциклопедия антиотладочных приемов: cкрытая установка SEH-обработчиков

Крис Касперски




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

Постановка проблемы

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

Всякий раз, когда в тексте программы встречается конструкция «MOV FS:[0], xxx», хакер сразу встает торчком — раз это FS:[0], значит, программа устанавливает собственный SEH-обработчик и, судя по всему, сейчас будет бросать исключения. Теоретически, возможно засунуть «MOV FS:[0], xxx» в самомодифицирующийся код, убрав его из дизассемблерных листингов, однако против аппаратной точки останова по записи на «MOV FS: [0], xxx» ничего не спасет. В момент установки нового SEH-обработчика отладчик тут же «всплывет», демаскируя защитный механизм. А SetUnhandledExceptionFilter вообще представляет собой API-функцию, экспортируемую KERNEL32.DLL, которую легко обнаружить любым API-шпионом, даже без анализа всего дизассемблерного кода!

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

Перезапись существующего обработчика

Вместо того чтобы устанавливать новый обработчик структурных исключений, некоторые (и достаточно многие) защиты предпочитают модифицировать указатель на уже существующий. Даже если приложение и не устанавливает никаких SEH-обработчиков, система все равно впихивает ему SEH-обработчик по умолчанию, смотрящий куда-то в дебри KERNEL32.DLL. На этом, кстати говоря, основан популярный примем поиска базового адреса загрузки KERNEL32.DLL, в котором нуждается shell-код, а также программы, написанные без использования таблицы импорта (из-за ошибки в системном загрузчике они работают только на XP и более поздних версиях).
Обработчик по умолчанию не делает ничего полезного и потому без него можно обойтись, «позаимствовав» указатель – на время или навсегда. Конкретный пример реализации приведен ниже:

Установка своего SEH-обработчика без перезаписи ячейки FS:[0]

souriz()
{
printf("hello, nezumi\n"); ExitProcess(0);
}

main()
{
int *p=0;
__asm{
mov eax, fs:[0]
lea ecx, souriz
add eax, 4
mov [eax], ecx
}
return *p;
}

Внешне код очень похож на классический способ установки SEH-обработчика, но, присмотревшись внимательнее, мы видим, что в нашем примере модифицируется отнюдь не ячейка «FS:[0]», а то, на что она указывает. Точка останова по записи на «FS:[0]» уже не сработает, однако сегментный регистр FS режет глаз, да и бряк на «FS:[0]» по доступу продолжает работать, а потому для эффективного противодействия хакеру требуются дополнительные уровни маскировки. Ну, и чего мы сидим? Вперед!

Прячем FS

Ослепить дизассемблеры совсем нетрудно. Перезаписать указатель на системный SEH-обработчик можно и без явного использования сегментного регистра FS. Самое простое, что можно сделать — скопировать его в любой другой сегментный регистр (например, GS). С точки зрения процессора, регистры FS и GS совершенно равноправны. Главное, чтобы в регистре содержался «правильный» селектор, а его название — уже дело десятое. Создавать новые селекторы мы не можем (точнее, можем, но это тема отдельного разговора), а вот загружать существующие — почему бы и нет?
Усиленный фрагмент защиты приведен ниже:

Прячем регистр FS от любопытных глаз

__asm{
mov ax, fs
mov gs, ax
}



__asm{
mov eax, gs:[0]
lea ecx, souriz
add eax, 4
mov [eax], ecx
}

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

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

Опытных реверсеров таким макаром не проведешь, но никаких гарантий, что GS в данный момент содержит именно FS, а не, например, DS, у нас нет. А потому статический анализ становится неоднозначным и требует реконструкции последовательности вызываемых функций. Причем, обращения к FS в явном виде может и не быть — его значение легко прочитать API-функцией GetThreadContext, на которую, конечно, нетрудно поставить точку останова, но точки останова – это уже динамический, а не статический анализ!

Самое интересное, что блок окружения потока, засунутый в селектор (который хранится в сегментном регистре FS), отображается на плоское адресное пространство, а значит, доступен для чтения и через остальные селекторы. Например, через сегментный регистр DS. На W2K блок окружения первичного потока начинается с адреса 7FFDВ000h (7FFDE000h на XP), поэтому вместо FS:[0] допустимо использовать конструкцию DS:[7FFDB000h]. Чтобы избежать краха, надо отталкиваться от того факта, что в настоящем блоке окружения потока по смещению 30h байт от его начала расположен указатель на блок окружения процесса, лежащий на 1000h байт ниже. Благодаря чему мы можем найти указатель на SEH-обработчик даже на неизвестной операционной системе!

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

Поиск блока окружения потока в стеке

int a; int *p=0;
unsigned char *pp = (unsigned char*) 0x7FFE0000;

for(a = 0; a < 6; a++)
{
pp -= 0x1000;
if (IsBadReadPtr(pp, 4)) continue;
if (IsBadReadPtr((pp + 0x30), 4)) continue;
if ( *((size_t*)(pp + 0x30)) == ((size_t) pp + 0x1000) )
{
*(size_t*) (*((size_t*)pp) + 4) = (size_t*) souriz;
return *p;
}
} printf("not found\n");

Во-первых, мы обошлись без ассемблерных вставок, реализовав алгоритм на чистом Си (с тем же успехом можно использовать Паскаль). Во-вторых, вместо характерного «FS» в программе появилась куча констант, смысл которых понятен только посвященным, да и то не без пристального анализа, сопровождаемого глубокой медитацией. В-третьих, факт передачи управления на функцию souriz по return *p (где p == 0) совершенно не очевиден. К тому же, сам указатель на souriz можно зашифровать, помешав дизассемблерам реконструировать перекрестные ссылки. Как это сделать на Си (без ассемблерных вставок), описывалось в 1Eh выпуске сишных трюков.

Существуют и другие способы поиска указателя на блок окружения потока. Рассмотрим только два самых популярных. Просматривая карту памяти (а просмотреть ее можно с помощью API-вызова VirtualQuery), даже удав заметит, что блоки окружения процесса и потока лежат в своих собственных секциях памяти с атрибутами Private и правами на чтение/запись. Размер каждого блока равен 1000h, плюс ко всему указатель на блок окружения процесса расположен по смещению 30h байт от блока окружения потока. То есть, если *((size_t*)(block_1+30h)) == block_2, то block_1 – блок окружения потока, а block_2 – блок окружения процесса и «MOV EAX, FS:[0]» равносильно MOV EAX, block_1/MOV EAX, [EAX]. Вывод: без FS можно по-любому обойтись.

Указатель на блок окружения потока также находится в стеке потока, куда его кладет операционная система. В W2K/XP это третье двойное слово от вершины. И хотя в последующих версиях его местоположение может измениться, вирусов это обстоятельство походу никак не заботит, и они используют его сплошь и рядом.

И что в итоге? Мы рассмотрели множество приемов скрытного обращения к ячейке FS:0, однако все они действуют только против дизассемблеров, а отладчики просто ставят сюда точку останова по доступу и все обращения к FS:0 немедленно палятся. Независимо от того, какой адрес используется смещение 0 по селектору FS или же смещение 7FFDВ000h по селектору DS.

Непорядок! Хорошая защита должна справляться не только с дизассемблерами, но и с отладчиками!

Кража чужих обработчиков

Системный обработчик структурных исключений расположен на дне стека потока – и обращаться к блоку окружения для его поисков совсем не обязательно, поскольку местоположение обработчика непостоянно и зависит от версии операционной системы. С учетом этого мы должны выработать эвристический алгоритм поиска.
Системный обработчик, назначаемый по умолчанию, есть не что иное, как функция «__except_handler3», расположенная в недрах KERNEL32.DLL и не экспортируемая наружу, но присутствующая в отладочных символах. Которые, теоретически, можно в любой момент скачать с серверов Microsoft, но практически – такое решение будет слишком громоздким, неудобным, ненадежным, да и довольно «прозрачным» для хакера.

Хорошо, будем отталкиваться от того, что «__except_handler3» смотрит в KERNEL32.DLL и что перед ним всегда расположено двойное слово «FFFFFFFFh», а после него — указатель на секцию данных KERNEL32.DLL опять-таки содержащий в себе двойное слово «FFFFFFFFh». Последнее обстоятельство системнозависимо, но справедливо как для W2K, так и для XP, а потому его можно использовать без особых опасений.
Практический пример приведен ниже:

Прямой поиск указателя на SEH-обработчик в стеке

for (a=0;a<69;a++,pp++)
{
if (IsBadReadPtr((pp+2), 4)) break;
if (*pp == 0xFFFFFFFF)
{
if (IsBadReadPtr(*(pp + 2), 4)) continue;
if (*((unsigned int*)*(pp + 2)) == 0xFFFFFFFF)
{
*(pp + 1) = (unsigned int*) souriz;
return *p;
}
}
} printf("not found\n");

Точка останова на FS:0 на этот раз идет лесом и не срабатывает, поскольку обращения к этой ячейки памяти уже не происходит. К тому же, разобраться, что именно ищет программа в стеке, можно после серии экспериментов (ну или чтения этой статьи). Способов поиска системного обработчика исключений намного больше одного. Это существенно усложняет задачу хакера и универсальных «отмычек» тут нет, что в плане защиты очень даже хорошо. Однако просмотр цепочки обработчиков структурных исключений (в Ольге осуществляется через меню View -> SEH Chain) немедленно разоблачает хакнутый обработчик, на который несложно установить точку останова на исполнение со всеми вытекающими отсюда последствиями.

Рукотворный SetUnhandledExceptionFilter

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

Самый большой недостаток функции SetUnhandledExceptionFilter в том, что ее вызов очень трудно замаскировать, но трудно еще не значит невозможно. К тому же реализация функции проста как движок от «запора». Фактически, она всего лишь устанавливает глобальную переменную BasepCurrentTopLevelFilter, хранящуюся внутри KERNEL32.DLL и используемую только функцией UnhandledExceptionFilter.

Дизассемблерный листинг API-функции SetUnhandledExceptionFilter из W2K

.text:7945BC45_SetUnhandledExceptionFilter@4 proc near
.text:7945BC45
.text:7945BC45lpTopLevelExceptionFilter= dword ptr4
.text:7945BC45
.text:7945BC45 8B 4C 24 04movecx, [esp+lpTopLevelExceptionFilter]
.text:7945BC49 A1 F0 A1 48 79moveax, _BasepCurrentTopLevelFilter
.text:7945BC4E 89 0D F0 A1 48 79mov_BasepCurrentTopLevelFilter, ecx
.text:7945BC54 C2 04 00retn4
.text:7945BC54_SetUnhandledExceptionFilter@4 endp

Все что нам нужно — это найти BasepCurrentTopLevelFilter внутри SetUnhandledExceptionFilter (или UnhandledExceptionFilter) и прописать сюда указатель на свой собственный обработчик исключений. К сожалению, это не избавляет нас от необходимости импортирования SetUnhandledExceptionFilter/UnhandledExceptionFilter или получения эффективного адреса путем ручного разбора таблицы экспорта KERNEL32.DLL. Да, конечно, ручной разбор с использованием хэш-сум вместо имен API-функций до некоторой степени скрывает наши намерения от хакера. Увы, нет ничего тайного, что ни стало бы явным. Даже если выбранный хэш-алгоритм математически необратим, запустив программу под отладчиком, всегда можно установить, какой именно API-функции какой хэш соответствует.

В последних версиях Windows появилась шифровка указателей, и BasepCurrentTopLevelFilter хранится в закодированном виде. Естественно, возможность «ручной» работы с указателями никуда не делась и в NTDLL.DLL появились функции RtlEncodePointer/RtlDecodePointer, имена которых говорят сами за себя. Все это существенно усложняет реализацию защиты и делает ее экономически нецелесообразной, вынуждая искать другие пути. И такие пути действительно есть!
Библиотечный обработчик структурных исключений, поставляемый вместе с языками высокого уровня, интенсивно использует API-функцию UnhandledExceptionFilter, что позволяет нам перехватывать ее путем правки таблицы импорта (или любым другим способом). Конечно, модификация импорта — грязный трюк, привлекающий к себе внимание, поэтому лучше хакнуть непосредственно саму библиотечную функцию обработки исключений. В случае MS VC эта функция носит имя «__XcptFilter». Первые байты трогать нежелательно — иначе IDA-Pro ее не распознает, впрочем, байт байту рознь. IDA-Pro пропускает относительные вызовы, поскольку они непостоянны и подвержены сезонным вариациям.

Нам нужно найти CALL func и заменить func адресом нашей функции my_func, выполняющей некоторые действия и при необходимости возвращающей управление оригинальной func. Анализ кода «__XcptFilter» обнаруживает вызов «_xcptlookup», осуществляемый в основном блоке кода, то есть не «шунтируемый» никакими ветвлениями, что очень хорошо:

Дизассемблерный фрагмент библиотечной функции __XcptFilter

.text:00401C9A __XcptFilterproc near
.text:00401C9A
.text:00401C9A arg_0= dword ptr8
.text:00401C9A ExceptionInfo= dword ptr0Ch
.text:00401C9A
.text:00401C9Apushebp
.text:00401C9Bmovebp, esp
.text:00401C9Dpushebx
.text:00401C9Epush[ebp+arg_0]
.text:00401CA1call_xcptlookup; _xcptlookup ? my_invisible_seh
.text:00401CA6testeax, eax

Обнаружить наш обработчик исключений практически невозможно. Он отсутствует в SEH-цепочке (точнее, присутствует, но прячется внутри обработчика, устанавливаемого RTL языка высокого уровня), и Ольга в упор его не видит. Конечно, при пошаговой трассировке хакерский обработчик будет выявлен, – вот только трассировать мегабайты системного и библиотечного кода никто не будет. Дизассемблирование также не покажет ничего подозрительного, поскольку IDA-Pro не проверяет целостность библиотечных функций, и никто из хакеров не тратит время на их анализ, а потому предложенный прием оказывается весьма живучим в плане взлома.

Содержание
загрузка...
Журнал Хакер #151Журнал Хакер #150Журнал Хакер #149Журнал Хакер #148Журнал Хакер #147Журнал Хакер #146Журнал Хакер #145Журнал Хакер #144Журнал Хакер #143Журнал Хакер #142Журнал Хакер #141Журнал Хакер #140Журнал Хакер #139Журнал Хакер #138Журнал Хакер #137Журнал Хакер #136Журнал Хакер #135Журнал Хакер #134Журнал Хакер #133Журнал Хакер #132Журнал Хакер #131Журнал Хакер #130Журнал Хакер #129Журнал Хакер #128Журнал Хакер #127Журнал Хакер #126Журнал Хакер #125Журнал Хакер #124Журнал Хакер #123Журнал Хакер #122Журнал Хакер #121Журнал Хакер #120Журнал Хакер #119Журнал Хакер #118Журнал Хакер #117Журнал Хакер #116Журнал Хакер #115Журнал Хакер #114Журнал Хакер #113Журнал Хакер #112Журнал Хакер #111Журнал Хакер #110Журнал Хакер #109Журнал Хакер #108Журнал Хакер #107Журнал Хакер #106Журнал Хакер #105Журнал Хакер #104Журнал Хакер #103Журнал Хакер #102Журнал Хакер #101Журнал Хакер #100Журнал Хакер #099Журнал Хакер #098Журнал Хакер #097Журнал Хакер #096Журнал Хакер #095Журнал Хакер #094Журнал Хакер #093Журнал Хакер #092Журнал Хакер #091Журнал Хакер #090Журнал Хакер #089Журнал Хакер #088Журнал Хакер #087Журнал Хакер #086Журнал Хакер #085Журнал Хакер #084Журнал Хакер #083Журнал Хакер #082Журнал Хакер #081Журнал Хакер #080Журнал Хакер #079Журнал Хакер #078Журнал Хакер #077Журнал Хакер #076Журнал Хакер #075Журнал Хакер #074Журнал Хакер #073Журнал Хакер #072Журнал Хакер #071Журнал Хакер #070Журнал Хакер #069Журнал Хакер #068Журнал Хакер #067Журнал Хакер #066Журнал Хакер #065Журнал Хакер #064Журнал Хакер #063Журнал Хакер #062Журнал Хакер #061Журнал Хакер #060Журнал Хакер #059Журнал Хакер #058Журнал Хакер #057Журнал Хакер #056Журнал Хакер #055Журнал Хакер #054Журнал Хакер #053Журнал Хакер #052Журнал Хакер #051Журнал Хакер #050Журнал Хакер #049Журнал Хакер #048Журнал Хакер #047Журнал Хакер #046Журнал Хакер #045Журнал Хакер #044Журнал Хакер #043Журнал Хакер #042Журнал Хакер #041Журнал Хакер #040Журнал Хакер #039Журнал Хакер #038Журнал Хакер #037Журнал Хакер #036Журнал Хакер #035Журнал Хакер #034Журнал Хакер #033Журнал Хакер #032Журнал Хакер #031Журнал Хакер #030Журнал Хакер #029Журнал Хакер #028Журнал Хакер #027Журнал Хакер #026Журнал Хакер #025Журнал Хакер #024Журнал Хакер #023Журнал Хакер #022Журнал Хакер #021Журнал Хакер #020Журнал Хакер #019Журнал Хакер #018Журнал Хакер #017Журнал Хакер #016Журнал Хакер #015Журнал Хакер #014Журнал Хакер #013Журнал Хакер #012Журнал Хакер #011Журнал Хакер #010Журнал Хакер #009Журнал Хакер #008Журнал Хакер #007Журнал Хакер #006Журнал Хакер #005Журнал Хакер #004Журнал Хакер #003Журнал Хакер #002Журнал Хакер #001