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

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

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

Виртуальная машина


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

Вступление
Что же такое виртуальная машина, спросите Вы. В простейшем случае - это интерпретатор, способный переводить команды, написанные на понятном только ему языке в машинный код, понятный процессору компьютера. Вообще EXE файлы, создаваемые компиляторами содержат машинный код, иначе называемый native кодом, поддающийся дизассемблированию. Совсем немногие языки программирования способны создавать код, отличный от машинного. Оно и понятно - зачем городить огород, ведь в любом случае процессору понятен только машинный native код. Напротив, код выполняемый на виртуальной машине - это так называемый интерпретируемый код. Переходником между ним и процессором как раз и служит виртуальная машина, которую чаще всего называют интерпретатором. Первоначально основной целью идеи создания интерпретаторов было упрощение портирования программ под другие операционные системы. Но метод интерпретации стал популярен и немного в другой области. Его стали использовать авторы профессиональных защит для затруднения исследования и взлома программ крекерами. Думаете, почему программы написанные на Visual Basic’е так сложны для анализа и кряки к ним выходят довольно редко? Вся проблема в том, то Visual Basic умеет компилировать программы не только в машинный код, но и в интерпретируемый P-Code, который исполняется виртуальной машиной MSVBVM60.DLL, без которой не запустится ни одна VB программа. С другими языками программирования до недавнего времени об использовании виртуальных машин не было и речи, но теперь есть софт и для перевода кода, созданного такими средами программирования как Delphi и C++ из машинного в интерпретируемый. Но обо всем по порядку. Ниже я рассмотрю способы перевода программ, написанных в самых известных средах программирования в интерпретируемый, трудновзламываемый код. Думаю после этого Ваши программы наконец-то станут гораздо более защищены, что весьма позитивно скажется на продажах.

Виртуальная машина Visual Basic 6.0
Как я уже говорил Visual Basic имеет собственную виртуальную машину - так называемую Microsoft Visual Basic Virtual Machine 6.0 (MSVBVM60.DLL). Движок интерпретатора содержится в секции Engine этой библиотеки и работает только в том случае, если вы компилируете программу в P-Code. По умолчанию VB компилирует программы в Native Code, поэтому чтобы включить компиляцию в интерпретируемый код нужно в среде программирования зайти в меню Project -> Properties. Далее на вкладке Compiler поставить радиокнопку в положение Compile to p-code и нажать OK. Теперь программа будет компилироваться в интерпретируемый код и взломать ее будет в разы сложней. Хотя если у крэкера есть декомпилятор P-Code’а то он сможет увидеть чуть ли не исходник Вашей программы. Но тут есть нюанс - бесплатных декомпиляторов, способных декомпилировать программу до исходника нет, а коммерческие стоят настолько дорого, что крекеру они будут не по карману, да и интереса у него не будет покупать этот декомпилятор, так как программ, скомпиленных в P-Code в сети не так и много. В общем, если Вы пишете программы на VB 6.0, не забывайте об этой возможности!

Я надеюсь Вам стало интересно, что же будет в EXE файле после компиляции? Так вот, точка входа в программу будет стандартно выглядеть так:

push offset VB_Header call MSVBVM60.DLL::ThunRTMain

Отсюда следует, что любую VB программу запускает функция ThunRTMain. Единственный параметр, который ей передается - ссылка на VB_Header - структуру, полностью описывающую настройки программы, содержащиеся в ней функции и ссылки на таблицу переходников импорта, а также ссылки на массивы форм. В этих структурах активно используется COM технология описания объектов форм, понятная VB интерпретатору. Но самое главное, что нужно функции ThunRTMain из этих структур - во что откомпилирована программа. Если программа откомпилирована в P-Code, то Visual Basic подключает к работе свой интерпретатор, иначе настраивает объекты и передает управление Main функции программы. Перед интерпретированием движок настраивает также все объекты интерфейса, но Main функцию интерпретирует сам, что чрезвычайно затрудняет отладку программы крэкером, так как по сути ему приходится отлаживать не программу а ее интерпретатор. К великой радости крэкера отладчик P-Code’а существует и при всем при том бесплатен, но это несильно упрощает взлом программы, так как этот отладчик довольно глючный, а распакованные программы вообще отказывается отлаживать, потому не забудьте перед релизом запаковать программу любым EXE упаковщиком, например UPX. Так что, как говорят в рекламе: "отсюда правило" - хочешь затруднить исследование программы - компилируй ее в P-Code.

exdec

Виртуальная машина Visual Studio .NET

В новой платформе .NET на каком бы Вы языке не писали - программа будет компилироваться в интерпретируемый код. Если в VB 6.0 библиотека интерпретатора, требуемая программе после ее компиляции занимала 1.3 Mb, то здесь разработчики пошли дальше - .NET Framework занимает от 20 Mb и если его нет на машине клиента, то программы, написанные в среде .NET работать не будут. Интерпретируемый язык сильно изменился и теперь называется IL. На данный момент существуют несколько декомпиляторов этого языка, которые без особого труда переведут EXE файл программы в исходник, поэтому получается не защита программы, а как раз наоборот. Тут самое время Вас обрадовать - в сети существуют специальные продукты для запутывания .NET кода:
1. обфускаторы IL кода, способные убрать всю лишнюю информацию из EXE. После такой обработки исследовать программу будет крайне сложно даже под декомпилятором.
2. есть также программа для конвертирования IL кода в машинный native код. Но подобного рода утилиты сильно глючат и то что программа после конвертирования будет нормально работать зависит от многих факторов.
Один из таких обфускаторов также входят в состав некоторых версий Visual Studio, так что не забывайте об этой функции среды программирования.

Reflector

Интерпретирование Delphi

Конечно в VB и VS .NET с интерпретацией все хорошо, но как же быть тем, кто пишет программы на Delphi? Delphi не умеет создавать интерпретируемый код. Именно поэтому программы написанные на данном языке программирования работают быстрее программ, написанных например на Visual Basic’е. Но увы, взламывают их тоже быстрее. Так как же защитить Delphi программы от анализа и взлома? До недавнего времени я бы ответил, что никак, но теперь есть замечательная программа DotFix NiceProtect, разработанная в нашей компании. Умеет она не много не мало, а переводить отдельные процедуры твоей программы на трудноломаемый псевдокод. Мы пошли дальше стандартных интерпретируемых языков, приделав к протектору метаморфный преобразователь машинного кода для еще большего усложнения защищаемых участков программы. Конечно всю программу защищать не имеет смысла и я рекомендую ограничиться защитой только тех функций, в которых программа производит проверку пароля, активацию, разблокирование опций, установку trial’а... в общем всех тех, которые крэкеру видеть нежелательно. Теперь расскажу как использовать наш продукт. Для начала определитесь, какие функции в своей программе Вы решили перевести в P-Code. Затем открывайте Delphi, в меню Project выбираем Options и щелкаем по вкладке Linker. Там радиокнопку Map File установим в Detailed, жмем OK и можем компилировать свою программу. В папке с ней должен появиться *.map файл. Из него то и будет брать процедуры DotFix NiceProtect. Теперь, когда все готово запускайте DotFix NiceProtect, задав "Input file" щелкаем по разделу "Code Protection". Если все сделано правильно должен появиться список процедур, используемых в программе. Там будет куча ненужных процедур, созданных компилятором и ближе к концу списка будут написанные Вами. Выбирайте те функции, которые хотите защитить и помечайте галочками. Единственное что следует иметь ввиду: виртуализация поддерживается не для всех конструкций ассемблерного кода и имеются ограничения, ввиду этого защищена будет функция не от начала до конца а до первой незнакомой конструкции. Потому для более тщательной защиты, рекомендую ознакомиться с маркерами, которыми рекомендуется пометить критичный код (подробнее в документации к DotFix NiceProtect). Теперь можно полазить по настройкам протектора и включить при надобности упаковку файла и прочее. Затем идем на вкладку "Protection" и жмем кнопку "Start". Все! Программа защищена. Теперь нелишне сохранить проект на будущее. Фишка сохранения в том, то при последующих компиляциях если адреса процедур в EXE файле будут меняться - DotFix NiceProtect автоматически их найдет и пересчитает адреса, если Вы новую версию программы тоже решите защитить по аналогии с предыдущей. Потому все телодвижения можно делать всего один раз, а потом использовать готовый проект. Ладно, ближе к делу - жмем кнопку "Start" и радуемся - кряки к программе появятся нескоро, а может и вообще не появятся - все зависит от того насколько грамотно Вы реализовали защиту (внешнюю в лице DotFix NiceProtect и внутреннюю). Очень рекомендую почитать мою статью Пишем профессиональную защиту

DotFix NiceProtect. Лучший друг шароварщика :)
DotFix NiceProtect

Принцип работы DotFix NiceProtect’а
Думаю не лишним будет узнать что делает DotFix NiceProtect с Вашим EXE файлом после того как нажмем на кнопку "Start". Протектор имеет встроенный дизассемблер, позволяющий проанализировать код защищаемых функций и перевести все поддерживаемые инструкции в формат, понятный только интерпретатору DotFix NiceProtect’а. Затем в программе создаются две секции. Одна содержит движок виртуальной машины и содержит интерпретатор кода, вторая секция содержит функции Вашей программы, переведенные в P-Code. А что же остается на том месте, где располагался оригинальный код защищенных функций? Там будет ссылка на вызов интерпретатора. Остальное пространство попросту не используется и заполняется мусорным кодом. Вот в общем и вся идея VM. Несмотря на то, что в принципе интерпретации ничего нового нет, он обрел новую силу только сейчас, так как стал применяться для более качественной защиты программного обеспечения.

Разрабатываем свой интерпретатор

Чтобы лучше разобраться с принципом интерпретирования напишем простой скриптовый язык, обрабатывающий команды, понятные только ему и выполняющий их используя понятные процессору команды. Думаю, Вы наверняка найдете применение этому коду, так как сейчас авторы многих программ стремятся помочь пользователю поднастроить программу под себя и нередко включают в свои продукты для этих целей скриптовые языки, которые сами и придумывают. Рекомендую и Вам не пренебрегать в своих программах использованием скриптов, так как если Вы когда-нибудь забросите программу - пользователи смогут самостоятельно расширить ее возможности за счет написания скриптов. Как альтарнативу скриптам многие используют плагины в программах, но плагины нельзя модифицировать, а это не всегда полезно. Ну вот, когда мы знаем что писать, давайте определимся с требованиями к интерпретатору скриптового языка. Во первых он должен уметь понимать не только команды, но и передаваемые им параметры, при этом параметров командам может быть передано сколько угодно - это будет определяться тем, какое число данных требуется для команды. во вторых интерпретатор должен иметь простоту добавления в него команд, с минимальными изменениями в исходном коде. В третьих - мы должны передавать интерпретатору команду и параметры одной строкой, а он должен сам понять что есть команда, а что параметры и правильно выполнить все это, верно отделив парметры друг от друга. Данные будут передаваться нашему интерпретатору в следующем виде: "команда параметр_1, параметр_2, параметр_3, ... ,параметр_n". Число параметров ограничим десятью (от 0 до 9) - думаю больше использовать нам вряд ли придется. Возьмем эти данные за основу и напишем движок интерпретатора.

procedure EngineVM(strScript: string); var //общие переменные strCommand, strParameters: string; strParametr: array[0..9] of string; sSpace: integer; i: integer; //переменные команды зашифровать strText: pointer; strLen: integer; strByte: byte; label encrypt; begin //находим первый пробел в строке sSpace:=InStr(1,strScript,#32)-1; if sSpace<>-1 then begin //все что находится до пробела считаем командой strCommand:=copy(strScript,1,sSpace); //остальное - параметры strParameters:=copy(strScript,sSpace+2,Length(strScript)); //заносим параметры записанные через запятую в массив i:=0; while InStr(1,strParameters,’,’)>0 do begin //ищем запятую sSpace:=InStr(1,strParameters,’,’)-1; //сохраняем в массив параметр от начала строки до найденной запятой strParametr:=trim(copy(strParameters,1,sSpace)); //отрезаем от строки strParameters добавленный параметр strParameters:=copy(strParameters,sSpace+2,Length(strParameters)); //увеличиваем счетчик параметров i:=i+1; end; //добавляем последний параметр отдельно, //так как после него вместо запятой конец строки strParametr:=copy(strParameters,1,Length(strParameters)); end else begin //исли пробелов в строке нет, значит у команды скрипта //нет параметров, потому за команду принимается все строка strCommand:=strScript; end; //Выполняем скрипт //сюда Вы можете добавить сколько угодно команд //команда вывода сообщения (требует 3 параметра) if strCommand=’сообщение’ then begin MessageBox(Form1.Handle,pchar(strParametr[0]),pchar(strParametr[1]),StrToInt(strParametr[2])); //команда шифровки строки и вывода шифрованной строки на экран end else if strCommand=’зашифровать’ then begin strText:=pointer(strParametr[0]); strByte:=StrToInt(strParametr[1]); strLen:=Length(strParametr[0]); asm mov ecx, strLen mov eax, strText dec eax mov bh, strByte @encrypt: xor byte ptr[eax+ecx],bh loop @encrypt end; ShowMessage(strParametr[0]); end else begin ShowMessage(’Invalid command’); end; end;

VM Engine

Несмотря на то что в коде интерпретатора откомментировано практически все - я рассмотрю некоторые участки поподробнее для более глубокого понимания. Первое, что Вы встретите непонятное - это функция InStr. Такой функции нет в актуальной на момент написания статьи версии языка Delphi, она присутствует только в Visual Basic’е и служит для нахождения подстроки в строке. В отличии от дельфевой функции Pos рассматриваемая функция позволяет искать подстроку не только с начала строки, но и начиная с любого символа, что для нас крайне полезно так как мы ищем по очереди запятые в строчке скрипта чтобы отделить все параметры скриптовой команды и занести их в массив. Тут я не стал сильно заморачиваться и написал аналог VB’шной функции InStr на Delphi. Функция эта очень проста, так что думаю Вы разберетесь с ней сами - вот она:

function InStr(index: integer; str1: string; str2: string): integer; var i,len, pos: integer; begin pos:=0; len:=length(str2); for i:=index to length(str1) do begin if copy(str1,i,len)=str2 then begin pos:=i; break; end; end; result:=pos; end;

Здесь Index - номер символа строки Str1, с которого начинать искать строчку Str2. Рекомендую использовать эту функцию в своих программах, как лучшую на мой взгляд замену Pos. Еще может возникнуть вопрос, для чего нужен препроцессинг параметров и занесение их в массив. Все просто - для удобства работы с параметрами в дальнейшем.
Теперь перейдем к самому интересному куску кода. Среди кучи нормальных Delphi команд красуется небольшой листинг:

strText:=pointer(strParametr[0]); strByte:=StrToInt(strParametr[1]); strLen:=Length(strParametr[0]); asm mov ecx, strLen mov eax, strText dec eax mov bh, strByte @encrypt: xor byte ptr[eax+ecx],bh loop @encrypt end;

В ассемблерном отладчике Delphi очень наглядно видно, что ассемблерная вставка в нашей функции присутствует практически без изменений

VM Engine Assembler

Ассемблерная вставка была использована по двум причинам: для удобства (на ассемблере написать простенькую шифровку намного проще, чем на Delphi) и для того, чтобы Вы смогли наглядно представить перевод скриптовых команд в понятный процессору машинный код. Думаю не лишним будет рассмотреть данный код поподробнее. Начнем с регистров процессора eax, ebx и ecx. Они служат для хранения 4 байтных данных. Чтобы получить доступ к 2 младшим байтам например регистра ebx - нужно обращаться к регистру bx (соответственно для eax - ax, ecx - cx). Если нам нужно использовать только старший или младший байт 2 байтного регистра bx - используем соответственно регистры bh и bl. В нашем конкретном случае каждый байт строки ксорится с байтом, переданным во втором параметре скриптовой командой, потому для хранения этой команды мы используем однобайтный регистр bh. @encrypt - это метка. Собака перед именем метки ставится для того, чтобы указать компилятору что метка локальная, а не глобальная для всей процедуры нашего интерпретатора. Командам ассемблера передаются 2 операнда или значение и операнд. Команда mov перемещает в регистр, являющийся первым операндом содержащиеся во втором операнде данные. Данные во втором операнде должны быть представлены либо регистром либо числом, переданным напрямую. Главное о чем нужно всегда помнить, что размер данных должен соответствовать или быть меньше размера регистра. Команда dec имеет дело с одним операндом, который должен быть представлен исключительно в виде регистра. Эта команда уменьшает число, записанное в передаваемом регистре на 1. Последовательность encrypt и loop encrypt представляет собой цикл. Счетчик цикла записывается в регистр ecx. Команда xor byte ptr[eax+ecx],bh означает что нужно отXORить byte по адресу в памяти равному eax+ecx с байтом который содержится в регистре bh. Так как ecx содержит длину строчки, а eax - адрес этой строчки в памяти, то учитывая автоматическое уменьшение ecx (так как этот регистр является счетчиком цикла) можно определить что строчка шифруется с конца. Так оно и есть - процессору так проще. Мы конечно можем добавить пару лишних команд чтобы шифровка производилась с начала строки, но зачем усложнять программу и тратить на это лишние такты работы процессора?
Теперь, когда думаю Вы во всем алгоритме интерпретатора разобрались - добавим на форму Memo и кнопку. В Memo мы будем вводить команды движку, а при нажатии на кнопку они будут циклично выполняться интерпретатором. Собственно обработчик нажатия на эту кнопку будет выглядеть так:

procedure TForm1.Button1Click(Sender: TObject); var i: integer; begin for i:=0 to Memo1.Lines.Count-1 do begin //вызываем интерпретатор EngineVM(Memo1.Lines.Strings); end; end;

Теперь осталось добавить в движок побольше команд, сделать возможность возвращения командами результата, ввести макросы, переменные и кучу всего интересного.

Заключение

Надеюсь у Вас не осталось вопросов по интерпретированию и принципам защиты программ этим методом. Если же они все же есть, а также есть желание углубленно все это дело изучить - почитайте и другие статьи на данном сайте. Поняв ассемблер и зная один из языков высокого уровня типа Delphi или C++ Вы легко освоите принцип работы виртуальных машин. Удачи!


Комментарии

отсутствуют

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


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

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

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

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