Как противостоять контрольным точкам останова
Контрольные точки, установленные на важнейшие системные функции, – мощное оружие в руках взломщика. Путь, к примеру, защита пытается открыть ключевой файл. Под Windows существует только один документированный способ это сделать – вызвать функцию CreateFile (точнее CreateFileA или CreateFileW для ASCII и UNICODE-имени файла соответственно). Все остальные функции, наподобие OpenFile, доставшиеся в наследство от ранних версий Windows, на самом деле представляют собой переходники к CreateFile.
Зная об этом, взломщик может заблаговременно установить точку останова на адрес начала этой функции (благо он ему известен) и мгновенно локализовать защитный код, вызывающий эту функцию, ну а остальное, как говорится, дело техники.
Но не всякий взломщик осведомлен, что открыть файл можно и другим путем – вызвать функцию ZwCreateFile (равно как и NtCreateFile), экспортируемую NTDLL.DLL, или обратится напрямую к ядру вызовом прерывания INT 0x2Eh. Сказанное справедливо не только для CreateFile, но и для всех остальных функций ядра. Причем для этого не нужны никакие привилегии, и такой вызов можно осуществить даже из прикладного кода!
Опытного взломщика, такой трюк надолго не остановит, но почему бы ему ни приготовить один маленький сюрприз, поместив вызов INT 0x2E в блок __try. Это приведет к тому, что управление получит не ядро системы, а обработчик данного исключения, находящийся за блоком _try. Взломщик же, не имеющий исходных текстов, не сможет быстро определить: относится ли данный вызов к блоку _try или нет. Отсюда: он может быть легко введен в заблуждение – достаточно имитировать открытие файла, не выполняя его на самом деле! Кроме того, ничего не мешает использовать прерывание INT 0x2E для взаимодействия компонентов свой программы – взломщику будет очень не просто отличить какой вызов пользовательский, а какой системный.
Хорошо, с ядром все понятно, а как же быть с функциями модулей USER и GDI, например, GetWindowsText, использующейся для считывания введенной пользователем ключевой информации (как правило, серийного номера или пароля)? На помощь приходит то обстоятельство, что практически все эти функции начинаются с инструкций PUSH EBP\MOV EBP,ESP, которые прикладной код может выполнить и самостоятельно, передав управление не на начало функции, а на три байта ниже. (Поскольку PUSH EBP изменяет стек, приходится прибегать к передаче управления посредством JMP вместо CALL).
Контрольная точка, установленная взломщиком на начало функции, не возымеет никакого действия! Такой трюк может сбить с толку даже опытного хакера, хотя рано или поздно он все равно раскусит обман, но…
Если есть желание окончательно отравить взломщику жизнь, следует скопировать системную функцию в свой собственный стек и передать на него управление – контрольные точки взломщика "отдыхают"! Основная сложность заключается в необходимости распознания всех инструкций с относительными адресными аргументами и их соответствующей коррекции. Например, двойное слово, стоящее после инструкции CALL, представляет собой не адрес перехода, а разность целевого адреса и адреса следующей за CALL инструкции. Перенос инструкции CALL на новое место потребует коррекции ее аргумента. Впрочем, эта задача не так сложна, как может показаться на первый взгляд (глаза страшатся, а руки делают), и результат оправдывает средства – во-первых, при каждом запуске функции можно произвольным образом менять ее адрес, во-вторых, проверкой целости кода легко обнаружить программные точки останова – а аппаратных точек на все вызовы просто не хватит!
Разве ж не заслуживают награды за свою целеустремленность те единицы, которую такую защиту взломают?! (Под наградой здесь подразумевается отнюдь не сама взломанная программа, а глубокое чувство удовлетворения от того, что "я это сделал!").
Еще легче противостоять аппаратным точкам останова на память – поскольку их всего четыре и каждая может контролировать не более двойного слова, взломщик может одновременно контролировать не более 16 байт памяти. Если же обращения к буферам, содержащим ключевую информацию, будут происходить не последовательно байт за байтам от начала до конца, а произвольно, и количество самих буферов окажется больше четырех, отследить все операции чтения-записи в них станет невозможно.
Некоторые отладчики поддерживают возможность установки точки останова на диапазон памяти, но ее функциональность вызывает большие сомнения – единственный способ контролировать целый регион – трассировать исследуемую программу, проверяя, не обращается ли очередная команда к охраняемому диапазону и если да, – генерировать исключение.
Во-первых, команд, манипулирующих с памятью очень много, и можно придумать самые неожиданные комбинации – например, установить указатель стека на требуемую ячейку памяти и вызвать RET для чтения содержащегося в ней значения. Во-вторых, возникшее при этом исключение, может служить хорошим средством избавления от трассировщка (см. раздел "Как противостоять трассировке").
Таким образом, справится с контрольными точками, защитному механизму совсем не трудно!
Точка останова представляет собой однобайтовую команду 0xCC, генерирующую исключение 0x3 при попытке ее выполнения (в просторечии "дергающие отладочным прерыванием"). Обработчик INT 0x3 получает управление и может делать с программой абсолютно все, что ему заблагорассудится, но прежде – до вызова прерывания – в стек заносятся текущие регистр флагов, указатель кодового сегмента (регистр CS), указатель команд (регистр IP), запрещаются прерывания (очищается флаг IF) и сбрасывается флаг трассировки – словом, вызов отладочного прерывания не отличатся от вызова любого прерывания вообще. (см. рис)
Чтобы узнать в какой точке программы произошел останов, отладчик извлекает из стека сохраненное значение регистров, не забывая о том, – CS:IP указывают на следующую выполняемую команду.
Рисунок 37 0x005 Состояние стека на момент входа в обработчик прерывания
Условно точки останова (называемые так же контрольными точками) можно разделить на две категории: точки останова жестко прописанные в программе самим разработчиком и точки динамические устанавливаемые самим отладчиком. Ну, с первыми все ясно – хочешь остановить программу и передать управление отладчику в там-то месте – пишешь __asm{ int 0x3} и – надевай тигра Шляпу!
Несколько сложнее установить точку в произвольное место программы – сначала отладчик должен сохранить текущее значение ячейки памяти по указанному адресу, затем записать сюда код 0xCC, а перед выходом из отладочного прерывания вернуть все на место и модифицировать сохраненный в стеке IP, для перемещения его на начало восстановленной команды (иначе, он будет указывать на ее середину).
Какими недостатками обладает механизм точек останова 8086-процессора? Первое, и самое неприятное, состоит в том, что точка устанавливая точку останова, отладчик вынужден непосредственно модифицировать код. Отлаживая программа тривиальной проверкой собственной целостности может легко обнаружить факт отладки и даже удалить точку останова! Не стоит использовать конструкции наподобие if (CalculateMyCRC()!=MyValidCRC) {printf("Hello, Hacker!\n");return;} их слишком легко обнаружить и нейтрализовать, подправив условный переход так, чтобы он всегда передавал управление нужной ветке программы. Лучше расшифровывать полученным значением контрольной суммы критические данные или некоторый код.
Простейшая защита может выглядеть, например, так (только не удивляйтесь откуда взялись 32-разрядные регистры в процессоре 8086 – пример, разумеется, предназначен для 386+, сохранившего точки останова от своего предшественника, причем их активно используют не только прикладные отладчики, но даже… сам Айс!):
int main(int argc, char* argv[])
{
// зашифрованная
строка Hello, Free World!
char s0[]="\x0C\x21\x28\x28\x2B\x68\x64\x02\x36\
\x21\x21\x64\x13\x2B\x36\x28\x20\x65\x49\x4E";
__asm
{
BeginCode: ; //начало контролируемого кода
pusha ; //сохранение всех регистров общего назначения
lea ebx,s0 ; // ebx=&s0[0]
GetNextChar: ; // do
XOR eax,eax ; // eax = 0;
LEA esi,BeginCode;// esi = &BeginCode
LEA ecx,EndCode ; // выислиление длины...
SUB ecx,esi ; // ...контролируемого кода
HarvestCRC: ; // do
LODSB ; // загрузка очередного байта в al
ADD eax,eax ; // выисление контрольной суммы
LOOP HarvestCRC ; // until(--cx>0)
xor [ebx],ah ; // расшифровка очередного символа s0
inc ebx ; // указатель на след. симв.
cmp [ebx],0 ; // until (пока не конец строки)
jnz GetNextChar ; // продолжить расшифровку
popa ; // восстановить все регистры
EndCode: ; // конец контролируемого кода
NOP ; // Safe BreakPoint here
}
printf(s0); // вывод строки на экран
return
0;
}
Листинг 224
При нормальном запуске на экране должна появиться строка "Hello, Free World!", но при прогоне под отладчиком при наличии хотя бы одной точки останова, установленной в пределах от BeginCode до EndCode на экране появится бессмысленный мусор наподобие: "Jgnnm."Dpgg"Umpnf#0"
Причем, Soft-Ice неявно помещает точку останова в начало каждой следующей команды при трассировке программы по Step Over (<F10>)! Разумеется, это искажает контрольную сумму, чем и пользуются защита.
Самое простое решение проблемы - положить кирпич на клавишу <F8> (покомандная трассировка) и идти пить чай, пока программа будет расшифровываться. Шутка, конечно. А если говорить серьезно, то необходимо вспомнить в каком веке мы живем и, отбросив каменные топоры, установить аппаратную точку останова (см. "Приемы против отладчиков защищенного режима"). {>>>>> сноска Кстати, значительно усилить защиту можно, если поместить процедуру подсчета контрольной суммы в отдельный поток, занимающийся (для сокрытия свой деятельности) еще чем-нибудь полезным так, чтобы защитный механизм по возможности не бросался в глаза.}
Наши же предки (хакеры восьмидесятых) в этой ситуации обычно вручную расшифровывали программу, а затем затирали процедуру расшифровки NOP-ми, после чего отладка программы уже не представляла проблемы (естественно, если в защите не было других нычек). До появления IDA расшифровщик приходилось писать на Си (Паскале, Бацике) в виде самостоятельной программы, теперь же эта задача упростилась, и заниматься расшифровкой стало можно непосредственно в самом дизассемблере.
Техника расшифровки сводится к воспроизведению расшифровщика на языке IDA-Си – в данном случае сначала необходимо вычислить контрольную сумму от BginCode до EndCode подчитывая сумму байтов, используя при этом младший байт контрольной суммы для загрузки следующего символа, а затем полученным значением "поксорить" строку s0.
Все это можно сделать следующим скриптом (предполагается, что в дизассемблированном тексте соответствующие метки уже расставлены):
auto a; auto p; auto crc; auto ch;
for (p=LocByName("s0");Byte(p)!=0;p++)
{
crc=0;
for(a=LocByName("BeginCode");a<(LocByName("EndCode"));a++)
{
ch=Byte(a);
// Поскольку IDA не поддерживает типов byte и word
// (а напрасно) приходится заниматься битовыми
// выкрутасами – сначала очищать младший байт crc,
// а затем копировать в него считанное значение ch
crc = crc & 0xFFFFFF00;
crc = crc | ch;
crc=crc+crc;
}
// Берем старший байт от crc
crc=crc & 0xFFFF;
crc=crc / 0x100;
// Расшифровываем очередной байт строки
PatchByte(p,Byte(p) ^ crc);
}
Листинг 225
Если под рукой нет IDA, эту же операцию можно осуществить и в HIEW-е:
NoTrace.exe vW PE 00001040 a32 <Editor> 28672 ? Hiew 6.04 (c)SEN 00401003: 83EC18 sub esp,018 ;"^" 00401006: 53 push ebx 00401007: 56 push esi 00401008: 57 push edi 00401009: B905000000 000005 ;" ¦" 0040100E: BE30604000 г=[Byte/Forward ] =============¬ 406030 ;" @`0" 00401013: 8D7DE8 ¦ 1>mov bl,al ¦ AX=0061 ¦p][-0018] 00401016: F3A5 ¦ 2 add ebx,ebx ¦ BX=44C2 ¦гнать 00401018: A4 ¦ 3 ¦ CX=0000 ¦отсюда-> 00401019: 6660 ¦ 4 ¦ DX=0000 ¦ 0040101B: 8D9DE8FFFF ¦ 5 ¦ SI=0000 ¦ [0FFFFFFE8] 00401021: 33C0 ¦ 6 ¦ DI=0000 ¦.0040101B: 8D9DE8FFFFFF L==============================-.00401021: 33C0 xor eax,eax.00401023: 8D3519104000 lea esi,[000401019] ; < BeginCode.00401029: 8D0D40104000 lea ecx,[000401040] ; < EndCode.0040102F: 2BCE sub ecx,esi.00401031: AC lodsb 00401032: 03C0 add eax,eax 00401034: E2FB loop 000001031 00401036: 3023 xor [ebx],ah 00401038: 43 inc ebx 00401039: 803B00 cmp b,[ebx],000 ;" " 0040103C: 75E3 jne 000001021 0040103E: 6661 popaдосюда-> 00401040: 90 nop 00401041: 8D45E8 lea eax,[ebp][-0018] 00401044: 50 push eax 00401045: E80C000000 call 000001056 0040104A: 83C404 add esp,004 ;"¦"1Help 2Size 3Direct 4Clear 5ClrReg 6 7Exit 8 9Store 10Load
На первой стадии производится подсчет контрольной суммы. Загрузив файл в HIEW, находим нужный фрагмент (<ENTER>, <ENTER> для перехода в режим ассемблера и <F8>, <F5> для прыжка в точку входа, далее находим в стартовом коде процедуру main), нажимаем <F3> для разрешение правки файла, вызываем редактор скрипта-расшифровщика (<CTRL-F7>, впрочем, эта комбинация варьируется от версии к версии) и вводим следующий код:
mov bl, al
add ebx, ebx
Вместо EBX можно использовать и другой регистр, но не EAX – HIEW, считывая очередной байт обнуляет EAX целиком. Теперь установим курсор на строку 0x401019 и, нажимая <F7>, погоним расшифрошик до строки 0x401040, не включая последнюю. Если все сделано правильно в старшем байте BX должно находится значение 0x44, - это и есть контрольная сумма.
На второй стадии находим шифрованную строку (ее смещение грузится в ESI и равно .406030) и ксорим ее по 0x44. (Нажимаем <F3> для перехода в режим правки, <CTRL-F8> для задания ключа шифрования – 0x44, а затем ведем расшифровщик по строке, нажимая <F8>)
NoTrace.exe vW PE 00006040 <Editor> 28672 ? Hiew 6.04 (c)SEN
00006030: 48 65 6C 6C-6F 2C 20 46-72 65 65 20-57 6F 72 6C Hello, Free Worl
00006040: 20 65 49 4E-00 00 00 00-7A 1B 40 00-01 00 00 00 eIN z<@ O
Остается лишь забить NOP-ми XOR в строке 0x401036, иначе при запуске программы он испортит расшифрованный текст (зашифрует его вновь) и программа, работать, естественно не будет.
Теперь, после снятия защиты, ее можно безболезненно отлаживать сколько душе угодно – да, контрольная сумма по-прежнему считается, но теперь она не используется (если бы в защите была проверка на корректность CRC, пришлось бы нейтрализовать и ее, но в этом примере для упрощения понимания ничего подобного нет).