anti-debug-07

энциклопедия антиотладочных приемов —\\ скрытая установка SEH-обработчиков\\ выпуск #07h

крис касперски, ака мыщъх, a.k.a nezumi, a.k.a souriz, a.k.a elraton, no-email

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

Обработчик по умолчанию не делает ничего полезного и потому без него можно обойтись, «позаимствовав» указатель на время или навсегда. Конкретный пример реализации приведен ниже:

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; } Листинг 1 установка своего SEH-обработчика без перезаписи ячейки FS:[0] Внешне этот код очень похож на классический способ установки SEH-обработчика, однако, присмотревшись повнимательнее, мы видим, что в нашем примере модифицируются отнюдь не ячейка «FS:[0]», а то, на что она указывает. Точка останова по записи на «FS:[0]«уже не сработает, однако, сегментный регистр FS режет глаз, да и бряк на FS:[0] по доступу продолжает работать, а потому для эффективного противодействия хакеру требуются дополнительные уровни маскировки. Ну, и чего мы сидим? Вперед! Рисунок 1 скрытая установка SEH-обработчика ===== прячем FS ===== Ослепить дизассемблеры совсем нетрудно. Перезаписать указатель на системный SEH-обработчик можно и без явного использования сегментного регистра FS. Самое простое, что только можно сделать — скопировать его в любой другой сегментный регистр (например, GS). С точки зрения процессора регистры FS и GS совершенно равноправны. Главное, чтобы в регистре содержался «правильный» селектор, а его название — дело десятое. Создавать новые селекторы мы не можем (точнее, можем, но это тема отдельного разговора), но загружать уже существующие — почему бы и нет?! Усиленный фрагмент защиты приведен ниже: asm{

mov ax, fs

mov gs, ax

}

asm{ mov eax, gs:[0] lea ecx, souriz add eax, 4 mov [eax], ecx } Листинг 2 прячем регистр FS от любопытных глаз Небольшое пояснение. Поскольку, ни один известный мне компилятор не использует регистр 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 (IsBadReadPtr1) continue; if ( *2) == 3) == block_2, то block_1 – блок окружения потока, а block_2 – блок окружения процесса и «MOV EAX, FS:[0]» равносильно MOV EAX, block_1/MOV EAX, [EAX], то есть без FS можно по любому обойтись. Рисунок 2 блок окружения потока на карте памяти процесса Указатель на блок окружения потока так же находится в стеке потока, куда его кладет операционная система. В W2K/XP это третье двойное слово от вершины. И хотя в последующих версиях его местоположение может измениться, вирусов это обстоятельство походе никак не заботит и они используют его сплошь и рядом. И что в итоге? Мы рассмотрели множество приемов скрытного обращения к ячейке FS:0, однако, все они действуют только против дизассемблеров, а отладчики просто ставят сюда точку останова по доступу и _все_ обращения к FS:0 немедленно палятся — независимо от того какой адрес используется смещение 0 по селектору FS или же смещение 7FFDВ000h по селектору DS. Непорядок! Хорошая защита должна справляется не только с дизассемблерами, но и с отладчиками! ===== кража чужих обработчиков ===== Системный обработчик структурных исключений расположен на дне стека потока и обращаться к блоку окружения для его поисков совсем необязательно. Поскольку, местоположение обработчика непостоянно и зависит от версии операционной системы мы должны выработать эвристический алгоритм поиска. Системный обработчик, назначаемый по умолчанию, есть ни что иное как функция except_handler3, расположенная в недрах KERNEL32.DLL и _не_ экспортируемая наружу, однако, присутствующая в отладочных символах, которые теоретически можно в любой момент скачать с серверов Microsoft, но практически такое решение будет слишком громоздким, неудобным, ненадежным, да и довольно «прозрачным» для хакера.

Рисунок 3 указатель на системный SEH-обработчик лежит на дне стека потока

Хорошо будем отталкиваться от того, что except_handler3 смотрит в KERNEL32.DLL и что перед ним всегда расположено двойное слово FFFFFFFFh, а после него — указатель на секцию данных KERNEL32.DLL опять-таки содержащий в себе двойное слово FFFFFFFFh. Последнее обстоятельство системнозависимо, однако, они справедливо как для W2K, так и для XP, а потому его можно использовать без особых опасений. Практический пример приведен ниже: for (a=0;a<69;a++,pp++) { if (IsBadReadPtr4) break; if (*pp == 0xFFFFFFFF) { if (IsBadReadPtr(*(pp + 2), 4)) continue; if (*5) == 0xFFFFFFFF) { *(pp + 1) = (unsigned int*) souriz; return *p; } } } printf(«not found\n»); Листинг 4 прямой поиск указателя на SEH-обработчик в стеке Точка останова на FS:0 на этот раз идет лесом и не срабатывает, поскольку, обращение к этой ячейки памяти уже не происходит. К тому же разобраться что именно ищет программа в стеке можно после серии экспериментов (ну или чтения этой статьи ;-). Впрочем, способов поиска системного обработчика исключений намного больше одного, что существенно усложняет задачу хакера и универсальных «отмычек» тут нет, что в плане защиты очень даже хорошо, однако, просмотр цепочки обработчиков структурных исключений (в Ольге это осуществляется через меню View → SEH Chain) немедленно разоблачает хакнутый обработчик на который несложно установить точку останова на исполнение со всеми вытекающими отсюда… Рисунок 4 просмотр SEH-цепочек в Ольге ===== рукотворный SetUnhandledExceptionFilter ===== API-функция SetUnhandledExceptionFilter, как уже отмечалось в предыдущих выпусках, сама по себе представляет проблему для отладчиков, поскольку, установленный ею фильтр исключений верхнего уровня при запуске программы под отладчиком не выполняется и приходится использовать разнообразные плагины для Ольги, чтобы заставить систему считать, что никакого отладчика здесь нет или же, как вариант, насильственно включать фильтр верхнего уровня в цепочку обработчиков структурных исключений. Самый большой недостаток функции SetUnhandledExceptionFilter в том, что ее вызов очень трудно замаскировать, но трудно еще не значит невозможно. К тому же реализация функции проста как движок от запора. Фактически, она всего лишь устанавливает глобальную переменную BasepCurrentTopLevelFilter, хранящуюся внутри KERNEL32.DLL и используемую только функцией UnhandledExceptionFilter. .text:7945BC45_SetUnhandledExceptionFilter@4 proc near .text:7945BC45 .text:7945BC45lpTopLevelExceptionFilter= dword ptr 4 .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 Листинг 5 дизассемблерный листинг API-функции SetUnhandledExceptionFilter из W2K Все что нам нужно — это найти BasepCurrentTopLevelFilter внутри SetUnhandledExceptionFilter (или UnhandledExceptionFilter) и прописать сюда указатель на свой собственный обработчик исключений. К сожалению, это не избавляет нас от необходимости импортирования SetUnhandledExceptionFilter/UnhandledExceptionFilter или получения эффективного адреса путем ручного разбора таблицы экспорта KERNEL32.DLL. Да, конечно, ручной разбор с использованием хэш-сум вместо имен API-функций до некоторой степени скрывает наши намерения от хакера, однако, нет ничего тайного что бы ни стало явным. Даже если выбранный хэш-алгоритм математически необратим, запустив программу под отладчиком всегда можно установить какой именно API функции какой хэш соответствует. Рисунок 5 дизассемблерный листинг API-функции SetUnhandledExceptionFilter из Висты, как видно, со времен W2K ее реализация сильно усложнилась К тому же, в последних версиях Windows появилась шифровка указателей и BasepCurrentTopLevelFilter хранится в закодированном виде. Естественно, возможность «ручной» работы с указателями никуда не делать и в NTDLL.DLL появились функции RtlEncodePointer/RtlDecodePointer имена которых говорят сами за себя, однако, все это существенно усложняет реализацию защиты, что делает ее экономически нецелесообразной, вынуждая нас искать другие пути и такие пути действительно есть! Библиотечный обработчик структурных исключений, поставляемый вместе с языками высокого уровня, интенсивно использует API-функцию UnhandledExceptionFilter, что позволяет нам перехватывать ее путем правки таблицы импорта (или любым другим способом). Конечно, модификация импорта — грязный трюк, привлекающий к себе внимание, поэтому лучше хануть непосредственно саму библиотечную функцию обработки исключений. В случае MS VC эта функция носит имя XcptFilter. Первые байты трогать нежелательно — иначе IDA-Pro ее не распознает, впрочем, байт байту рознь. IDA-Pro пропускает относительные вызовы, поскольку они непостоянны и подвержены сезонным вариациям.

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

.text:00401C9A

.text:00401C9A arg_0= dword ptr 8

.text:00401C9A ExceptionInfo= dword ptr 0Ch

.text:00401C9A

.text:00401C9Apushebp

.text:00401C9Bmovebp, esp

.text:00401C9Dpushebx

.text:00401C9Epush[ebp+arg_0]

.text:00401CA1call_xcptlookup; _xcptlookup my_invisible_seh

.text:00401CA6testeax, eax

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

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

1)
pp + 0x30), 4
2)
size_t*)(pp + 0x30
3)
size_t) pp + 0x1000) ) { *(size_t*) (*((size_t*)pp) + 4) = (size_t*) souriz; return *p; } } printf(«not found\n»); Листинг 3 поиск блока окружения потока в стеке Во-первых, мы обошлись без ассемблерных вставок, реализовав алгоритм на чистом Си (с тем же успехом можно использовать Паскаль), во-вторых, вместо характерного «FS» в программе появилась куча констант, смысл которых понятен только посвященным, да и то не без пристального анализа, сопровождаемого глубокой медитацией. В-третьих, факт передачи управления на функцию souriz по return *p (где p == 0) _совершенно_ неочевиден, к тому же, сам указатель на souriz можно зашифровать, помешав дизассемблерам реконструировать перекрестные ссылки. Как это сделать на Си (без ассемблерных вставок) описывалось в 1Eh выпуске сишных трюков. Существуют и другие способы поиска указателя на блок окружения потока. Рассмотрим только два самых популярных из них. Просматривая карту памяти (а просмотреть ее можно с помощью API-вызова VirtualQuery) даже удав заметит, что блоки окружения процесса и потока лежат в своих собственных секциях памяти с атрибутами Private и правами на чтение/запись, причем размер каждого блока равен 1000h, плюс ко всему указатель на блок окружения процесса расположен по смещению 30h байт от блока окружения потока. То есть, если *((size_t*)(block_1+30h
4)
pp+2), 4
5)
unsigned int*)*(pp + 2