allasm.ru

    Меню

 

Дао регистрационных защит (рабочий вариант)

…идем мы [Andrew Dolgov] с Сергеем Кожиным (кто не в курсе - это автоp parmatosser'a) на пойнтовкy к немy. Такой диалог: Я: Ты бы дал мне нормальный ключ, а то этот пиратский генератор как-то не катит. Он: Hафиг? Я сам им пользуюсь, он меньше и pработает быстрее.

фидошное

Мир давно привык к тому, что популярные технологии далеко не всегда оказываются хорошими. Вот и в сфере условно-бесплатного программного обеспечения наибольшее распространение получили защиты, генерирующие регистрационный номер на основе имени пользователя (регистрационные защиты). Суть этого механизма заключается в том, что на основе некоторой функции f(name) разработчик преобразует регистрационное имя клиента в регистрационный номер и за некоторую плату отсылает его клиенту. Защита же в свою очередь проделывает с регистрационным именем ту же самую операцию, а затем сравнивает сгенерированный регистрационный номер с регистрационным номером, введенным пользователем. Если эти номера совпадают, то все ОК и, соответственно, wrong reg num в противном случае (см. рис. 0x00F).

Таким образом, защитный механизм содержит в себе полноценный генератор регистрационного кода и все, что требуется хакеру: найти процедуру генерации и, подсунув ей свое собственное имя, просто подсмотреть возращенный результат! Другая слабая точка: компаратор, т. е. процедура, сравнивающая введенный и эталонный регистрационный номера. Если на оба плеча компаратора подать один и тот же регистрационный номер (не важно введенный пользователем или сгенерированный защитой), он, со всей очевидностью, скажет "ОК" и защита примет любого пользователя как родного. Еще один способ взлома: проанализировав алгоритм генератора отладчиком и/или дизассемблером, хакер сможет создать свой собственный генератор регистрационных номеров.

Все, чем может досадить хакеру автор защиты, - затруднить анализ и реконструкцию алгоритма генерации. Первое осуществляется оригинальными приемами программирования, противостоящих отладке и/или дизассемблированию, а второе - "размазыванием" кода по десяткам процедур, активным использованием глобальных переменных и запутанным взаимодействием различных фрагментами кода.

Стоит ли и говорить, что запутывание алгоритма малоэффективно и отдает "ребячеством", а подавляющее большинство антиотладочных приемов бессильно против современных отладчиков; кроме того, далеко не все антиотладочные приемы удается реализовать на языках высокого уровня. Спускаться же на уровень ассемблера практически никто из разработчиков не хочет.

Причем, если генератор реализован в одной-двух процедурах (а чаще всего генераторы реализуются именно так!), хакеру нет никакой нужды тратить время на его анализ, и можно прибегнуть к тупому "выкусыванию" кода генератора и копированию его тела в свою собственную программу-оболочку, позволяющую передавать генератору произвольные параметры, в роли которых обычно выступают имена пользователя, company name и прочие регистрационные данные. Впрочем, "выдергиванию" кода легко помешать. Действительно, если рассредоточить код генератор по множеству служебных функций со сложным взаимодействием и неочевидным обменом данных, то без кропотливого анализа всей защиты, выделение всех, относящихся к ней компонентов, окажется невозможным! (Кстати, с точки зрения закона создание собственных генераторов намного более предпочтительнее, чем несанкционированное выдирания фрагментов "живого" кода из чужой программы).


Рисунок 2 0х00F принцип работы регистрационной защиты

Рассмотрим простую реализацию данного защитного механизма на примере программы crackme.58DD2D69h. До сих пор для изучения защитного кода мы пользовались одним лишь дизассемблером, но это не единственный возможный подход к задаче. Не меньшим успехом у хакеров пользуются и отладчики. Отметим, что отладка - более агрессивный способ исследования: в этом случае взлом программы осуществляется "в живую" и со стороны защиты возможны любые "подлянки". Антиотладочный код может запросто "завесить" вашу систему и вообще, выкинуть то, чего вы от него никак не ожидаете. С другой стороны, отладчик обладает многими замечательными (в плане взлома) возможностями, о реализации которых в дизассемблерах пока приходится только мечтать. В первую очередь это относится к точкам останова (по-английски break point), с которыми мы чуть позже с успехом и воспользуемся.

Самым популярным среди хакеров отладчиком был, есть и остается отладчик Soft-Ice от компании NuMega, представляющий собой профессионально-ориентированный инструмент, и потому вызывающий большие трудности у новичков в его освоении. Однако потраченные усилия стоят того! Разумеется, никто не ограничивает свободу читателя в выборе инструментария, - вы можете использовать Microsoft Windows Debugger, Borland Turbo Debugger, Intel Enhanced Debugger, DeGlucker или любой другой отладчик по своему вкусу . Рядовые задачи они решают не хуже айса, а узкоспециализированные отладчики (такие, например, как CUP и Exe Hack) в своих областях даже обгоняют soft-ice. Но уникальность "Айса" как раз и заключается в том, что он покрывает рекордно широкий круг задач и платформ. Существуют его реализации для MS-DOS (ну вдруг кому ни будь понадобится старушка!), Windows 3.1, Windows 9x и Windows NT. Все эти версии Айса несколько различаются межу собой по набору и синтаксису команд, однако эти отличия не столь принципиальны, чтобы вызывать какие либо проблемы. На всякий случай: здесь описывается soft-ice 2.54 под Windows NT.

Итак, загружаем отладчик (под NT это можно сделать в любое время, а в Windows 9x только на стадии загрузки компьютера) и запускаем ломаемое приложение, которое немедленно запрашивает у нас имя и регистрационный номер. Поскольку, регистрационный номер нам доподлинно не известен, приходится набрать что-нибудь "от балды".


Рисунок 3 0х00A реакция защиты на неверно введенный регистрационный номер

Защита, обложив нас матом, сообщает, что "regnum" есть "wrong" и никакой регистрации нам не видать! А чего мы ждали?! Угадать регистрационный номер ни с первой, ни со второй, ни даже с тысячной попытки нереально (регистрационные номера по обыкновению до безобразия длинны) и тупым перебором взломать программу нам не удастся. На это, собственно, и рассчитывал автор защиты. Однако у нас есть преимущество: знание ассемблера позволяет нам заглянуть внутрь кода и проанализировать алгоритм генерации регистрационных номеров. То есть, атаковать защиту не в лоб, а, обойдя укрепленные позиции, напасть с тыла.

Сразу же возникает вопрос: как определить местонахождение генератора, не прибегая к полному анализу исследуемой программы? Давайте представим себе, что генератор это взяточник, а мы - ОБХСС. Роль денег будет играть регистрационное имя, вводимое пользователем. Код, позарившийся на взятку, очевидно и будет самим генератором! То есть, в основе взлома по сути своей лежит перехват обращения к исходным регистрационным данным, избежать которого защита в принципе не может (телепатических возможностей существующие процессоры увы лишены).

Для осуществления такого перехвата нам потребуется всего лишь установить на регистрационное имя так называемую точку останова (break point). Процессор на аппаратном уровне будет контролировать этот регион памяти и при первой же попытке обращения к нему, прервет выполнение программы, сообщая отладчику адреса машинной команды, рискнувшей осуществить такой доступ. Естественно, для установки точки останова требуется знать точное расположение искомой строки в памяти. Спрашиваете, как мы его найдем? Начнем с того, что содержимое окна редактирования надо как-то считать. В Windows это осуществляется посылкой окну сообщения WM_GETTEXT с указанием адреса буфера-приемника. Однако, низкоуровневая работа с сообщениями - занятие муторное и непопулярное. Гораздо чаще программисты используют API-функции, предоставляющие приятный и удобный в обращении высокоуровневый интерфейс. В Platform SDK можно найти по крайней мере две таких функции: GetWindowText и GetDlgItemText. Статистика показывает, что первая из них встречается чуть ли не на порядок чаще, что и не удивительно, т. к. она более универсальна чем ее "коллега".

Перехватив вызов функции, читающей содержимого окна, мы сможем подсмотреть значение переданного ей указателя на буфер, в который и будет скопирована наша строка. Очевидно, что это и есть тот самый адрес, на который мы стремимся установить точку останова! Теперь любой код, обращающийся к этой области, вызовет отладочное исключение и "разбудит" отладчик. Благодаря этому мы обнаружим защитный механизм в сколь угодно большой программе так же быстро как и в маленькой.

Спрашиваете, как мы сможем перехватить вызов функции? Да все с помощью той же самой точки останова! Единственное, что нам для этого потребуется - адрес самой функции. Но вот какой именно функции? Как уже было сказано выше, функций, пригодных для чтения текста из окна редактирования, существует по меньшей мере две. Программист мог использовать либо ту, либо другую, либо вообще третью…

Поскольку исследуемое нами приложения написано на Microsoft Visual C++ с применением библиотеки MFC (что видно по копирайтам, содержащимся в теле файла, и содержимому таблицы импорта), то представляется достаточно маловероятным, чтобы программист, разрабатывающий его, использовал прямые вызовы win32 API. Скорее всего, он, как истинный поклонник объективно ориентированного программирования, сосредоточился исключительно на MFC-функциях, и употребил CWnd::GetWinowText или производные от него методы. К сожалению, неприятной особенностью библиотеки MFC является отсутствие символических имен функций в таблице экспорта и она экспортирует их лишь по порядковому номеру (так же называемому ординалом - от английского ordinal). При наличии сопутствующих библиотек мы без труда определим какому именно ординалу соответствует то или иное имя, однако, вся проблема как раз и заключается в том, что далеко не всегда такие библиотеки у нас есть. Ведь не можем же мы устанавливать на свой компьютер все версии всех компиляторов без разбора?!

Зацепку дает тот факт, что CWnd::GetWindowText по сути своей является сквозным "переходником" от win32 API функции GetWindowTextA. Поскольку все, что нам сейчас требуемся, - это выяснить адрес регистрационной строки, то не все ли равно перехватом какой именно функции это делать? Материнская функция-обертка работает с тем же самым буфером, что и дочь. Это типичное не только для MFC, но и для подавляющего большинства других библиотек. В любом случае на нижнем уровне приложений находятся вызовы win32 API и поэтому нет никакой нужды досконально изучать все существующие библиотеки. Достаточно иметь под рукой SDK! Однако не стоит так же бросаться и в другую крайность, отвергая идею изучения архитектуры высокоуровневых библиотек вообще. Приведенный пример оказался "прозрачен" лишь благодаря тому, что функции GetWindowTextA передается указатель на тот же самый буфер, в котором и возвращалась введенная строка. Но в некоторых случаях функции GetWindowTextA передается указатель на промежуточный буфер, который впоследствии копируется в целевой. Так что ознакомление (хотя бы поверхностное) с архитектурой популярных библиотек очень полезно. [врезка] как узнать имя функции по ординалу

Если динамическая библиотека экспортирует свои функции по ориналу и только по ординалу, то непосредственно определить имена функций невозможно, поскольку их там нет. Однако при наличии соответствующей библиотеки (обычно поставляющейся вместе со средой разработки) наша задача значительно упрощается. Ведь как-то же определяют линкеры ординалы функций по их именам! Так почему же нам не проделать обратную операцию? Давайте воспользуемся уже полюбившейся нам утилитой DUMPBIN из комплекта поставки Platform SDK, запустив ее с ключом /HEADERS и, естественно, именем анализируемой библиотеки. В частности, для определения ординала функции CWnd::GetWindowText мы должны найти в каталоге \Microsoft Visual Studio\VC98\MFC\Lib файл MFC42.lib и натравить на него DUMPBIN:

> dumpbin /HEADERS MFC42.lib > MFC42.headers.txt
> type MFC42.headers.txt | MORE
  Version      : 0
  Machine      : 14C (i386)
  TimeDateStamp: 35887C4E Thu Jun 18 06:32:46 1998
  SizeOfData   : 00000033
  DLL name     : MFC42.DLL
  Symbol name  : ?GetWindowTextA@CWnd@@QBEXAAVCString@@@Z
: (public: void __thiscall CWnd::GetWindowTextA(class CString &)const )
  Type         : code
  Name type    : ordinal
  Ordinal      : 3874

…затем в образовавшемся файле находим нужное нам имя и смотрим всю информацию по нему и, среди всего прочего - ординал (в данном случае: 3874h)

Но вернемся к нашим баранам. Нажатием вызываем soft-ice и даем ему команду "bpx GetWindowTextA" Откуда, спрашиваете взялась буква 'A'? Это суффикс, указывающий на ее принадлежность к ANSI-строкам. Функции, обрабатывающие Unicode-строки, имеют префикс 'W' (в Windows 9x они не реализованы и представляют собой лишь "заглушки", а ядро Windows NT, наоборот, работает исключительно с уникодом и уже ANSI - функции представляют собой переходники; более подробно об этом можно прочитать в Platform SDK), выходим из отладчика повторным нажатием или аналогичной по действию командой "x" и вводим в ломаемое приложение свое имя и произвольный регистрационный номер, подтверждая серьезность своих намерений нажатием . Если отладчик был правильно настроен, то он тут же "всплывает". В противном случае вам следует внимательно изучить прилагаемое к нему руководство или на худой конец его русский перевод, который без труда можно найти в сети.

В общем, будет считать, что все перипетии борьбы с отладчиком уже позади и сейчас мы находимся в точке входа в функцию GetWindowTextA. Как узнать адрес переданного ей буфера? Разумеется, через стек. Рассмотрим ее прототип, приведенный в SDK:

int GetWindowText( 
    HWND hWnd,        // handle to window or control with text 
    LPTSTR lpString,  // address of buffer for text 
    int nMaxCount     // maximum number of characters to copy 
    ); 

Поскольку, все win32 API функции придерживаются соглашения stdcall и передают свои аргументы слева направо, то стек, на момент вызова функции, будет выглядеть так:


Рисунок 4 0x00B состояние стека на момент вызова функции GetWindowText

Переведем окно дампа в режим отображения двойных слов командой "DD" и командой "d ss:esp + 8" заставим его отобразить искомый адрес. Запомним его (запишем на бумажке) или выделим мышью и скопируем в буфер (последние версии soft-ice поддерживают мышь). В частности, на компьютере автора содержимое стека выглядело так:

:dd
:d ss:esp+8
0023:0012F9EC 002F4018  0000000F  00402310  004015D8      .@/......#@...@.
0023:0012F9FC 0012FA04  0012FE14  002F4018  6C361C58      .........@/.X.6l
0023:0012FA0C 6C361C58  0012F9F8  0012FB44  00401C48      X.6l....D...H.@.
0023:0012FA1C 00000002  6C2923D8  00402310  00000111      .....#)l.#@.....

Выделенное жирным шрифтом число и есть адрес буфера, готового принять прочитанную из окна строку. Посмотрим, что у нас там? Переключившись из режима двойных слов в режим байтов командой "DB", мы говорим отладчику "D SS:2F4018" и… ну конечно же видим вокруг себя один мусор, что и не удивительно, ведь функция GetWindowTextA еще и начинала своего выполнения! Что ж, приказываем Айсу выйти из функции ("P RET") и… вот она, наша строка!

:db
:d ss:2f4018
:p ret
0023:002F4018 4B 72 69 73 20 4B 61 73-70 65 72 73 6B 79 00 00  Kris Kaspersky..
0023:002F4028 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0023:002F4038 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0023:002F4048 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

Теперь установим точку останова на адрес начала строки (в листинге, приведенном выше он обведен рамкой) или на всю строку целиком. Заметим, что обоим решениям присущи свои недостатки: если защита игнорирует несколько первых символов имени, то первый примем просто не сработает. С другой стороны, точки останова на диапазон адресов аппаратно не поддерживаются и отладчик вынужден прибегать к хитрым манипуляциям с атрибутами страницы, заставляя процессор генерировать исключение при всякой попытке доступа к ней, а затем вручную анализировать - произошло ли обращение к контролируемой области или нет. Естественно, это значительно снижает производительность и отлаживаемое приложение исполняется со скоростью, которой не позавидует и черепаха! Поэтому, к этому трюку имеет смысл прибегать лишь тогда, когда не сработал первый (а не срабатывает он крайне редко).

Уничтожив ставшей ненужной точку останова на GetWindowText (команда "bc *") мы устанавливаем новую точку останова "bpm ss:2F4018" (разумеется, на вашем компьютере адрес строки может быть и другим) и покидаем отладчик нажатием . Не желая коротать свои дни в одиночестве, отладчик тут же всплывает, сигнализируя нам о том, что некий код попытался обратиться к нашей строке:

001B:77E9736D	REPNZ	SCASB
001B:77E9736F	NOT	ECX
001B:77E97371	DEC	ECX
001B:77E97372	OR	DWORD PTR [EBP-04],-01

Судя по адресу, мы имеем дело с некоторой системной функцией (ибо они традиционно размешаются в верхних адресах), но вот с какой именно? Сейчас выясним! Долго ли умеючи! Наскоро набив на клавиатуре трехбуквенное сочетание "mod" мы заставляем отладчик вывести список всех модулей системы на экран:

:mod
hMod Base     PEHeader Module Name      File Name
     80400000 804000C8 ntoskrnl         \WINNT\System32\ntoskrnl.exe
     77E10000 77E100D8 user32           \WINNT\system32\user32.dll
     77E80000 77E800D0 kernel32         \WINNT\system32\kernel32.dll
     77F40000 77F400C8 gdi32            \WINNT\system32\gdi32.dll
     77F80000 77F800C0 ntdll            \WINNT\system32\ntdll.dll
     78000000 780000D8 msvcrt           \WINNT\system32\msvcrt.dll

Очевидно, что адрес 77E9736Dh принадлежит динамической библиотеке kernel32.dll, а точнее, - функции lstrlenA, которая, как и следует из ее названия определяет длину строки. Поскольку, в определении длины для нас нет ничего интересного, мы безо всякого зазрения совести оставляем этот код жить на бозе и вновь выходим из отладчика, позволяя ему продолжить поиски защитного кода.

Следующее всплытие отладчика оказывается более информативным, смотрите (внимание: в силу архитектурных особенностей x86 процессоров, отладочное исключение возникает не до, а после выполнения команды, "зацепившей" точку останова, а потому отладчик подсвечивает не ее саму, а следующую за ней команду):

001B:004015F7  MOV     CL,[EAX+ESI]		; эта команда "зацепила" breakpoint
001B:004015FA  MOVSX   AX,BYTE PTR [EAX+ESI+01]	; здесь отладчик получил управление
001B:00401600  MOVSX   CX,CL
001B:00401604  IMUL    EAX,ECX
001B:00401607  AND     EAX,0000FFFF
001B:0040160C  AND     EAX,8000001F ; STATUS_BEGINNING_OF_MEDIA
001B:00401611  JNS     00401618
001B:00401613  DEC     EAX

Используемая адресация наталкивает нас на мысль, что EAX, возможно, параметр цикла, а вся эта конструкция посимвольно читает строку. Очень похоже, что в мы находимся самом "сердце" защитного механизма - генераторе серийного номера. Если мы посмотрим чуть-чуть ниже, то в глаза бросится очень любопытная строка (в тексте она выделена жирным шрифтом) :

001B:0040164E	PUSH	ECX
001B:0040164F	PUSH	EDX
001B:00401650	CALL	[MSVCRT!_mbscmp]
001B:00401656	ADD	ESP,08
001B:00401659	TEST	EAX,EAX
001B:0040165B	POP	ESI
001B:0040165C	PUSH	00
001B:0040165E	PUSH	00
001B:00401660	JNZ	00401669
001B:00401662	PUSH	00403030
001B:00401667	JMP	0040166E

Вероятно, здесь-то защита и сравнивает введенный пользователем регистрационный номер с только что сгенерированным эталоном! Переведем курсор на строку 401650h и дадим команду "HERE", обозначающую буквально "сюда!" Теперь последовательно дадим команды "D DS:ECX" и "D DS:EDX", посредством которых мы сможем подсмотреть содержимое указателей, передаваемых функции в качестве аргументов. Скорее всего, один из них принадлежит введенной нами строки, а другой - сгенерированному защитой регистрационному номеру.

:d ecx
0023:002F40B8 36 36 36 00 00 00 00 00-00 00 00 00 00 00 00 00  666.............
0023:002F40C8 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

:d edx
0023:002F4068 47 43 4C 41 41 4C 54 51-51 5B 57 52 54 00 35 38  GCLAALTQQ[WRT.58
0023:002F4078 44 44 32 44 36 39 2E 2E-2E 00 00 00 00 00 00 00  DD2D69..........

Итак, наше предположение на счет "введенного регистрационного номера" полностью подтверждается, и шансы на то, что абракадабра "GCLAALTQQ[WRT" и есть эталонный регистрационный номер весьма велики (обратите внимание на завершающий ее нуль, отсекающий остаток строки "…58DD2D69", который по невнимательности можно принять за саму строку.

Выйдем из отладчика и попытаемся ввести "GCLAALTQQ[WRT" в программу… Защита, благополучно проглотив регистрационный номер, выводит диалог с победной надписью "ОК". Получилось! Нас признали зарегистрированным пользователем! Вся операция не должна была занять порядка пары-тройки минут. Обычно для подобных защит большего и не требуется. С другой стороны, на их написание автор потратил как минимум полчаса. Это очень плохой баланс между накладными расходами на создание защиты и ее стойкостью. Тем не менее, использование таких защит вовсе не лишено смысла (ведь не все же пользователи - хакеры). Нельзя сказать, что создатели защит совсем уж не представляют насколько их легко вскрыть. Косвенным это подтверждением этого являются убедительные просьбы не ломать защиту, а зарегистрироваться и способствовать развитию отечественного рынка (что особенно характерно для российских программистов). Иной раз они бывают настолько красноречивы и длинны, что за время, потраченное на сочинение подобных опусов, можно было бы значительно усилить защиту.

Вышеописанная технология взлома доступна невероятно широкому кругу людей и не требует даже поверхностного знания ассемблера и операционной системы. Просто ставим точку останова на GetWindowText, затем еще одну на строковой буфер и, дождавшись всплытия отладчика, пытаемся найти в каком месте происходит сравнение введенного регистрационного номера со сгенерированным на основе имени эталоном. Любопытно, но большинство кракеров довольно смутно представляют себе "внутренности" операционной системы и знают API куда хуже прикладных программистов. Воистину "умение снять защиту еще не означает умения ее поставить". Чего греха таить! И автор этой книги сначала научился ломать и лишь затем программировать.

Однако мы не закончили взлом программы. Да, мы узнали регистрационный код для нашего имени, но понравится ли это остальным пользователям? Ведь каждый из них хочет зарегистрировать программу на себя. Кому приятно видеть чужое имя?! Вернемся к коду, сравнивающему строки введенного и эталонного регистрационного номера. Если мы заменим в строке 0040164Eh команду PUSH ECX (опкод 52h) на команду PUSH EDX (опкод 51h), то защита станет сравнивать эталонный регистрационный номер с… самим эталонным регистрационным номером! Разумеется, не совпадать с самим собой регистрационный номер просто не может и какие бы строки мы не вводили, защита воспримет их как правильные. Другой путь - заменить условный переход JNZ в строке 401660h (в тексте он выделен квадратиком) на безусловный переход JZ (тогда защита будет "проглатывать" любые регистрационные номера, кроме правильных), или же забить его любой незначащей командой подходящего размера, например SUB EAX, EAX (тогда будут "проглатываться" любые регистрационные номера, включая правильные), хотя последнее и неоригинально. Запускаем HIEW, переводим его в ASM-режим двойным нажатием , переходим по адресу 401660h (, ".401660") и меняем "jne 1669" на "je 1669", скидываем изменения в файл и запускаем программу. Вводим в нее любую понравившуюся вам комбинации и… это работает!!!

Замечу, что это не самый лучший способ взлома и в ряде случаев он не срабатывает. Типичные защитные механизмы имеют как минимум два уровня обороны. На первом осуществляется проверка корректности введенного регистрационного номера и, если он воспринимается защитой как правильный, то данные пользователя заносятся в реестр или дисковый файл. Затем, при перезапуске программы, защитный механизм извлекает пользовательские данные из места их постоянного хранения и проверяет: а соответствует ли имя пользователя его регистрационному номеру?

Блокировав первую проверку, мы добьемся лишь того, что позволим защите сохранить неверные данные, но наш обман будет немедленно раскрыт как только программа попытается загрузить поддельные данные! Конечно, второй "укрепрайон" защитного механизма можно разбить тем же самым способом, которым мы воспользовались для захвата первого (только на этот раз вместо перехвата функции GetWindowText следует установить точки останова на функции, манипулирующие с файлом и реестром), однако это очень утомительно. Другой, и все такой же утомительный, путь - отследить все вызовы процедуры генерации регистрационного номера по перекрестным ссылкам (если одна и та же процедура вызывалась из разных мест защитного механизма), либо же по ее сигнатуре (если создатель защиты дублировал процедуру генерации). Действительно, крайне маловероятно, чтобы разработчик использовал не один, а несколько независимых вариантов генератора. Но даже в последнем случае очень трудно избежать отсутствия совпадающих фрагментов (во всяком случае на языках высокого уровня). Далеко не каждый программист знает, что "(!a) ? b = 0 : b = 1" и "if (a) b=1; els b=0" в общем случае компилируются в идентичный код. Реализовать один и тот же алгоритм так, чтобы ни в одном из вариантов не присутствовало повторяющихся фрагментов кода, представляется достаточно нетривиальной задачей! Тем не менее, выделение уникальной последовательности, присущей одному лишь защитному коду, - задача ничуть не менее нетривиальная, особенно если в защите присутствует множество проверок, расположенных в самых неожиданных местах.

К счастью, помимо изменения двоичного кода программы (которое, кстати, не очень-то приветствуется законом), существует и другая стратегия взлома: создание собственного генератора регистрационных номеров или, в просторечии, ключеделки. Для осуществления своего замысла хакеру необходимо проанализировать алгоритм оригинального генератора и затем написать аналогичный самостоятельно. Преимущества такого подхода очевидны: во-первых, ключеделка вычисляет действительно правильный регистрационный номер и сколько бы раз защита его ни проверяла - менее правильным он все равно не станет. Во-вторых, с юридической точки зрения создание собственного генератора регистрационных номеров более мягкое преступление, чем модификация защитного кода программы. Правда, возможность наказания за нелегальное использование ПО у законников все равно остается, так что, право же, не стоит так рисковать. Но не будем углубляться в дебри юриспруденции, - пусть трактовкой законов занимаются судьи и адвокаты, нам же - хакерам - лучше сосредоточить свои усилия на машинном коде. Вернемся немного назад, в то самое место, где отладчик зафиксировал обращение к первому байту строки, содержащей имя пользователя, и прокрутим экран дизассемблера немного вверх, до тех пор, пока не встретим начало цикла генератора, определяющееся наименьшим адресом условного (безусловного) перехода, направленного назад (подробнее см. "Фундаментальные основы хакерства" by me главы "Идентификация циклов" и "Идентификация условных операторов").

001B:004015EF	PUSH	ESI
001B:004015F0	XOR	ESI,ESI
001B:004015F2	DEC	ECX
001B:004015F3	TEST	ECX,ECX
001B:004015F5	JLE	00401639
001B:004015F7	MOV	CL,[EAX+ESI]	; эта команда обратилась к строке
001B:004015FA	MOVSX	AX,BYTE PTR [EAX+ESI+01]
001B:00401600	MOVSX	CX,CL
001B:00401604	IMUL	EAX,ECX
001B:00401607	AND	EAX,0000FFFF
001B:0040160C	AND	EAX,8000001F
001B:00401611	JNS	00401618		; адрес направлен "вниз", это не цикл
001B:00401611				; а оператор "IF"
001B:00401613	DEC	EAX
001B:00401614	OR	EAX,-20
001B:00401617	INC	EAX
001B:00401618	ADD	AL,41
001B:0040161A	LEA	ECX,[ESP+0C]
001B:0040161E	MOV	[ESP+14],AL
001B:00401622	MOV	EDX,[ESP+14]
001B:00401626	PUSH	EDX
001B:00401627	CALL	0040192E
001B:0040162C	MOV	EAX,[ESP+08]
001B:00401630	INC	ESI
001B:00401631	MOV	ECX,[EAX-08]
001B:00401634	DEC	ECX
001B:00401635	CMP	ESI,ECX
001B:00401637	JL	004015F7		; "наивысший" адрес из всех
001B:00401637				; 4015F7 - начало цикла генератора
001B:00401637				; 401637 - конец  цикла генератора
001B:00401639	LEA	EAX,[ESP+10]
001B:0040163D	LEA	ECX,[EDI+60]
001B:00401640	PUSH	EAX
001B:00401641	CALL	00401934
001B:00401646	MOV	ECX,[ESP+10]
001B:0040164A	MOV	EDX,[ESP+0C]
001B:0040164E	PUSH	ECX
001B:0040164F	PUSH	EDX
001B:00401650	CALL	[MSVCRT!_mbscmp]	; ђ тут сравниваются строки
					;    очевидно это конец генератор

Прежде нем приступать к восстановлению алгоритма генерации регистрационных номеров, отметим, что отладчики вообще-то не предназначены для декомпиляции кода и нам лучше прибегнуть к помощи дизассемблера. Найти же в дизассемблерном листинге требуемый фрагмент очень просто, - ведь адрес процедуры генератора нам уже известен. Для быстрого перемещения к исследуемому коду в IDA достаточно отдать к консоли команду Jump(0x4015EF) , а в HIEW'e - , ".4015EF". Так или иначе мы встретим следующие строки (а еще лучше, если из мазохистских соображений, мы будем анализировать этот код под отладчиком, поскольку дизассемблер - особенно IDA - доступен не всем):

001B:004015EF	PUSH	ESI
001B:004015F0	XOR	ESI,ESI
001B:004015F2	DEC	ECX
001B:004015F3	TEST	ECX,ECX
001B:004015F5	JLE	00401639

Регистр ESI здесь инициализируется явно (ESI ^ ESI := 0), а вот чему равен ECX?! Прокручиваем экран отладчика вверх до тех пор, пока не встретим машинную команду, присваивающую ECX то или иное значение:

001B:004015D8	MOV	EAX,[ESP+04]
001B:004015DC	MOV	ECX,[EAX-08]
001B:004015DF	CMP	ECX,0A
001B:004015E2	JGE	004015EF

Ага, здесь в ECX пересылается значение ячейки по адресу [EAX-08], но что это за ячейка и куда указывает сам EAX? Что ж, под отладчиком (в отличии от дизассемблера) его содержимое очень просто подсмотреть! Достаточно дать команду "D EAX" и область памяти на которую указывает EAX немедленно отобразится в окне дампа:

:d eax
0023:002F4018 4B 72 69 73 20 4B 61 73-70 65 72 73 6B 79 00 00  Kris Kaspersky..
0023:002F4028 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0023:002F4038 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0023:002F4048 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

Да это же только что введенная нами строка! А в регистр ECX тогда загружается что? Смотрим: так, значение ECX равно 0Eh или 14 в десятичной системе исчисления. Очень похоже на длину этой строки (как известно, MFC -строки, точнее объекты класса CString хранят свою длину в специальном 32-разрядном поле, родимым пятном которого как раз и является смещение на 8 байт влево относительно начала самой строки). Действительно, имя "Kris Kaspersky" как раз и насчитывает ровно 14 символов (считая вместе с пробелом). Тогда становятся понятными две следующие машинные команды: CMP ECX,0Ah/JGE 4015EFh, осуществляющие контроль строк на соответствие минимально допустимой длине. При попытке ввода имени, состоящего из девяти или менее символов, программа откинет его как непригодное для регистрации. Это важный момент! Многие хакеры игнорируют подобные тонкости алгоритма и создают не вполне корректные генераторы, не осуществляющие таких проверок вообще. Как следствие - пользователь вводит свое короткое имя в генератор (например, "KPNC"), получает регистрационный код, подсовывает его защите и… обложив матом хакера, вводит в генератор другое имя - на сей раз подлиннее. А если защита имеет ограничение на предельно допустимую длину? Сколько так пользователю придется мотаться между защитой и генератором?

Ладно, оставим вопросы профессиональной этики и вернемся к коду генератора, черкнув в лежащем справа от Клавы листке белой бумаги, что EAX указывает на имя пользователя, а ECX содержит его длину.

001B:004015F2	DEC	ECX
001B:004015F3	TEST	ECX,ECX
001B:004015F5	JLE	00401639

Здесь: мотаем цикл до тех пор, пока не будут обработаны все символы строки (читатели, знакомые с "Фундаментальными основами хакерства" уже наверняка распознали в этой конструкции цикл for).

Теперь заглянем в тело цикла, спустившись еще на одну строчку вниз:

001B:004015F7	MOV	CL,[EAX+ESI]

Здесь происходит загрузка очередного символа строки (и именно этот код вызвал всплытие отладчика при установленной точке останова, так что, надеюсь, вы его все еще помните). Поскольку, EAX - указатель на имя, то ESI с большой степени вероятности - параметр цикла. Правда, немного странно, что очередной символ строки помешается в младший байт регистра ECX, который судя по всему представляет собой счетчик цикла, но это все потом… Пока же мы нам известно лишь то, что начальное значение ESI равно нулю, а потому строка скорее всего обрабатывается от первого до последнего символа (хотя некоторые защиты поступают и наоборот).

001B:004015FA	MOVSX	AX,BYTE PTR [EAX+ESI+01]

MOVe whith Signed eXtension (пересылка со знаковым расширением) загружает следующий байт строки в регистр AX, автоматически расширяя его до слова и загаживая тем самым указатель на саму строку с именем. На редкость уродливый код! Но дальше - больше.

001B:00401600	MOVSX	CX,CL

Преобразуем первый прочитанный символ строки к слову (обратим внимание, что здесь и далее под "первым" и "вторым" символом мы будем понимать отнюдь не NameString[0] и NameString[0], а NameString[ESI] и NameString[ESI + 1] соответственно, а сам ESI условно обозначим как index или, сокращенно, idx). Обратим внимание на несовершенство компилятора. Эту команду можно было записать более экономно как MOVSX CX, [ESI+EAX]

001B:00401604	IMUL	EAX,ECX

Подставим вместо регистров их смысловые значения, мы получаем: EDX:EAX := NameString[idx] * String[idx + 1]

001B:00401607	AND	EAX,0000FFFF

Преобразуем EAX к машинному слову, откидывая старшие 16 бит.

001B:0040160C	AND	EAX,8000001F

Выделяем пять младших бит от оставшегося слова (почему именно пять? просто переведите 1Fh в двоичную форму и сами увидите). Так же, выделяется и старший, знаковый, бит слова, однако, он всегда равен нулю, так как его принудительно сбрасывает предыдущая команда. Зачем же тогда его компилятор так старательно выделает? Осел он - вот почему. Программист присваивает результат беззнаковой переменной, вот компилятор и понимает его буквально!

001B:00401611	JNS	00401618

Если знаковый бит не установлен (ха! а с какой такой радости ему быть установленным?!), то прыгаем на 401618h. Ну что ж! Прыгаем, так прыгаем, избавляя себя от "радости" анализа нескольких никогда не исполняющихся команд защитного кода:

001B:00401618	ADD	AL,41
001B:0040161A	LEA	ECX,[ESP+0C]
001B:0040161E	MOV	[ESP+14],AL
001B:00401622	MOV	EDX,[ESP+14]

Первая машинная команда добавляет к содержимому регистра AL константу 41h (литера 'А' в символьном представлении) и полученная сумма перегоняется в регистр EDX, минуя по пути локальную переменную [ESP + 14].

С конструкцией LEA ECX, [ESP + 0Ch] разобраться несколько сложнее. Во-первых, ячейка [ESP +0Ch] явным образом не инициализируется в программе, а, во-вторых, значение регистра ECX ни здесь, ни далее не используются. Если бы оптимизирующие компиляторы не выкидывали все лишние операции присвоения (т .е. такие, чей результат не используется), мы бы просто списали эту команду на ляп разработчика защитного механизма, но сейчас такая стратегия уже не проходит. К тому же это удачный повод для знакомства с плавающими фреймами, без умения работать с которыми невозможно побороть практически ни одну современную защиту.

Для начала давайте вспомним устройство "классического" кадра стека. При выходе в функцию компилятор сохраняет в стеке прежнее значение регистра EBP (а так же при желании и всех остальных регистров общего назначения, если они действительно должны быть сохранены), а затем приподнимает регистр ESP немного "вверх", резервируя тем самым то или иное количество памяти для локальных переменных. Область памяти, расположенная между сохраненным значением регистра EBP и новой вершиной стека, и называется кадром. Начальный адрес только что созданного кадра копируется в регистр EBP, и этот регистр используется в качестве опорной точки для доступа ко всем локальным переменным. По мере разбухания стека поверх кадра могут громоздиться и другие данные, заталкиваемые туда машинными командами PUSH и PUSHF (например: аргументы функций, временные переменные, сохраняемые регистры и т. д.). Достоинство этой системы заключается в том, что для доступа к локальным переменным нам достаточно знать всего лишь одно число - смещение переменной относительно вершины кадра стека. Благодаря этому, машинные команды, обращающиеся к одной и той же локальной переменной, из какой бы точки функции они ни шли, выглядят одинаково. То есть, нам не требуется никаких усилий, чтобы догадаться, что MOV EAX, [EBP + 69h] и MOV [EBP + 69h], ECX в действительности обрабатывают одну локальную переменную, а не две. Между прочим, вы зря смеетесь! Хотите получить кукурузный початок в зад? Ну так получайте! (Знаю, что больно, но ведь я же предупреждал!).

Поскольку регистров общего назначения в архитектуре IA-32 всего семь, то отдавать даже один из них на организацию поддержки фиксированного кадра стека по меньшей мере не логично, тем более, что локальные переменные можно адресовать и через ESP. Ну и в чем же разница? - спросите вы. А разница между тем принципиальна! В отличии от EBP, жестко держащего верхушку кадра за хвост, значение ESP изменяется всякий раз, когда в стек что-то вложат или, наоборот, что-то вытащат оттуда. Рассмотрим это на следующем примере: MOV EAX, [ESP+10h]/PUSH EAX/MOV ECX, [ESP + 10h]/PUSH ECX/MOV [ESP + 18h], EBP, - как вы думаете, к каким локальным переменным здесь происходит обращение? На первый взгляд, значение ячейки [ESP + 10h] дважды засылается в стек, а затем в ячейку [ESP +18h] копируется содержимое регистра EBP. На самом же деле тут все не так! После засылки в стек содержимого регистра EAX, указатель вершины стека приподнимается на одно двойное слово вверх и дистанция между ним и локальными переменными неотвратимо увеличивается! Следующая машинная команда - MOV ECX, [ESP + 10h] на самом деле копирует в регистр ECX содержимое совсем другой ячейки! А вот [ESP + 18h] после засылки ECX указывает на ту же самую ячейку, что вначале копировалась в регистр EAX. Ну и как теперь насчет "посмеяться"?

Такие оптимизированные кадры стека по-русски называются "плавающими", а в англоязычной литературе обычно обозначаются аббревиатурой FPO - Frame Pointer Omission. Это едва ли не самое страшное проклятие для хакеров. Основной камень преткновения заключается в том, что для определения смещения переменной в кадре мы должны знать текущее состояние регистра ESP, а узнать его можно лишь путем отслеживания всех предшествующих ему машинных команд, манипулирующих с указателем верхушки стека и, если мы случайно упустим хоть одну из них, вычисленный таким трудом адрес локальной переменной окажется неверным! Следовательно, неверным окажется и результат дизассемблирования!!! Вернемся к нашему примеру LEA ECX, [ESP + 0Ch]. Будем прокручивать экран "CODE" отладчика вверх до тех пор, пока не обнаружим пролог функции или не накопим по меньшей мере 0Ch байт, закинутых на стек командами PUSH (в квадратных скобках показано смещение соответствующих ячеек относительно вершины стека на момент вызова нашего LEA).

001B:00401580	PUSH	FF			[ +24h]
001B:00401582	PUSH	00401C48		[ +20h]
001B:00401587	MOV	EAX,FS:[00000000]
001B:0040158D	PUSH	EAX			[ +1Сh]
001B:0040158E	MOV	FS:[00000000],ESP
001B:00401595	SUB	ESP,10			[ +18h] (40161A:04h)
001B:00401598	PUSH	EDI			[ +08h]
001B:00401599	MOV	EDI,ECX
…
001B:004015CD	PUSH	EAX			[ +04h]
…
001B:004015EF	PUSH	ESI			[ +00h]

Ну, что Шура, я Вам могу сказать, - если считать, что SUB ESP, 10h открывает фрейм функции, то LEA ECX, [ESP + 0Ch] лежит по смещению 04h от его начала, - аккурат посередине. А что у нас здесь? Листаем код ниже (в квадратных скобках показано смещение соответствующих ячеек относительно начала кадра стека):

001B:00401595	SUB	ESP,10			[ +00h]
001B:00401598	PUSH	EDI			[ +20h]
001B:00401599	MOV	EDI,ECX
001B:0040159B	LEA	ECX,[ESP+04]		[ +00h]
001B:0040159F	CALL	40190Ah
001B:004015A4	LEA	ECX,[ESP+0C]		[ +08h]
001B:004015A8	MOV	DWORD PTR [ESP+1C],00h
001B:004015B0	CALL	40190Ah
001B:004015B5	LEA	ECX,[ESP+08]		[ +04h]
001B:004015B9	MOV	BYTE PTR [ESP+1C],01
001B:004015BE	CALL	40190Ah

Ага! Вот теперь мы видим, что указатель на локальную переменную, расположенную по смещению 04h от начала кадра стека (далее просто var_04h) передается функции 40190Ah очевидно для ее, переменной, инициализации. Но вот что делает эта загадочная функция? Если, находясь в отладчике, нажать для входа в ее тело, мы обнаружим следующий код:

001B:0040190A	JMP	[00402164h]

Узнаете? Ну да, это характерный способ вызова функций из динамических библиотек. Но вот какая функция какой именно библиотеки сейчас вызывается? Ответ хранит ячейка 402164h, содержащая непосредственно сам вызываемый адрес. Посмотрим ее содержимое?

:dd
:d 402164
0010:00402164 6C29198E  6C294A70  6C2918DD  6C298C74      ..)lpJ)l..)lt.)l

Остается только узнать какому модулю принадлежит адрес 6C9198Eh. Не выходя из soft-ice даем ему команду "mod" и смотрим (протокол, приведенный ниже по понятным соображениям сильно сокращен):

Base	 PEHeader Module Name		File Name
10000000 10000100 pdshell		\WINNT\system32\pdshell.dll
6C120000 6C1200A8 mfc42loc		\WINNT\system32\mfc42loc.dll
6C290000 6C2900F0 mfc42		\WINNT\system32\mfc42.dll
6E380000 6E3800C8 indicdll		\WINNT\system32\indicdll.dll

Легко видеть, что адрес 6C29199Eh принадлежит модулю MFC42.DLL, что совершенно неудивительно ввиду того, что данная программа действительно интенсивно использует библиотечку MFC. Чтобы не вычислять принадлежность всех остальных функций вручную давайте просто загрузим символьную информацию из MFC42.DLL в отладчик. Запустив NuMega "Symbol Loader" (если только вы еще не сделали этого ранее), выберите команду "Load Exports" в меню "File", а затем, перейдя в папку "\WINNT\System32\" дважды щелкните по строке с именем "MFC42.DLL". Теперь, тот же самый код под отладчиком будет выглядеть так:

001B:004015B5	LEA	ECX,[ESP+08]
001B:004015B9	MOV	BYTE PTR [ESP+1C],01
001B:004015BE	CALL	MFC42!ORD_021B

Умница soft-ice определил не только название динамической библиотеки, экспортирующей вызываемую функцию, но и ее ординал! Что же касается имени функции, его можно вычислить с помощью DUMPBIN и библиотеки MFC42.lib. Даем команду "DUMPBIN /HEADRES MFC42.LIB >MFC42.headrs.txt" и затем в образовавшемся файле простым контекстным поиском ищем строку "Ordinal : 539", где "539" наш ординал 021Bh записанный в десятичном виде (именно так выдает оридиналы этот dumpbin). Если все идет пучком, мы должны получить следующую информацию:

Version      : 0
Machine      : 14C (i386)
TimeDateStamp: 35887C4E Thu Jun 18 06:32:46 1998
SizeOfData   : 00000020
DLL name     : MFC42.DLL
Symbol name  : ??0CString@@QAE@PBG@Z (__thiscall CString::CString(unsigned short *))
Type         : code
Name type    : ordinal
Ordinal      : 539

Так, это конструктор объекта типа CString, а указатель, передаваемый ему, стало быть и есть тот самый this, что указывает на свой экземпляр CString! Следовательно, var_4 - это локальная переменная типа "MFC-строка". Теперь, не грех вернуться к изучению прерванной темы (а прервали мы ее на строке 40161Ah, где осуществлялась загрузка указателя на var_4 в регистр ECX посредством машинной команды LEA; регистр же EDX, как мы помним, содержит в себе результат умножения двух символов исходной строки, преобразованный в литерал):

001B:00401626	PUSH	EDX
001B:00401627	CALL	MFC42!ORD_03AB

Следующими двумя командами мы заталкиваем полученный литерал в стек, передавая его в качестве второго аргумента функции MFC42!ORD_03AB (первый аргумент функций типа __thiscall передается через регистр ECX, содержащий указатель на экземпляр соответствующего объекта, с которым мы сейчас и манипулируем). Преобразовав ординал в символьное имя функции, мы получаем "оператор +=", что очень хорошо вписывается в обстановку окружающей действительности. Другими словами, здесь осуществляется посимвольное наращивание строки var_4 генерируемыми налету литералами.

001B:0040162C	MOV	EAX,[ESP+08]

Что у нас в [ESP + 8]? Прокручивая экран с дизассемблерным листингом вверх, находим, что здесь лежит самая первая ячейка из принадлежащих кадру стека. Условимся называть ее var_0. Давайте определим, что же за информация в ней находится?

001B:00401595	SUB	ESP,10				; [ +00h]
001B:00401598	PUSH	EDI				; [ +04h]
…
001B:004015C3	LEA	EAX, [ESP+04]			; var_0
001B:004015C7	LEA	ECX,[EDI+000000A0]
001B:004015CD	PUSH	EAX				; [ +08h]
001B:004015CE	MOV	BYTE PTR [ESP+20],02
001B:004015D3	CALL	MFC42!ORD_0F21			; CWnd::GetWindowText

Кое-что начинает уже проясняться. Переменная var_0 содержит указатель на MFC-строку, бережно хранящую в себе регистрационное имя пользователя.

001B:00401630	INC	ESI

Указатель текущего символа перемещается на одну позицию вправо (ведь вы помните, что в ESI содержится именно указатель на текущий обрабатываемый символ регистрационной строки, верно?).

001B:00401631	MOV	ECX,[EAX-08]			; EAX := var_4
001B:00401634	DEC	ECX
001B:00401635	CMP	ESI,ECX
001B:00401637	JL	004015F7

Первая машинная команда из четырех загружает длину регистрационной MFC-строки в регистр ECX, команда "DEC" уменьшает ее на единицу, а "CMP ESI, ECX" сравнивает полученное значение с индексом текущего обрабатываемого символа регистрационной строки. И, до тех пор, пока индекс не достигнет предпоследнего символа строки, условный переход "JL" прыгает на адрес 4015F7h, мотая цикл.

001B:00401639	LEA	EAX,[ESP+10]
001B:0040163D	LEA	ECX,[EDI+60]
001B:00401640	PUSH	EAX
001B:00401641	CALL	MFC42!ORD_0F21
001B:00401646	MOV	ECX,[ESP+10]
001B:0040164A	MOV	EDX,[ESP+0C]
001B:0040164E	PUSH	ECX
001B:0040164F	PUSH	EDX
001B:00401650	CALL	[MSVCRT!_mbscmp]

По факту завершения цикла, защита сравнивает только что сгенерированную ей строку с регистрационным номером, введенным пользователем и, в зависимости от результатов этого сравнения, пользователь либо признается легальным чувяком, либо получает от ворот поворот.

Брр! Вы еще не запутались?! Что ж, тогда давайте подытожим все вышесказанное краткими комментариями к защитному коду:

:ESI            = 0 (индекс)			[index];
:[ESP+08h], EAX - на регистрационную строку	[NameString];
:[ESP+0Ch]      - на генерируемую строку	[GenString]
001B:004015F7	MOV	CL,[EAX+ESI]		; CL := (char) NameString[index]
001B:004015FA	MOVSX	AX,BYTE PTR [EAX+ESI+1];AX := (uint)((char) NameString[index+1])
001B:00401600	MOVSX	CX,CL			; 
001B:00401604	IMUL	EAX,ECX			; EAX := EAX * ECX
001B:00401607	AND	EAX,0000FFFF		; EAX := LOW_WORD(EAX)
001B:0040160C	AND	EAX,8000001F		; EAX := EAX ^ 1Fh
001B:00401611	JNS	00401618		; GOTO 401618h
001B:00401618	ADD	AL,41			; EAX := EAX + 'A'
001B:0040161A	LEA	ECX,[ESP+0C]		; ECX := &GenString
001B:0040161E	MOV	[ESP+14],AL		; tmp := AL
001B:00401622	MOV	EDX,[ESP+14]		; EDX := tmp
001B:00401626	PUSH	EDX			; 
001B:00401627	CALL	0040192E		; GetString += EDX
001B:0040162C	MOV	EAX,[ESP+08]		; EAX := &NameString
001B:00401630	INC	ESI			; index++
001B:00401631	MOV	ECX,[EAX-08]		; ECX := NameString->GetLength()
001B:00401634	DEC	ECX			; ECX--
001B:00401635	CMP	ESI,ECX			; 
001B:00401637	JL	004015F7		; if (index < ECX) GOTO 4015F7h

Вот теперь - другое дело и нам уже ничего не стоит восстановить исходный код генератора.

for (int idx=0;idx>String.GetLength()-1;idx++)
	RegCode+= ((WORD) sName[a]*sName[a+1] % 0x20) + 'A'; 

Остается лишь написать собственный генератор регистрационных номеров. Это можно сделать на любом симпатичном вам языке, например на ассемблере. На диске находится один вариант (file://CD/SRC/crackme.58DD2D69h/HACKGEN/KeyGen.asm). Ключевая процедура может выглядеть так:

; ГЕНЕРАЦИЯ РЕГИСТРАЦИОННОГО НОМЕРА
; ========================================================================
	MOV	ECX, [Nx]		; ECX := strlen(NameString)
	SUB	ECX, 2		; выкусываем перенос строки
	DEC	ECX		; уменьшаем длину строки на единицу
	MOV	EBX, 20h		; магическое число
	LEA	ESI, hello	; указатель на буфер с именем пользователя
	LEA	EDI, buf_in	; ^ указатель на буфер для генерации

; ЯДРО ГЕНЕРАТОРА
; ========================================================================
gen_repeat:		;<<<---------------------------------------------; CORE
	LODSW		; читаем слово				; CORE
	MUL	AH	; AX := NameString[ESI]*NameString[ESI+1]	; CORE
	XOR	EDX, EDX	; EDX := NULL				; CORE
	DIV	EBX	; DX := NameString[ESI]*NameString[ESI+1] % 1Ah	; CORE
	ADD	EDX, 'A'	; переводим в символ			; CORE
			;					; CORE
	XCHG	EAX, EDX	;					; CORE
	STOSB		; записываем результат			; CORE
	DEC	ESI	; на символ назад				; CORE
LOOP	gen_repeat	; ---- цикл --------------------------------->>> ; CORE

Испытаем написанный генератор. Запустив откомпилированный файл KeyGen.exe на выполнение, введем в качестве регистрационного имени какую ни будь текстовую строку (например, свое собственное имя или псевдоним), - не пройдет и секунды как генератор выдаст подходящий regnum в ответ. В частности, имени "Kris Kaspersky" соответствует следующий регистрационный код: "GCLAALTQQ[WRT"


Рисунок 5 0х00С демонстрация работы ключеделки

Генератор успешно работает и вычисляет правильные регистрационные номера. Однако, вводить регистрационный номер вручную не только утомительно, но и неэлегантно. Да, можно скопировать его и через буфер обмена, но все равно возня будет еще та. В конечном итоге, компьютер на то и придуман, чтобы служить пользователю, но не наоборот. Идеальный crack - это такой crack который не докучает пользователю теми вопросами, ответ на которые знает сам, равно как и не требует от последнего никаких действий, которые он может выполнить и самостоятельно. Единственное, что требует такой crack - своего запуска. Короче, хорошая программа должна заботиться о себе сама!

Первое, что приходит на ум: просто пропадчить защитный код на диске или в памяти. В предыдущей главе мы как раз разбирали как это сделать. Однако, падчики, во-первых, просто вопиюще незаконны, во-вторых, крайне чувствительны к версии билда. Генераторы регистрационных номеров, напротив, весьма мирно уживаются с уголовным кодексом, поскольку они не подделывают, а именно генерируют регистрационный номер на основе имени, введенного пользователем (см. эпиграф) и их написание столь же "незаконно", сколько открытие мастерской по изготовлению дубликатов ключей например. К тому же алгоритм генерации регистрационного номера если и изменяется, то во всяком случае не в каждой версии программы .

Во времена старушки MS-DOS эта проблема решалась перехватом прерывания int 16h с целью эмуляции ввода с клавиатуры. Ломалка, грубо говоря, прикидывалась пользователем и подсовывала защищенной программе сначала имя, а затем и сгенерированный регистрационный номер. От самого же пользователя не требовалось ничего, кроме запуска такой программы. Ну разве не красота? К сожалению, с переходом на Windows прямой контроль над прерываниями оказался безвозвратно утерян и все трюки старой Лисы перестали работать…

Но, "мало того, что их сосед в жилом доме свинью держит, так он еще и круглосуточно над ней измывается..." . Незадачливого музыканта подвела хорошая межквартирная слышимость (читай: хреновая звукоизоляция). Так вот, Windows с точки зрения безопасности - та же хрущоба и слышимость в ней о-го-го! Архитектура подсистемы пользовательского интерфейса, достающаяся NT/9x в наследство от незаконно рожденной Windows 1.0, неотделима от концепции сообщений (messages) - эдакой собачей будке, перенесенной с заднего двора на самое видное место. Любой процесс в системе может посылать сообщения окнам любого другого процесса, что позволяет ему управлять этими окнами по своему усмотрению. Хотите "подсмотреть" содержимое чужого окна? Пожалуйста! Пошлите ему SendMessage с WM_GETTEXT и все дела! Хотите послать окну свою строку с приветствием? Нет проблем, - SendMessage в купе с WM_SETTEXT спасут отца русской демократии! Аналогичным образам вы можете нажимать на кнопки, двигать мышь, раскрывать пункты меню, словом полностью контролировать работу приложения. Самое интересное, что уровень привилегий при этом никак не проверяется, - процесс с гостевыми правами может свободно манипулировать окнами, принадлежащими процессу-администратору. Знаете, в NT/w2k есть такое забавное окошко "запуск программы от имени другого пользователя", обычно используемое для запуска привилегированных приложений из сеанса непривилегированного пользователя? Ну вот например захотели проверить вы свой жесткий диск на предмет целостности файловой структуры, а перезапускать систему под "Администратором" вам лень (точнее, просто не хочется закрывать все активные приложения). На первый взгляд никакой угрозы для безопасности в этом нет, ведь "запуск программы от имени другого пользователя" требует явного ввода пароля! А вот получи треска гранату, - любое злопакостное приложение сможет перехватить ваш пароль только так! Причем, речь идет не о какой-то непринципиальной недоработке, которая легко устранима простой заплаткой (в просторечии называемой "падчем").

Нет! Все так специально и задумывалось. Не верите? Откроем Рихтера "…система отслеживает сообщения WM_SETTEXT и обрабатывает их не так, как большинство других сообщений. При вызове SendMessage внутренний код функции проверяет, не пытаетесь ли вы послать сообщение WM_SETTEXT. Если это так, функция копирует строку из вашего адресного пространства в блок памяти и делает его доступным другим процессам. Затем сообщение посылается потоку другого процесса. Когда поток-приемник готов к обработке WM_SETTEXT, он определяет адрес общего блока памяти (содержащего новый текст окна) в адресном пространстве своего процесса. Параметру lParam пристраивается значение именного этого адреса, и WM_SETTEXT направляется нужной оконной процедуре. Не слишком ли накручено, а?" Выходит, разработчики оконной подсистемы искусственно и крайне неэлегантно обошли подсистему защиты Windows, разделяющую процессы по их адресным пространствам. Естественно, это делалось отнюдь не с целью диверсии, - просто запрети Microsoft посылку сообщений между процессами куча существующих приложений (написанных большей частью под Windows 3.x) тут же перестала бы работать! А значит, эмуляция ввода с клавиатуры жила, жива и будет жить!

Единственное, что нужно знать - так это дескриптор (handle) окна, которого вы хотите "осчастливить" своим сообщением. Существует множество путей получить эту информацию. Можно например воспользоваться API-функцией FindWindow, которая возвращает дескриптор окна по его названию (текстовой строке, красующейся в заголовке) или тупо переворошить все окна одно за другим, в надежде что рано или поздно среди них встретиться подходящее. Перечисление окон верхнего уровня осуществляется функцией EnumWindows, а дочерних окон (к которым диалоговые элементы управления как раз и принадлежат) - EnumChildWindows.

Собственно, получить дескриптор главного окна ломаемого приложения - не проблема, ведь мы знаем его имя, которое в большинстве случаев однозначно идентифицирует данное окно среди прочих запущенных приложений. С дочерними окнами справиться не в пример сложнее. Ладно, кнопки еще можно распознать по их надписи (получаем дескрипторы всех дочерних окон вызовом EnumChildWindows, а затем посылаем каждому из них сообщение WM_GETTEXT с требованием сказать как кого зовут, после чего нам останется лишь сопоставить дескрипторы кнопок с их названиями). К сожалению с окнами редактирования такой фокус не пройдет, ибо по умолчанию они вообще не содержат в себе никакой информации, - вот и разбирайся это окно для ввода регистрационного имени или номера?

На помощь приходит тот факт, что порядок перечисления окон всегда постоянен и не меняется от одной операционной системе к другой. То есть, определив назначения каждого из дочерних окон экспериментально (или с помощью шпионских средств типа Spyxx из комплекта SDK) мы можем жестко прописать их номера в своей программе. Например, применительно к crackme.58DD2D69h это может выглядеть так: запускаем наш любимый soft-ice и даем команду "HWND" для выдачи списка всех окон, включая дочерние, зарегистрированных в системе.

0B0416    #32770 (Dialog)              6C291B81    43C CRACKME_
  0B0406    Button                      77E18721    43C CRACKME_
  0B040A    Static                      77E186D9    43C CRACKME_
  0D0486    Edit                        6C291B81    43C CRACKME_
  0904C6    Static                      77E186D9    43C CRACKME_
  0D0412    Edit                        6C291B81    43C CRACKME_
  0A047C    Button                      77E18721    43C CRACKME_

Ага! Вот они окна редактирования (см. текст выделенный жирным шрифтом), - третье и пятое по счету дочернее окно в списке перечисления. Одно из них наверняка принадлежит строке регистрационного имени, а другое - регистрационного номера. Но как узнать какое кому? Воспользовавшись ключом xc, заставим sof-ice выдать более подробную информацию по каждому из окон:

HWND -xc
	Hwnd		: 0D0486    (A0368EF8)
	Class Name	: Edit
	Module		: CRACKME_
	Window Proc	: 6C291B81 (SuperClassed from: 77E19896)
	Win Version	: 0.00
	Parent		: 0B0416    (A0368A88)
	Next		: 0904C6    (A0368FB8)
	Style		:
	Window Rect	: 387, 546, 615, 566 (228 x 20)
	Client Rect	: 2, 2, 226, 18 (224 x 16)
	…
	Hwnd		: 0D0412    (A03690A8)
	Class Name	: Edit
	Module		: CRACKME_
	Window Proc	: 6C291B81 (SuperClassed from: 77E19896)
	Win Version	: 0.00
	Parent		: 0B0416    (A0368A88)
	Next		: 0A047C    (A0369168)
	Style 		:
	Window Rect	: 387, 572, 615, 592 (228 x 20)
	Client Rect	: 2, 2, 226, 18 (224 x 16)

Как легко установить по координатам вершин окон, первое из них находится на 26 пикселей выше другого (546 против 572), следовательно первое окно - окно регистрационного имени, а второе - окно регистрационного номера.

Теперь, когда порядковые номера окон редактирования известны можно накрапать следующую несложную программку:

// ПЕРЕЧИСЛЕНИЕ ДОЧЕРНИХ ОКОН crackme
// ===========================================================================
// получаем хэндлы всех интересующих нас окон
// (порядок окон определяем либо экспериментально, либо тестовым прогоном
// с отладочным выводом информации по каждому из окон)
BOOL CALLBACK EnumChildWindowsProc(HWND hwnd,LPARAM lParam)
{
	static N = 0;

	switch(++N)
	{
		case 3:	// окно с именем пользователя
				username = hwnd;
				break;

		case 4:	// text со строкой "reg. num."
				hackreg = hwnd;
				break;

		case 5:	// окно для ввода регистрационного номера
				regnum = hwnd;
				break;

		case 6:	// конопка ввода
				input_but = hwnd;
				return 0;
	}
	return 1;
}

Теперь перейдем непосредственно к технике эмуляции ввода. Ну, ввод/вывод текста в окна редактирования больших проблем не вызывает: WM_SETTEXT/WM_GETTEXT и все пучком, а вот "программно" нажать на кнопку несколько сложнее. Но ведь вам же хочется, чтобы программа не только ввела в соответствующие поля всю необходимую регистрационную информацию, но и самостоятельно долбанула по , чтобы закончить ввод?!

Как показывает практика, посылка сообщения BM_SETSTATE элементу управления типа "кнопка" не приводит к ее нажатию. Почему? Наша ошибка заключается в том, что для корректной эмуляции ввода мы во-первых, должны установить фокус (WM_SETFOCUS), а после перевода кнопки в состояние "нажато" этот фокус убить (WM_KILLFOCUS), ведь, как известно даже желторотым пользователям, кнопки срабатывают не в момент их нажатия, но в момент отпускания. Не верите? Поэкспериментируйте с любым приложениям и убедитесь в справедливости сказанного. Кстати, забавный трюк: если под NT/w2k в сообщение WM_KILLFOCUS передать недействительный дескриптор окна, получающего на себя бразды правления, то операционная система по понятным соображениям не передаст фокус несуществующему окну, но у активного окна фокус все-таки отберет. Windows 9x, напротив, оставляет фокус активного окна неизменным! Вот такая разница между двумя операционными системами. Еще одна делать на последок. Если в роли убийцы фокуса выступает функция SendMessage по поток, эмулирующий ввод, блокируется вплоть до того момента, пока обработчик нажатия кнопки не возвратит циклу выборки сообщений своего управления. Чтобы этого не произошло, - используйте функцию PostMessage, которая посылает убийцу фокуса и, не дожидаясь от него ответа, как ни в чем не бывало продолжает выполнение.


Рисунок 6 0х00D "автоматическое" считывание имени пользователя, ввод регистрационного номера и эмуляция нажатия на клавишу "ввод"

Испытаем наш автоматический регистратор? (file://CD/SRC/crack-me58DD2D69h/HACKGEN2/autocrack.c). Запустив защищенную программу и при желании заполнив поле имени пользователя (если его оставить пустым, автоматический регистратор использует имя по умолчанию), мы дрожащей от волнения рукой запускаем autocrack.exe… Держите нас! Это сработало! Вот это автоматизация! Вот это хакерство! Вот это мы понимаем! [врезка] как сделать исполняемые файлы меньше

Даже будучи написанным на чистом ассемблере, исполняемый файл генератора регистрационных номеров занимает целых 16 килобайт! Хорошенький монстр, нечего сказать! Хакерам, чей первый компьютер был IBM PC с процессором Pentium-4, может показаться, что 16 килобайт это просто фантастически мало, однако еще в восьмидесятых годах существовали компьютеры с объемом памяти равным этому числу! Впрочем, зачем нам так далеко ходить, - откроем первое издание настоящей книги: "Без текстовых строк исполняемый файл [генератора] занимает менее пятидесяти байт и еще оставляет простор для оптимизации". Сравните пятьдесят байт и шестнадцать килобайт, - переход с MS-DOS на Windows увеличил аппетит к памяти без малого в триста раз!

Вообще-то, с чисто потребительской точки зрения никакой проблемы в этом нет. Размеры жестких дисков сегодня измеряются сотнями гигабайт и лишний десяток килобайт особой погоды не делает. К тому же, наш исполняемый файл замечательно ужимается pkzip'ом до семисот с небольшим байт, что существенно для его передачи по медленным коммуникационным сетям, - да только где такие нынче найдешь?!

С чисто же эстетической точки зрения держать у себя такой файл действительно нехорошо. Обиднее всего, что на 99% генератор состоит из воздуха и воды, - нулей, пошедших на вырывание секций по адресам, кратным 4Кб. Три секции (кодовая секция .text, секция данных .data и таблица импорта .itable) плюс PE-заголовок, - вместе они эти самые 16 Кб и создают. Полезного же кода в исполняемом файле просто пшик - немногим менее двухсот байт. Конечно, двести это не пятьдесят и с переходом на Windows мы все равно проигрываем и в компактности, и в скорости, но все-таки кое-какой простор для оптимизации у нас имеется.

Начнем с того, что прикажем линкеру использовать минимальную кратность выравнивания из всех имеющихся, - составляющую всего четыре байта. Указывав в командной строке ключ "/ALIGN:4" мы сократим размер исполняемого файла с 16.384 до 1.032 байт! Согласитесь, что с таким размером уже можно жить!

Причем, это далеко не предел оптимизации! При желании можно: а) выкинуть MS-DOS stub, который все равно бесполезен; б) подчистить IMAGE_DIRECTORY; в) использовать незадействованные поля OLD EXE/PE-заголовков для хранения глобальных переменных; г) объединить секции .text, .data, .rdata в одну общую секцию, сведя тем самым эффективную кратность выравнивая к одному и высвободив еще трохи места за счет ликвидации двух секций. Словом, возможности для самовыражения под Windows все-таки имеются!

  [C] Крис Касперски