Фрагмент из второго издания книги "Техника и философия хакерских атак 2.000" — Архив WASM.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:
Код (Text):
> 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:
Код (Text):
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 поддерживают мышь). В частности, на компьютере автора содержимое стека выглядело так:
Код (Text):
: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") и… вот она, наша строка!
Код (Text):
: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" (разумеется, на вашем компьютере адрес строки может быть и другим) и покидаем отладчик нажатием
. Не желая коротать свои дни в одиночестве, отладчик тут же всплывает, сигнализируя нам о том, что некий код попытался обратиться к нашей строке: Код (Text):
001B:77E9736D REPNZ SCASB 001B:77E9736F NOT ECX 001B:77E97371 DEC ECX 001B:77E97372 OR DWORD PTR [EBP-04],-01Судя по адресу, мы имеем дело с некоторой системной функцией (ибо они традиционно размешаются в верхних адресах), но вот с какой именно? Сейчас выясним! Долго ли умеючи! Наскоро набив на клавиатуре трехбуквенное сочетание "mod" мы заставляем отладчик вывести список всех модулей системы на экран:
Код (Text):
: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 процессоров, отладочное исключение возникает не до, а после выполнения команды, "зацепившей" точку останова, а потому отладчик подсвечивает не ее саму, а следующую за ней команду):
Код (Text):
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, возможно, параметр цикла, а вся эта конструкция посимвольно читает строку. Очень похоже, что в мы находимся самом "сердце" защитного механизма - генераторе серийного номера. Если мы посмотрим чуть-чуть ниже, то в глаза бросится очень любопытная строка (в тексте она выделена жирным шрифтом) :
Код (Text):
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", посредством которых мы сможем подсмотреть содержимое указателей, передаваемых функции в качестве аргументов. Скорее всего, один из них принадлежит введенной нами строки, а другой - сгенерированному защитой регистрационному номеру.
Код (Text):
: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 главы "Идентификация циклов" и "Идентификация условных операторов").
Код (Text):
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 - доступен не всем): Код (Text):
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 то или иное значение:
Код (Text):
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 немедленно отобразится в окне дампа:
Код (Text):
: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 содержит его длину.
Код (Text):
001B:004015F2 DEC ECX 001B:004015F3 TEST ECX,ECX 001B:004015F5 JLE 00401639Здесь: мотаем цикл до тех пор, пока не будут обработаны все символы строки (читатели, знакомые с "Фундаментальными основами хакерства" уже наверняка распознали в этой конструкции цикл for).
Теперь заглянем в тело цикла, спустившись еще на одну строчку вниз:
Код (Text):
001B:004015F7 MOV CL,[EAX+ESI]Здесь происходит загрузка очередного символа строки (и именно этот код вызвал всплытие отладчика при установленной точке останова, так что, надеюсь, вы его все еще помните). Поскольку, EAX - указатель на имя, то ESI с большой степени вероятности - параметр цикла. Правда, немного странно, что очередной символ строки помешается в младший байт регистра ECX, который судя по всему представляет собой счетчик цикла, но это все потом… Пока же мы нам известно лишь то, что начальное значение ESI равно нулю, а потому строка скорее всего обрабатывается от первого до последнего символа (хотя некоторые защиты поступают и наоборот).
Код (Text):
001B:004015FA MOVSX AX,BYTE PTR [EAX+ESI+01]MOVe whith Signed eXtension (пересылка со знаковым расширением) загружает следующий байт строки в регистр AX, автоматически расширяя его до слова и загаживая тем самым указатель на саму строку с именем. На редкость уродливый код! Но дальше - больше.
Код (Text):
001B:00401600 MOVSX CX,CLПреобразуем первый прочитанный символ строки к слову (обратим внимание, что здесь и далее под "первым" и "вторым" символом мы будем понимать отнюдь не NameString[0] и NameString[0], а NameString[ESI] и NameString[ESI + 1] соответственно, а сам ESI условно обозначим как index или, сокращенно, idx). Обратим внимание на несовершенство компилятора. Эту команду можно было записать более экономно как MOVSX CX, [ESI+EAX]
Код (Text):
001B:00401604 IMUL EAX,ECXПодставим вместо регистров их смысловые значения, мы получаем: EDX:EAX := NameString[idx] * String[idx + 1]
Код (Text):
001B:00401607 AND EAX,0000FFFFПреобразуем EAX к машинному слову, откидывая старшие 16 бит.
Код (Text):
001B:0040160C AND EAX,8000001FВыделяем пять младших бит от оставшегося слова (почему именно пять? просто переведите 1Fh в двоичную форму и сами увидите). Так же, выделяется и старший, знаковый, бит слова, однако, он всегда равен нулю, так как его принудительно сбрасывает предыдущая команда. Зачем же тогда его компилятор так старательно выделает? Осел он - вот почему. Программист присваивает результат беззнаковой переменной, вот компилятор и понимает его буквально!
Код (Text):
001B:00401611 JNS 00401618Если знаковый бит не установлен (ха! а с какой такой радости ему быть установленным?!), то прыгаем на 401618h. Ну что ж! Прыгаем, так прыгаем, избавляя себя от "радости" анализа нескольких никогда не исполняющихся команд защитного кода:
Код (Text):
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).
Код (Text):
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 от его начала, - аккурат посередине. А что у нас здесь? Листаем код ниже (в квадратных скобках показано смещение соответствующих ячеек относительно начала кадра стека):
Код (Text):
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 очевидно для ее, переменной, инициализации. Но вот что делает эта загадочная функция? Если, находясь в отладчике, нажать
для входа в ее тело, мы обнаружим следующий код: Код (Text):
001B:0040190A JMP [00402164h]Узнаете? Ну да, это характерный способ вызова функций из динамических библиотек. Но вот какая функция какой именно библиотеки сейчас вызывается? Ответ хранит ячейка 402164h, содержащая непосредственно сам вызываемый адрес. Посмотрим ее содержимое?
Код (Text):
:dd :d 402164 0010:00402164 6C29198E 6C294A70 6C2918DD 6C298C74 ..)lpJ)l..)lt.)lОстается только узнать какому модулю принадлежит адрес 6C9198Eh. Не выходя из soft-ice даем ему команду "mod" и смотрим (протокол, приведенный ниже по понятным соображениям сильно сокращен):
Код (Text):
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". Теперь, тот же самый код под отладчиком будет выглядеть так:
Код (Text):
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). Если все идет пучком, мы должны получить следующую информацию:
Код (Text):
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, как мы помним, содержит в себе результат умножения двух символов исходной строки, преобразованный в литерал):
Код (Text):
001B:00401626 PUSH EDX 001B:00401627 CALL MFC42!ORD_03ABСледующими двумя командами мы заталкиваем полученный литерал в стек, передавая его в качестве второго аргумента функции MFC42!ORD_03AB (первый аргумент функций типа __thiscall передается через регистр ECX, содержащий указатель на экземпляр соответствующего объекта, с которым мы сейчас и манипулируем). Преобразовав ординал в символьное имя функции, мы получаем "оператор +=", что очень хорошо вписывается в обстановку окружающей действительности. Другими словами, здесь осуществляется посимвольное наращивание строки var_4 генерируемыми налету литералами.
Код (Text):
001B:0040162C MOV EAX,[ESP+08]Что у нас в [ESP + 8]? Прокручивая экран с дизассемблерным листингом вверх, находим, что здесь лежит самая первая ячейка из принадлежащих кадру стека. Условимся называть ее var_0. Давайте определим, что же за информация в ней находится?
Код (Text):
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-строку, бережно хранящую в себе регистрационное имя пользователя.
Код (Text):
001B:00401630 INC ESIУказатель текущего символа перемещается на одну позицию вправо (ведь вы помните, что в ESI содержится именно указатель на текущий обрабатываемый символ регистрационной строки, верно?).
Код (Text):
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, мотая цикл.
Код (Text):
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]По факту завершения цикла, защита сравнивает только что сгенерированную ей строку с регистрационным номером, введенным пользователем и, в зависимости от результатов этого сравнения, пользователь либо признается легальным чувяком, либо получает от ворот поворот.
Брр! Вы еще не запутались?! Что ж, тогда давайте подытожим все вышесказанное краткими комментариями к защитному коду:
Код (Text):
: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Вот теперь - другое дело и нам уже ничего не стоит восстановить исходный код генератора.
Код (Text):
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). Ключевая процедура может выглядеть так:
Код (Text):
; ГЕНЕРАЦИЯ РЕГИСТРАЦИОННОГО НОМЕРА ; ======================================================================== 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" для выдачи списка всех окон, включая дочерние, зарегистрированных в системе.
Код (Text):
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 выдать более подробную информацию по каждому из окон:
Код (Text):
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), следовательно первое окно - окно регистрационного имени, а второе - окно регистрационного номера.
Теперь, когда порядковые номера окон редактирования известны можно накрапать следующую несложную программку:
}Код (Text):
// ПЕРЕЧИСЛЕНИЕ ДОЧЕРНИХ ОКОН 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 все-таки имеются! © Крис Касперски
Фрагмент из второго издания книги "Техника и философия хакерских атак 2.000"
Дата публикации 11 июн 2003