Фрагмент из первого издания книги "Техника и философия сетевых атак" — Архив WASM.RU
Новый рубеж
Мир давно привык к тому, что популярные технологии вовсе не обязательно бывают хорошими. Именно так произошло и в области защиты условно-бесплатного программного обеспечения. Наибольшее распространение получила защита, основанная на регистрации клиента. Слабость этого механизма в том, что регистрационный код, генерируемый на основе имени пользователя, может быть проверен единственно возможным способом: аналогичной генерацией и последующей сверкой.
Т.е. имеются два полностью идентичных генератора - у автора в виде отдельного приложения и в защитном механизме. Таким образом, остается только извлечь из защищенного приложения эту процедуру и обеспечить удобный обмен данными с пользователем. Иначе говоря, написать собственный генератор регистрационных номеров.
Все, что способен сделать автор защиты, - затруднить анализ и извлечение защитного механизма. Первое осуществляется оригинальными приемами программирования, специальными антиотладочными приемами; а второе - "размазыванием" кода по десяткам процедур, активным использованием глобальных переменных и взаимодействия с разными фрагментами кода.
Наверное, излишне говорить, что запутывание алгоритма малоэффективно и больше похоже на "ребячество", антиотладочные приемы бессильны против современных отладчиков; кроме того, их очень трудно полноценно реализовать на языках высокого уровня.
Чаще всего нет никакой нужды тратить время на анализ генератора, когда его можно просто "выкусить" и перенести в свой код, а потом передать необходимые параметры. Однако этому легко помешать. Действительно, если организовать генератор не в виде локальной процедуры, заключающей в себе весь необходимый код, а в виде множества процедур со сложным взаимодействием и неочевидным обменом данных, то без анализа архитектуры защиты (и выделения всех относящихся к ней компонентов) копирование кода невозможно.
Поэтому предпочтительнее все же первый метод. Кроме того, он не вступает в противоречие с законодательными ограничениями, тогда как любой фрагмент чужого кода в вашей программе называется плагиатом и законом уже наказывается. Чтить же уголовный кодекс хакеры должны в первую очередь.
Рассмотрим простую реализацию данного механизма защиты на примере программы file://CD/SRC/CRACK04/Crack04.exe
До сих пор мы пользовались дизассемблером для изучения кода программ. Но это не единственный возможный подход к задаче. Не меньшим успехом у хакеров пользуются отладчики. Однако отладка более агрессивный способ исследования. Необходимо постоянно помнить, что "операция" осуществляется "вживую" и возможны любые нюансы. Антиотладочный код может "завесить" систему или сделать то, чего вы никак не ожидаете. Однако становятся доступными многие возможности, о реализации которых в дизассемблерах можно только мечтать. Например, контрольные точки останова, которыми мы чуть позже с успехом и воспользуемся.
Самым популярным на сегодняшний день отладчиком является Soft-Ice от NuMega. Это очень мощный профессиональный инструмент. Новички часто испытывают большие трудности при его настройке, поэтому в приложении подробно описывается, как это сделать.
Разумеется, никто не ограничивает свободу читателя в выборе отладчика, однако в настоящее время не существует программ, которые могли бы составить реальную конкуренцию Софт-Айсу. Это вовсе не означает, что другие программы не пригодны для взлома. Большинство из них могут решать рядовые задачи с не меньшим успехом, а узкоспециализированные - в своей области заметно обгоняют Айс. Но уникальность Айса в том, что он покрывает рекордно широкий круг задач и платформ. Кроме того, очень приятен и удобен. Я не знаю ни одного другого отладчика, поддерживающего командную строку.
Однако обо всех преимуществах не расскажешь в двух строках, поэтому рассмотрим его в действии. Запустим исследуемое приложение. Программа просит нас ввести имя и регистрационный номер. Попробуем набрать что-нибудь "от балды".
Разумеется, ничего не получается, и таким способом, скорее всего, программу зарегистировать никогда не удастся. На это и рассчитывал автор защиты. Однако у нас есть приимущество. Знания ассемблера позволяют заглянуть внутрь кода и проанализировать его алгоритм.
Конкретно нас интересует механизм генерации регистрационных номеров. Как обнаружить его в изучаемом коде? Один из самых легких способов - отследить обращение к введенной строке. Код, читающий ее, очевидно, либо непосредственно входит в генератор, либо лежит в непосредственной близости. Остается только узнать, по какому адресу строка расположена в памяти.
Хорошая задачка! Откуда же узнать этот адрес? Неужели придется утомительно анализирать код? Разумеется, нет. Существуют гораздо более оригинальные приемы. Начнем с того, что содержимое окна редактирования надо как-то считать. Для этого нужно послать окну сообщение 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. Как узнать адрес переданного ей буфера? Разумеется, через стек. Рассмотрим ее прототип:
Код (Text):
int GetWindowText( HWND hWnd, // handle to window or control with text LPTSTR lpString, // address of buffer for text int nMaxCount // maximum number of characters to copy );Следовательно, стек будет выглядить так:
Код (Text):
----------------------¬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. Теперь любой обращающийся к строке код будет принадлежать непосредственно защите. Мы, вероятно, уже находимся в непосредственной близости от защитного механизма, но иногда бывает так, что программист читает строку в одном месте программы, а использует результат совсем в другом. Поэтому дождемся повторного отладочного исключения. Рассмотрим код, вызвавший его:
Код (Text):
015F:004015F7 8A0C06 MOV CL,[EAX+ESI]Используемая адресация наталкивает нас на мысль, что eax, возможно, параметр цикла, а вся эта конструкция посимвольно читает строку. Очень похоже, что в самом центре генератора серийного номера. Если мы посмотрим чуть-чуть ниже, то в глаза бросится очень любопытная строка:
Код (Text):
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 куда хуже прикладных программистов. Воистину, тут подходит фраза: "умение снять защиту еше не означает умения ее поставить".
На этом фоне популярность такого подхода выглядит загадочной. Нельзя сказать, что авторы защит не представляют, насколько легко ее вскрыть. Подтвеждением являются просьбы (особенно у российских программистов) к кракерам не ломать защиту, а зарегистрироваться и способствоать развитию отечественного рынка. Иной раз настолько красноречивые и длинные, что за время, потраченное на сочинение подобных опусов, можно было бы значительно улучшить реализацию защиты, что несомненно дало бы гораздо больший эффект.
Однако мы не закончили взлом программы. Да, мы узнали регистрационный код для нашего имени, но понравится ли это остальным пользователям? Ведь каждый из них хочет зарегистрировать программу на СЕБЯ. Кому будет приятно видеть чужое имя?
Вернемся к коду, сравнивающему эти строки:
Код (Text):
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 генерируют идентичный код. Поэтому написать одну и ту же процедуру, но так, чтобы ни в одном из вариантов не было повторяющихся фрагментов кода, представляется очень нетривиальной задачей.
Другим возможным решением помимо изменения кода программы (которое далеко не приветствуется законом) может быть написание собственного генератора регистрационных номеров или, в просторечии, ключеделки. Для этого необходимо проанализировать алгоритм генератора и написать аналогичный самостоятельно.
Вернемся немного назад:
Код (Text):
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 Посмотрим, что у нас получилось:
^^^^^^^^^^^^^^^^^^^^^^Код (Text):
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):
.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):
.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):
.text:004015F7 loc_0_4015F7: .text:004015F7 mov cl, [esi+eax]Загрузка очередного символа строки. Поскольку eax - содержит базовый адрес, то очевидно, что esi - смещение в строке. Выше видно, что начальное значение его равно нулю. Логично, что строка обрабатывается от первого до последнего символа, хотя часто бывает и наоборот.
Код (Text):
.text:004015FA movsx ax, byte ptr [esi+eax+1]MOVe and Sign eXtension (пересылка со знаковым расширением) загружает байт в регистр AX, автоматически расширяя его до слова.
Код (Text):
.text:00401600 movsx cx, clОбратим внимание на несовершенство компилятора. Эту команду можно было записать более экономно как movsx cx, [esi+eax]
Код (Text):
.text:00401604 imul eax, ecxПодставим всесто регистров их смысловые значения и получим String[idx]*String[idx+1].
Код (Text):
.text:00401607 and eax, 0FFFFhПреобразуем eax к машинному слову.
Код (Text):
.text:0040160C mov ecx, 20h .text:00401611 cdqCDQ - Convert Double word to Quad word - Преобразование двойного слова в счетверенное слово
Код (Text):
.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):
.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):
.text:0040162D inc esiПеремещаем указатеь idx на следующий символ в строке.
Код (Text):
.text:0040162E mov ecx, [eax-8] .text:00401631 dec ecx .text:00401632 cmp esi, ecx .text:00401634 jl short loc_0_4015F7Очевидно, что эти строки также относятся к циклу for. Поэтому уже можно восстановить исходный код генератора.
Код (Text):
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). Без текстовых строк исполняемый файл занимает менее пятидесяти байт и еще оставляет простор для оптимизации. Ключевая процедура может выглядеть так:
Код (Text):
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 есть одно несущественное упущение. Он не проверяет минимальную длину строки. Но на деле это не вызывает больших неудобств, зато экономит пяток байт кода.
Генератор успешно работает и вычисляет правильные регистрационные номера. Теперь можно начинать его публичное распространение. Отметим, что последнее совершенно не запрещено законом. И ничьих прав не ущемляет. Использование же генераторов все же вызывает конфликтную ситуацию, т.к. пользователь вводит поддельный регистрационный номер. С другой стороны, это недоказуемо, т.к. сгенерированные номера ничем не отличаются от настоящих. Тем не менее я категорически не советую уповать на это. Лицензиозные соглашения пишутся не для того, чтобы их нарушать. Точно так же и создание собственного генератора не должно побужать к его использованию, отличному от познавательного. Перечислите автору требуемую сумму или откажитесь от использования программы. Истинный хакер так и поступит. В этом и заключается его отличие от кракеров. Хакер по определению первоклассный специалист, который всегда заработает на необходимое программное обеспечение (или, если он действительно хакер, то напишет свое).
Я понимаю, что такая трактовка может встретить возражение. Действительно, зачем что-то ломать, если хакер все равно должен приобретать лицензиозный софт? Но разве в этом есть что-то нелогичное? Хакер - это взрослый ребенок, удовлетворяющий свое любопытство. Конечно, очень трудно, обладая такими знаниями и навыками, удержаться от соблазна нарушить закон. Более того, я не знаю ни одного человека, который поступал бы именно так. Увы, хакерство действительно оказывается тесно связанным с криминалом. Это, к сожалению, так. © Крис Касперски
Фрагмент из первого издания книги "Техника и философия сетевых атак"
Дата публикации 11 июн 2003