Новый рубеж

Мир давно привык к тому, что популярные технологии вовсе не обязательно бывают хорошими. Именно так произошло и в области защиты условно-бесплат­ного программного обеспечения. Наибольшее распространение получила защита, основанная на регистрации клиента. Слабость этого механизма в том, что регистрационный код, генерируемый на основе имени пользователя, может быть проверен единственно возможным способом: аналогичной генерацией и последу­ющей сверкой.

Т.е. имеются два полностью идентичных генератора — у автора в виде отдельного приложения и в защитном механизме. Таким образом, остается только извлечь из защищенного приложения эту процедуру и обеспечить удобный обмен данными с пользователем. Иначе говоря, написать собственный генератор регист­рационных номеров.

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

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

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

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

Рассмотрим простую реализацию данного механизма защиты на примере программы file:/ /CD:SOURCEVCCRACK04RELEASEcrack04.exe.

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

Самым популярным на сегодняшний день отладчиком является Soft-Ice от NuMega. Это очень мощный профессиональный инструмент. Новички часто испытывают большие трудности при его настройке, поэтому в приложении подробно описывается, как это сделать.

Разумеется, никто не ограничивает свободу читателя в выборе отладчика. однако в настоящее время не существует программ, которые могли бы составить реальную конкуренцию Soft-Ice. Это вовсе не означает, что другие программы не пригодны для взлома. Большинство из них могут решать рядовые задачи с не меньшим успехом, а узкоспециализированные — в своей области заметно обго­няют Soft-Ice. Но уникальность Soft-Ice в том, что он покрывает рекордно широкий круг задач и платформ. Кроме того, очень приятен и удобен.

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

Разумеется, ничего не получается, и таким способом, скорее всего, программу зарегистрировать никогда не удастся. На это и рассчитывал автор защиты. Однако у нас есть преимущество. Знание ассемблера позволяет заглянуть внутрь кода и проанализировать его алгоритм.

Конкретно нас интересует механизм генерации регистрационных номеров. Как обнаружить его в изучаемом коде? Один из самых легких способов — отследить обращение к введенной строке. Код, читающий ее, очевидно, либо непосредствен­но входит в генератор, либо лежит ь иеносредстиенной близости. Остается только узнать, по какому адресу строка расположена в памяти.

Хорошая задачка! Откуда же узнать этот адрес? Неужели придется утомитель­но анализировать код? Разумеется, нет. Существуют гораздо более оригинальные приемы. Начнем с того, что содержимое окна редактирования надо как-то считать. Для этого нужно послать окну сообщение WM_GETTEXT и адрес, буфера, куда этот текст следует принять. Однако этот способ не снискал популярности, и программисты обычно используют функции API. В SDK можно найти по крайней мере две функции, пригодные для этой цели. — GelWindowText и GetDlgItemText. Причем первая используется гораздо чаще.

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

Итак, нам нужно установить точку останова на вызываемую функцию. Чтобы узнать, на какую, вновь заглянем в список импорта crack04.exe. Как мы помним, это приложение использует MFC, а следовательно, крайне маловероятно, чтобы программист, писавший его, воспользовался напрямую Win32 API, а не библио­течной функцией. Вероятнее всего CWnd::GetWindowText. Попробуем найти ее среди списка импортируемых функций. Для этого можно воспользоваться любой утилитой (например, IDA) или даже действовать вручную. Так или иначе, мы обнаружим, что ординал этой функции OxF22. Этого достаточно, чтобы устано­вить точку останова и перехватить чтение введенной строки.

Однако легко видеть, что CWnd::GetWindowText это всего лишь "переходник" от Win32 API GetWindowTextA. Поскольку нам нужно выяснить только сам адрес строки, то все равно перехватом какой функции мы это сделаем, так как и та, и другая работают с одним и тем же буфером. Это применимо не только к MFC, но и к другим библиотекам. В любом случае на самом низком уровне приложений находятся вызовы Win32 API, поэтому нет никакой нужды досконально изучать все существующие библиотеки, достаточно иметь под рукой SDK.

Однако это никак еще не означает, что можно вообще не интересоваться архитектурой высокоуровневых библиотек. Приведенный пример оказался "про­зрачен" только потому, что GetWindowTextA передавался указатель на тот же самый буфер, в котором и «возвращалась введенная строка. Но разве не может быть иначе? GetWindowTextA передается указатель локального буфера, который затем копируется в результирующий. Поэтому полезно хотя бы бегло ознакомить­ся с архитектурой популярных библиотек.

Но давайте, наконец, перейдем к делу. Для этого вызовем отладчик и (если это Soft-Ice) дадим команду bpx GetWindowTextA. Попутно укажем, откуда взялась буква 'А'. Она позволяет отличить 32-разрядные функции, работающие с Unicode строками (W), от функций, работающих с ANSI-строками (А). Нам это помогает отличать новые 32-разрядные функции от одноименных 16-разрядных. Подробности можно найти в SDK.

После этого введем свое имя и произвольный регистрационный номер и нажмем Enter. Если отладчик был правильно настроен, то он тут же "всплывет".

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

Рассмотрим се прототип:

int GetWindowText(

HWWD hWnd, // handle to window or control with text

LPTSTR lpString, // address of buffer for text

int nMaxCount // maximum number of characters to copy I ;

Следовательно, стек будет выглядеть так:

DWORD DHORB DWORD EIP 0x0
DWORD NMaxCount 0x4
DWORD LpString 0x8
    0xc

 

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

Теперь необходимо поставить точку останова на начало строки или на весь диапазон. Первое может не сработать, если защита игнорирует несколько первых символов имени, а второе замедляет работу. Обычно сначала выбирают первое, а если оно не сработало (что бывает крайне редко), то второе.

Двойное слово IpString это указатель на строку. Однако это только 32-битное смещение. Но относительно какого сегмента? Разумеется, DS. Поэтому установка точки останова может выглядеть так: bpx ds:xxxxx г. Первый код, читающий строку, на самом деле не принадлежит к отлаживаемой программе. В этом можно убедиться, если несколько раз дать команду р ret, — до тех пор, пока мы не выйдем из функции MFC42!OF22. Как мы помним это ординал CWnd::GetWindow-Text. Теперь любой обращающийся к строке код будет принадлежать непосредст­венно защите. Мы, вероятно, уже находимся в непосредственной близости от защитного механизма, но иногда бывает так, что программист читает строку в одном месте программы, а использует результат совсем в другом. Поэтому дождемся повторного отладочного исключения. Рассмотрим код, вызвавший его:

015F:004015F7 8AОС06 MOV CL, [EAX+ESI]

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

015F:0040164B 51 PUSH ECX

015F: 0040164C 52 PUSH EDX

015F:0040164D FF15D0214000 CALL [MSVCRT!_mbscmp]

Вероятно, она сравнивает введенный нами и сгенерированный регистрацион­ный номер! Переведем курсор на нес и дадим команду here. И последовательно дадим команды d ds:ecx и d ds:edx. В одном случае мы увидим свою строку, а во втором истинный регистрационный номер. Выйдем из отладчика и попытаемся ввести его в программу. Получилось! Нас признали зарегистрированным пользо­вателем!

Вся эта операция не должна была занять больше пары минут. Обычно для подобных защит больше и не требуется. С другой стороны, на се написание автор потратил в лучшем случае минут пять-десять. Это очень плохой баланс между накладными расходами на создание защиты и ее стойкостью.

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

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

Однако мы не закончили взлом программы. Да, мы узнали регистрационный код для нашего имени, но понравится ли это остальным пользователям? Ведь каждый из них хочет зарегистрировать программу насебя. Кому будет приятно видеть чужое имя?

Вернемся к коду, сравнивающему эти строки: .

015F:00401643 8В4С2410 MOV ЕСХ, [ESP+10]

015F;00401647 8В54240C MOV EDX, [ESP+OC]

015F:0040164B PUSH ЕСХ

015F:0040164C 52 PUSH EDX

015F:0040164D FF15D0214000 CALL [MSVCRT!_mbscmpl

015F:00401653 83C40B ADD ESP, 08

015F:00401656 8 SCO TEST ЕAХ,ЕАХ

015F:00401658 5E POP ESI

015F;00401659 бАОО PUSH 00

015F;0040165B 6AOO PUSH 00

015F:00401650 7507 JNZ 00401666

Давайте заменим в строке 0040164С 0х52 на 0х51, тогда защита будет сравнивать строку с ней самой. Разумеется, сама с собой строка не совпадать никак не может. Конечно, можно заменить JNZ на JMP или J2, но это будет не так оригинально.

Замечу, что этот способ срабатывает очень редко. Чаще всего проверка будет не одна и в самых неожиданных местах. Достаточно вспомнить, что регистраци­онные данные запоминаются защитой в реестре или внешнем файле. Блокировав первую проверку, мы добьемся того, что позволим защите сохранить неверные данные. Очень вероятно, что при их загрузке автор предусмотрел проверку на валидность. Ее можно отследить аналогичным образом, перехватив вызовы фун­кций, манипулирующих с реестром, однако это было бы очень утомительно. Впрочем, не настолько утомительно, как может показаться на первый взгляд. В самом деле, не интересуясь механизмом ввода данных, можно отследить все вызовы процедуры генерации. Возможны по крайней мере два варианта. Автор либо использовал вызов одной и той же процедуры из разных мест, либо дублировал ее по необходимости. В первом случае нас выручат перекрестные ссылки (наиболее полно их умеет отслеживать 30URCER), во втором — сигна­турный поиск. Крайне маловероятно, что автор использовал не один, а несколько вариантов процедуры генератора. Но даже в этом случае не гарантировано отсутствие совпадающих фрагментов. И уж тем более на языках высокого уровня. Далеко не каждый программист знает, что (! а) ? Ь=0 : Ь==1 и if (la) Ь=-0, else b==l генерируют идентичный код. Поэтому написать одну и ту же процедуру, но так, чтобы ни в одном из вариантов не было повторяющихся фрагментов кода, представляется очень нетривиальной задачей.

Другим возможным решением помимо изменения кода программы (которое далеко не приветствуется законом) может быть написание собственного генера­тора регистрационных номеров или, в просторечии, ключеделки. Для этого надо проанализировать алгоритм генератора и написать аналогичный самостоятельно.

Вернемся немного назад:

15F:0040161B 88542414 mov [esp+14], dl

15F:0040161F 8B542414 mov edx,[esp+14]

15F:00401623 52 push edx