Исследование кода, генерируемого Delphi
|
Сидит девочка за MAC'ом, а рядом грузин за IBM. Вдруг девочка подскакивает и говорит грузину:
- Скажи MAC.
- Ну мак.
- У меня компьютер MAC, а ты - голубой маньяк! Ла-ла-ла-ла ла-ла-ла.
Прошло еще 5 минут. Девочка говорит грузину:
- Дяденька, скажи Система 7.
- Ну Сыстэма сэм.
- У меня система семь, а ты - п#дераст совсем! Ла-ла-ла-ла ла-ла-ла.
И сказал грузин тогда девочке:
- Дэвачка, скажы АйБиЭм савмэстымый компутэр.
- Ну IBM-совместимый компьютер.
- В рот мой х@й тебя е#ал. Ла-ла-ла-ла ла-ла-ла.
|
Введение
На этот раз я представляю Вам сугубо теоретическое исследование, и все рассматриваемые программы написал сам. Кроме них нам понадобятся Delphi и исходный код VCL (я использовал Delphi 4.0 Client/Server Edition), а также дизассемблер IDA Pro (я пользуюсь v3.8b). Полагаю Вы понимаете Ассемблер и имеете опыт в написании программ на Delphi с применением VCL.
Delphi генерирует огромное количество мёртвого и практически одинакового кода для любого приложения, использующего VCL. Тем не менее множество приложений относительно успешно создаются на Delphi, как же бедным исследователям отделять зёрна от плевел?
Я набросал в несистематическом порядке несколько элементов управления (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: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
|
Рассмотрим восстановление стека подробнее. В стеке в настоящий момент содержится:
- указатель на 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.
|