Всем привет!
В данной теме речь пойдет еще об одном методе защите вашего ПО. А именно о полиморфном коде (ничего общего с полиморфизмом из ООП тут нет).
Думаю, что те, кто впервые задаются вопросом защиты своих проектов помимо VMProtector`а, SE и тд не понимают что такое полиморфный код. Поэтому для начала я постараюсь почти на пальцах объяснить вам что це такое.
Если в двух словах, то это код, которые изменяет сам себя (WHAT?!!!). У большинства возникнут два вопроса, как и зачем? На второй я могу ответить сразу. Чтобы скрыть те методы, которые являются критичными, допустим у вас есть метод проверки лицензии, который вы никому не хотите показывать, даже собственному коту. Для этого можно воспользоваться полиморфным кодом, то бишь вызвать метод а после этого зашифровать его или вовсе затереть в памяти нулями или мусором, очень спасает от дампов. Вторая ситуация, у вас есть метод, который вы также не хотите показывать, но еще вы хотите, чтобы его вообще не было в программе. Тоже хорошо подойдет полиморфный код, мы можем сохранить машинный код метода в файл (добавить шифрование) и при запуске программы расшифровывать файл, считать байты метода и выполнить их. Вот в основном для чего я использую полиморфный код.
Теперь второй вопрос, а как мы реализуем данную черную магию 8 ступени?
Для начала расскажу немного теории, она вам пригодится, чтобы осознать насколько все просто. Если вы не прогуливали пары по информатике в школе, то, думаю, учитель вам объяснил, что программа при запуске загружается и выполняется в оперативной памяти, это сделано для быстрой работы программы и для уменьшения нагрузки на HDD. А если вы еще и внимательно слушали своего учителя, то, уверен, он говорил, что у любого значения в оперативной памяти есть адрес, по которому это значение можно считать.
То есть когда мы запускаем свой лоадер, он выгружается в оперативную память, вместе со всеми ресурсами, исходным кодом и тд.
Теперь немного вспомним простого программирования на C++. Вот пример кода
int a = 1337;
int* pA = &a;
Код
int a = 1337;
int* pA = &a;
У нас есть переменная a, в которой хранится значение. У этой переменной есть адрес в оперативной памяти и язык C++ позволяет получить этот адрес с помощью указателя (в данном случае pA). В pA не будет храниться значения переменной "a", однако там будет находиться адрес этой переменной, и мы можем также изменять эту переменную через указатель
*pA = 5;
Код
*pA = 5;
Но к чему я пишу все это, ведь я показываю на примере переменных, а что же делать с методами? А кто сказал, что там нельзя поступить также?
вот пример
void someFunc()
{
...
}
int main()
{
void(*_pFunc)() = someFunc;
}
Код
void someFunc()
{
...
}
int main()
{
void(*_pFunc)() = someFunc;
}
Что я сделал в этом примере? Я создал указатель на метод someFunc. Структура тут следующая, сначала надо указать тип нашего метода (в данном случае это void), дальше указываем, что данная переменная - указатель (звездочка) и после этого пишем имя нашего указателя (_pFunc). После этого надо во вторых скобках указываем типы аргументов (только типы), но в данном примере никаких аргументов у метода нет, поэтому это я пропускаю. Собственно остается присвоить этому указателю адрес (тут можно не ставить значок &) и указатель готов. Кстати с помощью этого указателя можно вызывать наш метод.
_pFunc();
Код
_pFunc();
Отлично, теперь вы научились получать адреса методов. Но как теперь это использовать? Для этого вспомним, что в C++ мы можем приводить любые типы к любым типам, это может сыграть вам как на пользу, так и во вред. В данном случае мы можем привести тип указателя на метод к типу указателя на массив байт, почему бы и нет, или даже к статическому типу (DWORD, int и тд) и получить просто адрес в виде числа. То есть такой пример абсолютно легален.
void(*_pFunc)() = someFunc;
byte* funcBytes = reinterpret_cast<byte*>(_pFunc);
Код
void(*_pFunc)() = someFunc;
byte* funcBytes = reinterpret_cast<byte*>(_pFunc);
Теперь у нас есть указатель на байты метода. Это уже 50% нашей работы. Теперь возникают вопрос, а как понять размер метода в памяти? Для этого есть два метода (мб больше, но я нашел только два). Первый, через stub - то бишь мы создаем еще один пустой метод сразу же под исходным, получаем его
адрес и просто высчитываем разницу (в конфигурации Release с отключенной оптимизацией это работает идеально). Однако для тех же классов и тд данная фича не подходит, поэтому можно написать такой костыль
Мы будем будет идти до конца метода и увеличивать счетчик байтов.
//Первый костыль
DWORD getMemSize(void* func, void* stubFunc)
{
return reinterpret_cast<DWORD>(stubFunc) - reinterpret_cast<DWORD>(func);
}
//Второй костыль
while (*funcBytes != 0xCC)
{
++funcBytes;
count++;
}
Код
//Первый костыль
DWORD getMemSize(void* func, void* stubFunc)
{
return reinterpret_cast<DWORD>(stubFunc) - reinterpret_cast<DWORD>(func);
}
//Второй костыль
while (*funcBytes != 0xCC)
{
++funcBytes;
count++;
}
Ну вот мы почти и закончили первый способ применения (перезапись байтов во время выполнения). Остается, как бы это неожиданно не прозвучало бы, перезаписать байты. Однако для этого нам надо повысить права доступа на нужный нам участок памяти, для этого можно использовать WinAPI функцию VirtualProtect (кстати так можно изменять права доступа и к константам).
BOOL rewriteMemory(DWORD* startAdress, DWORD size, byte* src)
{
DWORD oldProtect;
if (!VirtualProtect(startAdress, size, PAGE_EXECUTE_READWRITE, &oldProtect))
return FALSE;
for (DWORD i = 0; i < size; i++)
{
*startAdress = src[i];
startAdress++;
}
if (!VirtualProtect(startAdress, size, oldProtect, &oldProtect))
return FALSE;
return TRUE;
}
Код
BOOL rewriteMemory(DWORD* startAdress, DWORD size, byte* src)
{
DWORD oldProtect;
if (!VirtualProtect(startAdress, size, PAGE_EXECUTE_READWRITE, &oldProtect))
return FALSE;
for (DWORD i = 0; i < size; i++)
{
*startAdress = src[i];
startAdress++;
}
if (!VirtualProtect(startAdress, size, oldProtect, &oldProtect))
return FALSE;
return TRUE;
}
Теперь вы можете шифровать / расшифровывать / удалять методы из памяти вашей программы.
Ну и второй способ применения полиморфного кода. Загрузка метода из файла / массива байт и выполнение его на стеке.
Тут задача тоже довольно простая. Получаем байты метода (как получить я уже показал), записать, зашифровать, посолить, поперчить и сохранить в файл. Дальше можно удалять метод.
Теперь открываем файл, читаем массив байт, расшифровываем и.... приводим наш указатель на массив байт к указателю на метод.
ОЧЕНЬ ВАЖНО!!! В качестве адреса надо указывать адрес на первый элемент.
void(*_pFunc)() = reinterpret_cast<void(*)()>&_bytes[0];
Код
void(*_pFunc)() = reinterpret_cast<void(*)()>&_bytes[0];
ГОТОВО! Можно также выполнять эти байты (и не забываем изменять права доступа через VirtualProtect).
Теперь немного важной информации по поводу того, что будет хранить байт код и чего не будет.
- Байт код не хранит адреса на импортируемые функции, так что вам придется передавать адреса на классы, импортируемые функции и тд самостоятельно (думаю я рассказал, как получить адрес нужной функции).
- Байт код не хранит строки, то есть если у вас будет строка в методе, то в байт коде будет лишь адрес на нее (тоже самое относится и к массивам). Однако если вы посимвольно будете добавлять в массив значения, то данные операции будут спокойно сохранены.
- Никаких статических массивов, только динамическое выделение памяти.
Однако все это зависит от компилятора и его настроек. Просто говорю, что мне выдавала VisualStudio.
А на этом у меня все. Я старался как можно подробнее изложить всю суть полиморфного кода. Надеюсь вы не сильно заскучали при чтении этого очень длинного текста.
p.s.
#моястатья.
- Не надо писать то, что я называю функции методами, я от ООП не отошел.
- Под байт кодом я подразумеваю машинный код.
Загрузка...