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

  Вниз по реке
На челноке,
Весла оставив на песке,
Вспомни урок, да,
Красный стрелок, да,
Он одинок, да,
Он мертвый, он мертвый.
Мертвый охотник на мертвых
поднимет ружье.

Аукцыон, "Охотник",
альбом "Как я стал предателем" 1989

Введение

На этот раз я представляю Вам сугубо теоретическое исследование, и все рассматриваемые программы написал сам. Кроме них нам понадобятся Delphi и исходный код VCL (я использовал Delphi 4.0 Client/Server Edition), а также дизассемблер IDA Pro (я пользуюсь v3.8b). Полагаю Вы понимаете Ассемблер и имеете опыт в написании программ на Delphi с применением VCL.

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

Вопрос этот совершенно не нов, первым (из известных мне) трудом подобного рода был материал LaZaRuS'а Нахождение стандартных функций в программах на Delphi/C++ Builde на Fravia летом этого года. Для тех, кто не знаком с английским, краткий конспект шедевра LaZaRuSа: он сделал тоже, что и я (трудно в наше время быть оригинальным...) - написал тестовое приложение (правда, он использовал Borland C++ Builder), а потом дизассемблировав его W32Dasm'ом, попытался выяснить, как выглядят типичные действия по заполнению окна регистрации на Ассемблере.

Получилось у него примерно следующее:

функция, закрывающая модальный диалог, должна поместить значение, возвращаемое методов ShowModal, помещает это значение по смещению 0144h. Сами значения можно посмотреть в файле Source/Rtl/Win/windows.pas:

  ID_OK=1

  ID_CANCEL=2

  ID_ABORT=3

  ID_RETRY=4

  ID_IGNORE=5

  ID_YES=6

  ID_NO=7

  ID_CLOSE=8

  ID_HELP=9

для разрешения/запрещения кнопок используется смещение 40h в классе TButton;

для быстрого поиска функции MessageBox() (скажем, в SoftICE) нужно найти вызов GetActiveWindow(), затем собственно MessageBox() вслед за ним SetActiveWindow() (!);

фукнция, записывающая или считывающая текст из элементов управления TEdit, делает это через указатель по смещению 01CCh.

А теперь плохая новость - для Delphi 4 (я исхожу из предположения, что все новые программы обычно пишутся на самых новых средствах разработки) всё вышеизложенное не соответсвует действительности.

Обнаружение нужных классов

Я набросал в несистематическом порядке несколько элементов управления (TEdit, TButton и TBitBtn - именно они чаще всего применяются в диалогах регистрации), и написал примерно такой непритязательный код:

type

  TForm1 = class(TForm)

    Edit1: TEdit;

    Edit2: TEdit;

    Button1: TButton;

    Button2: TButton;

    Button3: TButton;

    BitBtn1: TBitBtn;

    BitBtn2: TBitBtn;

    BitBtn3: TBitBtn;

    procedure BitBtn1Click(Sender: TObject);

    procedure FormShow(Sender: TObject);

    procedure Button1Click(Sender: TObject);

    procedure Button2Click(Sender: TObject);

  private

    { Private declarations }

    procedure MyClickHandler(Sender: TObject);

  public

    { Public declarations }

  end;



var

  Form1: TForm1;



implementation



{$R *.DFM}



procedure TForm1.BitBtn1Click(Sender: TObject);

begin

 MessageDlg('BitBtn1Click',mtConfirmation, [mbOk], 0);

 ModalResult := mrOk;

end;



procedure TForm1.MyClickHandler(Sender: TObject);

begin

 MessageDlg('MyClickHandler',mtConfirmation, [mbOk], 0);

 ModalResult := mrCancel;

end;



procedure TForm1.FormShow(Sender: TObject);

begin

 MessageDlg('FormShow',mtConfirmation, [mbOk], 0);

 BitBtn2.OnClick := MyClickHandler;

end;



procedure TForm1.Button1Click(Sender: TObject);

var

 S: String;

begin

 S := Trim(Edit1.Text) + Trim(Edit2.Text);

 Application.MessageBox(PChar(S),'Button1Click',IDOk);

end;



procedure TForm1.Button2Click(Sender: TObject);

begin

 MessageDlg('Button2Click',mtConfirmation, [mbOk], 0);

 Edit1.Enabled := not Edit1.Enabled;

 Button3.Enabled := not Button3.Enabled;

end;

Чтобы мне было легко идентифицировать мой же собственный код, я поместил в каждой функции вызов MessageDlg(). Также здесь не все обработчики назначаются во время проектирования - функция MyClickHandler() назначается обработчиком динамически при показе формы (в методе FormShow()). Компилируем, запускаем - безделица, конечно, но работает... Размер EXE-файла 329728 байт! И это буквально за пять минут! Да я - серьезный программист!

Далее неплохо было бы дизассемблировать полученный файл.

Общее замечание: строки в Delphi в бинарном виде выглядят не как во всех прочих языках - т.е. не оканчиваются нулевым символом, отчего IDA Pro не опознаёт их как строки. Вначале идёт один байт - длина, а далее - сама строка, причём её конец никак более не обозначен. Это верно для так называемых коротких строк, длина которых меньше 256 байт. К несчастью, именно такими строками пользуется механизм поддержки классов.

Надо заметить, что, несмотря на все свои достоинства, IDA Pro не справляется со всеми тонкостями программ, написанных на Delphi - утверждает, что на месте VTBL находится код, не распознаёт строк в стиле Pascal'я и прочие мелочи - так что нас выручит только её интерактивность. И, кстати, не забудьте применить файл сигнатур для VCL 4 - для моего файла IDA Pro опознала аж 2297 библиотечных функций!

Для начала посмотрим, как выглядит стартовая процедура Start() (004444A8h):

 push    ebp

 mov     ebp, esp

 add     esp, 0FFFFFFF4h

 mov     eax, offset dword_0_444398

 call    @@InitExe       ; ::`intcls'::InitExe

 mov     eax, ds:off_0_445CDC

 mov     eax, [eax]

 call    @TApplication@Initialize ; TApplication::Initialize

 mov     ecx, ds:off_0_445DAC

 mov     eax, ds:off_0_445CDC

 mov     eax, [eax]

 mov     edx, ds:off_0_443F30

 call    @TApplication@CreateForm ; TApplication::CreateForm

 mov     eax, ds:off_0_445CDC

 mov     eax, [eax]

 call    @TApplication@Run ; TApplication::Run

 call    @@Halt0         ; ::`intcls'::Halt0

Самым многообещающим здесь выглядит вызов метода TApplication::CreateForm(), аргументом ему передаётся некий указатель - на структуру RTTI (Run-Time Type Information, информация о типе времени исполнения) класса нашей формы TForm1. Исследуем ее.

По смещению DWORD от начала структуры RTTI расположен указатель на VTBL. Далее идут 12 нулей (возможно выравнивание по границе, а возможно эти три DWORDа тоже что-нибудь означают). А по смещению 10h в расположен указатель (DWORD) на некую рекурсивную структуру, которую я назвал список наследственности:

 

смещение тип описание
0 BYTE значение не выяснено
1 BYTE длина N Pascal-строки
2 String имя класса
N+2 DWORD ещё один указатель на VTBL
N+6 DWORD указатель на указатель (!) предка этого класса; обычно он указывает на 4 байта дальше себя, но я не берусь этого гарантировать
N+10 WORD значение не выяснено
N+12 BYTE длина Pascal-строки
N+13 String имя модуля, где определяется этот класс

Путешествуя по этому списку, можно с лёгкостью выяснить генеалогическое дерево класса TForm1:

TForm, файл Forms

TCustomForm, файл Forms

TScrollingWinControl, файл Forms

TWinControl, файл Controls

TControl, файл Controls

TComponent, файл Classes

TPersistent, файл Classes

TObject, файл System

У последнего указатель на предка содержит нулевое значение - видимо, означая конец списка.

Вернёмся к структуре RTTI класса TForm1. По смещению 14h находится указатель на компоненты, которыми владеет данный класс. Это все элементы списка Components во время разработки. Эта структура имеет довольно простой вид:

 

смещение тип описание
0 WORD число CompCount различных классов компонентов
2 DWORD указатель на массив указателей на структуры RTTI этих классов. Первым элементом этого массива является WORD - число его элементов, далее расположены указатели на структуры RTTI.

Сразу вслед за ней идут CompCount структур, описывающих эти компоненты:

 

смещение тип описание
0 WORD смещение в классе, по которому находится указатель на компонент
1 WORD значение не выяснено
2 WORD индекс в массиве структур RTTI - по нему определяется класс компонента
N+2 WORD длина Pascal-строки
N+6 String имя компонента (например, Edit1)

Самым важным здесь являются смещение на компонент во включающем классе и его тип. Запомним их для компонентов в форме TForm1:

 

имя компонента cмещение в классе тип компонента
Edit1 02C4h 0 - TEdit
Edit2 02C8h 0 - TEdit
Button1 02CCh 1 - TButton
Button2 02D0h 1 - TButton
Button3 02D4h 1 - TButton
BitBtn1 02D8h 2 - TBitBtn
BitBtn2 02DCh 2 - TBitBtn
BitBtn3 02E0h 2 - TBitBtn

Снова вернёмся к структуре RTTI класса TForm1. По смещению 18h находится указатель на одну из самых полезных структур - на массив обработчиков событий (но только тех, которые заданы во время проектирования!). Первым элементом этого массива идёт WORD, определяющий длину этого массива, а его элементы имеют такие поля:

 

смещение тип описание
0 WORD тип обработчика
2 DWORD указатель на функцию-обработчик
6 BYTE длина Pascal-строки
7 String имя функции-обработчика

Тип определяет количество и размерность аргументов. Для обработчиков OnClick он равен 13h, для OnShow 0Fh.

Не прошло и получаса, а я уже нашёл свой код. Мы рассмотрим его чуть позже (пока Вы можете назвать найденные функции как в оригинале), а сейчас продолжим рассмотрение структуры RTTI класса. По смещению 24h записывается размер класса (DWORD) - для TForm1 он составляет 02E4h байт. Сравните его с таблицей смещений компонентов. По смещению 28h находится указатель на структуру RTTI класса-предка. У объекта TObject он равен нулю. По смещению 20h находится указатель на Pascal-строку - имя класса. Я повторю всю вышеизложенную информацию в следующей таблице:

 

смещение тип описание
0 DWORD указатель на VTBL
4 12 байт значение не выяснено
10h DWORD указатель на список наследований
14h DWORD указатель на компоненты, которыми владеет данный класс
18h DWORD указатель на массив обработчиков событий
1Ch DWORD значение не выяснено
20h DWORD указатель на Pascal-строку - имя класса
24h DWORD размер класса
28h DWORD указатель на структуру RTTI класса-предка данного класса

По смещению 2Ch идёт таблица методов. Порядок следования методов в ней мне не до конца ясен, однако я уверен, что в ней должны содержаться конструктор и деструктор данного класса.

Настало время рассмотреть обнаруженные нами методы подробнее. Я рассмотрю их в том порядке, в каком их расположила Delphi в массиве обработчиков событий.

BitBtn1Click

BitBtn1Click    proc near

                push    ebx

                mov     ebx, eax

                push    0

loc_0_444149:

                mov     cx, ds:word_0_444168

                mov     dl, 3

                mov     eax, offset aBitbtn1click

                call    @MessageDlg

loc_0_44415C:

                mov     dword ptr [ebx+22Ch], 1

                pop     ebx

                retn

BitBtn1Click    endp

Простой и понятный код. Подспудно выясняется, что закрытие формы осуществляется записью DWORD'а (ModalResult) по смещению 022Ch в экземпляре классе. Обратите внимание на механизм передачи параметров - по умолчанию Delphi использует соглашение вызова register - параметры передаются слева-направо, используя регистры EAX, EDX и ECX, очистку стека производит вызываемая функция. Соответственно, первый (неявный) аргумент для этой функции, представляющий собой указатель на класс, передаётся в регистре EAX.

OnFormShow

OnFormShow      proc near

                push    ebx

                mov     ebx, eax

                push    0

                mov     cx, ds:word_0_4441F4

                mov     dl, 3

                mov     eax, offset aFormshow

                call    @MessageDlg

                mov     eax, [ebx+2DCh]

                mov     [eax+108h], ebx

                mov     dword ptr [eax+104h], offset MyClickHandler

                pop     ebx

                retn

OnFormShow      endp

Здесь тоже можно увидеть кое-что интересное. Во-первых, смещение 02DCh не напоминает Вам о компоненте BitBtn2? Во-вторых, обратите внимание, что здесь присваиваются два указателя. Почему? Потому что мы присваиваем не просто указатель на функцию. Все обработчики являются "of object" - т.е. методами классов. Соответственно, присваивается сначала указатель на экземпляр класса (в данном случае Self) по смещению 0108h, а затем - указатель на нашу функцию MyClickHandler(). Замечу, что больше указатель на эту функцию не встречается. Это сильно затрудняет поиск динамически назначенных обработчиков событий. Нам может помочь только ещё одно обстоятельство - все строковые константы, используемые в функции, Delphi располагает следом за самой функцией.

Button1Click

Button1Click    proc near

var_10          = dword ptr -10h

var_C           = dword ptr -0Ch

var_8           = dword ptr -8

var_4           = dword ptr -4

                push    ebp

                mov     ebp, esp	; фрейм стека для локальных переменных

                xor     ecx, ecx

                push    ecx

                push    ecx

                push    ecx

                push    ecx   ; 4 нуля в стек

                push    ebx

                mov     ebx, eax	; в eax - указатель на экземпляр класса

                xor     eax, eax

                push    ebp

                push    offset loc_0_4442B0

		push 	dword ptr fs:[eax]

                mov     fs:[eax], esp

...

loc_0_4442B0:

		jmp     @@HandleFinally

IDA Pro неправильно опознала аргументы функций - ведь они передаются в регистрах, а не через стек. Кроме того, здесь задействуется механизм обработки исключений. Для передачи управления при исключениях Delphi использует сегментный регистр FS - в FS:[0] помещается текущий указатель стека ESP, предыдущее же значение перед этим помещается в стек. Кроме того, в стек также помещается адрес функции - обработчика блока finally. Также обратите внимание на инициализацию четырёх локальных переменных типа DWORD нулями.

     lea     edx, [ebp+var_C]

     mov     eax, [ebx+2C8h]	; смещение 02C8h не напоминает Вам о Edit2?

     call    @TControl@GetText ; TControl::GetText

     mov     eax, [ebp+var_C]

     lea     edx, [ebp+var_8]

     call    @Trim

     mov     eax, [ebp+var_8]

     push    eax

     lea     edx, [ebp+var_C]

     mov     eax, [ebx+2C4h]	; а 02C4h - о Edit1?

     call    @TControl@GetText ; TControl::GetText

     mov     eax, [ebp+var_C]

     lea     edx, [ebp+var_10]

     call    @Trim

     mov     edx, [ebp+var_10]

     lea     eax, [ebp+var_4]

     pop     ecx

     call    @@LStrCat3      ; ::'intcls'::LStrCat3

     push    1

     mov     eax, [ebp+var_4]

     call    @@LStrToPChar   ; ::'intcls'::LStrToPChar

     mov     edx, eax

     mov     ecx, offset aButton1click

     mov     eax, ds:off_0_445CDC

     mov     eax, [eax]

     call    @TApplication@MessageBox ; TApplication::MessageBox

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

                xor     eax, eax

                pop     edx

                pop     ecx

                pop     ecx

                mov     fs:[eax], edx

                push    offset loc_0_4442B7



loc_0_444292:                           ; CODE XREF: CODE:004442B5j

                lea     eax, [ebp+var_10]

                call    @@LStrClr       ; ::`intcls'::LStrClr

                lea     eax, [ebp+var_C]

                call    @@LStrClr       ; ::`intcls'::LStrClr

                lea     eax, [ebp+var_8]

                mov     edx, 2

                call    @@LStrArrayClr  ; ::`intcls'::LStrArrayClr

                retn

...

offset loc_0_4442B7:

                pop     ebx

                mov     esp, ebp

                pop     ebp

                retn

Рассмотрим восстановление стека подробнее. В стеке в настоящий момент содержится:

FS:[0]

указатель на finally-функцию

EBP - прежнее значение стека

EBX

ECX = 0

ECX = 0

ECX = 0

ECX = 0

оригинальное значение EBP

адрес возврата из функции

Хотя перед этим в стек была помещена 1 - её нет в стеке. Почему? Потому что она является последним аргументом функции TApplication::MessageBox(). Но ведь у этой функции всего три аргумента, и они все передаются в регистрах - скажете Вы! Ничего подобного, Вы забыли, что всем методам классов передаётся неявно ещё один аргумент (под номером ноль) - указатель на экземпляр класса. При возврате же вызываемая функция сама производит очистку стека.

Итак, сначала извлекается предыдущее значение FS:[0], указатель на finally-функцию и прежнее значение стека, и восстанавливается значение FS:[0]. Дальше в стек помещается адрес процедуры очистки стека. После инструкции retn стек будет выглядеть так:

EBX

ECX = 0

ECX = 0

ECX = 0

ECX = 0

оригинальное значение EBP

адрес возврата из функции

Далее снимается оригинальное значение регистра EBX, стек восстанавливается в первоначальное состояние (которое хранилось всё время выполнения процедуры в регистре EBP). Стек сейчас выглядит так:

оригинальное значение EBP

адрес возврата функции

Восстанавливается предыдущее значение регистра EBP (указатель стека для вызывающей процедуры) и после инструкции retn мы возвращаемся в вызывающую функцию с полностью восстановленным стеком.

Button2Click

Button2Click    proc near

                push    ebx

                push    esi

                mov     ebx, eax ; в eax - указатель на экземпляр класса

                push    0

                mov     cx, ds:word_0_44431C

                mov     dl, 3

                mov     eax, offset aButton2click_0

                call    @MessageDlg

                mov     esi, [ebx+2C4h] ; смещение на Edit1

                mov     eax, esi

                mov     edx, [eax]

                call    dword ptr [edx+50h] ; вызов TEdit::GetEnabled

                mov     edx, eax	; результат в eax

                xor     dl, 1		; xor boolean с 1 - его же not

                mov     eax, esi

                mov     ecx, [eax]

                call    dword ptr [ecx+60h] ; вызов TEdit::SetEnabled

                mov     esi, [ebx+2D4h] ; смещение на Button3

                mov     eax, esi

                mov     edx, [eax]

                call    dword ptr [edx+50h]

                mov     edx, eax

                xor     dl, 1

                mov     eax, esi

                mov     ecx, [eax]

                call    dword ptr [ecx+60h]

                pop     esi

                pop     ebx

                retn

Button2Click    endp

Эта функция инвертирует свойство Enabled поля ввода и кнопки. Свойство Enabled определено для класса TComponent (общий предок для TEdit и TButton) так:

property Enabled: Boolean read GetEnabled write SetEnabled 

 stored IsEnabledStored default True;

Доступ к этому свойству осуществляется через методы GetEnabled & SetEnabled, что мы и видим здесь - через индекс в VTBL.

Дальше