Советы по Delphi

         

Исследование кода, генерируемого Delphi


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

Введение

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

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

Вопрос этот совершенно не нов, первым (из известных мне) трудом подобного рода был материал LaZaRuS'а "Нахождение стандартных функций в программах на Delphi/C++ Builder" (http://www.phase-one.com.au/fravia/laza_s11.htm) на 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) на некую рекурсивную структуру, которую я назвал список наследственности:

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

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

  • TForm, файл Forms
  • TCustomForm, файл Forms
  • TScrollingWinControl, файл Forms
  • TWinControl, файл Controls
  • TControl, файл Controls
  • TComponent, файл Classes
  • TPersistent, файл Classes
  • TObject, файл System
У последнего указатель на предка содержит нулевое значение - видимо, означая конец списка.

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

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

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

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

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

Смещение на компонент во включающем классе и его тип
Имя компонента Смещение в классе Тип компонента
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, определяющий длину этого массива, а его элементы имеют такие поля:

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

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

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

Структура RTTI класса TForm1
Смещение Тип Описание
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:004442B5 j 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.

Часть 2. Обработка исключений и сообщений

Итак, продолжим. На сей раз я наваял приложение, использующее несколько более продвинутые технологии, предоставляемые Delphi - exceptions handling ( перехват исключений ), virtual & dynamic функции, обработку формой сообщений Windows, производные классы и загрузку строковых ресурсов из реестра. Исходный код моей программы мог бы выглядеть как-нибудь так:

    uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;
type
TRPEnum = ( RP_One, RP_Two, RP_Tree ); TRPEnumSet = set of TRPEnum;
TRPException = class(Exception) private RP_Array: array[7..9] of string; Code: TRPEnumSet; public Procedure Old_one_virtual; virtual; Procedure Old_one_dynamic; dynamic; Constructor Create; Destructor Destroy; override; end;
TRPExceptionChild = class(TRPException) Procedure Old_one_virtual; override; Procedure Old_one_dynamic; override; end;
TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); procedure FormCreate(Sender: TObject); private { Private declarations } FDesignedWidth, FDesignedHeight: Integer; procedure BuggyOne; Procedure WMSizing( var Message: TMessage ); message WM_SIZING; public { Public declarations } end;
var
Form1: TForm1;
implementation

{$R *.DFM}

resourcestring
BuggyOneCaption = 'BuggyOne'; MalformedException = 'Malformed exception';
(* TRPException *)
Constructor TRPException.Create;
begin
Application.MessageBox('Create', 'TRPException', ID_OK); inherited Create('BuggyOne object'); end;

Destructor TRPException.Destroy;
begin
Application.MessageBox('Destroy', 'TRPException', ID_OK); Inherited; end;

Procedure TRPException.Old_one_virtual;
begin
Application.MessageBox('Old_one_virtual','TRPException', ID_OK); end;

Procedure TRPException.Old_one_dynamic;
begin
Application.MessageBox('Old_one_dynamic','TRPException', ID_OK); end;

(* TRPExceptionChild *)
Procedure TRPExceptionChild.Old_one_virtual;
begin
Application.MessageBox('Old_one_virtual','TRPExceptionChild', ID_OK); end;

Procedure TRPExceptionChild.Old_one_dynamic;
begin
Application.MessageBox('Old_one_dynamic','TRPExceptionChild', ID_OK); end;

(* TForm1 *)
procedure TForm1.BuggyOne;
var
RP_E: TRPExceptionChild; N: Integer; begin
MessageDlg(BuggyOneCaption,mtConfirmation,[mbOk],0); try
RP_E := TRPExceptionChild.Create; RP_E.Code := [RP_One]; RP_E.RP_Array[7] := 'Seven'; N := 9; RP_E.RP_Array[8] := 'Eight'; RP_E.RP_Array[N] := 'Nine inch nails'; RP_E.Code := RP_E.Code + [RP_Two]; Raise RP_E; MessageDlg('Not will showed at the end',mtConfirmation,[mbOk],0); finally
MessageDlg('In finally part',mtConfirmation,[mbOk],0); end;
MessageDlg('Not will showed at the end',mtConfirmation,[mbOk],0); end;

procedure TForm1.Button1Click(Sender: TObject);
begin
MessageDlg('Button1Click',mtConfirmation,[mbOk],0); try BuggyOne; except on E:TRPException do begin MessageDlg('Button1Click in exception block',mtConfirmation,[mbOk],0); E.Old_one_virtual; E.Old_one_dynamic; end; on E:TRPExceptionChild do begin MessageDlg(MalformedException,mtConfirmation,[mbOk],0); end; end; MessageDlg('Button1Click at the end',mtConfirmation,[mbOk],0); end;

Procedure TForm1.WMSizing( var Message: TMessage );
var
PRect : ^TRect; Begin
PRect := Pointer (Message . LParam ); if PRect^. Right - PRect^. Left < FDesignedWidth then begin if Message.WParam in [ WMSZ_BOTTOMLEFT, WMSZ_LEFT, WMSZ_TOPLEFT ] then PRect^.Left := PRect^ . Right - FDesignedWidth else PRect^.Right := PRect^ . Left + FDesignedWidth; end; if PRect^ . Bottom - Prect^.Top < FDesignedHeight then begin if Message . WParam in [ WMSZ_TOP, WMSZ_TOPLEFT, WMSZ_TOPRIGHT ] then PRect^.Top := PRect^ . Bottom - FDesignedHeight else PRect^. Bottom := PRect^ . Top + FDesignedHeight; end; End;

procedure TForm1.FormCreate(Sender: TObject);
begin
FDesignedWidth := Width; FDesignedHeight := Height; MessageDlg('FormCreate',mtConfirmation,[mbOk],0); end;

Не вершина программистского мастерства, конечно, но для наших целей вполне годится. Итак, запустим дизассемблер ( я использовал IDA 3.8b ) и не забудьте применить файл сигнатур для библиотеки VCL версии 4 ( d4vcl ) - в моём случае IDA опознала 2172 функции.

А пока IDA делает грязную работу за нас, можно предаться чтению документации ( весьма рекомендую заниматься этим время от времени - можно узнать столько интересного :-). Итак, что мы можем узнать из официальной документации по Delphi о тонкой разнице между динамическими (dynamic) и виртуальными (virtual) методами ?

Virtual методы расположены в таблице виртуальных методах, по традиции называемой VTBL, которая дублируется для каждого производного класса. Если в производном классе переопределяется новый метод, указатель на него будет в этой таблице под тем же индексом, что и в VTBL класса-предка - но указывать он будет на перегруженный метод. За счёт этого достигается наилучшая скорость - вызов функции по указателю через смещение в VTBL. С другой стороны, для каждого нового класса полностью дублируется вся VTBL ! Короче, классический случай ножниц "скорость против размера".

Dynamic методы имеют несколько другой способ хранения. Им назначается некоторый индекс - но не в таблице, а в hash-структуре. Также эта структура не дублируется для каждого производного класса - если переопределяется dynamic метод, он переопределяется для данного класса - и всё. Но вызов dynamic методов имеет больше накладных расходов - при вызове Delphi просматривает все классы-предки данного класса в поисках метода с нужным индексом.

Посмотрим, как всё вышесказанное выглядит на Ассемблере:

Я надеюсь, Вы ещё помните, насколько полезна бывает RTTI ? RTTI класса нашей единственной формы расположена по адресу 0x44016C. На сей раз она содержит по смещению 1Ch ненулевое значение, а указатель на hash-массив dynamic методов. Структура эта имеет примерно такой вид:

Структура RTTI класса TForm1
Смещение Тип Описание
0 WORD Размер N hashа
2 WORD индексов dynamic методов
N * 2 + 2 DWORD N указателей на функции

Что ещё более интересно, в нашём случае индекс единственной функции - WMSizing 0x214. Если Вы посмотрите в файле заголовков messages.pas, 0x214 ( eq 532 ) есть значение сообщения WM_SIZING. В Borlandе, видимо, простые парни работают...

Итак, сейчас у нас есть более полное описание RTTI, я позволю себе повторить его здесь полностью:

Более полное описание RTTI
Смещение Тип Описание
0 DWORD указатель на VTBL
4 DWORD значение не выяснено (vmtIntfTable)
8 DWORD значение не выяснено (vmtAutoTable)
Ch DWORD значение не выяснено (vmtInitTable)
10h DWORD указатель на список наследований
14h DWORD указатель на компоненты, которыми владеет данный класс
18h DWORD указатель на массив обработчиков событий
1Ch DWORD указатель на hash dynamic методов
20h DWORD указатель на Pascal-строку - имя класса
24h DWORD размер класса
28h DWORD указатель на структуру RTTI класса-предка данного класса
2Ch DWORD указатель на метод SafeCallException
30h DWORD указатель на метод AfterConstruction
34h DWORD указатель на метод BeforeDestruction
38h DWORD указатель на метод Dispatch
3Ch DWORD указатель на метод DefaultHandler
40h DWORD указатель на метод NewInstance
44h DWORD указатель на метод FreeInstance
48h DWORD указатель на метод Destroy
4Ch DWORDs начало VTBL

Давайте рассмотрим самую примечательную функцию в моей программе - TForm1.Button1Click. Примечательна она исключительно тем, что вызывает функцию BuggyOne, выбрасывающую исключение, которое затем сама же и ловит двумя руками. BuggyOne proc near var_4 = dword ptr -4 push ebp mov ebp, esp push 0 ; инициализация в 0 var_4 push ebx ; сохранить ebx push esi ; и esi xor eax, eax push ebp ; и ещё ebp push offset loc_0_44064F ; поместим в стек адрес ; finally кода push dword ptr fs:[eax] ; и прежнее значение стека ; обработки исключений mov fs:[eax], esp ; в стек обработки исключений ; помещается указатель на текущее значение стека push 0 lea edx, [ebp+var_4] ; загрузим в var_4 строку из mov eax, offset off_0_440368 ; ресурсов call @LoadResString ... loc_0_440646: lea eax, [ebp+var_4] ; очистить строку в var_4 call @@LStrClr ; ::`intcls'::LStrClr retn ; ------------------------------------------------------------------ loc_0_44064F: jmp @@HandleFinally ; ::`intcls'::HandleFinally ; ------------------------------------------------------------------ jmp short loc_0_440646 Обратите внимание на две вещи:

  1. инициализация стека исключений. В стек помещается указатель 0x44064f, далее значение стека заносится с fs:[0]. По адресу 0x44064f нет ничего примечательного - просто переход на одинаковый для всех кусок кода HandleFinally. Но вот сразу за ним идёт код, который, казалось бы, никогда не достигается - переход на 0x440646. Но вот этот-то код как раз и есть finally часть в обработке исключений. В данном случае - это освобождение строки var_4.

HandleFinally: @@HandleFinally: mov eax, [esp+4] mov edx, [esp+8] test dword ptr [eax+4], 6 jz short loc_0_403294 mov ecx, [edx+4] ; адрес перехода на HandleFinally mov dword ptr [edx+4], offset loc_0_403294 push ebx push esi push edi push ebp mov ebp, [edx+8] add ecx, 5 ; добавим к нему 5 call @System@_16583 ; System::_16583 call ecx ; и вызовем как функцию pop ebp pop edi pop esi pop ebx loc_0_403294: mov eax, 1 retn На момент вылета на этот код в fs:[0] и eax содержится указатель стека, в котором находятся ранее занесённые в него ( смотрите начало процедуры BuggyOne; также я привык изображать вершину стека сверху, а не как оно есть на самом деле ):

  • [eax + 4] прежнее значение стека в fs:[0]
  • [eax + 8] указатель на инструкцию перехода к HandleFinally
  • [eax + 0xC] ebp

Т.е. происходит следующее - инструкция следом за переходом на HandleFinally является процедурой обработки finally-части ( так как размер инструкции "jmp HandleFinally" равен ровно 5 байт )

  • Загрузка строки из ресурса. Строка BuggyOneCaption описана как resourcestring - это значит, что Delphi помесила её в строковую таблицу. Прототип функции LoadResString ( из Sys/System.pas ):
  •     type
    PResStringRec = ^TResStringRec; TResStringRec = record Module: ^Longint; Identifier: Integer; end;
    function LoadResString(ResStringRec: PResStringRec): string;

    Module - handler загруженного модуля, содержащего в себе ресурс. Для нашей программы это hInstance самого приложения ( поскольку главная форма находится в том же модуле, что и объект TApplication ).

    Identifier - целое число, меньшее 65536, или указатель на LPSZ строку - имя ресурса. По адресу 0x440368 содержится: off_0_440368 dd offset dword_0_4424D8 ; hModule приложения dd 0FF5Dh ; Identifier Число 0xFF5D = 65373 < 65536, так что наша строка идентифицируется по числовому значению. Посмотрим ресурсы моей программы в редакторе ресурсов Restorator ( кстати, весьма рекомендую эту программу для исследований приложений на Delphi - она умеет показывать описание Delphi-форм ! ). Наша строка нашлась в секции string tables под номером секции 4086, смещение -3. Как это соотносится с ранее найденным значением идентификатора ? Очень просто: 65373 = 4086 * 16 - 3; Всё гениальное просто ( однако не всё простое гениально ). xor eax, eax push ebp push offset loc_0_44061D ; новый finally handler push dword ptr fs:[eax] mov fs:[eax], esp mov dl, 1 mov eax, ds:off_0_44016C ; ptr to TRPExceptionChild RTTI call sub_0_440378 ; TRPExceptionChild::Create mov ebx, eax mov al, ds:byte_0_440660 ; db 1 eq RP_One из TRPEnum mov [ebx+18h], al lea eax, [ebx+0Ch] mov edx, offset aSeven ; "Seven" call @@LStrAsg ; ::`intcls'::LStrAsg mov esi, 9 lea eax, [ebx+10h] mov edx, offset aEight ; "Eight" call @@LStrAsg ; ::`intcls'::LStrAsg lea eax, [ebx+esi*4-10h] mov edx, offset aNineInchNails ; "Nine inch nails" call @@LStrAsg ; ::`intcls'::LStrAsg mov al, [ebx+18h] or al, ds:byte_0_44069C ; db 2 eq RP_Two из TRPEnum mov [ebx+18h], al mov eax, ebx call @@RaiseExcept ; ::`intcls'::RaiseExcept ... loc_0_44061D: jmp @@HandleFinally ; ---------------------------------------- jmp short loc_0_440607 ... loc_0_440607: push 0 mov cx, ds:word_0_44065C mov dl, 3 mov eax, offset aInFinallyPart call @MessageDlg retn Дальше совсем просто. Инициализируется новый обработчик finally части, при этом указатель на старое значение стека также помещается в стек. Далее вызывается конструктор TRPExceptionChild::Create - первым аргументом ему передаётся указатель на RTTI класса TRPExceptionChild, а вторым ( в регистре dl, я не знаю для чего ) 1 - указатель на созданный экземпляр класса возвращается в регистре eax, и затем пересылается в ebx, который используется в дальнейшем как базовый регистр. Члену Code присваивается значение RP_One ( eq 1 ) из набора TRPEnum. Можно заметить, что Code расположена в классе TRPException по смещению 0x18h. Затем идёт присваивание значений массиву строк - массив начинается по смещению 0xC. Довольно непонятно выглядит присваивание последнему ( 9ому элементу массива ): он должен быть расположен по смещению 0xC + (3 - 1) * 4 = 0x14; 9 * 4 - 0x10 даёт то же самое 0x14, но какова логика ! Затем к нашему набору Code добавляется RP_Two ( eq 2 ). Потом вызывается процедура RaiseExcept с единственным аргументом в eax - адресом нашего класса.

    Пожалуй, в BuggyOne больше нет ничего интересного.

    Button1Click ... push offset loc_0_440728 push dword ptr fs:[edx] mov fs:[edx], esp mov eax, ebx call BuggyOne ; процедура, генерирующая исключение xor eax, eax pop edx pop ecx pop ecx mov fs:[eax], edx jmp short loc_0_440790 ; -------------------------------------------------------- loc_0_440728: jmp @@HandleOnException ; ::`intcls'::HandleOnExceptions ; -------------------------------------------------------- dd 2 ; размер фильтров исключений dd offset off_0_4400F4 ; адрес RTTI TRPException dd offset loc_0_440741 ; адрес код для TRPException dd offset off_0_44016C ; TRPExceptionChild dd offset loc_0_44076B ; On TRPExceptionChild ; ---------------------------------------------------------------- loc_0_440741: mov ebx, eax push 0 mov cx, ds:word_0_4407C8 mov dl, 3 mov eax, offset aButton1clickIn ; строка "Button1Click in exception block" call @MessageDlg mov eax, ebx mov edx, [eax] ; вызов TRPExceptionChild::Old_one_virtual call dword ptr [edx] mov eax, ebx mov bx, 0FFFFh ; вызов TRPExceptionChild::Old_one_dynamic ; имеет индекс 0xFFFF call @@CallDynaInst ; ::`intcls'::CallDynaInst jmp short loc_0_44078B Здесь можно увидеть в действии механизм фильтрации и обработки исключений. Опять в fs:[0] помещается указатель на стек, но на сей раз в него помещён адрес инструкции перехода к процедуре обработке исключений HandleOnExceptions. Следом за ней расположен массив фильтров исключений. Он имеет весьма незатейливую структуру:

    Структура массива фильтров исключений
    Смещение Тип Описание
    0 DWORD Размер N массива фильтров исключений
    4 DWORD Указатель на RTTI класса - объекта исключение
    8 DWORD Указатель на код, вызываемый при исключении этого класса
    4 + M * 4 DWORD Указатель на M-ную RTTI класса - объекта исключение
    8 + M * 4 DWORD Указатель на код, вызываемый при исключении M-ного класса

    Далее мы можем наблюдать вызовы virtual & dynamic функций - соответственно, TRPExceptionChild::Old_one_virtual & TRPExceptionChild::Old_one_dynamic.

    1. Вызов TRPExceptionChild::Old_one_virtual
      Простой и понятный вызов функции по указателю. Первым членом любого класса идёт указатель на VTBL - по смещению 0x0; метод Old_one_virtual является единственным в VTBL класса TRPExceptionChild - соответственно, он расположен под индексом 0. Под тем же самым индексом в VTBL класса TRPException расположен виртуальный метод TRPException::Old_one_virtual

  • Вызов TRPExceptionChild::Old_one_dynamic
    В hash dynamic методов класса TRPException метод TRPException::Old_one_dynamic прописан под индексом 0xFFFF. Сложно сказать, что будет, если, имея dynamic метод с индексом 0xFFFF, Вы попробуете обработать событие Windows с номеров 0xFFFF ( можете попробовать сделать это самостоятельно ). Остаётся надеятся, что Delphi всё-таки отслеживают занятые индексы для динамических методов. Как видите, для вызова динамических методов используется вызов функции CallDynaInst с передаваемым в регистре bx индексом метода.


  • Содержание раздела