Современные решения

для защиты Windows приложений

и восстановления исходного кода
Автор: Сергей Чубченко. Дата публикации: 03.05.2005

Пишем профессиональную защиту


Ликбез о защите программ на Visual Basic

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

Языки программирования

Как известно, про защиту программ, написанных на Delphi и C++ рассказано уже довольно много. Большинство авторов уже сошлись на мнении, что лучше всего для них использовать всем известные навесные защиты (например наш DotFix NiceProtect. Последние версии современных защит настолько серьезно защищают таблицу импорта и код, что можно считать их отличным средством для защиты Delphi и C++ приложений. Совсем иначе обстоит дело с программами на Visual Basic 6.0. Навесные защиты пока не научились защищать VB-код и снимаются не сложнее упаковщиков (на момент публикации статьи), что сильно огорчает VB разработчиков. Давайте разберемся, с чем же это связано. Начнем с таблицы импорта, то есть, с той самой таблички, что хранит адреса и имена вызываемых программой функций. Она в основной своей массе вызывает не стандартные API функции, а их аналоги из библиотеки MSVBVM60.DLL. Это, вместе с необходимостью подстраиваться под все версии VB (а каждая версия VB привязывает создаваемое приложение к собственной версии рантайм-библиотеки MSVBVMXX.DLL, где XX - номер версии VB), создает большие проблемы для навесной защиты. Не будем вникать в проблемы защиты импорта, а поговорим немного про защиту методом "спертых байт", которую в последнее время используют многие навесные защиты. Она представляет собой перемешивание с мусором части кода программы (обычно - несколько десятков байт от точки входа) и мешает крэкеру восстановить программу после снятия с нее навесной защиты. Этот метод также не сработает в программах, написанных на VB, потому что на точке входа в программу можно замусорить лишь 2 ассемблерные инструкции: "push <смещение кода программы>" и "call <MSVBVM60.ThunRTMain>", остальное инициализируется самой функцией ThunRTMain, и перед ее вызовом все должно быть в незашифрованном виде. В основном, именно это не дает создать нормальную навесную защиту для Visual Basic программ. Ниже я расскажу про наиболее сложные для взлома способы защиты, которые Вы можете реализовать сами в своих программах. Итак, приступим.

Метод "глючной арифметики"

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

Генерация пароля из имени пользователя

’циклично шифруем каждый символ имени пользователя числом n, которое варьируется от 0 до 10 Public Function GetPass(sName As String) Dim n As Byte For i = 1 To Len(sName) sPass = sPass & Hex(Asc(Mid$(sName, i, 1)) Xor n) n = n + 1 If n > 10 Then n = 0 Next GetPass = sPass End Function

Получение имени пользователя из пароля

Public Function GetName(sPass As String) Dim n As Byte For i = 1 To Len(sPass) Step 2 sName = sName & Chr(Val("&H" & Mid$(sPass, i, 2)) Xor n) n = n + 1 If n > 10 Then n = 0 Next GetName = sName End Function

Как видно из функций - они похожи и это является одним из свойств логической операции XOR - эта операция полностью обратима. Вторая функция в данном случае отличается от первой лишь преобразованием HEX в CHR (без этого не обойтись, так как если мы не будем преобразовывать пароль в HEX в первой функции - в нем могут появиться непечатаемые символы, что явно не понравится конечному пользователю. Теперь, когда пользователь введет пароль и имя, мы можем легко проверить правильность этих данных, сгенерировав пароль функцией GetPass по имени и сравнив с паролем, что ввел пользователь. В случае различия кодов мы можем вывести сообщение об ошибке. Но это вступление. Создадим глобальные переменные strName и strPass в разделе объявлений любого модуля:

Public strName as string Public strPass as string

И занесем в них имя и пароль, введенные пользователем. Зачем это нужно? Во всех расчетах в программе, в конце вычисления, мы будем плюсовать результат вычитания первого (введенного пользователем) и второго (который нам вернет процедура GetPass) пароля. Что это нам даст? Если пароли равны, то плюсоваться будет ноль и результат вычисления не изменится, в противном случае программа попросту начнет работать не так как нужно, поскольку результат ее работы будет неверным. Вот небольшой пример использования данного метода, если нам нужно посчитать произведение числа 2 на 2:

srtResult=(2*2)+(val("&H" & GetPass(strName))-Val("&H" & strPass))

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

frmMain.Show (val("&H" & GetPass(strName))-val("&H" & strPass))

Если параметр будет не ноль и не написать что-нибудь типа "on error resume next", то программа может просто вызвать недопустимую операцию после взлома, так как в качестве параметра для функции загрузки формы в случае неверного пароля может передаваться что угодно. Однако, этот способ защиты с трудом, но можно обойти. Ниже я рассмотрю действительно сложные алгоритмы, которые, тем не менее, желательно использовать совместно с рассмотренным методом "глючной арифметики".

Вызов функции по имени

К примеру, нам нужно, чтобы пункт "Сохранить" в программе был доступен только после регистрации ее пользователем. Для этого пишем отдельно функцию сохранения и именуем ее, к примеру "save" (почему выбрано такое маленькое имя, станет понятно дальше). Теперь нам нужно, чтобы из имени пользователя можно было получить слово save. Простейший способ - это использовать уже знакомый нам XOR. Для этого будем ксорить имя пользователя с этим словом побайтно:
user name
XOR
savesaves
Если имя пользователя больше 4 символов, мы просто нарастим второй параметр криптовки (слово "save") до нужного нам размера (минимальное имя пользователя - 4 символа). Результат XOR’а это и есть пароль, который мы дадим пользователю, когда он купит нашу программу. Как известно, операция XOR обратима, то есть мы легко можем из имени и пароля получить обратно строку "savesaves", проксорив имя с паролем. Саму же строчку "save" мы легко получим, считав первые 4 символа. Надеюсь, Вы еще не забыли, что введенные пользователем данные желательно хранить в глобальных переменных? Так вот, в обработчик кнопки "Сохранить" мы напишем следующее:

CallByName frmMain, GetFunction(strName, strPass), vbMethod

callbyname здесь – это весьма позитивная штука, поскольку данный оператор присутствует только в Visual Basic’е и не имеет аналогов ни в Delphi, ни в C++. Служит он для вызова функции по ее имени, которое может храниться где угодно, включая переменные. Первым параметром данной функции служит объект, который содержит вызываемую нами функцию, вторым - имя функции и третьим - тип функции (vbGet, vbLet, vbMethod, vbSet). Имя функции мы будем получать из имени пользователя и его пароля функцией GetFunction. Соответственно, если мы проXORим верное имя с паролем, то первые четыре символа будут именем функции - их и возвратит функция GetFunction.

Public Function GetFunction(xStringToCrypt, xStringKey) ’если имя функции меньше имени пользователя - нарастим If Len(xStringKey) < Len(xStringToCrypt) Then For i = 1 To Len(xStringToCrypt) xStringKey = xStringKey & xStringKey If Len(xStringKey) > Len(xStringToCrypt) Then Exit For Next xStringKey = Mid$(xStringKey, 1, Len(xStringToCrypt)) ’иначе урезаем имя функции :) - это недопустимо - сделай проверку этого сам Else xStringKey = Mid$(xStringKey, 1, Len(xStringToCrypt)) End If ’шифруем For i = 1 To Len(xStringToCrypt) sCrypt = Asc(Mid$(xStringKey, i, 1)) Xor Asc(Mid$(xStringToCrypt, i, 1)) sCryptedString = sCryptedString & Chr(sCrypt) Next sRepeateString = InStr(2, sCryptedString, Left$(sCryptedString, 4)) If sRepeateString > 0 Then sCryptedString = Left$(sCryptedString, sRepeateString - 1) GetFunction = sCryptedString End Function

В противном случае GetFunction может возвратить что угодно, но не "save", при этом программа сглючит. Чтобы этого не произошло - напишем свой код так:

On Error GoTo test CallByName frmMain,vbMethod, GetFunction(strName, strPass) Exit Sub test: msgbox "Пароль неверный"

Теперь программа просто выведет сообщение, в случае если пароль неправильный. И пусть крэкер ломает функцию "on error" чтобы сообщение не выводилось - все равно без пароля программа работать не будет. Небольшое предостережение: если в Вашей программе функций мало, то крэкер оттрассирует перебираемые функцией callbyname варианты и методом подбора найдет нужную функцию, подставит в процедуру генерации пароля и получит код. Поэтому эту защиту есть смысл применять только в больших проектах, где функций несколько десятков или сотен и все перебрать крэкеру будет просто лень.

Test Protection 1 by DotFix Software

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

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

Пароль - функция на ассемблере

Вот мы и дошли до самого интересного. А что, если в качестве пароля использовать ассемблерную функцию, возвращающую одну из составляющих имени пользователя, например, ASCII-код второго символа имени? Неплохо, но что это нам даст? Это нам позволит сравнить пароль пользователя с результатом работы ассемблерной функции. В тоже время имя пользователя мы можем использовать в качестве ключа для шифровки нашей ассемблерной функции от чужих глаз. Я для этих целей использую алгоритм blowfish. У этого алгоритма в VB6 есть одна особенность - в качестве ключа для шифровки он принимает только цифры, поэтому проще шифровать не всем именем пользователя, а например его контрольной суммой. Это я думаю, Вы реализуете сами, здесь же для простоты мы будем шифровать ASCII-кодом третьего символа имени пользователя. То есть - пользователь вводит имя и пароль, мы декриптуем пароль третьим символом имени и запускаем полученную в результате декриптовки ассемблерную функцию с помощью API функции CallWindowProc. Функция должна нам возвратить ASCII-код второго символа имени. Если это так - пользователь ввел верный пароль, иначе, если в качестве пароля был введен просто мусор - произойдет ошибка либо при декриптовке, либо при вызове этого мусора и программа вызовет недопустимую операцию. От этого нас спасет уже известный нам On Error GoTo test. Хотя передача мусора непосредственно в CallWindowsProc несколько нежелательна (процессору на исполнение пойдет мусор - что может привести к зависанию ранних версий Windows, например 98). Но в 99% случаев ввод пользователем мусора приведет лишь к ошибке в функции декриптовки и до исполнения ассемблерного кода дело не дойдет. Сама же функция проверки в общем виде представлена ниже.

’получим ассемблерный код sASM = BlowFish.DecodeString(strPass, Asc(Mid$(strName, 3, 1))) ’переведем его в массив байт ’такой функции нет в бейсике, ее Вы найдете в следующем листинге ’или можете написать самостоятельно, благо это дело пяти минут Call ToBytes(sASM) ’вызываем ассемблерную функцию, для этого передаем API функции 'CallWindowsProc адрес на первый байт нашего ассемблерного кода sASCII = CallWindowProc(VarPtr(bytes(0))) ’сравниваем If sASCII = Asc(Mid$(strName, 2, 1)) Then MsgBox "Пароль верный" Else MsgBox "Пароль неверный" End If

Private Sub ToBytes(strBin As String) For i = 0 To Len(strBin) - 1 Bytes(i) = Asc(Mid$(strBin, i + 1, 1)) Next End Sub

Функция представлена в общем виде. Для ее работоспособности нам потребуется объявить API функцию CallWindowsProc:

Public Declare Function CallWindowProc Lib "user32" Alias "CallWindowProcA"
(ByVal lpPrevWndFunc As Long, ByVal hWnd As Long, ByVal Msg As Long, ByVal wParam As Long,
ByVal lParam As Long) As Long

и глобальный массив bytes:

Public bytes() as byte

в разделе объявлений программы, а также - подключить класс модуль blowfish и объявить его так:

Dim BlowFish As New clsBlowFish

Собственно сама ассемблерная функция должна иметь вид:

[bits 32] mov eax, 12 ret

12 - возвращаемый символ, он передается в регистр eax и должен задаваться генератором ключей для Вашей программы в зависимости от второго ASCII-символа имени пользователя. С генератором ключей придется немного потрудиться, так как потребуется написать функцию, которая будет изменять этот символ, перекомпилировать ассемблерную программу и шифровать ее. Можно сделать проще: просто откомпилировать один вариант ассемблерной функции, дизассемблировать его и поглядеть, где 12 заносится в eax и пусть генератор ключей меняет этот байт, а не перекомпилирует все заново. Теперь осталось разобраться, как же откомпилировать эту функцию в машинный код? Для этого лучше использовать компилятор ассемблера nasm, так как он умеет создвать не EXE, а BIN-файлы. Этот BIN-файл мы и будем криптовать BlowFish’ем и при вводе этой криптованной строки пользователем дешифровывать ее и заносить в массив байт. Чтобы пользователю удобнее было вводить пароль, желательно шифровать функцию с установкой параметра HEX в true, тогда BlowFish будет возвращать шестнадцатеричные коды байт. При этом пароль увеличится в 2 раза, но будет состоять только из цифр и букв от A до F.

Test Protection 2 by DotFix Software

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

Подробнее о вставке ассемблерных процедур в код на Visual Basic можно прочитать в 2 моих статьях на эту тему на данном сайте. Первая статья размещена здесь: Ассемблер в VB6 часть 1, вторая тут: Ассемблер в VB6 часть 2.

DotFix LiteProtect by DotFix Software
Окно регистрации нашей программы DotFix LiteProtect. При защите своих проектов я использую многие методы сразу - это увеличивает стойкость защиты.

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

Вывод

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

Модуль со всеми описанными в статье функциями можно скачать по ссылке

INFO

Еще советую шифровать строки в программах, для этого можно использовать наш продукт VB AntiCrack.

WARNING

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


Комментарии

Добавил: gl.sys Дата: 03.05.2005

Метод "глючной арифметики" на самом деле не так хорош как пишет автор. А если легальный пользователь ошибется при вводе серийника?

Добавил: GPcH Дата: 05.05.2005

Ты наверное плохо прочитал статью. Для избежания проблем перед использованием "глючной арифметики" нужно обрабатывать ошибки. В данном конкретном случае можно использовать полную фильтрацию:
on error resume next
При этом любая ошибка не приведет к краху программы. Если отключать проверку ошибок нет желания - можно ввести дополнительные проверки серийника. В любом случае все ошибки можно обработать и на легального пользователя это не повлияет, а вот на того, кто скачает кривой кряк может и повлияет, но тут уже никаких гарантий, так как кряк писать будешь уж точно не автор, потому и никаких гарантий работоспособности взломанной программы автор давать не может.


Добавление комментария


Ваше имя (на форуме):

Ваш пароль (на форуме):

Комментарии могут добавлять только пользователи,
зарегистрированные на форуме данного сайта. Если Вы не
зарегистрированы, то сначала зарегистрируйтесь тут

Комментарий: