Безопасность в Дельфи

       

Миграция птиц


Раздел "Анти крэковые мучения" Анатолий Орлов aka Anatolix,
06.03.2001

Смысл названия статьи смогут понять только те,
кто в свое время играл в замечательную игру
Betrayal At Krondor.

Вместо предисловия.

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

Стандартное отречение.

Данная статья (надеюсь) поможет вам создать достаточно сильную защиту. Однако вы не должны забывать, что общая стойкость защиты определяется самым слабым ее звеном. Так что данные методы не в коем случае не являются самостоятельной системой защиты и должны использоваться в дополнение к другим средствам. Большая часть информации, приведенной здесь, получена в результате собственных исследований и, фактически, является мнением автора. Так что за найденные ошибки просьба больно не пинать. Хотя мягкие и нежные пинки разрешаются и приветствуются в "обсуждении статьи" или Anatolix@narod.ru

Глава 1. Контрольные суммы.

Наверное, все из вас знают, как читать тело программы из памяти.

void Func1() { // Функция чье тело читается // Символизирует собой проверку пароля ShowMessage("Hello, World !"); } void EndOfFunc1() { // Функция символизирующая собой конец первой функции. } void __fastcall TForm1::Button1Click(TObject *Sender) { int i=0; for(unsigned char* foo=(unsigned char*)Func1;foo &lt (unsigned char*)EndOfFunc1;foo++ ) { i+=*foo; } ShowMessage(i); } //--------------------------------------------------------------------------- void __fastcall TForm1::FormCreate(TObject *Sender) { Func1(); }

Это становится возможным из-за того, что Win32 реализует собой, так называемую, Flat Memory Model, где данные и код фактически находятся в одном адресном пространстве. Что и позволяет читать тело функции как данные просто преобразовав указатель на функцию в другой тип.

Как вы видите, пример, фактически, реализует собой подсчет простейшей контрольной суммы. Теперь необходимо убедиться что контрольная сумма считается корректно. Т.е. исключить такую ситуацию, что на одной машине контрольная сумма будет одна, а на другой другая. На контрольную сумму потенциально способно влиять две вещи: Relocation и Import.

Relocation

В общем случае в Exe/Dll модулях присутствует, так называемая, Relocation Table содержащая в себе поправки, которые Win32 сделает к машинному коду в случае загрузки его не в то место, которое предполагалось ранее. Если вы загляните в Опции Линкера, то вы увидите такой параметр, как ImageBase. Фактически, это адрес, по которому должен загружаться Exe.

Все смещения в машинном коде рассчитаны на то, что программа будет загружена именно в указанный здесь адрес (обычно 0x400000 т.к. Windows 9X не может загружать ниже этого адреса). В случае, если система не может загрузить модуль по этому адресу, она загружает его по другому адресу и применяет Relocation Table для изменения ссылок (причиной невозможности загрузки может быть то, что адрес уже занят, или показывает куда-нибудь, куда загрузить в принципе невозможно (например, меньше 0x400000 в Win9X))

Естественно, при применении Relocation Table изменится машинный код и посчитанная нами контрольная сумма будет уже не правильной.

Проблема, вообще говоря, в данном случае решается сама собой т.к. C++ Builder(и DELPHI наверняка тоже, хотя сам не проверял), по умолчанию создают EXE вообще без Relocation Table (т.е. неперемещаемые). Это легко проверить, просто откомпилировав любой проект с ImageBase=0x10000 и запустив его на Win9X. Windows его запустить не сможет, в прямой форме потребовав Relocation Table.

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

Таким образом, можно не обращать внимания на Relocation Table и не бояться, что она испортит контрольную сумму, в случае если вы считаете контрольную сумму Exe-файла. Во всех Dll Relocation Table присутствует и активно используется (хотя опять же, при желании можно ее убрать, но тогда возникнут проблемы другого сорта), так что считать контрольную сумму dll не рекомендуется.

Import

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

Действия Cracker'а

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

Посмотрите на кусок листинга, который произвел IDA. Умная программа проследила все ссылки на процедуру(XREF), а именно: вызов из FormCreate и получение адреса в Button1Click

sub_401844 proc near ; CODE XREF: _TForm1_FormCreate p ; DATA XREF: _TForm1_Button1Click+13 o var_28 = dword ptr -28h var_18 = word ptr -18h var_C = dword ptr -0Ch var_4 = byte ptr -4 push ebp mov ebp, esp add esp, 0FFFFFFD8h mov eax, offset unk_403344 call sub_40216C mov [ebp+var_18], 8 mov edx, offset aHelloWorld ; "Hello, World !" lea eax, [ebp+var_4] call sub_402214 inc [ebp+var_C] mov eax, [eax] call j_@Dialogs@ShowMessage$qqrx17System@AnsiString ; Dialogs::ShowMessage(System::AnsiString) dec [ebp+var_C] lea eax, [ebp+var_4] mov edx, 2 call sub_4022CC mov ecx, [ebp+var_28] mov large fs:0, ecx mov esp, ebp pop ebp retn sub_401844 endp

После этого подозрение, естественно, падет на Button1Click, и ее код тоже будет приведен в соответствие.

Противодействие

Необходимо каким-нибудь хитрым методом запутать дизассемблер. Это не очень сложно, т.к. до интеллекта человека ему далеко. Здесь я не имею в виду стандартные способы запутывания дизассемблеров типа pop Address; ret;. Я просто хочу, чтобы он просто не отследил ссылку на процедуру, и хочу сделать это средствами языка. В начале я пытался сделать что-то типа этого

void Func1() { // Функция чье тело читается // Символизирует собой проверку пароля ShowMessage("Hello, World !"); } void EndOfFunc1() { // Функция, символизирующая собой конец первой функции. } int Qwe=(int)Func1+12345; int Asd=(int)EndOfFunc1+12345; void __fastcall TForm1::Button1Click(TObject *Sender) { int i=0; for(int foo=Qwe;foo &lt Asd;foo++ ) { i+=*(unsigned char*)(foo-12345); } ShowMessage(i); } //--------------------------------------------------------------------------- void __fastcall TForm1::FormCreate(TObject *Sender) { void (*foo)()=(void (*)())(Qwe-12345); foo(); }

Я считал, что умный компилятор выполнит операции сложения во время компиляции, и дизассемблер не разберется (например, когда вы пишите int foo=2+3, хитрый оптимизирующий компилятор сразу пишет foo=5). Действительно, ссылки на TForm1::Button1Click и TForm1::FormCreate исчезли, но вместо этого появилась ссылка на Unit1::Initialization. Это уже гораздо лучше, но совсем замечательно было бы если бы ссылок вообще не было - тогда, возможно, функция вообще не была бы посчитана кодом.

В чем же дело ?.

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

Попытки обхода.

Я попытался даже добраться до ассемблера

asm { mov qwe,offset EndOfFunc1-offset Func1 }

Мне казалось, что величина offset EndOfFunc1-offset Func1 по идее должна быть постоянна, но для BCB/DELPHI это не так.

Это связано с (относительно) новым форматом obj файлов BCB (в Delphi это уже есть давно), из которых теперь можно прилинковывать отдельные процедуры.

Объяснение

Есть три поцедуры:

void Proc1() {}; void Proc2() {}; void Proc3() {}; Предположим, линковщик обнаружил, что процедура Proc2() не используется. Тогда он изымает ее из кода, и соответственно, расстояние между Proc1 и Proc3 изменяется. Однако тем, кто пишет на других компиляторах, можно попробовать взять это на вооружение. Там, возможно, obj линкуются полностью.

Что же делать?

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

Еще одна неприятность.

Кроме изучения листинга в IDA есть еще возможность поставить точку останова на чтение памяти в SoftIce, и он остановится прямо в процедуре, считающей контрольную сумму. Его уже никак не обмануть, но для того чтобы поставить точку останова нужно иметь подозрение, что считается контрольная сумма именно этого участка. Тогда как в IDA на это напарываешься, просто изучая код. Но в игры по установке breakpoint можно играть вдвоем: читайте тело процедуры еще из 20-30 мест программы, в том числе и на Application->OnIdle, пусть Cracker сам застрелится.

Общая последовательность действий при взломе

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

1) Файл дизассемблируется в IDA.

IDA - Interactive Disassembler, т.е. дизассемблер, который взаимодействует с программистом при дизассемблировании, а не так что "нате листинг и делайте с ним что хотите". Последние версии IDA опознают компилятор, опознают стандартные библиотеки (для этого ему не нужна отладочная информация, они используют собственные базы. Есть утилиты по созданию этих баз, если ты имеешь библиотеки). Он опознает стандартные конструкции языка (типа Switch-case), т.е. в комментарии к ассемблерному листингу будет написана, чем эта конструкция была раньше (правда это работает не для всех компиляторов). К нему есть plug-ins которые опознают RTTI Delphi/Builder, т.е. любая(!) функция, описанная как published, будет показана с настоящим именем.

Не называйте ваши события PasswordCheck, или нет, называйте, но только какие-нибудь левые и не относящиеся к защите, если удастся пустить cracker'а по ложному пути - будет клево!

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

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

2) Потом предпринимается попытка все сломать, не прибегая к помощи отладчика. Обычно в коде ищется характерная строка типа "Trial Expired", смотрятся откуда на нее есть обращения, и производится попытка сломать программу, не прибегая к услугам отладчика. Это получается почти в 50% случаев и сломанная таким образом защита, считается очень простой.

Никогда не храните строки типа "Trial Expired" открытым текстом.

3) Если ничего не получается, в действие вступает SoftIce.

SoftIce представляет собой, так называемый, Kernel Level Debugger. Выглядит это так: вместо запуска Win.com запускается WinIce.exe, и отладчик грузит под собой Windows. В любой момент можно нажать Ctrl+D и отлаживать Windows. Отладчик использует возможности процессора по установке контрольных точек. SoftIce абсолютно наплевать на все ухищрения с прерываниями, которые должны были бы повесить любой нормальный отладчик. Обнаружить его по замеру времени выполнения критических процедур нельзя, т.к. он останавливает системное время(мне кажется, энергонезависимые часы в это время должны идти так что на Win9X можно портами прочитать показания часов и сравнить их с системным временем, однако на WinNT такие вещи не пройдут, а SoftIce для NT есть). Обнаружить такой метод отладки в принципе нельзя, однако SoftIce обнаружить можно. Мне кажется, лазейка оставлена специально, т.к, например, Symbol Loader(утилита для загрузки отладочных таблиц и символов экспорта) в правом нижнем углу пишет SoftIce is Active/SoftIce is not Active. Должна же она как-то это определить. Однако, есть несколько программ, которые SoftIce прячут, и обнаружить его после этого нельзя.

Основной метод взлома выглядит следующим образом. Загружается символы экспорта из Kernel32 и User32. Загружается программа. Ставится Break Point на MessageBoxA или на другую функцию, которой вы воспользуетесь для вывода сообщения о том, что пароль не правилен. Проводится попытка ввода пароля, и SoftIce останавливает вашу программу в ядре Windows в функции MessageBoxA, несколько раз нажимается F12(кнопка останавливающая программу после следующей команды Ret, т.е. фактически возвращающей на одну процедуру вверх по иерархии вызовов), и вот вы уже в центре защитных процедур.

Никогда не выдавайте сообщение сразу после проверки пароля. Выдайте его, например, на Application->OnIdle.

В более тяжелом случае ставится BreakPoint на hmemcpy, эта такая недокументированная функция Windows, которая используется для копирования памяти. Она вызывается, если вы просто ввелт символ в Edit, а так же если вы читаете текст окна(в том числе и Edit). BreakPoint в ней срабатывает почти всегда, единственная проблема - вызывается она слишком часто.

Анализировать код в SoftIce очень неудобно, во-первых, нет таких возможностей по навигации (не отслеживает XREFs), как в IDA, во-вторых в то время пока вы в отладчике, Windows фактически остановлена, т.е. вы не можете нажать Alt+Tab и посмотреть help по WinAPI(не то, что бы я не помню WinApi, но для отладки необходимо помнить не только примерный порядок параметров, но и точные типы, и конвенцию вызова, чтобы понять в каком порядке параметры кладутся на стек, иногда даже приходится смотреть .h файлы). И что больше всего меня бесит - WinAmp в это время не играет. Так что отладка обычно ведется на 2 компьютерах. На одном отлаживаешь, а к другому бегаешь help смотреть. Именно поэтому, я (и наверное другие crackers) предпочитаю анализировать код в IDA, а SoftIce используют только для нахождения адресов процедур. Именно поэтому, так важно затруднить анализ листинга.

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

Глава 2. Самомодификация кода

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

Не рекомендуется, хотя и можно использовать кодирование для скрытия алгоритмов кодирования.

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

Кодирование функций, конечно, затрудняет анализ, но не настолько, чтобы его нельзя было провести. Гораздо лучше здесь использовать что-нибудь из ассиметричных систем кодирования.

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

Внешние паковщики.

Есть куча программ типа AsPack и AsProtect (www.aspack.com), которые самостоятельно сжимают вашу программу без вашей помощи, таким образом, осуществляя довольно мощную защиту. Однако, этот метод я считаю неспортивным, поэтому подробно рассматривать его здесь не буду (Хотя если вы не интересуетесь спортом, вы можете использовать эти вещи поверх основной защиты для того, чтобы отсеять самых слабых спортсменов еще на старте). Единственно, что данная защита стала последнее время популярной, и за нее принялось большое количество профессиональных cracker'ов. Так что если пойти, например, на www.reversing.net то вы обнаружите огромное количество статей по взлому этих средств. Однако, полностью автоматических распаковщиков пока нет (хотя уже ждать не долго осталось), и для применения результатов исследований необходимо иметь некоторую подготовку.

Простейший пример самомодифицирующегося кода

void MyFunction() { ShowMessage(0xF1); } void EndOfMyFunction() { } void __fastcall TForm1::Button1Click(TObject *Sender) { DWORD OldProtection; VirtualProtect(&MyFunction,4096,PAGE_EXECUTE_READWRITE,&OldProtection); MyFunction(); for(unsigned char* foo=(unsigned char*)&MyFunction;foo &lt (unsigned char*)&EndOfMyFunction;foo++) { if (*foo==0xF1) *foo=128; } MyFunction(); }

Пояснения

MyFunction выводит на экран число 0xF1. Цель примера изменить код функции так, чтобы во второй раз она вывела другое число. Число 0xF1 выбрано из соображений того, что оно больше нигде не встречается в коде функции (Я сначала выбрал 0xFF, но оно встретилось 3 раза, и при втором вызове функции я получил Access Violation). Однако, если вы используете другой компилятор, у вас оно может и встретится.

Функция VirtualProtect изменяет вид защиты сегмента. По идее передаваемый ей адрес должен быть кратен 4096, но как видите, и так работает. Кроме того, стоит проследить, чтобы MyFunction не пересекла границу страницы памяти длиной 4096. Здесь я не стал все это отслеживать, чтобы не загрязнять код простого примера всяческими проверками.

Данный код работает и под WindowsNT. Что в принципе не удивляет, т.к. тот же ASPack под NT работает. Видимо NT считает необходимым прикрывать только свою память, а программе позволяет сходить с ума, как она хочет.

Фокус

А теперь берем, и комментируем вызов функции VirtualProtect. Запускаем программу и ... все работает. "Так нафиг ее вызов вообще нужен?" - спросите вы. Для того чтобы это понять, достаточно запустить программу без отладчика, и вы увидите большой и красивый Access Violation. Когда вы запускаете программу под отладкой, отладчик уже сам требует разрешения на запись в тело программы. Он ставит в теле программы точки останова (CC).

Примечание: это потенциально можно использовать тем, кто пишет Trial компоненты.

Кстати, поставленная отладчиком Builder/Delphi точка останова будет влиять на контрольную сумму программы, она имеет машинный код CC, ее можно найти. Это я просто так предупреждаю, чтобы не было проблем при отладке, с точки защиты толку от этого эффекта нет никакого - SoftIce такой фигней не занимается.

Метод кодирования функции.

Допустим, мы хотим получить функцию, которая в Exe файле зашифрована, а при загрузке программы расшифровывается и запускается. Компилятор, естественно, не умеет производить шифрованные функции, поэтому придется сначала скомпилировать программу, а потом пропатчить ее exe файл.

Примечание: было бы интересно написать программу, которая кодирует себя при первом запуске (и при этом уничтожает кодирующие процедуры). Однако, на сколько я знаю, программа сама себя изменить не может. Хотя в Win 9X можно все, даже то, что нельзя. Так что способ самоизменения, наверняка, есть. Если кто-нибудь знает какой-нибудь человеческий метод самоизменения (без использования .bat файлов и т.п.), напишите мне или, вообще, поделитесь с общественностью, буду очень благодарен. В данный момент возможны схемы, когда программа делает свою копию и ее патчит, но я их рассматривать не буду (по крайней мере, не в этой статье).

Сначала необходимо найти адрес функции в Exe файле. Вы можете узнать адрес в памяти, прочитав map файл. Необходимо заставить линкер сгенерить detailed map file. Но для того чтобы пропатчить exe это не подходит. Смещения в exe отличаются. Я для этого просто использую IDA. Находим в нем функцию и говорим patch program, а он нам сообщает смещение в exe. Сам патч производить удобнее в HIEW или другом HexEditor. Я буду использовать для кодирования простейший Xor, так что HIEW для этого вообще идеален, но в реальность алгоритм кодирования должен быть гораздо более, сильным и придется писать программу для изменения кода exe(типа сообщаешь ей два смещения, а она кодирует все между ними).

Если у вас нет IDA, придется искать другие способы определения смещения. Самый простой Получить DUMP процедуры и найти ее в файле. Например, ставим breakpoint на начало процедуры, останавливаемся на нем и открываем окно CPU. Там слева от кода показан адрес процедуры в памяти. Например, 00401848. Снизу окно памяти. Тыкаем на него правой клавишей и выбираем Go to Address, набираем 0x401848 (В Delphi может быть $401848, самое главное не 401848, это будет считаться десятичным адресом) и записываем или запоминаем несколько байт (записывать придется достаточно много т.к. все процедуры начинаются одинаковым прологом). Потом берем HexEditor и ищем в нем этот код, убедитесь, что это именно та процедура, которая вам нужна (т.е. что другой такой код больше негде не находится)

Пример

void MyFunction() { ShowMessage("It's work !"); } void EndOfMyFunction() { } void __fastcall TForm1::Button1Click(TObject *Sender) { DWORD OldProtection; VirtualProtect(&MyFunction,4096,PAGE_EXECUTE_READWRITE,&OldProtection); for(unsigned char* foo=(unsigned char*)&MyFunction;foo &lt (unsigned char*)&EndOfMyFunction;foo++) { *foo^=0xFF; } MyFunction(); }

Это пример кодирования процедуры, запускать это сразу смысла не имеет, т.к. работать это не будет. Необходимо это скомпилировать, а затем проксорить все тело процедуры MyFunction с 0xFF. (Кто сказал, что ксорить это не модно и это легко взломать? (выйти из строя - 10 отжиманий). А вообще-то, это действительно не модно, и действительно легко взломать, но я здесь объясняю маленько другие вещи, так что для того чтобы все было понятно, я буду ксорить, а когда вы будете писать свои защиты, делайте что-нибудь другое.)

После этого программу можно запускать. У меня получилось, что нужно проксорить адреса с 0xE48 по 0xE92 включительно. После чего программа заработала. После того как я дизассемблировал exe с закодированной процедурой IDA(во время дизассемблирования IDA выдал 8 warning "Cannot Disassemble"). В результате чего MyFunction, вообще не была посчитана процедурой, и превратилась в некоторую локацию памяти, выглядит все это примерно вот так: loc_401848: ; CODE XREF: _TForm1_Button1Click+3D.p ; DATA XREF: _TForm1_Button1Click+17.o ... stosb jz short loc_40185E jl short loc_401888 daa inc edi ; --------------------------------------------------------------------------- dd 0FFBFCCBFh ; --------------------------------------------------------------------------- pop ss retf ; --------------------------------------------------------------------------- dd 99FFFFF6h, 0F717BA38h db 0FFh ; --------------------------------------------------------------------------- loc_40185E: ; CODE XREF: .text:00401849.j inc ebp ; --------------------------------------------------------------------------- dd 0FFBFCCFFh dd 1703BA72h, 0FFFFF636h, 740BBA00h, 0F0D017FFh, 0B200FFFFh dd 3BA720Bh db 45h, 0FDh dword_40187D dd 17FFFFFFh, 0FFFFF5DCh ; CODE XREF: .text:00401889.j db 74h, 0B2h, 27h ; --------------------------------------------------------------------------- loc_401888: ; CODE XREF: .text:0040184B.j wait jbe short near ptr dword_40187D ; --------------------------------------------------------------------------- dd 0FFFFFFFFh, 3CA21A74h

Естественно, что ссылки из этой процедуры отслежены не были, и строка "It's work" была оставлена ничейной (если бы это была строка "trial expired", то узнать с помощью дизассемблера, откуда она используется, было бы нельзя).

Кстати, немного выше по тексту я сказал, что заставить компилятор произвести закодированную процедуру нельзя. Это так, но можно компилятор обмануть: что-то типа __declspec( naked ) void MyFunction() { asm { db ... db ... db ... } }

__declspec( naked ) запрещает компилятору трогать нашу функцию своими грязными руками и создавать код пролога / эпилога, который у нас уже есть внутри закодированного блока.

Правда, в общем случае просто подставить код из exe не удастся. В данном конкретном случае это не удастся, потому что в этой процедуре мы используем строку "It's work !", которая в самой процедуре не хранится (там только ссылка на нее), а хранится в секции инициализированных данных. Здесь же компилятор, естественно, саму строку не создаст (действительно, с чего бы ему это делать?). Но проблема вполне решаемая, я решать ее не буду (по крайней мере, в этой статье).

Действия Cracker'а

Ответ один - ProcDump. Можно конечно попробовать узнать алгоритм кодирования и все раскодировать, но это не есть путь Cracker'а.

ProcDump

Утилита для снятия дампов файлов из памяти совмещенная с утилитой для восстановления тех самых дампов, и превращения их в Exe файлы. Несмотря на внешнюю простоту, для пользования требует некоторых навыков. Однако, в составе с ней поставляются скрипты для распаковки нескольких распространенных Exe-паковщиков. В том числе и для ASPack нескольких версий.

Если вы хотите восстановить только что закодированный нами Exe, то вы должны сделать следующее:

  1. Найдите версию ProcDump 1.6.2
  2. Запустите наш Exe. И нажмите кнопку, чтобы процедура раскодировалась в памяти. И так и оставьте программу с выскочившим сообщением.
  3. Запустите ProcDump
  4. Проверте Options
    • * Recompute Object Size=false
    • * Optimize PE Structure=true
    • * Check Header Sections=true
    • * Rebuild Header=true
    • Radio Button переставьте в * Use Actual Import Infos
    • После чего найдите ваш процесс в списке процессов (в верхнем) и выберите его. В низу появится список его модулей. Нажмите правой клавишей на процесс в верхнем окне (обязательно в верхнем, можно дампить отдельные модули, но туда пока не лезьте, когда дампится отдельный модуль ему не восстанавливают таблицу импорта и нужно будет делать это руками) и выберите Dump(Full). Выберите ему имя и запишите его.

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

    Проблемы восстановления.

    Снять дамп с программы в памяти не проблема. Проблема в том, чтобы восстановить таблицу импорта. Дело в том, что все адреса функций из других dll которые наша программа использует в момент работы уже записаны как абсолютные адреса, и если dll поменяет место загрузки, то работать уже ничего не будет. А на другом компьютере тем более ничего работать не будет. В данном случае мы приказали ProcDump "позаимствовать" таблицу импорта из оригинального Exe.

    Что же делать ?

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

    Эпилог

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

    Anatoliy A. Orlov aka Anatolix
    06.03.2001




    Содержание  Назад  Вперед