Dельфиний дZен №1 : Самомодифицирующийся код

altИтак, чтобы долго тут не размусоливать, приведу маленький, но вполне рабочий пример самомодификации кода на Delphi. Компилировался пример на D7 со всеми настройками проекта по умолчанию (в частности оптимизация была включена). На всякий случай к исходникам приложил и бинарник, потому что вполне возможно, что этот код у тебя сразу правильно не скомпилится.

program Project1;
{$APPTYPE CONSOLE}
uses Windows;
const offset = 32; // Смещение может быть и другое
procedure Main;
var
i, x: Integer;
OldPageProtection: Cardinal;
base: PByte;
begin
x := 1;
base := PByte(Cardinal(@Main) + offset);
VirtualProtect(Pointer(base),1,PAGE_EXECUTE_READWRITE,OldPageProtection);
for i := 1 to 5 do
begin
inc(x);
base^ := base^ xor 8; // inc(x) dec(x)
end;
VirtualProtect(Pointer(base),1,OldPageProtection,OldPageProtection);
Write(‘x = ‘);
WriteLn(x);
Readln;
end;
begin
Main;
end.
Давай посмотрим, что же в этом коде такого необычного. На первый взгляд ничего особенного: крутится цикл и переменная Х увеличивается на единицу 5 раз (ну и еще какое-то шаманство непонятное :) ). То есть на экран должно быть выведено: «x = 6». Но при запуске программа выводит строку «x = 2». Как же так, почему? Да потому, программа за время работы несколько раз модифицировала свой код!

Вот с этого места можно начинать подробно. Для наших экспериментов ничего кроме стандартной среды Delphi не понадобится. Только сразу же надо освоить одну комбинацию горячих клавиш: ALT+CRTL+C. Это вызов окна CPU, то есть дизассемблера с отладчиком в одном флаконе. Кроме того, надо достать из закоулков памяти хоть какие-то знания по ассемблеру. А куда ж без него? Мы же машинный код модифицировать собрались. Да, еще нужен обычный виндовый калькулятор, если не умеешь в уме считать шестнадцатеричные числа.

Разберем программу по порядку. Танцевать будем от печки, то есть от begin. С инициацией переменной X все понятно.

Затем устанавливается указатель base. Смотрим: base:= PByte(Cardinal(@Main) + offset); К адресу функции Main прибавляется какое-то смещение 32. Так на что устанавливается base? Запускаем пример на отладку: 3 раза жмем F7 и оказываемся в начале функции Main. Жмем ALT+CRTL+C:

У меня начало функции находится по адресу $00403A80 (у тебя может быть другой адрес). Прибавляем к адресу смещение 32, получается $403AA0. То есть base указывает на инструкцию с кодом 46, которая соответствует строчке «inc(x)» в коде. Запомни это.

С помощью функции VirtualProtect мы разрешаем любые действия (флаг PAGE_EXECUTE_READWRITE) со страницей памяти, к которой принадлежит указатель base. Это для того, чтобы была возможность переписать исполняемый код, то есть собственно совершить самомодификацию. (Если VirtualProtect закомментировать, то программа после запуска тихо и быстро умирает, т.к. исполняемый код по умолчанию изменять запрещено) В конце листинга VirtualProtect вызывается снова, чтобы восстановить прежние атрибуты страницы памяти. Надо стараться быть аккуратным.

Дальше начинается крутиться цикл. В нем переменная Х увеличивается на 1, это как бы понятно. А потом содержимое ячейки памяти по указателю base «проксоривается» с числом 8. А что у нас по этому указателю? Предыдущая строчка: «inc(x)»! Машинная инструкция $46 соответствует ассемблерной мнемонике INC ESI, но после модификации в этой ячейке памяти лежит $4E, которая соответствует мнемонике DEC ESI. Вот она самомодификация! На следующем витке DEC ESI снова превращается в INC ESI. Получается, что на каждой итерации цикла Х не увеличивается постоянно, а то увеличивается, то уменьшается на 1. Т.к. число итераций нечетное, то последняя итерация увеличивает Х. Поэтому и получается на выходе 2, а не 6, как если бы самомодификации не было. Осталось только вывести результат на экран.

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

Другой метод «обнаружения себя», основан на небольшом ассемблерном извращении. Сначала я его не использовал, т.к. хотел показать пример на чистом Паскале, но он очень распространен и фактически является стандартом. Поэтому если ты не испытываешь органической неприязни к асму, то должен обязательно узнать об этом методе. Это может выглядеть примерно так:


asm
call @label;
@label:
pop base;
end;
VirtualProtect(Pointer(base),1,PAGE_EXECUTE_READWRITE,OldPageProtection);
for i := 1 to 5 do
begin
inc(x);
PByte(base + offset)^ := PByte(base + offset)^ xor 8;
end;
VirtualProtect(Pointer(base),1,OldPageProtection,OldPageProtection);

Вся соль с мясом находятся между строчками asm и end; Инструкция call вызывает саму себя, а в качестве адреса возврата в стек заносится адрес следующей за call’ом инструкции. Вот этот-то адрес нам и нужен. Извлекаем его в переменную base с помощью pop и дело в шляпе. Остается только пересчитать offset. Это будет разность между адресом inc(x) и адресом pop base. Считаем как обычно в окне CPU:

Следует отметить, что эти методы самомодификации не способствуют переносимости кода, т.к. другая версия Delphi или с другими настройками компилятора вполне может создать совсем другой код. Тогда программа будет работать неправильно, т.к. offset окажется неправильным. В этом случае вычисляй offset сам: в окне CPU найди инструкцию, соответствующую inc(x) и вычти из ее адреса адрес начала функции.

Как видишь в самомодифицирующемся коде нет ничего сложного и магического. И его реализация доступна не только ассемблерным гуру, но обычным Delphi-программерам. Хотя в обычных ситуациях потребность в самомодификации возникает крайне редко. Тогда зачем это нужно? Ну во первых это просто интересно. А вообще такой прием можно использовать в защитных механизмах (защите от хакеров, кракеров и проч.) При умном использовании он сильно усложнит задачу взломщику, хотя и имеет бооольшой недостаток: его легко идентифицировать по использованию функции VirtualProtect.

Понравилась статья? Поделиться с друзьями: