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

Ваш аккаунт

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

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

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

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

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

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

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

Но даже если алгоритм реализован без ошибок, вскоре будет найден не единственный верный пароль. Например:

Password - KkEC++        - TSn besO+is tSn eneVr of Oce goTo 

Однако теперь уже совсем нетрудно проанализировать полученный текст и угадать настоящий пароль. С большой вероятностью исходный текст можно просто угадать! Или, по крайней мере, продолжить словарный перебор. Благо теперь это не трудно. Предположим, что 'TSn' это искаженное 'The', следовательно, ожидаемый пароль 'KPNC++', а вся фраза читается так:

'The best is the enemy of the good' 

Мы действительно смогли найти пароль и взломать далеко не самою простую систему защиты. Большинство полулярных приложений защищено гораздо проще и ломается быстрее.

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

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

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

Пародоксально, но этот способ крайне редко применяется разработчиками. Из всех программ, защищенных подобным образом, я навскидку могу вспомнить только FDR 2,1, в котором фрагмент кода, отвечающего за регистрацию, расшифровывался "магическим словом" 'Pink Floyd'. Обычно применяют более наивные защитные механизмы, которым посвящена следующая глава.

Новый рубеж

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

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

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

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

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

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

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

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

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

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

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

************** Рисунок 8 **********************

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

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

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

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

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

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

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

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

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

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 
    ); 

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

                      ----------------------¬  0x0
                DWORD ¦         EIP         ¦
                      +---------------------+  0x4
                DWORD ¦      nMaxCount      ¦
                      +---------------------+  0x8
                DWORD ¦      lpString       ¦
                      +---------------------+  0xC
                      ¦   ..............    ¦

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

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

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

015F:004015F7  8A0C06              MOV     CL,[EAX+ESI] 

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

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  8B4C2410            MOV     ECX,[ESP+10] 
015F:00401647  8B54240C            MOV     EDX,[ESP+0C] 
015F:0040164B  51                  PUSH    ECX 
015F:0040164C  52                  PUSH    EDX 
015F:0040164D  FF15D0214000        CALL    [MSVCRT!_mbscmp] 
015F:00401653  83C408              ADD     ESP,08 
015F:00401656  85C0                TEST    EAX,EAX 
015F:00401658  5E                  POP     ESI 
015F:00401659  6A00                PUSH    00 
015F:0040165B  6A00                PUSH    00 
015F:0040165D  7507                JNZ     00401666 

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

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

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

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

015F:004015F7  8A0C06              MOV     CL,[EAX+ESI] 
015F:004015FA  660FBE440601        MOVSX   AX,BYTE PTR [EAX+ESI+01] 
015F:00401600  660FBEC9            MOVSX   CX,CL 
015F:00401604  0FAFC1              IMUL    EAX,ECX 
015F:00401607  25FFFF0000          AND     EAX,0000FFFF 
015F:0040160C  B91A000000          MOV     ECX,0000001A 
015F:00401611  99                  CDQ 
015F:00401612  F7F9                IDIV    ECX 
015F:00401614  8D4C240C            LEA     ECX,[ESP+0C] 
015F:00401618  80C241              ADD     DL,41 
015F:0040161B  88542414            MOV     [ESP+14],DL 
015F:0040161F  8B542414            MOV     EDX,[ESP+14] 
015F:00401623  52                  PUSH    EDX 
015F:00401624  E805030000          CALL    0040192E 
                                   ^^^^^^^^^^^^^^^^ 
015F:00401629  8B442408            MOV     EAX,[ESP+08] 

Если мы попытаемся заглянуть в процедуру 0x040192E, то вероятнее всего утонем в условных переходах и вложенных вызовах. Сложность и витиеватость кода наталкивают на мысль, что это библиотечная процедура. Но какая? Дело в том, что отладчик не был правильно настроен и экспортировал только системные функции. Исследуемое приложение активно использует MFC42.DLL, поэтому для загрузки символьной информации о функциях последнего необходимо его явно загрузить. Это делается директивой EXP в файле winice.dat Посмотрим, что у нас получилось:

015F:0040161B  88542414            MOV     [ESP+14],DL 
015F:0040161F  8B542414            MOV     EDX,[ESP+14] 
015F:00401623  52                  PUSH    EDX 
015F:00401624  E805030000          CALL    MFC42!ORD_03AC 
                                        ^^^^^^^^^^^^^^^^^^^^^^ 

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

Однако отладчики не предназначены для подробного анализа кода. Гораздо удобнее изучать логику программы с помощью дизассемблера. Найти же требуемый фрагмент очень просто. Достаточно вспомнить, что адрес уже известен. Переместим курсор на строку .text:0040161B, для чего в IDA дадим с консоли команду Jump(MK_FP(0,0x40161B)) и прокрутим экран немного вверх, пока не встретим следующие строки:

.text:004015D3                 call    j_?GetWindowTextA!!AMPER!!CWnd!
                                       !AMPER!!!!AMPER!!QBEXAA 
.text:004015D8                 mov     eax, [esp+4] 
.text:004015DC                 mov     ecx, [eax-8] 
.text:004015DF                 cmp     ecx, 0Ah 
.text:004015E2                 jge     short loc_0_4015EF 

Очевидно, последний условный переход выполняется, когда длина введенной строки больше девяти символов. Для понимания этого необходимо знать, что CString хранит свою длину в двойном слове, находящемся до начала строки. Итак, непосредственно относящийся к защите код начинается с адреса 0x4015EF. Рассмотрим его:

.text:004015EF loc_0_4015EF: 
.text:004015EF                 push    esi 
.text:004015F0                 xor     esi, esi 
.text:004015F2                 dec     ecx 
.text:004015F3                 test    ecx, ecx 
.text:004015F5                 jle     short loc_0_401636 

Это типичный цикл for. Заглянем в его телo:

.text:004015F7 loc_0_4015F7: 
.text:004015F7                 mov     cl, [esi+eax] 

Загрузка очередного символа строки. Поскольку eax - содержит базовый адрес, то очевидно, что esi - смещение в строке. Выше видно, что начальное значение его равно нулю. Логично, что строка обрабатывается от первого до последнего символа, хотя часто бывает и наоборот.

.text:004015FA                 movsx   ax, byte ptr [esi+eax+1] 

MOVe and Sign eXtension (пересылка со знаковым расширением) загружает байт в регистр AX, автоматически расширяя его до слова.

.text:00401600                 movsx   cx, cl 

Обратим внимание на несовершенство компилятора. Эту команду можно было записать более экономно как movsx cx, [esi+eax]

.text:00401604                 imul    eax, ecx 

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

.text:00401607                 and     eax, 0FFFFh 

Преобразуем eax к машинному слову.

.text:0040160C                 mov     ecx, 20h 
.text:00401611                 cdq 

CDQ - Convert Double word to Quad word - Преобразование двойного слова в счетверенное слово

.text:00401612                 idiv    ecx 
.text:00401614                 lea     ecx, [esp+28h+var_1C] 
.text:00401618                 add     dl, 41h 

Поскольку 0x41 - это код символа 'A', то, вновь выполнив смысловую подстановку, получим: _dl = (String[idx]*String[idx+1]) % 0x20 + 'A'. Т.е автор вычисляет хеш-сумму строки. Обратим внимание, что она будет инъективна для интервала 'A'-'_' и, более того, нечувствительна к регистру!

Этот код можно назвать "кодом черной магии". С первого взгляда не понятно как он работает и чем обусловлена нечувствительность к регистру. Обычно для этого программист сначала переводит все буквы в заглавные и только потом начинает разбор строки. Или делает это на лету явным сравнением типа cmp xx, 'a'.

Оригинальные приемы всегда ценятся хакерами, особенно когда они позволяют сократить немного байт и тактов процессора.

.text:0040161B                 mov     byte ptr [esp+28h+var_14],dl 
.text:0040161F                 mov     edx, [esp+28h+var_14] 
.text:00401623                 push    edx 
.text:00401624                 call    CString::operator+=(char) 

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

.text:0040162D                 inc     esi 

Перемещаем указатеь idx на следующий символ в строке.

.text:0040162E                 mov     ecx, [eax-8] 
.text:00401631                 dec     ecx 
.text:00401632                 cmp     esi, ecx 
.text:00401634                 jl      short loc_0_4015F7 

Очевидно, что эти строки также относятся к циклу for. Поэтому уже можно восстановить исходный код генератора.

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

Теперь нетрудно написать собственный генератор регистрационных номеров. Это можно сделать на любом симпатичном вам языке, например на ассемблере. На диске находится один вариант (file://CD/SRC/CRACK04/key_gen.asm). Без текстовых строк исполняемый файл занимает менее пятидесяти байт и еще оставляет простор для оптимизации. Ключевая процедура может выглядеть так:

Reprat:                         ; 
      LODSW                   ; Читаем слово 
       MUL     AH              ; Password[si]*Password[si+1] 
       XOR     DX,DX           ; DX == NULL 
       DIV     BX              ; Password[si]*Password[si+1] % 0x20 

       ADD     DL,'A'          ; Переводим в символ 
       XCHG    AL,DL 
       STOSB                   ; Записываем результат 
       DEC     SI 
       LOOP    Reprat 

Испытаем написанный генератор. Заметим, что в key_gen.asm есть одно несущественное упущение. Он не проверяет минимальную длину строки. Но на деле это не вызывает больших неудобств, зато экономит пяток байт кода.

****************** рисунок 9 ****************

Генератор успешно работает и вычисляет правильные регистрационные номера. Теперь можно начинать его публичное распространение. Отметим, что последнее совершенно не запрещено законом. И ничьих прав не ущемляет. Использование же генераторов все же вызывает конфликтную ситуацию, т.к. пользователь вводит поддельный регистрационный номер. С другой стороны, это недоказуемо, т.к. сгенерированные номера ничем не отличаются от настоящих. Тем не менее я категорически не советую уповать на это. Лицензиозные соглашения пишутся не для того, чтобы их нарушать. Точно так же и создание собственного генератора не должно побужать к его использованию, отличному от познавательного. Перечислите автору требуемую сумму или откажитесь от использования программы. Истинный хакер так и поступит. В этом и заключается его отличие от кракеров. Хакер по определению первоклассный специалист, который всегда заработает на необходимое программное обеспечение (или, если он действительно хакер, то напишет свое).

Я понимаю, что такая трактовка может встретить возражение. Действительно, зачем что-то ломать, если хакер все равно должен приобретать лицензиозный софт? Но разве в этом есть что-то нелогичное? Хакер - это взрослый ребенок, удовлетворяющий свое любопытство. Конечно, очень трудно, обладая такими знаниями и навыками, удержаться от соблазна нарушить закон. Более того, я не знаю ни одного человека, который поступал бы именно так. Увы, хакерство действительно оказывается тесно связанным с криминалом. Это, к сожалению, так.

Перехват WM_GETTEXT

Довольно часто разработчики защит читают содержимое окна, посылая ему сообщение WM_GETTEXT. Это ставит в тупик неопытных кракеров. Устанавка точек останова на GetWinowsText и GetDlgItemText ни к чему не приведет. В таком случае необходимо использовать шпионские средства для анализа взаимодействия приложения с окном. В Windows все делается посредством сообщений, поэтому их перехват позволит выяснить алгоритм работы защитного механизма.

Выбор программ-шпионов достаточно широк. Очень неплохо для этой цели подходит BC от NuMega, однако достаточно и более скромных средств. Например, распространяемый вместе с Microsoft Spy++.

****************** рисунок 0A ****************

Рассмотим полученный рапорт:

00000E9C S .WM_GETTEXT cchTextMax:30 lpszText:0063F750 
00000E9C R .WM_GETTEXT cchCopied:14  lpszText:0063F750 ("Kris Kasperski") 

Умница spyxx даже показал адрес, по которому считанная строка располагается в памяти. Впрочем, он мало что нам дает. Скорее всего буфер расположен в стеке, и активно используется приложением. Нам необходимо перехватить WM_GETTEXT непосредственно в отладчике. Для этого нужно знать дескриптор окна. В этом нам и поможет шпион.

Перехват сообщений в Софт-Айсе осуществляется командой BMSG. Подробности ее использования можно найти в документации или встроенной помощи. После ввода строки в окно редактирования и нажатия на ENTER отладчик всплывет со следующим сообщением:

Break due to BMSG 0428 WM_GETTEXT  (ET=513.11 milliseconds) 
hWnd=0428 wParam=001E lParam=28D70000 msg=000D WM_GETTEXT 
                      ^^^^^^^^^^^^^^^ 

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

Впрочем, в данном примере он отсутствует. Crack0A просто демонстрирует один из вариантов обмена с окном. Аналогичным образом происходит и динамический обмен с окном. Подробное изложение его механизма несложно для понимания, и его можно найти в MSDN. Приблизительно же происходит следующее. Если содержимое окна изменено, то оно посылает сообщение EN_CHANGE (через WM_COMMAND), в ответ ему приходит запрос WM_GETTEXT. Такой механизм очень популярен и используется многими программистами. С другой стороны, все, что делает GetWindowText, - это посылает окну WM_GETTEXT и возвращает полученный результат.

Фактически удобнее и быстрее всегда перехватывать именно это сообщение, а не функции API или библиотек, которые очень трудно удержать в голове.

Ограничение времени использования

Другим популярным ограничением DEMO-версий является ограниченное время использования. Бывают по крайней мере два вида ограничений. В первом отсчет времени идет от момента первого запуска, а во втором программа работает до некоторой заранее установленной даты. Разумеется, первое гораздо удобнее, но и более уязвимо, т.к. необходимо где-то сохраниь дату первого запуска (причем убедиться, что он именно первый). Есть очень немного способов это сделать. Практически разработчики ограничены реестром или внешним файлом. Изменять код самой программы недопустимо, т.к. это вызовет протест со стороны антивирусов, а, значит, и со стороны использующих их клиентов. Под MS-DOS программы прошлого поколения могли писать в инженерные цилиндры жесткого диска, неиспользуемый конец последнего кластера файла, неиспользуемые поля CMOS. Сегодня ситуация изменилась. Современные операционные системы типа Windows NT вообще не дадут непривилегированному пользователю прямого доступа к диску. Идет активное внедрение сетевых технологий, а следовательно, защитный механизм должен успешно функционировать и на сетевой машине. Таким образом, практически единственной подходящей кандидатурой выглядит реестр. Однако все обращения к нему очень легко отследить и отредактировать. Или можно переустановить операционную систему, уничтожив реестр.

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

Рассмотрим для примера crack05 (file://CD/SRC/CRACK05/Crack05.exe) Программа при первом запуске запоминает текущую дату и по истечении 20 дней с этого момента прекращает работу. Переустановка (т.е. удаление и восстановление с оригинала) не помогает. Где же записан момент первого запуска? Быть может, в реестре? Это предположение нетрудно проверить любым монитором реестра. Запустим, например, "Regmon for Windows NT/9x" by Mark Russinovich. Теперь все обращения к реестру будут протоколироваться. Так выглядит протокол при первом запуске защиты:

40 Crack05 OpenKey      HKCU\SOFTWARE\CRACK05   NOTFOUND 
41 Crack05 CreateKey    HKCU\SOFTWARE\CRACK05   SUCCESS hKey: 0xC29AF430 
42 Crack05 SetValueEx   HKCU\SOFTWARE\CRACK05   SUCCESS 0x36D3A94F 
43 Crack05 CloseKey     HKCU\SOFTWARE\CRACK05   SUCCESS 

А так при последующих:

35 Crack05 OpenKey      HKCU\SOFTWARE\CRACK05   SUCCESS hKey: 0xC29AFE60 
36 Crack05 QueryValueEx HKCU\SOFTWARE\CRACK05   SUCCESS 0x36D3FC04 
37 Crack05 CloseKey     HKCU\SOFTWARE\CRACK05   SUCCESS 

Попробуем удалить раздел HKEY_CURRENT_USER\SOFTWARE\CRACK05 (предварительно сделав резервную копию реестра). Последующий запуск защита воспримет как первый. На процедуру вскрытия ушло меньше пары минут. Однако, периодическое редактирование реестра утомительно и просто неудобно. Полноценный взлом предполагает полную блокировку защитного механизма, что мы сейчас и сделаем.

Протокол позволяет понять алгоритм работы защиты. Первоначально программа пытается найти в реестре раздел HKEY_CURRENT_USER\SOFTWARE\CRACK05. Если он отсутствует, то защита полагает, что на этом компьютере запущена впервые и записывает текущую дату. В противном случае вычисляется число дней с момента первого запуска. Можно изменить код так, чтобы независимо от результатов поиска управление всегда передавалось на ветку первого запуска.

Рассмотрим следующий код:

00401096                 lea     ecx, [esp+4] 
0040109A                 lea     edx, [esp+0Ch] 
0040109E                 push    ecx 
0040109F                 push    edx 
004010A0                 push    0 
004010A2                 push    0F003Fh 
004010A7                 push    0 
004010A9                 push    4031A4h 
004010AE                 push    0 
004010B0                 push    offset aSoftwareCrack0 
004010B5                 push    80000001h 
004010BA                 call    ds:RegCreateKeyExA 
004010C0                 test    eax, eax 
004010C2                 jnz     loc_4011C0 

Найти в листинге дизассемблера его можно двояко - среди перекрестных ссылок на RegCreateKeyExA:

0040200C RegCreateKeyExA dd ?                   ;DATA XREF:sub_401040+7Ar 

или по ссылке на строку aSoftwareCrack0:

00403088 aSoftwareCrack0 db 'SOFTWARE\CRACK05',0;DATA XREF:sub_401040+70o 

Обратим внимание на строку 0x04010C2. Вопреки ожиданиям, изменять этот условый переход ни в коем случае не надо. Заглянув в SDK, можно узнать, что RegCreateKeyExA возвращает ненулевое значение в случае фатальной ошибки. А результат завершения операции передается через локальную переменную [esp+ 0x4]. Если раздел был успешно создан, то возращается единица, в противном случае раздел уже существует.

Тогда становится понятен смысл следующего фрагмента:

004010C8                 cmp     dword ptr [esp+4], 1 
004010CD                 jnz     short loc_401116 

Чтобы защита каждый запуск считала первым, достаточно удалить условный переход jnz. Его можно заменить, например, на две однобайтовые операции nop. Попробуем сделать это сейчас. Переключим дизассемблер в режим hex-дампа и запишем, например, последовательность '83 7C 24 04 01 75 47'. Найдем ее в любом шестнадцатиричном редакторе и заменим на '83 7C 24 04 01 75 47'.

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

Рассмотрим другой пример реализации подобной защиты crack06. Использование монитора реестра нам ничего не дает. Быть может, программа сохранила дату в каком-нибудь файле? Обратимся к файловому монитору. Рассмотрим полученный протокол:

3495 Crack06 Open  "C:\WINDOWS\SYSTEM\CRACK06.DAT" CREATENEW 
3498 Crack06 Write "C:\WINDOWS\SYSTEM\CRACK06.DAT" Offset:0 Length:4 
3499 Crack06 Close "C:\WINDOWS\SYSTEM\CRACK06.DAT" 

Из него видно, что приложение создало новый файл в каталоге WINDOWS\SYSTEM. В нем легко затеряться среди сотен файлов, часто неясным образом созданных и неизвестно кому принадлежащих. В данном примере использовалось "говорящее" за себя имя, однако авторы защит склонны к бессмысленным комбинациям типа syswdg.dll Это признак низкой культуры программирования, не могущей служить образцом для подражания.

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

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

Рассмотрим несколько реализаций защитных механизмов, которые не так очевидны, как вышеописанные. Например, xformat 2.4 Криса Касперски сохранял месяц первого запуска в поле сотых долей секунды времени создания command.com . Антивирусы на это (как ни странно) не реагировали. Такое решение, очевидно, не слишком повышало стойкость защиты и не могло служить примером культурного программирования, но от неквалифицировнных пользоватетелей, вооруженных дисковыми сканерами, защищало надежно.

Некоторые защиты активно используют для этой цели незадействованные поля CMOS. Это очень примитивный способ, имеющий ряд серьезных ограничений. Защита слишком заметна и легко перехватывается. Действительно достаточно перехватить запись в порт 0x70, чтобы обнаружить защиту. Однако операционная система (наподобие win nt) не позволит напрямую обращаться к портам непривилегированным пользователям. Кроме того, CMOS не видна по сети. Наконец, зарезервированные поля могут быть использованы в новых версиях, что приведет к конфликтам и, возможно, к серьезным последствиям.

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

На самом деле несложно было найти по процедуре системного времени защитный механизм. Однако к системным часам существует множество способов доступа. Чтобы выяснить, какой именно использует приложение, необходимо ознакомиться с таблицой импорта. Так, например, crack07.exe импортирует только одну функцию, непосредственно связанную с опросом времени - GetTickCount.

.text:00401109                 call    j_?GetTickCount!!AMPER!!CTime!
                                       !AMPER!!!!AMPER!!SG?AV1!!AMPER!!XZ;

Сейчас в eax адрес двойного слова, содержащего упакованную дату и время.

.text:0040110E                 mov     edx, [eax] 

Загружаем упакованную дату\время в edx.

.text:00401110                 mov     edi, ds:printf 
.text:00401116                 sar     edx, 0Fh 

Избавляемся от часов, минут, секунд.

.text:00401119                 mov     esi, 7000h 

Упакованная дата окончания использования.

.text:0040111E                 sub     esi, edx 

Вычисляем, сколько осталось времени для использования приложения.

.text:0040112B                 test    esi, esi 
.text:0040112D                 pop     esi 
.text:0040112E                 jle     short loc_0_401140 
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

Срок истек (нуль или отрицательное число).

.text:00401130                 push    offset aWorking___ 

Ветка нормального исполнения программы.

.text:00401135                 call    edi 
.text:00401137                 add     esp, 4 
.text:0040113A                 mov     eax, ebx 
.text:0040113C                 pop     edi 
.text:0040113D                 pop     ebx 
.text:0040113E                 pop     ecx 
.text:0040113F                 retn 

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

Старые приложения, выполняемые в среде MS-DOS не могут быть взломаны подобным образом, т.к. они не импортируют никаких функций и найти защитный механизм в дизассемблере может быть непростой задачей. Рассмотрим, например, crack07.exe, скомпилированный Турбо-Паскалем под MS-DOS. IDA 3.8 уверенно распознает стандартные функции, среди которых нетрудно найти GetDate, но что делать, если она недоступна?

На самом деле приложения под MS-DOS могут получить системную дату двумя способами - функцией операционной системы f.0x2A (int 0x21) или BIOS f.04 (int 0x1A). Практически не встречается считывание счетчика дней, прошедших с момента 10/1/86 (f.0Ah int 0x1A) или непосредственным чтением регистров CMOS. Перехватить функции указанных прерываний позволит практически любой отладчик, например soft-ice.

Поскольку сегодня приложения под MS-DOS медленно, но верно вымирают, мы не будем останавливаться на этом подробно.

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

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

Ограничение числа запусков

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

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

Постоянное изменение счетчика позволяет, сравнив протоколы разных запусков, найти различия, которых обычно бывает немного. Один из них и будет искомым счетчиком.

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

Выяснить истину нам поможет отладчик или дизассемблер. В сегменте данных найдем строку:

.data:00403050 aCount1         db 'Count1',0        ; DATA XREF: _main+CB 
.data:00403050                                      ; _main+122o ... 

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

.text:0040120F                 mov     eax, [esp+5Ch+var_54] ; Count2 
.text:00401213                 mov     edx, [esp+5Ch+var_4C] ; Count1 
.text:00401217                 xor     eax, edx 
                               ^^^^^^^^^^^^^^^^ 

Расшифровываем значение счетчика. Count1 на самом деле ключ, а Count2 - зашифрованный счетчик. Такой примем позволит надежно скрыть защитный механизм от неквалифицированного пользователя, вооруженного редактором реестра.

.text:00401219                 dec     eax 

Уменьшаем значение счетчика на единицу.

.text:0040122D                 test    eax, eax 
.text:0040122F                 jz      short loc_0_401296 

Очередной промах компилятора. На самом деле инструкция test eax,eax не нужна, т.к. флаг нуля устанавливается инструкцией dec eax. Как нетрудно догадаться, это и есть тот самый условный переход, который по истечении отведенных запусков приложения прекращает его работу. В качестве тренировки читателю рекомендуется самостоятельно модифицировать его так, чтобы программа работала вечно. Разумеется, можно поступить иначе и удалить инструкцию dec, - кому как нравится.

.text:00401231                 push    0 
.text:00401233                 call    ds:time 
.text:00401239                 push    eax 
.text:0040123A                 call    ds:sran 
.text:00401240                 add     esp, 8 
.text:00401243                 call    ds:rand 

Генерируем случайное число - меняем ключ шифрования после каждого запуска.

.text:00401249                 mov     edi, [esp+5Ch+var_54] ; Key 
.text:0040124D                 mov     ecx, [esp+5Ch+var_50] ; Real Count 
.text:0040125B                 xor     edi, eax 

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

res=4; 
RegQueryValueEx(hKey,"Count1",0,&TYPE,(LPBYTE) &Count1,&res); 
RegQueryValueEx(hKey,"Count2",0,&TYPE,(LPBYTE) &Count2,&res); 

Count2 =  Count2 ^ Count1; 
Count2--; 
printf("Count  %x \n",Count2); 
if (!Count2) return 0; 

srand((unsigned)time( NULL ) ); 
Count1 = (unsigned) rand(); 
Count2 =  Count2 ^ Count1; 

RegSetValueEx(hKey,"Count1",0,REG_DWORD,(CONST BYTE *) &Count1,4); 
RegSetValueEx(hKey,"Count2",0,REG_DWORD,(CONST BYTE *) &Count2,4); 

Однако программисты в своем большинстве достаточно ленивы и заняты, чтобы активно использовать подобные приемы. Чаще всего счетчики записаны в реестре "AS IS" и легко могут быть изменены редактором реестра на любое другое значение, ограниченное совестью взломщика.

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

Использовать их могут только беспечные или низкоквалифицированные разработчики. Стойкость таких защит очень низка и никак не оправдывает возложенных на них надежд.


Части: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15

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

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