Автор: (c)Крис Касперски ака мыщъх
Продолжая окучивать плодородную почву темы структурных исключений, поговорим о методах скрытой установки 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 и более поздних версиях).
Обработчик по умолчанию не делает ничего полезного и потому без него можно обойтись, "позаимствовав" указатель на время или навсегда. Конкретный пример реализации приведен ниже:
Листинг 1. Установка своего SEH-обработчика без перезаписи ячейки FS:[0].
Внешне этот код очень похож на классический способ установки SEH-обработчика, однако присмотревшись повнимательнее, мы видим, что в нашем примере модифицируется отнюдь не ячейка "FS:[0]", а то, на что она указывает. Точка останова по записи на "FS:[0]" уже не сработает, однако сегментный регистр FS режет глаз, да и бряк на FS:[0] по доступу продолжает работать, а потому для эффективного противодействия хакеру требуются дополнительные уровни маскировки.
Ну, и чего мы сидим? Вперед!
Рисунок 1. Скрытая установка SEH-обработчика.
Ослепить дизассемблеры совсем нетрудно. Перезаписать указатель на системный SEH-обработчик можно и без явного использования сегментного регистра FS. Самое простое, что только можно сделать - скопировать его в любой другой сегментный регистр (например, GS). С точки зрения процессора регистры FS и GS совершенно равноправны. Главное, чтобы в регистре содержался "правильный" селектор, а его название - дело десятое. Создавать новые селекторы мы не можем (точнее - можем, но это тема отдельного разговора), но загружать уже существующие - почему бы и нет?!
Усиленный фрагмент защиты приведен ниже:
Листинг 2. Прячем регистр FS от любопытных глаз.
Небольшое пояснение. Поскольку ни один известный мне компилятор не использует регистр GS для своих целей, то его можно инициализировать в одной процедуре, а использовать - в другой. Единственное условие - обе процедуры должны принадлежать одному потоку, поскольку каждый поток обладает собственным регистровым контекстом.
Начинающих хакеров обращение к регистру GS дробит на части, сваливая в вертикальный штопор. Короче, это как обухом по голове. Или серпом по яйцам (естественно, для тех, у кого они есть, а девушки среди хакеров нет-нет, да и встречаются). Кстати, насчет девушек. Ольга (в отличие от Айса) не показывает значения сегментных регистров, чем серьезно осложняет ситуацию.
Опытных реверсеров таким макаром уже не проведешь, однако никаких гарантий, что GS в данный момент содержит именно FS, а не DS, например, у нас нет, а потому статический анализ становится неоднозначным и требует реконструкции последовательности вызываемых функций. Причем обращения к FS в явном виде может и не быть - его значение легко прочитать API-функцией GetThreadContext, на которую, конечно, легко поставить точку останова, но точки останова - это уже динамический, а не статический анализ!
Самое интересное - блок окружения потока, засунутый в селектор, хранящийся в сегментном регистре FS, отображается на плоское адресное пространство и потому доступен для чтения через остальные селекторы, например, через сегментный регистр DS. На W2K блок окружения первичного потока начинается с адреса 7FFDВ000h и 7FFDE000h на XP, поэтому (не без риска, конечно) вместо FS:[0] допустимо использовать конструкцию DS:[7FFDB000h], а чтобы избежать краха, отталкиваться от того факта, что в настоящем блоке окружения потока по смещению 30h байт от его начала расположен указатель на блок окружения процесса, лежаший на 1000h байт ниже, благодаря чему мы можем найти указатель на SEH-обработчик даже на неизвестной операционной системе.
Конечно, реализация алгоритма существенно усложняется, но это даже хорошо, поскольку чем больше строк кода - тем дольше их будет анализировать хакер, тем более, если эти строки бессмысленны сами по себе.
Листинг 3. Поиск блока окружения потока в стеке.
Во-первых, мы обошлись без ассемблерных вставок, реализовав алгоритм на чистом Си (с тем же успехом можно использовать Паскаль), во-вторых, вместо характерного "FS" в программе появилась куча констант, смысл которых понятен только посвященным, да и то не без пристального анализа, сопровождаемого глубокой медитацией. В-третьих, факт передачи управления на функцию souriz по return *p (где p == 0) совершенно неочевиден, к тому же сам указатель на souriz можно зашифровать, помешав дизассемблерам реконструировать перекрестные ссылки. Как это сделать на Си (без ассемблерных вставок) описывалось в 30-м выпуске С-шных трюков.
Существуют и другие способы поиска указателя на блок окружения потока. Рассмотрим только два самых популярных из них. Просматривая карту памяти (а просмотреть ее можно с помощью 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 можно по любому обойтись.
Рисунок 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, а потому его можно использовать без особых опасений.
Практический пример приведен ниже:
Листинг 4. Прямой поиск указателя на SEH-обработчик в стеке.
Точка останова на FS:0 на этот раз идет лесом и не срабатывает, поскольку обращение к этой ячейки памяти уже не происходит. К тому же разобраться, что именно ищет программа в стеке, можно после серии экспериментов (ну, или чтения этой статьи). Впрочем, способов поиска системного обработчика исключений намного больше одного, что существенно усложняет задачу хакера и универсальных "отмычек" тут нет, что в плане защиты очень даже хорошо, однако просмотр цепочки обработчиков структурных исключений (в Ольге это осуществляется через меню View -> SEH Chain) немедленно разоблачает хакнутый обработчик, на который несложно установить точку останова на исполнение со всеми вытекающими отсюда...
Рисунок 4. Просмотр SEH-цепочек в Ольге.
API-функция SetUnhandledExceptionFilter, как уже отмечалось в предыдущих выпусках, сама по себе представляет проблему для отладчиков, поскольку установленный ею фильтр исключений верхнего уровня при запуске программы под отладчиком не выполняется, и приходится использовать разнообразные плагины для Ольги, чтобы заставить систему считать, что никакого отладчика здесь нет или же, как вариант, насильственно включать фильтр верхнего уровня в цепочку обработчиков структурных исключений.
Самый большой недостаток функции SetUnhandledExceptionFilter в том, что ее вызов очень трудно замаскировать, но трудно - еще не значит невозможно. К тому же, реализация функции проста как движок от запора. Фактически, она всего лишь устанавливает глобальную переменную BasepCurrentTopLevelFilter, хранящуюся внутри KERNEL32.DLL и использующуюся только функцией UnhandledExceptionFilter.
.text:7945BC45 _SetUnhandledExceptionFilter@4 proc near .text:7945BC45 .text:7945BC45 lpTopLevelExceptionFilter = dword ptr 4 .text:7945BC45 .text:7945BC45 8B 4C 24 04 mov ecx, [esp+lpTopLevelExceptionFilter] .text:7945BC49 A1 F0 A1 48 79 mov eax, _BasepCurrentTopLevelFilter .text:7945BC4E 89 0D F0 A1 48 79 mov _BasepCurrentTopLevelFilter, ecx .text:7945BC54 C2 04 00 retn 4 .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 __XcptFilter proc near .text:00401C9A .text:00401C9A arg_0 = dword ptr 8 .text:00401C9A ExceptionInfo = dword ptr 0Ch .text:00401C9A .text:00401C9A push ebp .text:00401C9B mov ebp, esp .text:00401C9D push ebx .text:00401C9E push [ebp+arg_0] .text:00401CA1 call _xcptlookup ; _xcptlookup -> my_invisible_seh .text:00401CA6 test eax, eax
Листинг 6. Дизассемблерный фрагмент библиотечной функции __XcptFilter.
Обнаружить наш обработчик исключений практически невозможно. Он отсутствует в SEH-цепочке (точнее, присутствует, но прячется внутри обработчика, устанавливаемого RTL языка высокого уровня) и Ольга в упор его не видит. Конечно, при пошаговой трассировке хакерский обработчик будет выявлен, вот только трассировать мегабайты системного и библиотечного кода никто не будет. Дизассемблирование также не покажет ничего подозрительного, поскольку IDA-Pro не проверяет целостность библиотечных функций, и никто из хакеров не тратит время на их анализ, а потому предложенный прием оказывается весьма живучим в плане взлома.