Техника и философия хакерских атак
Продолжение...
Предложенный алгоритм требует быстрого процессора для получения удовлетворительной скорости перебора паролей. Переписав эту программу на ассемблере, мы добьемся значительного выигрыша в скорости, однако для получения приемлемого быстродействия необходим эффективный алгоритм. Поскольку это не имеет прямого отношения к обсуждаемой тематике, то здесь не рассматривается. Скажу лишь, что, используя древовидный поиск и упреждающую логику, можно увеличить скорость в десятки раз.
Но даже если алгоритм реализован без ошибок, вскоре будет найден не единственный верный пароль. Например:
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