Реализация перехвата вызовов API (Delphi)

TITLE

Новорег

TITLE

Новорег
Регистрация
22 Июл 2016
Сообщения
86
Симпатии
288
Время онлайн
1ч 4м 31с
[LIKES=7]В принципе это очень редко встречающийся вариант перехвата, но раз уж я решил рассказать о всех возможных способах, то его тоже придется рассмотреть :)

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

В следующем примере я покажу как можно перекрыть метод TForm.CanResize который вызывается при изменении размеров формы. Задача кода, не дать изменить размеры формы по ширине больше чем 500 пикселей.

Конечно в этом примере целая куча минусов, во первых для достижения поставленной цели можно было перекрыть обработчик OnResize, во вторых, так как перекрывается CanResize от TForm, то эта модификация повлияет на все формы в проекте, но на то он и пример - его задача просто показать данную возможность.

Применение: Данный вариант перехвата может использоваться только для виртуальных и динамических методов классов, причем только в рамках собственного PE файла. Если данный код поместить в библиотеку и попробовать перехватить таким образом метод у основного приложения - ничего не выйдет, так как TForm приложения и TForm библиотеки это разные классы.

Код выглядит следующим образом:

unit uVMT;

interface

uses
Windows, Classes, Controls, Forms, StdCtrls;

type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
end;

var
Form1: TForm1;

implementation

{$R *.dfm}

function NewCanResizeHandler(Self: TObject;
var NewWidth, NewHeight: Integer): Boolean;
begin
Result := True;
if NewWidth > 500 then
NewWidth := 500;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
VMTAddr: PPointer;
OldProtect: Cardinal;
begin
asm
mov eax, Self
mov eax, [eax] // получаем указатель на VMT таблицу
add eax, VMTOFFSET TForm.CanResize // получаем адрес указателя на TForm.CanResize
mov VMTAddr, eax
end;
// снимаем блокировку модификации страницы
VirtualProtect(VMTAddr, 4, PAGE_EXECUTE_READWRITE, OldProtect);
try
// назначаем новый обработчик
VMTAddr^ := @NewCanResizeHandler;
finally
// возвращаем атрибуты страницы
VirtualProtect(Pointer(VMTAddr), 4, OldProtect, OldProtect);
FlushInstructionCache(GetCurrentProcess, VMTAddr, 4);
end;
if Width > 500 then
Width := 500;
MessageBox(Handle, 'Максимальная ширина формы установлена в 500 пикселей',
PChar(Application.Title), MB_ICONINFORMATION);
end;

end.



Теперь поподробнее:

1. Декларация обработчика перехвата выполнена с учетом нюансов, описанных в втором разделе данной статьи (появился параметр Self).

2. Для перехвата используется документированная директива VMTOFFSET, описанная в справке Delphi.

Вот кусочек с ее описанием:
Two additional directives allow assembly code to access dynamic and virtual method: VMTOFFSET and DMTINDEX.
VMTOFFSET retrives the offset in bytes of the virtual method pointer table entry of the virtual method argument from the beginning of the virtual method table (VMT). This directive needs a fully specified class name with a method name as a parameter, for example,TExample.VirtualMethod. Как ясно из описания, ее задача вернуть смещение на адрес виртуального метода относительно начала таблицы виртуальных методов (VMT). На начало VMT таблицы указывает непосредственно параметр Self. То есть, если изобразить это в виде кода, то получится следующее:


1
VMT := Pointer(Self)^;



3. После того как при помощи ассемблерной вставки получен указатель на адрес метода, производится его подмена на адрес нового обработчика виртуального метода.

4. Так как подмена производится в коде приложения, который обычно расположен в странице памяти у которой отсутствуют права на запись (обычно это права чтение и исполнение), то перед назначением нового обработчика выставляются права на запись.

Ну а теперь, запустите пример и проверьте его работу.

Как видите, по сути мы выполнили практически все те же шаги, что и в первом примере, т.е. получили адрес контролируемой функции, и заменили его на адрес нового обработчика, в котором происходит управление параметрами оригинального метода. Код для вызова оригинального обработчика (аналог inherited) я приводить не буду, т.к. все же данный метод перехвата показан только для расширения кругозора и крайне не желателен для реализации в боевом приложении :)

Вариант с перехватом динамических методов класса я рассматривать не буду — но принцип примерно похож.[/LIKES]
 
Сверху