Техника и философия хакерских атак
Продолжение...
MFC-программистам нетрудно понять, как он работает. Если происходит запись файла, то edx становится равно единице, если чтение - то нулю. Именно на этом и построена защита. В оригинале это могло выглядеть приблизительно так:
void CCRACK10Doc::Serialize(CArchive& ar) { // CEditView contains an edit control which handles all serialization if (ar.IsStoring()) { AfxMessageBox("Это ограниченная версия. Пожалуйста, приобретайте полную"); return; } ((CEditView*)m_viewList.GetHead())->SerializeRaw(ar); }
Все, что требуется сделать для ее ликвидации, - это заменить условный переход на безусловный. Или в качестве альтернативного варианта удалить ret. Тогда защита по-прежнему будет "ругаться", но начнет записывать файлы. По отношению к разработчику это даже будет более честно. Пользователь получит необходимый ему сервис, однако, постоянно раздражаемый nag-screen-ом, он с ненулевой вероятностью может приобрести коммерческую версию. С другой стороны по отношению к пользователю это будет выглядеть издевательством со стороны кракера. Особенно если он начнет выводить в диалоговом окне свои копирайты.
Попробуем убрать ret, заменив его, скажем, на nop. казалось бы это не отразится на работоспособности программы. Однако, запустив программу и попытавшись сохранить файл, мы получаем до боли знакомый GPF - "программа выполнила некорректную операцию и будет завершена". В чем же дело? С первого взгляда этот вопрос нас ставит в тупик, поэтому воспользуемся отладчиком и внимательно потрассируем измененный фрагмент. Причина обнаруживается достаточно быстро. Функция AfxMessageBox не сохраняет регистров eax и ecx, а код, расположенный ниже, их использует, никак не предполагая, что их содержимое было изменено. Следовательно, забота о сохранении, точнее о написании соответствующего кода, ложится на плечи взломщика. Это нетрудно и даже не утомительно - добавить пару команд push и pop, но как-то неаккуратно выглядит. Действительно, между условным переходом и вызовом функции нет свободного пространства. Можно, конечно, сместить всю функцию немного вниз, для чего свободного места предостаточно, но, может быть, можно найти решение с изменением меньшего числа байт? В самом деле, если убрать not, а je заменить на jne, мы получим два байта - как раз столько, чтобы сохранить пару регистров. Однако это потребует коррекции точки перехода, поскольку команды push расположены "выше" ее. В итоге мы получим такой вариант:
.00401417: 50 push eax .00401418: 51 push ecx .00401419: F6C201 test dl,001 .0040141C: 750E jne .00040142C .0040141E: 6A00 push 000 .00401420: 6A00 push 000 .00401422: 6854404000 push 000404054 .00401427: E834070000 call .000401B60 .0040142C: 59 pop ecx .0040142D: 90 nop .0040142E: 90 nop .0040142F: 90 nop .00401430: 8B4130 mov eax,[ecx][00030] .00401433: 8B4808 mov ecx,[eax][00008] .00401436: E81F070000 call .000401B5A .0040143B: C20400 retn 00004
Удостовертесь, что он действительно работает! Однако это еще не предел, и существует множество более изящных решений. Попробуйте найти их: и вы получите истинное удовольствие.
Итак, мы проделали большой путь - научились не только снимать ограничения с программ, но и дописывать недостающий код. Конечно, все, о чем рассказано в этой главе, - это лишь начало еще большего пути, который открывается перед нами. Что он сулит? Модификация программ непосредственно в исполняемом коде не только заменой пары байт, но и внесением принципиальных изменений в код и добавлением новых возможностей - воистину великая вещь! Читатель, вероятно, понял, что для рассказа об этом не хватило бы и отдельной книги, не то что одной главы. Но и в этом случае от него потребовались бы собственные исследования и копания в коде.
Навыки хакера не возникают просто так. Это длительный и упорный труд. Порой он становится неинтересен и скучен. Но когда-то приходится делать и такую работу, чтобы потом можно было действовать автоматически.
Ключевой файл
Настал черед рассмотреть и ключевые файлы. Обычно это самая сложная защита из всех вышеизложенных, поскольку может сочетать в себе как мощную шифровку, так и недостающие фрагменты кода. Но именно МОЖЕТ: далеко не каждый разработчик защиты берет ее на вооружение. Нередко бывает и обратное: защитный механизм, основанный на ключевом файле, поддается взлому так же легко, как генератор регистрационных номеров (просто происходит чтение из файла, а не из консоли).
Можно рассмотреть два типа атаки. В первом хакер воссоздает ключевой файл, не затрагивая сам защитный механизм, а во втором модифицирует последний так, чтобы удалить проверки наличия\валидности ключевого файла. Обычно хакеры выбирают первое, поскольку это зачастую проще и вызывает меньше конфликтов с законодательством. Теоретически "левый" ключевой файл можно объявить поддельным, но чтобы за это привлечь к уголовной ответственности, формат ключевого файла должен быть защищен авторскими правами, что далеко не всегда возможно. Действительно, сам формат не может быть защищен по нашему законодательству. Можно защитить, скажем, используемую функцию шифровки. Но для этого нужно ее разработать. А авторы защит, напротив, сами склонны "втихаря" применять патентованные алгоритмы, ничего не отчисляя за их использование. Кроме того, не известен еще ни один судебный процесс, который был бы возбужден по этому поводу. Это, конечно, никак не должно толкать на нарушение закона. Впрочем, и законов-то, относящихся к ключевым файлам, нет. В их отсутствии каждый должен руководствоваться собственной моралью и этикой. Другими словами, я призываю читателя использовать свои умения и навыки не для нанесения кому-либо ущерба, а только в образовательных и познавательных целях.
Все ключевые файлы можно разделить на следующие категории - в ключевом файле содержится имя пользователя и регистрационный номер (часто защифрованные). Защитный механизм проверяет их на соответствие и, если результат положительный, устанавливает флаг "регистрации" в единицу. Впрочем, имя пользователя может и отсутствовать - тогда проверяется только серийный номер. Разумеется, такие защиты очень уязвимы. Несмотря на то что многие авторы, стремясь осложнить жизнь хакерам даже додумались применить несимметричные криптоалгоритмы (которые при правильной реализации не дают ни малейшей возможности зашифровать свое имя и рег. номер, т.к. известен ключ только для расшифровки) или использовать цифровые подписи, - все это не спасает. Взломщики идут другим путем: в конечном счете любая функция проверки возращает результат завершения операции. Вот его-то и инвертируют. Или, скажем, удаляют саму проверку подлинности электронной подписи.
Некоторые программисты помещают в ключевой файл отсутствующий в теле программы код, который делает что-то полезное (чтобы зарегестрированная версия хоть чем-то отличалась от свободно распространяемой). Или используют файл как ключ к расшифровке закриптованных частей программы, которые добавляют новые возможности. Понятно, что такой подход относительно прост в программировании и практически неломаем. "Практически" означает, что усилия, затраченные на взлом, почти всегда превзойдут стоимость легальной копии. При желании в ключевой файл можно и полпрограммы засунуть (хотя и возникнут трудности с его распространением) или поместить 1024-битынй ключ, который (при правильной реализации криптосистемы) не удасться вскрыть до конца света.
Впрочем, обычно такие ключи не ломают, а просто "заимствуют" у легальных пользователей и широко распространяют. Увы, и редкие защиты используют "привязку" к чему-то большему, нежели имя пользователя.
Парадоксально, но в некоторых защитах (если так можно их назвать) роль ключа выполняет само имя файла! Не знаю, кажется ли это разработчикам оригинальной идеей или они пытаются таким образом сберечь место на дисках пользователей, но факт, что я видел за свою жизнь по крайней мере три таких защиты. Самые примитивные из них просто проверяли наличие файла, скажем с именем 'Reg.key' и, находя его, устанавливали флаг регистрации. Немного более сложные защиты помещали в имя файла инициалы пользователя и контрольную сумму для проверки. Разумеется, все это было зашифровано, чтобы не бросаться в глаза и не поддаваться элементарной подделке.
Рассмотрим простой пример crack11. Для начала попытаемся определить, предусматривает ли программа регистрацию и если да, то какую. Для этого необходимо найти ссылку на строку "UNREGISTRED" и уточнить источник ее вызова. Если это условный переход, то очевидно разработчиком предусмотрен некий путь регистрации.
.004011A5: 7517 jne .0004011BE .004011A7: 6838304000 push 000403038 ; "UNREG..." ^^^^^^^^^ .004011AC: FF1554204000 call printf ;MSVCRT.dll .004011B2: 83C404 add esp,004 ;"" .004011B5: 33C0 xor eax,eax .004011B7: 81C444010000 add esp,000000144 ;" D" .004011BD: C3 retn .004011BE: 8D542431 lea edx,[esp][00031] .004011C2: 52 push edx .004011C3: 6820304000 push 000403020 ;"REG..." .004011C8: FF1554204000 call printf ;MSVCRT.dll
Не правда ли, наглядно? Регистрация и в самом деле предусмотрена. Осталось выяснить источник ввода, с которым манипулирует защита. Прокрутим экран немного вверх:
.00401120: 51 push ecx .00401121: 684C304000 push 00040304C ; .00401126: FF1500204000 call FindFirstFileA;KERNEL32.dll
Очевидно, что регистрационная информация расположена в каком-то файле. Но в каком? На этот вопрос поможет ответить мониторинг файловых операций. Используем, например, Win95 File Monitor Марка Руссиновича. Его протокол очень интересен:
19 Crack11 FindOpen D:\KPNC\PHCK\SRC\VC\CRACK11\*.* SUCCESS . 20 Crack11 FindNext D:\KPNC\PHCK\SRC\VC\CRACK11\*.* SUCCESS .. 21 Crack11 FindNext D:\KPNC\PHCK\SRC\VC\CRACK11\*.* SUCCESS StdAfx.h 22 Crack11 FindNext D:\KPNC\PHCK\SRC\VC\CRACK11\*.* SUCCESS StdAfx.cpp 23 Crack11 FindNext D:\KPNC\PHCK\SRC\VC\CRACK11\*.* SUCCESS CRACK11.dsw 24 Crack11 FindNext D:\KPNC\PHCK\SRC\VC\CRACK11\*.* SUCCESS CRACK11.cpp
С первого взгляда не совсем ясно, как функционирует защита. Кажется, что это простое чтение оглавления текущего каталога. И нет никакой информации о том, какой файл там должен находиться. Выходит, мониторинг не дал нам никакого результата? Вовсе нет: отрицательный результат тоже результат.
Алгоритм защитного механизма немного прояснился. В текущей директории ищется какой-то файл. Вероятно, защита даже не знает, какой точно, поэтому и не задает маски поиска. Похоже, что она проверяет некоторую дополнительную информацию, например время создания, модификации или длину. Все эти поля могут выделять ключевой файл среди остальных. Возможно, меня спросят: как же защита может узнать дату, если в протоколе отсутствует функция чтения времени? На самом деле тот, кто хоть немного сталкивался с программированием под Dos\Windows, должен помнить, что FindFirstFile/FindNextFile возращают не только имя найденного файла, но и целый "букет" его свойств. Заглянем в SDK, чтобы уточнить последнее. В описании FindFirstFile встретится ссылка на следующую структуру:
typedef struct _WIN32_FIND_DATA { // wfd DWORD dwFileAttributes; FILETIME ftCreationTime; FILETIME ftLastAccessTime; FILETIME ftLastWriteTime; DWORD nFileSizeHigh; DWORD nFileSizeLow; DWORD dwReserved0; DWORD dwReserved1; TCHAR cFileName[ MAX_PATH ]; TCHAR cAlternateFileName[ 14 ]; } WIN32_FIND_DATA;
Какие поля использует защита, можно выяснить только с помощью дизассемблера\отладчика. Попробуем обойтись без IDA и проанализировать алгоритм с помощью одного hiew. Это совсем не так сложно, как может показаться на первый взгляд.
Начнем изучение логики работы защитного механизма с самого начала. "А где оно, это начало?" могут меня спросить. В самом деле, ответить не так просто. Однако достоверно известно, что FindFirstFileA лежит в непосредственной близости от начала защиты. Вот с этой точки мы и начнем свое путешествие по коду.
Прокрутим экран немного вверх, пока не встретим следующий код:
.0040110E: 6850304000 push 000403050 ;
Посмотрим, на что указывает данное смещение:
.00403050: 43 52 41 43-4B 20 4D 45-20 30 78 31-31 20 0A 00 CRACK ME 0x11
Это же первая выводимая программой строка! Следовательно, мы находимся в непосредственной близости от точки входа в защитный механизм.
.00401113: FF1554204000 call printf ;MSVCRT.dll .00401119: 83C404 add esp,004 ; .0040111C: 8D4C2414 lea ecx,[esp][00014]
Выделяется локальная переменная. По тому, как она будет использована чуть ниже (заслана в стек), можно выяснить по прототипу функции, что это структура WIN32_FIND_DATA.
.00401120: 51 push ecx
Вот эта переменная и засылается в стек.
.00401121: 684C304000 push 00040304C ; .00403040: 52 45 44 20-43 4F 50 59-20 0A 00 00-2A 2E 2A 00 RED COPY *.* ^^^^^^^^^^^ ^^^
А это, как видно, маска файлов для поиска.
.00401126: FF1500204000 call FindFirstFileA;KERNEL32.dll
Нашли первый подходящий файл. С этого момента нужно ожидать вызовов FindNextFile.
.0040112C: 8B2D0C204000 mov ebp,[00040200C]
А это что за смещение? Смотрим...
.0040200B: 008021000000 add [eax][000000021],al
С первого взгляда непонятно. Но обратим внимание: эта область находится в таблице адресов импорта kernel32.dll. Строго говоря, мы не можем твердо рассчитывать, что 0x2180 действительно указывает на импортируемую функцию. Как было показано в предыдущей главе, загрузчик попросту игнорирует записанное в ней значение и вычисляет адреса функций в том порядке, в каком они перечислены в таблице имен. Однако компиляторы, несмотря ни на что, все же правильно заполняют эту структуру, и если над ней не "поработал" разработчик защиты, то ее значение должно соответствовать истине. Попробуем в этом убедиться:
.00402180: 9D 00 46 69-6E 64 4E 65-78 74 46 69-6C 65 41 00 Э FindNextFileA
Выглядит правдоподобно. Таким образом, в ebp помещен адрес импортируемой функции, и все вызовы call ebp следует читать как call FindNextFileA.
.00401132: 8BF8 mov edi,eax
В eax, как мы помним, был помещен результат выполнения функции FindFirstFileA. SDK нам скажет, что его ненулевое значение свидетельствует об успехе операции (и наоборот).
.00401134: 33F6 xor esi,esi .00401136: 8A4C2440 mov cl,[esp][00040]
Это, очевидно, первый символ строки cFileName. Для того чтобы понять это, придется заглянуть в файлы определений и узнать размер переменной FILETIME. Далее, исходя из того что вся структура расположена в памяти по адресу [esp+0x14], нетрудно будет определить, к какому полю принадлежит [esp+0x40]. Однако обратим внимание на такую деталь: все остальные поля, кроме имен, - двойные слова. Выходит, CL не может быть ничем иным, кроме как именем файла.
.0040113A: 32D2 xor dl,dl .0040113C: 84C9 test cl,cl
В свете вышесказанного смысл этой строки очевиден. Строка завершается нулевым символом, и test cl,cl проверяет это.
.0040113E: 88542410 mov [esp][00010],dl
Объявление еще одной локальной переменной размером в байт. Начальное значение равно нулю, т.к. перед этим была выполнена операция xor dl,dl.
.00401142: B801000000 mov eax,000000001 ;
Очевидно, регистровая переменная с начальным значением 1.
.00401147: 741F je .000401168 -------- (1)
Условный переход выполняется только при достижении конца строки.
.00401149: 8A4C0440 mov cl,[esp][eax][00040]
Посимвольный разбор имени файла, eax при этом индекс. Обратим внимание: начальное значение eax равно единице, поэтому и разбор строки происходит не с первого символа, а только со второго! Интуиция вам уже подсказывает, что в этом должен быть заключен какой-то определенный смысл. Или... ошибка разработчика.
.0040114D: 0FBED9 movsx ebx,cl .00401150: 81E303000080 and ebx,080000003 ; .00401156: 7905 jns .00040115D .00401158: 4B dec ebx .00401159: 83CBFC or ebx,-004 ; .0040115C: 43 inc ebx
Этот очень витиеватый код, возможно, заставит вас призадуматься и окажется увлекательной головоломкой. Это маленькое чудо запрограммировано разработчиками MS VC. Действительно призадумаешься, прежде чем сказать в адрес MicroSoft пару крепких бранных слов. Уж больно этот код хорош. При ближайшем рассмотрении оказывается, что это аналог функции x mod 4, но насколько же хорошо он оптимизирован!
Если это так, то, похоже, защита зачем-то вычисляет хеш-сумму от имени файла. В том, что это хеш, сомнений практически нет. Теперь вспомним, что первый символ имени был пропущен, а сама хеш-сумма вычисляется в байтовой переменной. Не правда ли, это наводит на мысль, что первый символ имени файла и есть контрольная сумма остальных? И весь этот механизм придуман для того, чтобы отличать ключевые файлы от остальных.
.0040115D: 02D3 add dl,bl
DL в данном случае выступает в роли накопителя хеш-суммы.
.0040115F: 40 inc eax
Перемещает индекс на следующий символ.
.00401160: 84C9 test cl,cl .00401162: 75E5 jne .000401149 -------- (1)
Вспомним, что CL представляет собой последний анализируемый символ имени файла. А вся эта конструкция проверяет, не является ли он завершающим нулем. В противном случае продолжается цикл подсчета хеш-суммы.
.00401164: 88542410 mov [esp][00010],dl
После выхода из цикла вычисления хеша логично ожидать, что теперь этот хеш будет сравниваться с эталонным, предположительно - с первым символом имени. Однако нам встречается очень странный цикл.
.00401168: 48 dec eax
Уменьшаем индекс. Очевидно, разбор имени пошел в обратном направлении.
.00401169: 740E je .000401179 -------- (2)
Проверка, что индекс не равен нулю. Иначе говоря, шапка цикла - от текущей позиции и до первого символа.
.0040116B: 8A54043F mov dl,[esp][eax][0003F]
Загружаем очередной символ строки.
.0040116F: 80F206 xor dl,006 ;
Ксорим его. Правда, еще не понятно зачем.
.00401172: 8854043F mov [esp][eax][0003F],dl
И записываем обратно. Похоже, что это цикл расшифровки строки. Вероятно, в ее имени содержится что-то важное.
.00401176: 48 dec eax .00401177: 75F2 jne .00040116B -------- (3)
А это уже глюк компилятора. Неоптимально организованный цикл.
.00401179: 0FBE542440 movsx edx,b,[esp][00040]
Наконец-то мы обращаемся к первому символу. Заметим, что теперь уже расшифрованному.
.0040117E: 8B442410 mov eax,[esp][00010]
А это вычисленная хеш-сумма. Действительно, похоже, что сейчас их будут сравнивать.
.00401182: 83EA41 sub edx,041 ;"A"
Вычитаем из символа 'A'. Действительно, чтобы имя было читаемым, разработчику защиты пришлось добавить к хеш-сумме этот символ, иначе не было бы попадания в нужный диапазон.
.00401185: 25FF000000 and eax,0000000FF ;" "
Преобразуем результат в байт.
.0040118A: 3BC2 cmp eax,edx
Сравниваем значения друг с другом.
.0040118C: 7505 jne .000401193 -------- (4)
Если два значения идентичны, то флаг esi устанавливается в единицу. (показан ниже) В противном случае он остается равным нулю.
.0040118E: BE01000000 mov esi,000000001
Вот очень хороший кандидат на глобальный флаг регистрации.
.00401193: 8D4C2414 lea ecx,[esp][00014] .00401197: 51 push ecx .00401198: 57 push edi .00401199: FFD5 call ebp
Вызыв FindNextFile
.0040119B: 85C0 test eax,eax .0040119D: 7597 jne .000401136 -------- (5)
Продолжать цикл до тех пор, пока все файлы в текущем каталоге не будут исчерпаны.
.0040119F: 5F pop edi .004011A0: 85F6 test esi,esi
А вот и проверка флажка!
.004011A2: 5E pop esi .004011A3: 5D pop ebp .004011A4: 5B pop ebx .004011A5: 7517 jne .0004011BE -------- (2)
Очевидно, что переход осуществляется только когда он равен единце. В противном случае программа считается незарегистрированной.
.004011A7: 6838304000 push 000403038 ;
Посмотрим, куда он указыавает:
.00403030: 20 25 73 20-0A 00 00 00-55 4E 52 45-47 49 53 54 %s UNREGIST .00403040: 52 45 44 20-43 4F 50 59-20 0A 00 00-2A 2E 2A 00 RED COPY *.*
Точно, это незарегистрированная ветка.
.004011AC: FF1554204000 call printf ;MSVCRT.dll .004011B2: 83C404 add esp,004 .004011B5: 33C0 xor eax,eax .004011B7: 81C444010000 add esp,000000144 .004011BD: C3 retn
А вот эта ветка получает управление, только когда esi равен единице.
.004011BE: 8D542431 lea edx,[esp][00031]
Постойте-ка, а ведь edx указывает на расшифрованную строку! Очевидно для того, чтобы вывести ее на экран.
.004011C2: 52 push edx
Точно, prinft выводит ее!
.004011C3: 6820304000 push 000403020 ;" !!AMPER!!0 " .00403020: 52 45 47 49-53 54 45 52-45 44 20 46-4F 52 20 3A REGISTERED FOR :
Так вот что это за строка - это имя пользователя!
.004011C8: FF1554204000 call printf ;MSVCRT.dll
Защита оказалась довольно любопытной. Она сохраняла регистрационную информацию непосредственно в имени файла. Не самый плохой вариант, особенно учитывая, что длинные имена win95 позволяют вместить до 260 символов, что приблизительно равно размеру среднего ключевого файла. Любопытно, что некоторые разработчики заполняют такие файлы парой килобайт "мусора" для сбивания с толку. Интересно, насколько же наивным надо быть, чтобы думать, будто такой трюк может кого-то запутать.
Мы разобрали алгоритм защиты. А как ее сломать? Можно, конечно, в строке 0х0401134 установить начальное значение esi в единицу и тогда независимо от наличия ключевого файла окажется зарегистрированной. Но это не лучший путь, т.к. теперь программа выдает вместо имени регистрации откровенный мусор. Чтобы этого избежать, придется либо отключить вывод строки на экран, заменив его, скажем на другую строку с нашими инициалами, либо написать собственный генератор. Последнее, конечно, предпочтительнее.
Еще замечание. На самом деле автор предусмотрел два уровня защиты, последний из которых неплохо замаскирован и с первого взгляда вряд ли будет замечен. Рассмотрим еще раз фрагмент:
.0040118A: 3BC2 cmp eax,edx .0040118C: 7505 jne .000401193 -------- (4) .0040118E: BE01000000 mov esi,000000001 .00401193: 8D4C2414 lea ecx,[esp][00014] .00401197: 51 push ecx .00401198: 57 push edi .00401199: FFD5 call ebp .0040119B: 85C0 test eax,eax .0040119D: 7597 jne .000401136 -------- (5) .004011BE: 8D542431 lea edx,[esp][00031]
Подумаем, что будет, если ключевой файл окажется не последним в директории. Тогда edx в строке 0х04011BE будет указывать на имя совсем другого файла, превращенное расшифровкой в мусор, а вовсе не на регистрационную строку. Для чего это сделано? Очевидно, что ключевой файл создается последним в директории и все исправно работает. Но лишь до того момента, пока файлы этого каталога не будут куда-нибудь скопированы так, что ключевой файл окажется не последним в списке. Защита откажет в работе! Не правда ли, оригинальный способ защититься от копирования? Но какому пользователю это понравится? Он хочет получить полноценную копию, не обремененную ограничениями защиты. И мы поможем ему в этом. Но сначала создадим хотя бы один корректный ключевой файл.
Не будем спешить писать свой генератор. Гораздо проще положиться на сам защитный механизм и просто "подсмотреть" правильную строку и контрольную сумму для нашего имени. Создадим файл (или каталог - безразлично, но последнее приятнее) и дадим ему имя 'XKRIS KASPERSKI', где 'X' - зарезервированный символ для будущей контрольной суммы.
Теперь достаточно остановиться в строке 0х040118A и подсмотреть сгенерированную защитой строку. Необходимо воспользоваться отладчиком. Но каким? Ведь кроме Софт-Айса в мире немало и других программ. Например, для этой цели подойдет интегрированный отладчик в MS VC. Конечно, по своим возможностям он уступает Айсу, но прост и удобен в управлении, имеет хорошо спроектированный интерфейс и неплохое ядро, а также великолепную возможность работы с исходными текстами "родных" для него MFC библиотек, которые находятся на компакт-диске, вместе с VC.
Загрузим файл в отладчик и перейдем к строке 0х040118A. Если теперь нажать Ctrl-F10, то программа будет выполнена только до текущей строки, а затем управление вновь получит отладчик. Другими словами, аналог 'here' в SI и TD. Но как мы узнаем, что строка принадлежит именно нашему файлу, а не какому-нибудь другому? Самым простым способом выяснить, какой файл сейчас обрабатывается, это взглянуть на его имя. Как мы помним, оно расположено в стеке по адресу esp+0x40. Вызовем окно отображения дампа и перетащим мышью эту строку в окно или введем ее вручную.
Какая жалость: строка зашифрована и по ней никак не получается узнать имя. Но не будем спешить. Действительно, шифровка искажает имена файлов до неузнаваемости, но длину строки оставляет неизменной. Ключевой файл с именем 'XKRIS KASPERSKI' будет самым длинным из всех присутствующих в каталоге, что не сможет не броситься нам в глаза.
***************** Рисунок pf ***********
Строка '^MTOU&MGUVCTUMO' без первого символа - это и есть запись, соответствующая нашему (а точнее, моему) имени. Переименуем теперь папку 'XKRIS KASPERSKI' в 'XMTOU&MGUVCTUMO' и повторим те же манипуляции с отладчиком, чтобы, во-первых удостовериться, что мы нигде не ошиблись, а во-вторых, узнать контрольную сумму. Не вручную же нам ее вычислять!
***************** Рисунок p10 ***********
Вычисленная контрольная сумма, находящаяся в регистре eax, равна 0x16, тогда 0X16+'A' (0X41) == 0x57 = 'W'. Однако вся строка имени файла (включая и первый символ контрольной суммы) шифруется по xor 6. xor 'W',6 = 'Q': таким образом, ключевой файл должен называться 'QMTOU&MGUVCTUMO'. Переименуем его и запустим программу для проверки.
CRACK ME 0x11<R> REGISTERED FOR : KRIS KASPERSKI<R> Press any key to continue
В самом деле, работает. Осталось написать законченный генератор ключевых файлов на основе введенного имени пользователя (как это делается, уже было рассказано) и исправить недочет программы с последней записью в каталоге.
.00401179: 0FBE542440 movsx edx,b,[esp][00040] .0040117E: 8B442410 mov eax,[esp][00010] .00401182: 83EA41 sub edx,041 ;"A" .00401185: 25FF000000 and eax,0000000FF ;" " .0040118A: 3BC2 cmp eax,edx .0040118C: 7505 jne .000401193 -------- (2) .0040118E: BE01000000 mov esi,000000001 ;"" .00401193: 8D4C2414 lea ecx,[esp][00014] .00401197: 51 push ecx .00401198: 57 push edi .00401199: FFD5 call ebp .0040119B: 85C0 test eax,eax .0040119D: 7597 jne .000401136 -------- (3) .0040119F: 5F pop edi
Понятно, что между 0х040118E и 0х0401193 необходимо вставить jmp 0x040119F, чтобы при нахождении первого же ключевого файла защита выходила из цикла. Однако нам катастрофически не хватает свободного места. Кажется, что никакими ухищрениями не удасться выгадать хотя бы пару байт свободного пространства. Но как вам нравится такой вариант:
.0040118E: 46 inc esi .0040118F: EB0E jmps .00040119F -------- (2) .00401191: 0000 add [eax],al .00401193: 8D4C2414 lea ecx,[esp][00014]
Значение esi по умолчанию равно нулю, следовательно inc esi можно записиать как inc (0) == 1. Эта команда занимает всего один байт, за счет чего в наше распоряжение поступают целых четыре освободившихся байта. Можно сделать SHORT или NEAR прыжок, и при этом в худшем случае по крайне мере один байт остается свободным. Интересно, сможет ли читатель найти другие решения?
Так или иначе, еще одна защита выкинута в мусорную корзину. Все оказалось слишком просто. Это действительно очень простая защита. Пора переходить к более сложным вещам.
Я не буду рассматривать электронные подписи или другие проверки соответствия имени и регистрационной записи. Все это уже встретилось в главе по криптографии и ломается чаще всего не через атаку на криптоалгоритм (который трудно реализовать без ошибок), а именно удалением самой процедуры проверки или флага регистрации.
Гораздо интереснее случаи, когда при этом ключевой файл расшифровывает недостающий код, который добавляет новые функции в программу. Если нет ни одной зарегистрированной копии, то необходимо атаковать криптоалгоритм, а это сопряжено с огромными сложностями и трудозатратами (при условии, конечно, что последний реализован без ошибок). Обсуждая атаки на криптоалгоритмы, я показывал, что хотя положение далеко не всегда так безнадежно, как кажется, гораздо проще приобрести хотя бы один достоверный ключ и расшифровать весь недостающий код, после чего включить его в приложение и убрать все функции, манипулирующие с ключом. Это решение наиболее популярно среди взломщиков.
Способы затруднения анализа программ
Все методы взлома защитных механизмов (за исключением, возможно, криптографических) сводятся практически к двум моментам - локализации кода защиты в сотнях килобайт (мегабайт) кода приложения и анализу алгоритма работы защиты. Причем последнее, как было показано, зачастую необязательно для успешного взлома.
Строго говоря, сегодня уже не существует действительно хороших и надежных методов противодействия анализу защиты. Мощь современных технологий превзошла самые смелые ожидания прошлых лет. Эта фраза может показаться напыщенной, но в действительности и она не может дать истинного представления о вооружении хакера.
Аппаратная поддержка отладки в процессорах 386+ в совокупности с виртуальным режимом работы, привилегированными инструкциями и виртуальной памятью позволяет создавать отладчики, которые практически не могут быть обнаружены прикладной программой, и уж тем более для нее невозможно получить над ними контроль.
Сегодня получили широкое распространение отладчики-эмуляторы. Т.е. интерпретацию микрокода исполняет не процессор, а специально написанная для этой цели программа. При этом интерпретатор всегда исполняется в режиме супервизора по отношению даже к отлаживаемому коду нулевого кольца. У защиты нет ни малейших шансов обнаружить отладчик или помешать его работе.
Интерактивные дизассемблеры тесно взаимодействуют с пользователем, что позволяет обойти любые мыслимые ловушки, расставленные разработчиком защиты по всему коду.
Наконец, популярные операционные системы дают прикладным приложениям не так много привилегий, чтобы их хватило для противодействия отладчикам, даже не ориентированным на взлом. Еще недавно в MS-DOS любая программа могла исполняться в нулевом кольце и работать с аппаратурой исключительно через порты ввода\вывода, минуя операционную систему и BIOS. В Windows же это невозможно. Даже если приложение и установит свой vxd, это только облегчит задачу взломщика, т.к. взаимодействовать с ним защита сможет только через стандартный Win32 API и для хакера не составит труда перехватить и при желании проэмулировать работу vxd, но уже без электронного ключа или ключевой дискеты. Кроме того, сегодня, когда аппаратное обеспечение постоянно меняется прикладная программа никак не может взаимодействовать с ним через порты без риска натолкнуться на несовместимость. К тому же у сетевых рабочих станций весь обмен идет через сеть. Следовательно, любая прикладная программа должна взаимодействовать только с драйвером, но ни в коем случае не с железом, иначе это вызовет отказ от ее использования и переход на продукцию конкурента, что принесет убыток гораздо больший, нежели взлом.
Поэтому прогрессивные разработчики выбирают другие пути. Самый перспективный из них (но вместе с тем и самый сложный) - написание собственного эмулятора процессора, интерпретирующего код защиты. При этом невозможна ни декомпиляция, ни отладка штатными средствами. Взломщик должен проделать трудоемкую и кропотливую работу по изучению архитектуры виртуального процессора, и только потом можно будет написать декомпилятор или отладчик. Только ради чертовски хорошей программы можно потратить столько времени и усилий. Однако если один и тот же виртуальный процессор начнет серийно применяться для защиты программ, то каким бы трудоемким ни был его анализ, он будет рентабельным, ибо с лихвой окупит себя на взломе других приложений.
Можно в шутку сказать: не пытайтесь усложнить хакеру жизнь. Вы только напрасно потеряете силы и время, и ничего хорошего из этого не получится. Если это действительно хакер, то все ваши услия не увенчаются успехом. Однако многие соответствующие приемы любопытны сами по себе; кроме того, полезно знать, как с ними можно справиться. Поэтому ниже этот вопрос будет рассмотрен очень подробно. Возможно, большую часть проблем, относящуюся к MS-DOS, читатель уже неплохо знает. Однако я так же рассматриваю технологии противодействия взломщикам под Windows, которые не были ранее широко опубликованы и изучены. Замечу, что системный программист под Windows имеет все шансы написать программу, которую трудно взломать даже опытным хакерам.
Приемы против отладчиков
Самым первым отладчиком под MS-DOS был Debug.com фирмы MicroSoft. Совершенно очевидно, что этот инструмент годился разве что для забавы и изучения ассемблера.
Но рынок не терпит пустого места, и в то время отладчики росли как грибы после дождя. Многие из них не представляли ничего интересного и ушли недалеко от своего прототипа. Это была золотая эра для разработчиков защит. Достаточно было "запереть" клавиатуру, запретить прерывания, сбросить флаг трассировки, чтобы надолго отбить охоту у хакера к копанию в чужом коде.
Появление 80286 процессора принесло с собой не только новые возможности, но и волну шедевров программирования, отчасти не потерявших актуальности и по сей день. Это время навсегда запомнилось такими отладчиками как Afd PRO, написанной в 1987 году 'AdTec GmbH', знаменитым Turbo Debuger, созданным годом позже двумя братьями Chris'ом и Rich'ем Williams'ом, первым эмулирующим отладчиком Сергея Пачковки, написанным, правда, с большим опозданием: в 1991 году.
Так или иначе, разработчикам защит стали наступать на пятки. Особенно подлила масла в огонь NuMega, выпустившая в конце восьмидесятых замечательный отладчик Soft-Ice, до сих пор пользующийся у хакеров огромной популярностью. NuMega была первой фирмой, которая не только поставила хакер-ориентированное программное обеспечение на коммерческие рельсы, но и умело манипулировала ситуацией. С одной стороны, ее продукты не объявлялись как хакерские и официально предназнались для отладки своих программ, однако подавляющее большинство пользователей применяло их иначе.
Без сомнений, NuMega - первая и, быть может, единственная хакерская фирма, работающая на легальной основе. Очевидно, что возможности фирмы не идут в сравнение с возможностями одиночек. Разработка полноценного отладчика требует значительного времени, обширных познаний в разных областях (от аппаратного обеспечения до операционной системы) и усилий многих людей. Надо ли говорить, что вскоре написание полнофункционального отладчика уже представляло практически неразрешимую задачу для одиночек.
С этого момента воюющие стороны на некоторое время поменялись местами. За разработчиками защит стояли небольшие фирмы - порой из пяти-десяти человек, а то и индивидуальные программисты; за хакерами - вся мощь NuMega. Теперь даже не обладающий глубокими познаниями в области системного программирования взломщик мог успешно пользоваться готовым инструментарием, не особо интересуясь, как последний работает. Численность взломщиков росла, и в хакерское общество вливались многие малоприятные и малокультурные личности, постепено изменивщие значение слова "хакер".
Разумеется, разработчики защит, не желая погибать в гордом одиночестве, начали объединяться в крупные фирмы, вовлекая в них хакеров, которым стало душно среди нового поколения компьютерного андеграунда. Тогда никто не сомневался в успехе этих фирм. И все бы на этом могло закончиться. Опытные хакеры старшего поколения при финансовой поддержке фирм-гигантов создали бы хитроумные защиты, с которыми новички бы просто не справились. А всех, кто оказался способен их взломать, нетрудно было бы склонить к сотрудничеству. Хакеру всегда интереснее работать в коллективе единомышленников и профессионалов, нежели бахвальствовать и поливать грязью всех разработчиков (что андеграунд, в особенности русский, активно продолжает делать до сих пор).
Жизнь распорядилась иначе. Руководства многих компаний скептически относились к хакерам и неохотно брали их на работу. И даже если такое случалось хакеры вскоре уходили из приютивших их компаний, где программистов делали "каменщиками", вынужденными работать строго по плану, спущенному "сверху" людьми, далекими от компьютеров и программирования.
Возрастающая сложность программного обеспечения ограничивалась не только ресурсами машины, но и отсутствием возможности поиска и исправления ошибок в коде. Программисты шутили, что один процент времени уходит на написание программы, а 99 - на ее отладку. Поэтому в своем новом 80386 процессоре фирма Intel ввела специальные механизмы, обеспечивающие контроль исполнения кода на аппаратном уровне. Это означало, что за вечер можно было написать отладчик, против которого были бы бессильны все существующие защитные механизмы, поскольку они были созданы еще в то время когда о 386 процессоре никто не слышал и, разумеется, никак не пытался защититься от его возможностей.
Первой фирмой, достаточно быстро отреагировавшей на ситуацию, оказалась, естественно, NuMega, выпустившая новую версию своего отладчика для 386 процессора. Заметим, что защиты, использующие предоставленные новым процессором возможности, появились только годы спустя, безнадежно проигрывая в развитии хакерским средствам.
Однако судьба преподнесла враждующим сторонам большой сюрприз в лице новой операционной системы Windows. Принципиально новая архитектура сделала бесполезными все существующие отладчики. Разумеется, без отладчика был немыслим ни один серьезный компилятор и его появление было просто неизбежным. Однако MicroSoft отказалась опубликовать информацию, необходимую для его написания, предоставляя программистам только свои продукты. Что это были за отладчики! Медленные, неповоротливые и вовсе не предназначенные для работы с исполняемым кодом.
Впрочем, остальные фирмы этого так не оставили и довели дело до судебного разбирательства, на котором MicroSoft была вынуждена выложить недостающую документацию и дать возможность разрабатывать компиляторы и отладчики сторонним фирмам. Но созданные ими отладчики не превосходили Майкрософтовский и не были ориентированы на работу с исполняемым кодом без исходных текстов.
NuMega в очередной раз удивила мир своим шедевром. Ее отладчик оказался выше всяких похвал. Он ни в чем не полагался на операционную систему и работал непосредственно с аппаратурой, а точнее - между Windows и аппаратурой. В результате стало возможно отлаживать даже ядро операционной системы! Это был триумф, которому никто больше не рискнул подражать. NuMega до сих пор остается единственным поставщиком высококачественных хакер-ориентированных инструментов под Windows, оперативно реагируя на все изменения операционной системы.
Сегодня уже мало кто решается противостоять отладчику и включает в свое приложение антиотладочный код. Мода на это давно прошла. Однако технологии, разработанные нашими предшественниками, от этого отнюдь не становятся неинтересными. Мы рассмотрим их с самого начала. С того самого времени, когда на столе разработчиков гордо красовался новый компьютер XT.
Приемы против отладчиков реального режима
Чтобы понять, как противодействовать отладчикам реального режима, необходимо изучить сам процесс отладки. 8086 процессор предоставлял для этого одну команду, один флаг и две исключительные ситуации. Не слишком густо, но прожить можно.
Легче всего понять концепцию контрольных точек останова исполнения программы. Для этого используется однобайтовый код 0xCC. Когда процессор встречает его, он вызывает исключительную ситуацию (в просторечии "дергает прерыванием") int 0x3. При этом в стеке запоминается регистр флагов, указатель текущего кодового сегмента (регистр CS), указатель команд (регистр IP), запрещаются прерывания (очищается флаг FI) и сбрасывается флаг трассировки.
Что из этого следует? Отлаживаемая программа полностью, включая все аппаратные прерывания, "замораживается" и поступает в монопольное распоряжение процессора, а значит и отладчика. Последнему доступен весь код программы и операционной системы(!) для чтения, анализа и даже модификации. Отладчик может также изменять значение регистров и указателя команд. При этом доступ к последнему осуществляется через стек. Содержимое стека при входе в int 0x3 будет следующим:
--------------------------¬ <------- SP ¦ IP ¦ +-------------------------+ ¦ CS ¦ +-------------------------+ ¦ регистр флагов ¦ +-------------------------+ ¦ ¦
Обычно отладчики сразу сохраняют все регистры, а потом обращаются к ним через стек или присваивают их локальным переменным. Эта информация вам пригодится, если возникнет необходимость написать собственный отладчик или его подобие.
Какими недостатками обладает предложенный метод? Самое неприятное в нем то, что точка останова непосредственно модифицирует код (при этом вынуждая отладчик сохранить измененный байт, чтобы потом была возможность вернуться обратно). Поэтому для отлаживаемой программы не составит труда обнаружить этот факт и удалить точку останова. Для этого можно воспользоваться корректирующими кодами Рида-Соломона, которые позволяют определить местоположение контрольного байта или использовать контрольную сумму кода для расшифровки какого-нибудь фрагмента. Рассмотрим следующий пример crack12.asm:
Repeat: PUSH SI XOR AX,AX LEA SI,Start Get_CRC: LODSB ADD AH,AL CMP SI,offset Crypted JB Get_CRC POP SI LODSB XOR AL,AH STOSB CMP SI,offset _end JB Repeat Crypted:
Похоже на обычный расшифровщик, за исключением того, что ключ генерируется на основе контрольной суммы исполняемого кода. Этим убиваются два зайца: невозможна никакая модификация кода и, главное, контрольные точки останова исказят результат. Воспользуемся, например, TurboDebuger-ом или даже Soft-Ice.
Пошаговая трассировка программы никак не блокируется защитой, но очень утомительна. Попробуем установить точку останова. Но где? Ясно, что в строке Crypted ни в коем случае нельзя. Рассмотрим, что при этом произойдет:
****************** Рисунок 11 *************
Обратите внимание на значение регистра AL - оно равно 0xCC. Контрольная точка исказила код, и теперь он уже не может быь корректно расшифрован. Однако установка контрольной точки в пределах расшифровщика исказит вычисляемую контрольную сумму его тела. Это наглядно демонстрирует следующий дамп:
cs:0119 81FE5101 cmp si,0151 cs:011D72E6 jb 0105 cs:011F 5E pop si cs:0120 D162FF shl word ptr [bp+si-01],1 cs:0123 D915 fst dword ptr[di] cs:0125 F9 stc cs:0126 1B9B8A99 sbb bx,[bp+di-6676] cs:012A 9B wait cs:012B 93 xchg bx,ax cs:012C F8 clc cs:012D E8A0E9 call EAD0
Как мало этот мусор похож на работоспособный код! Разумеется, дальше трассировку продолжать бессмысленно. Так мы ни к чему не придем. Попробуем воспользоваться эмуляционным отладчиком, например cup386. Виртуальные контрольные точки никак не изменяют существующего кода, а поэтому не вносят никаких искажений в его работу.
Но представляет интерес решить эту проблему менее мощными средставми. Самое простое - положить кирпич на клавишу F7 и идти пить чай, а тем временем программа будет расшифровываться. Если же говорить серьезно, то необходимо дождаться расшифровки хотя бы первой инструкции. При этом допустима только пошаговая (F7), а не Step Over (F8) трассировка (последняя также устанавливает 0xCC в конце команды). Теперь можно безболезненно установить точку останова, т.к. код уже расшифрован и не модифицируется.
Этому, однако, можно воспрепятствовать, применив многопроходную расшифровку кода. Это, конечно, сложнее программируется, но затраченные усилия с лихвой окупятся, т.к. под обычным отладчиком это уже не будет исполняться. Кроме того, шифровка затрудняет дизассемблирование. Обычные дизассемблеры вообще не смогут обработать код и выдадут что-то наподобие следующего листинга:
crack12.lst Sourcer v5.10 9-Mar-99 1:53 pm Page 1 43C7:011F 6C D1 62 FF D9 15 db 6Ch,0D1h, 62h,0FFh,0D9h, 15h 43C7:0125 F9 1B 9B 8A 99 9B db 0F9h, 1Bh, 9Bh, 8Ah, 99h, 9Bh 43C7:012B 93 F8 E8 A0 E9 E9 db 93h,0F8h,0E8h,0A0h,0E9h,0E9h 43C7:0131 F6 F8 db 0F6h,0F8h 43C7:0133 57 38 76 7B 38 78 db 'W8v{8xttx'
Впрочем, это не относится к IDA, которая умеет обрабатывать скрипты. Например, следующий скрипт расшифрует весь необходимый код, позволяя дизассемблировать файл:
auto a,sym,ch; sym=0; for (a=0;a<0x1F;a++) sym=sym+Byte(MK_FP(0x1000,0x100+a)); sym = sym & 0xFF; Message("CRC = %x \n",sym); for (a=0x11F;a<0x151;a++) { ch = Byte(MK_FP(0x1000,a)); ch = ch ^ sym; PatchByte(MK_FP(0x1000,a),ch); }
Аналогичным образом можно снять любую мыслимую шифровку. Подробнее об этом уже рассказывалось, и я не буду повторяться. Разве что для разнообразия приведу скрипт для hiew, как для более распространенной утилиты. Для начала нам необходимо узнать контрольную сумму расшифровщика. Подсчитывать ее можно, например, в регистре BL, для чего достаточно одной команды
ADD BL,AL
Части: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15