Справочник функций

Ваш аккаунт

Войти через: 
Забыли пароль?
Регистрация
Информацию о новых материалах можно получать и без регистрации:

Почтовая рассылка

Подписчиков: -1
Последний выпуск: 19.06.2015

Техника и философия хакерских атак

Крис Касперски

Продолжение...

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

Оставить комментарий

Комментарий:
можно использовать BB-коды
Максимальная длина комментария - 4000 символов.
 
Реклама на сайте | Обмен ссылками | Ссылки | Экспорт (RSS) | Контакты
Добавить статью | Добавить исходник | Добавить хостинг-провайдера | Добавить сайт в каталог