Энциклопедия антиотладочных приемовСколько раз начинающие реверсеры спотыкались о настоящую банальщину — не сосчитать. Набив шишки, многие набрались опыта, взялись за виртуализированный код и потеряли спортивный интерес к антиотладке. Наша с тобой задача — попытаться взглянуть на известные приемы антиотладки под новым углом. Когда уже нечего больше искать, когда испробовано все — приходится копать вглубь, а не вширь, модифицируя существующие антиотладочные приемы самым неожиданным образом. Когда трассировка бессмысленнаОтладчики прикладного уровня хороши тем, что с ними можно вытворять все, что душе угодно. К примеру, можно уйти от трассировки инструкции путем изменения регистра сегмента стека. Звучит неправдоподобно, однако это действительно так. Рассмотрим конструкцию:
Если, трассируя такой код в отладчике прикладного уровня, мы сделаем два шага, то… остановимся по адресу ADDRESS+2? Не тут-то было! Инструкция, следующая за POP SS, выполнится автоматически, после чего мы окажемся по адресу ADDRESS+5. Каким образом это можно использовать для выполнения антиотладки? В принципе, выполнение одной-единственной инструкции ничего интересного не дает. Может быть, ты надеешься на call, но я поспешу тебя разочаровать: к сожалению, хотя инструкция вызова и выполнится, отладчик, скорее всего, установит точку останова на первую инструкцию вызываемой функции, и реверсер узрит все защитные «потроха». Необходимо придумать что-то более изощренное. Я предлагаю взять любой известный антиотладочный прием (например, чтение флага — выдает все отладчики прикладного уровня, на которые не навешаны специальные плагины) и «скрестить» его с антитрассировочным трюком так, чтобы инструкция «POP SS» располагалась непосредственно перед условным переходом, который решает судьбу дальнейшего выполнения программы. Следующий код (он был прикручен к виндовому «Блокноту» при помощи OllyDbg) наглядно демонстрирует эту особенность:
Разумеется, вместо банальной проверки флагов можно использовать другой, более неоднозначный код. Пошаговая трассировка вплоть до адреса 01008759 оканчивается неудачей. Разбавленная мусорным кодом, «фишка» способна заставить задуматься не слишком опытного реверсера. Метод хорош тем, что универсален и может применяться для выполнения критически важных защитных инструкций. Возможно, бывалого реверс-инженера это не остановит. С другой стороны, несколько десятков пар инструкций «push ss/pop ss», равномерно разбросанных в недрах защитного кода, заставят взломщика находиться в постоянном напряжении. Сгодится использование приема и для сокрытия вызовов, ведущих «в никуда», если защита пускает программу «лесом», обнаружив отладчик. JMP-ы, которые указывают на несуществующие адреса памяти, достаточно часто принимаются не самыми умными дизассемблерами отладочных средств за байты данных (в чем мы убедимся далее). И именно поэтому можно укрыть переход при помощи рассмотренного приема. Новичок будет недоумевать: инструкция «POP SS», которая меняет EIP, убивая программу — нечто невообразимое! Крадем таймер из kernel32.dllВсем, наверное, известен старый антиотладочный прием, основанный на использовании API-функции GetTickCount, которая входит в библиотеку kernel32.dll. Выглядит он приблизительно, как представлено ниже:
MSDN описывает функцию так: «The return value is the number of milliseconds that have elapsed since the system was started». Следовательно, приведенный код подсчитывает время выполнения одной инструкции и, если оно превышает заданное значение (миллисекунду), процесс завершается, поскольку программа выполняется под отладчиком. Новое — это хорошо забытое старое. Мы можем попробовать разнообразить жизнь реверсера, немного изменив принцип действия этого приема. Откроем под отладчиком DLL-библиотеку Kernel32.
Если вызов функции GetTickCount фигурирует в коде, сразу становится ясно: используется таймер. Это вызывает справедливые подозрения в антиотладке. Чтобы запутать реверсера, часто используется методика переноса кода API-функций в тело программы. Ей мы и воспользуемся. Попробуем перенести рассмотренную нами функцию в конец секции кода «Блокнота», по адресу 0100876F (адрес может быть любым — секция выравнивания, оставленная компилятором, достаточно велика), после чего напишем антиотладочный код:
После того, как код написан и занесен в PEфайл, остается лишь поменять точку входа файла в LordPE, заменив ее на 01008748. Файл, обработанный таким образом, отказывается работать под отладчиком. Результат анализа jnb-а, располагающегося по адресу 01008758, выглядит так:
Это — следствие того, что операнд условного перехода указывает на недоступный адрес памяти. Дизассемблер отладчика OllyDbg полагает, что такого не может случиться даже теоретически, и превращает инструкцию в набор данных. В прятки с дизассемблером, или упаковываем mov в xorПоговорим о еще одном способе усложнить жизнь реверсера. Выше было сказано, что усложнить работу реверсера можно путем переноса узнаваемых API-функций (вроде GetTickCount) в тело защищаемой программы. Попробуем создать «странный» код, который, на первый взгляд, кажется бессмысленным, но выполняет вполне осязаемые антиотладочные действия. Ситуацию рассмотрим ту же, что и в предыдущем случае — перенесение API-функции GetTickCount по адресу 0100876F. Представь, что реверсер достаточно опытен и знает все типовые API-функции вроде GetTickCount, которые используются в простейших антиотладочных приемах, «в лицо». Можно ли модифицировать перенесенный код так, чтобы он ничем себя не выдавал? Как вариант, — разбавить его инструкциями вроде «mov ax, ax». Будет ли это для реверс-инженера трудной задачей? Вряд ли. Более предпочтительна замена прямых адресов, используемых в коде, на подсчитываемые значения, однако есть другое элегантное решение, которое заставит вскипеть мозги реверсера. Как ты, вероятно, заметил, инструкция, располагающаяся по адресу 01008774, выглядит следующим образом:
Опкод инструкции — 0x8b02. Что произойдет, если спрятать одну инструкцию внутри другой? Наглядный пример: опкод 0x358B029090 имеет инструкция XOR EAX,9090028B. Таким образом, если расположить эту инструкцию по некоторому адресу ADDRESS, после чего передать при помощи безусловного перехода управление на адрес (ADDRESS+1), то выполнится совсем не XOR, а набор инструкций:
Поскольку первый байт отброшен, инструкция не воспринимается процессором как XOR, а последние 2 байта последовательности (0х9090) будут интерпретироваться как NOP-ы! Вот как реализовать это на практике для изменения кода перенесенной нами API-функции GetTickCount:
Код нуждается в дополнительных комментариях. Поскольку встроенные дизассемблеры отладчиков прикладного уровня наподобие OllyDbg анализируют теоретическую возможность выполнения определенных инструкций, они могут интерпретировать набор байтов, расположенных по некоторому адресу, либо как данные, либо как код. Если бы приведенный код не содержал инструкцию JE SHORT 01008776, дизассемблер принял бы решение, что инструкции XOR EAX,9090028B просто не существует, и выдал бы реверсеру всю подноготную нашего кода. К счастью, мы точно знаем, что в контексте данного куска кода инструкция JE SHORT 01008776 не передаст управление по адресу 01008776. Результат подобного «шаманизма» — достаточно запутанный антиотладочный код, который, тем не менее, работает! |