Различия

Здесь показаны различия между двумя версиями данной страницы.

Ссылка на это сравнение

articles:anti-debug-07 [2017/09/05 02:55] (текущий)
Строка 1: Строка 1:
 +====== anti-debug-07 ======
 +<​sub>​{{anti-debug-07.odt|Original file}}</​sub>​
 +
 +====== энциклопедия антиотладочных приемов —\\ скрытая установка 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] по доступу продолжает работать,​ а потому для эффективного противодействия хакеру требуются дополнительные уровни маскировки.
 +
 +Ну, и чего мы сидим? Вперед!
 +
 +{{anti-debug-07_Image_0.png?​400}}
 +
 +Рисунок 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 (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"​);​
 +
 +Листинг 3 поиск блока окружения потока в стеке
 +
 +Во-первых,​ мы обошлись без ассемблерных вставок,​ реализовав алгоритм на чистом Си (с тем же успехом можно использовать Паскаль),​ во-вторых,​ вместо характерного "​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 можно по любому обойтись.
 +
 +{{anti-debug-07_Image_1.png?​553}}
 +
 +Рисунок 2 блок окружения потока на карте памяти процесса
 +
 +Указатель на блок окружения потока так же находится в стеке потока,​ куда его кладет операционная система. В W2K/XP это третье двойное слово от вершины. И хотя в последующих версиях его местоположение может измениться,​ вирусов это обстоятельство походе никак не заботит и они используют его сплошь и рядом.
 +
 +И что в итоге? Мы рассмотрели множество приемов скрытного обращения к ячейке FS:0, однако,​ все они действуют только против дизассемблеров,​ а отладчики просто ставят сюда точку останова по доступу и _все_ обращения к FS:0 немедленно палятся — независимо от того какой адрес используется смещение 0 по селектору FS или же смещение 7FFDВ000h по селектору DS.
 +
 +Непорядок! Хорошая защита должна справляется не только с дизассемблерами,​ но и с отладчиками!
 +
 +===== кража чужих обработчиков =====
 +
 +Системный обработчик структурных исключений расположен на дне стека потока и обращаться к блоку окружения для его поисков совсем необязательно. Поскольку,​ местоположение обработчика непостоянно и зависит от версии операционной системы мы должны выработать эвристический алгоритм поиска.
 +
 +Системный обработчик,​ назначаемый по умолчанию,​ есть ни что иное как функция __except_handler3,​ расположенная в недрах KERNEL32.DLL и _не_ экспортируемая наружу,​ однако,​ присутствующая в отладочных символах,​ которые теоретически можно в любой момент скачать с серверов Microsoft, но практически такое решение будет слишком громоздким,​ неудобным,​ ненадежным,​ да и довольно "​прозрачным"​ для хакера.
 +
 +{{anti-debug-07_Image_2.png?​552}}
 +
 +Рисунок 3 указатель на системный SEH-обработчик лежит на дне стека потока
 +
 +Хорошо будем отталкиваться от того, что __except_handler3 смотрит в KERNEL32.DLL и что перед ним всегда расположено двойное слово FFFFFFFFh, а после него — указатель на секцию данных KERNEL32.DLL опять-таки содержащий в себе двойное слово FFFFFFFFh. Последнее обстоятельство системнозависимо,​ однако,​ они справедливо как для W2K, так и для XP, а потому его можно использовать без особых опасений.
 +
 +Практический пример приведен ниже:
 +
 +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"​);​
 +
 +Листинг 4 прямой поиск указателя на SEH-обработчик в стеке
 +
 +Точка останова на FS:0 на этот раз идет лесом и не срабатывает,​ поскольку,​ обращение к этой ячейки памяти уже не происходит. К тому же разобраться что именно ищет программа в стеке можно после серии экспериментов (ну или чтения этой статьи ;-). Впрочем,​ способов поиска системного обработчика исключений намного больше одного,​ что существенно усложняет задачу хакера и универсальных "​отмычек"​ тут нет, что в плане защиты очень даже хорошо,​ однако,​ просмотр цепочки обработчиков структурных исключений (в Ольге это осуществляется через меню View -> SEH Chain) немедленно разоблачает хакнутый обработчик на который несложно установить точку останова на исполнение со всеми вытекающими отсюда…
 +
 +{{anti-debug-07_Image_3.png?​553}}
 +
 +Рисунок 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 функции какой хэш соответствует.
 +
 +{{anti-debug-07_Image_4.png?​553}}
 +
 +Рисунок 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 не проверяет целостность библиотечных функций,​ и никто из хакеров не тратит время на их анализ,​ а потому предложенный прием оказывается весьма живучим в плане взлома.
 +
 +