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

Энциклопедия антиотладочных приемов


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

Когда трассировка бессмысленна

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

ADDRESS : PUSH SS
ADDRESS+1: POP SS
ADDRESS+2: MOV AX, AX
ADDRESS+5: NOP

Если, трассируя такой код в отладчике прикладного уровня, мы сделаем два шага, то… остановимся по адресу ADDRESS+2? Не тут-то было! Инструкция, следующая за POP SS, выполнится автоматически, после чего мы окажемся по адресу ADDRESS+5. Каким образом это можно использовать для выполнения антиотладки?

В принципе, выполнение одной-единственной инструкции ничего интересного не дает. Может быть, ты надеешься на call, но я поспешу тебя разочаровать: к сожалению, хотя инструкция вызова и выполнится, отладчик, скорее всего, установит точку останова на первую инструкцию вызываемой функции, и реверсер узрит все защитные «потроха». Необходимо придумать что-то более изощренное. Я предлагаю взять любой известный антиотладочный прием (например, чтение флага — выдает все отладчики прикладного уровня, на которые не навешаны специальные плагины) и «скрестить» его с антитрассировочным трюком так, чтобы инструкция «POP SS» располагалась непосредственно перед условным переходом, который решает судьбу дальнейшего выполнения программы. Следующий код (он был прикручен к виндовому «Блокноту» при помощи OllyDbg) наглядно демонстрирует эту особенность:

01008748 PUSH SS
; проверяем, имеется ли в наличии отладчик прикладного уровня
01008749 MOV EAX,DWORD PTR
FS:[18]
0100874F MOV EAX,DWORD PTR
DS:[EAX+30]
01008752 MOVZX EAX,BYTE PTR
DS:[EAX+2]
01008756 TEST AL,AL
; выполняем модификацию регистра SS и "пролетаем" выполнение инструкции
; по адресу 01008759, вследствие чего приложение завершает работу
01008758 POP SS
01008759 JNZ SHORT 01008777 ;
переходим "в пустоту"
0100875B JMP 0100739D ; переходим на точку входа notepad.exe

Разумеется, вместо банальной проверки флагов можно использовать другой, более неоднозначный код. Пошаговая трассировка вплоть до адреса 01008759 оканчивается неудачей. Разбавленная мусорным кодом, «фишка» способна заставить задуматься не слишком опытного реверсера. Метод хорош тем, что универсален и может применяться для выполнения критически важных защитных инструкций. Возможно, бывалого реверс-инженера это не остановит.

С другой стороны, несколько десятков пар инструкций «push ss/pop ss», равномерно разбросанных в недрах защитного кода, заставят взломщика находиться в постоянном напряжении. Сгодится использование приема и для сокрытия вызовов, ведущих «в никуда», если защита пускает программу «лесом», обнаружив отладчик. JMP-ы, которые указывают на несуществующие адреса памяти, достаточно часто принимаются не самыми умными дизассемблерами отладочных средств за байты данных (в чем мы убедимся далее). И именно поэтому можно укрыть переход при помощи рассмотренного приема. Новичок будет недоумевать: инструкция «POP SS», которая меняет EIP, убивая программу — нечто невообразимое!

Крадем таймер из kernel32.dll

Всем, наверное, известен старый антиотладочный прием, основанный на использовании API-функции GetTickCount, которая входит в библиотеку kernel32.dll. Выглядит он приблизительно, как представлено ниже:

call GetTickCount
xchg ebx, eax
call GetTickCount
sub eax, ebx
cmp eax, 1
jnb debugged
...
debugged: call ExitProcess

MSDN описывает функцию так: «The return value is the number of milliseconds that have elapsed since the system was started». Следовательно, приведенный код подсчитывает время выполнения одной инструкции и, если оно превышает заданное значение (миллисекунду), процесс завершается, поскольку программа выполняется под отладчиком. Новое — это хорошо забытое старое. Мы можем попробовать разнообразить жизнь реверсера, немного изменив принцип действия этого приема. Откроем под отладчиком DLL-библиотеку Kernel32.

dll и посмотрим на код GetTickCount:
7C80934A > BA 0000FE7F
MOV EDX,7FFE0000
7C80934F 8B02
MOV EAX,DWORD PTR DS:[EDX]
7C809351 F762 04
MUL DWORD PTR DS:[EDX+4]
7C809354 0FACD0 18
SHRD EAX,EDX,18
7C809358 C3 RETN

Если вызов функции GetTickCount фигурирует в коде, сразу становится ясно: используется таймер. Это вызывает справедливые подозрения в антиотладке. Чтобы запутать реверсера, часто используется методика переноса кода API-функций в тело программы. Ей мы и воспользуемся. Попробуем перенести рассмотренную нами функцию в конец секции кода «Блокнота», по адресу 0100876F (адрес может быть любым — секция выравнивания, оставленная компилятором, достаточно велика), после чего напишем антиотладочный код:

01008748 CALL 0100876F; вызываем "наш" GetTickCount
0100874D XCHG EAX,EBX
0100874E CALL 0100876F; снова вызываем "наш" GetTickCount
01008753 SUB EAX,EBX
01008755 CMP EAX,1
01008758 JNB 02000000; переходим к заведомо недоступному адресу
0100875E JMP 0100739D

...
; перенесенная функция GetTickCount:
0100876F MOV EDX,7FFE0000
01008774 MOV EAX,DWORD PTR
DS:[EDX]
01008776 MUL DWORD PTR
DS:[EDX+4]
01008779 SHRD EAX,EDX,18
0100877D RETN

После того, как код написан и занесен в PEфайл, остается лишь поменять точку входа файла в LordPE, заменив ее на 01008748. Файл, обработанный таким образом, отказывается работать под отладчиком. Результат анализа jnb-а, располагающегося по адресу 01008758, выглядит так:

01008758 0F DB 0F
01008759 83 DB 83
0100875A A2 DB A2
0100875B 78 DB 78; CHAR
'x'
0100875C . FF00
INC DWORD PTR DS:[EAX]

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

В прятки с дизассемблером, или упаковываем mov в xor

Поговорим о еще одном способе усложнить жизнь реверсера. Выше было сказано, что усложнить работу реверсера можно путем переноса узнаваемых API-функций (вроде GetTickCount) в тело защищаемой программы. Попробуем создать «странный» код, который, на первый взгляд, кажется бессмысленным, но выполняет вполне осязаемые антиотладочные действия.

Ситуацию рассмотрим ту же, что и в предыдущем случае — перенесение API-функции GetTickCount по адресу 0100876F. Представь, что реверсер достаточно опытен и знает все типовые API-функции вроде GetTickCount, которые используются в простейших антиотладочных приемах, «в лицо». Можно ли модифицировать перенесенный код так, чтобы он ничем себя не выдавал? Как вариант, — разбавить его инструкциями вроде «mov ax, ax». Будет ли это для реверс-инженера трудной задачей? Вряд ли. Более предпочтительна замена прямых адресов, используемых в коде, на подсчитываемые значения, однако есть другое элегантное решение, которое заставит вскипеть мозги реверсера. Как ты, вероятно, заметил, инструкция, располагающаяся по адресу 01008774, выглядит следующим образом:

01008774 8B02
MOV EAX,DWORD PTR DS:[EDX]

Опкод инструкции — 0x8b02. Что произойдет, если спрятать одну инструкцию внутри другой? Наглядный пример: опкод 0x358B029090 имеет инструкция XOR EAX,9090028B. Таким образом, если расположить эту инструкцию по некоторому адресу ADDRESS, после чего передать при помощи безусловного перехода управление на адрес (ADDRESS+1), то выполнится совсем не XOR, а набор инструкций:

ADDRESS : DB 35
ADDRESS+1 : MOV EAX, DWORD PTR DS:[EDX]
ADDRESS+3 : NOP
ADDRESS+4 : NOP

Поскольку первый байт отброшен, инструкция не воспринимается процессором как XOR, а последние 2 байта последовательности (0х9090) будут интерпретироваться как NOP-ы! Вот как реализовать это на практике для изменения кода перенесенной нами API-функции GetTickCount:

0100876F BA 0000FE7F MOV EDX,7FFE0000
;при помощи следующего Jmp-а мы "проскакиваем" мимо фейкового XOR-а.
01008774 EB 07 JMP SHORT 0100877D
; следующая инструкция — XOR — не будет выполнена никогда!
01008776 35 8B029090
XOR EAX,9090028B
0100877B EB 04
JMP SHORT 01008781; переходим к дальнейшим действиям
; инструкция, расположенная ниже, ведет вовсе не к XOR-у,а на байт "ниже" — к инструкции MOV!
0100877D EB F8
JMP SHORT 01008777
; следующей инструкцией мы заставляем отладчик думать, что XOR все-таки используется
0100877F 74 F5
JE SHORT 01008776
01008781 F762 04
MUL DWORD PTR DS:[EDX+4]
01008784 0FACD0 18
SHRD EAX,EDX,18
01008788 C3
RETN

Код нуждается в дополнительных комментариях. Поскольку встроенные дизассемблеры отладчиков прикладного уровня наподобие OllyDbg анализируют теоретическую возможность выполнения определенных инструкций, они могут интерпретировать набор байтов, расположенных по некоторому адресу, либо как данные, либо как код. Если бы приведенный код не содержал инструкцию JE SHORT 01008776, дизассемблер принял бы решение, что инструкции XOR EAX,9090028B просто не существует, и выдал бы реверсеру всю подноготную нашего кода. К счастью, мы точно знаем, что в контексте данного куска кода инструкция JE SHORT 01008776 не передаст управление по адресу 01008776. Результат подобного «шаманизма» — достаточно запутанный антиотладочный код, который, тем не менее, работает!

Содержание