Дорогие друзья, эта статья, как и все остальные на нашем сайте, написана не для того, чтобы дать Вам очередной способ взлома программных продуктов, а чтобы научить Вас самостоятельно создавать и эффективно использовать серьезные защитные алгоритмы в своих собственных разработках. |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
На этот раз я представляю Вам сугубо теоретическое исследование, и все рассматриваемые программы написал сам. Кроме них нам понадобятся 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'ом, попытался выяснить, как выглядят типичные действия по заполнению окна регистрации на Ассемблере. Получилось у него примерно следующее: |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
А теперь плохая новость - для 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 байт! И это буквально за пять минут! Да я - серьезный программист! Далее неплохо было бы дизассемблировать полученный файл. |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Надо заметить, что, несмотря на все свои достоинства, 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) на некую рекурсивную структуру, которую я назвал список наследственности:
Путешествуя по этому списку, можно с лёгкостью выяснить генеалогическое дерево класса TForm1: |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
У последнего указатель на предка содержит нулевое значение - видимо, означая конец списка. Вернёмся к структуре RTTI класса TForm1. По смещению 14h находится указатель на компоненты, которыми владеет данный класс. Это все элементы списка Components во время разработки. Эта структура имеет довольно простой вид:
Сразу вслед за ней идут CompCount структур, описывающих эти компоненты:
Самым важным здесь являются смещение на компонент во включающем классе и его тип. Запомним их для компонентов в форме TForm1:
Снова вернёмся к структуре RTTI класса TForm1. По смещению 18h находится указатель на одну из самых полезных структур - на массив обработчиков событий (но только тех, которые заданы во время проектирования!). Первым элементом этого массива идёт WORD, определяющий длину этого массива, а его элементы имеют такие поля:
Тип определяет количество и размерность аргументов. Для обработчиков OnClick он равен 13h, для OnShow 0Fh. Не прошло и получаса, а я уже нашёл свой код. Мы рассмотрим его чуть позже (пока Вы можете назвать найденные функции как в оригинале), а сейчас продолжим рассмотрение структуры RTTI класса. По смещению 24h записывается размер класса (DWORD) - для TForm1 он составляет 02E4h байт. Сравните его с таблицей смещений компонентов. По смещению 28h находится указатель на структуру RTTI класса-предка. У объекта TObject он равен нулю. По смещению 20h находится указатель на Pascal-строку - имя класса. Я повторю всю вышеизложенную информацию в следующей таблице:
По смещению 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
Рассмотрим восстановление стека подробнее. В стеке в настоящий момент содержится: |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Хотя перед этим в стек была помещена 1 - её нет в стеке. Почему? Потому что она является последним аргументом функции TApplication::MessageBox(). Но ведь у этой функции всего три аргумента, и они все передаются в регистрах - скажете Вы! Ничего подобного, Вы забыли, что всем методам классов передаётся неявно ещё один аргумент (под номером ноль) - указатель на экземпляр класса. При возврате же вызываемая функция сама производит очистку стека. Итак, сначала извлекается предыдущее значение FS:[0], указатель на finally-функцию и прежнее значение стека, и восстанавливается значение FS:[0]. Дальше в стек помещается адрес процедуры очистки стека. После инструкции retn стек будет выглядеть так: |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Далее снимается оригинальное значение регистра EBX, стек восстанавливается в первоначальное состояние (которое хранилось всё время выполнения процедуры в регистре 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. |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||