Дорогие друзья, эта статья написана не для того, чтобы дать Вам очередной способ взлома программных продуктов, а чтобы научить Вас самостоятельно создавать и эффективно использовать серьезные защитные алгоритмы в своих собственных разработках.
Введение
На этот раз я представляю Вам сугубо теоретическое исследование, и все рассматриваемые программы написал сам. Кроме них нам понадобятся 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'ом, попытался выяснить, как выглядят типичные действия по заполнению окна регистрации на Ассемблере.
Получилось у него примерно следующее:
Обнаружение нужных классов
Я набросал в несистематическом порядке несколько элементов управления (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:
Вернёмся к структуре RTTI класса TForm1. По смещению 14h находится указатель на компоненты, которыми владеет данный класс. Это все элементы списка Components во время разработки. Эта структура имеет довольно простой вид:
Структура RTTI класса TForm1 | ||
Смещение | Тип | Описание |
0 | WORD | число CompCount различных классов компонентов |
2 | DWORD | указатель на массив указателей на структуры RTTI этих классов. Первым элементом этого массива является WORD - число его элементов, далее расположены указатели на структуры RTTI. |
Сразу вслед за ней идут CompCount структур, описывающих эти компоненты:
CompCount структур | ||
Смещение | Тип | Описание |
0 | WORD | смещение в классе, по которому находится указатель на компонент |
1 | WORD | значение не выяснено |
2 | WORD | индекс в массиве структур RTTI - по нему определяется класс компонента |
N+2 | WORD | длина Pascal-строки |
N+6 | String | имя компонента (например, 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-функцию и прежнее значение стека, и восстанавливается значение FS:[0]. Дальше в стек помещается адрес процедуры очистки стека. После инструкции 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 Обратите внимание на две вещи:
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; также я привык изображать вершину стека сверху, а не как оно есть на самом деле ):
Т.е. происходит следующее - инструкция следом за переходом на HandleFinally является процедурой обработки finally-части ( так как размер инструкции "jmp HandleFinally" равен ровно 5 байт )
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.