program vmthooks;
{$ifdef fpc}{$mode delphi}{$endif}
uses
{$if defined(mswindows)}
windows,
{$elseif defined(unix)}
cthreads,
baseunix,
unixtype,
{$else}
{$error not supported}
{$ifend}
sysutils;
type
TInstanceMethod = function(AClass: TClass): TObject;
{ Make a memory page writable, write the new pointer, restore perms.}
{$if defined(mswindows)}
procedure PatchVMTSlot(AClass: TClass; Offset: PtrInt; NewProc: Pointer);
var
VMTSlot : PPointer;
OldProt : DWORD;
begin
{ The VMT pointer is stored at AClass + Offset.
Because Offset is negative we go *before* the class pointer. }
VMTSlot := PPointer(PByte(AClass) + Offset);
VirtualProtect(VMTSlot, SizeOf(Pointer), PAGE_READWRITE, @OldProt);
VMTSlot^ := NewProc;
VirtualProtect(VMTSlot, SizeOf(Pointer), OldProt, @OldProt);
end;
{$elseif defined(unix)}
{ Returns the system page size }
const
_SC_PAGESIZE = 30; // Linux x86/x86-64/arm
// _SC_PAGESIZE = 29; // macOS — uncomment if targeting macOS
function sysconf(name: cint): clong; cdecl; external;
function GetPageSize: PtrUInt;
begin
Result := PtrUInt(sysconf(_SC_PAGESIZE));
if Result = 0 then
Result := 4096; // safe fallback
end;
{ Aligns an address DOWN to the nearest page boundary }
function PageAlignDown(Addr: PtrUInt; PageSize: PtrUInt): PtrUInt;
begin
Result := Addr and not (PageSize - 1);
end;
{ Makes the page(s) containing the given memory range writable,
patches the pointer, then restores the original protection. }
procedure PatchVMTSlot(AClass: TClass; Offset: PtrInt; NewProc: Pointer);
var
VMTSlot: PPointer;
PageSize: PtrUInt;
AlignedAddr: PtrUInt;
RangeSize: PtrUInt;
begin
VMTSlot := PPointer(PByte(AClass) + Offset);
PageSize := GetPageSize;
AlignedAddr := PtrUInt(VMTSlot) and not (PageSize - 1);
RangeSize := (PtrUInt(VMTSlot) + SizeOf(Pointer)) - AlignedAddr;
if fpMProtect(Pointer(AlignedAddr), RangeSize, PROT_READ or PROT_WRITE) <> 0 then
raise Exception.CreateFmt('mprotect rw failed: errno %d', [fpGetErrno]);
VMTSlot^ := NewProc;
{ Do not restore to PROT_READ here unless you know the original page perms. }
end;{$else}
{$error not supported}
{$endif}
{ Replacement methods — must match the calling convention exactly. }
type
TSomeClass = class
public
procedure DoSomething; virtual;
end;
var
OriginalNewInstance : Pointer;
OriginalFreeInstance : Pointer;
{ NewInstance receives the class reference in Self (first implicit arg).
It must return a fully initialised TObject. }
function HookedNewInstance(AClass: TClass): TObject;
type
TOriginal = function(AClass: TClass): TObject;
begin
WriteLn('** HookedNewInstance called for ', AClass.ClassName);
{ Call the original so real allocation still happens: }
Result := TOriginal(OriginalNewInstance)(AClass);
end;
{ FreeInstance receives the object instance in Self. }
procedure HookedFreeInstance(Instance: TObject);
type
TOriginal = procedure(Instance: TObject);
begin
WriteLn('** HookedFreeInstance called for ', Instance.ClassName);
TOriginal(OriginalFreeInstance)(Instance);
end;
{ Install / uninstall }
procedure InstallHooks(AClass: TClass);
begin
{ Save originals before overwriting }
OriginalNewInstance := PPointer(PByte(AClass) + vmtNewInstance )^;
OriginalFreeInstance := PPointer(PByte(AClass) + vmtFreeInstance)^;
PatchVMTSlot(AClass, vmtNewInstance, @HookedNewInstance);
PatchVMTSlot(AClass, vmtFreeInstance, @HookedFreeInstance);
end;
procedure UninstallHooks(AClass: TClass);
begin
PatchVMTSlot(AClass, vmtNewInstance, OriginalNewInstance);
PatchVMTSlot(AClass, vmtFreeInstance, OriginalFreeInstance);
end;
{ ------------------------------------------------------------------ }
{ Demo }
{ ------------------------------------------------------------------ }
procedure TSomeClass.DoSomething;
begin
WriteLn('DoSomething');
end;
var
Obj: TSomeClass;
begin
InstallHooks(TSomeClass);
{ triggers HookedNewInstance }
Obj := TSomeClass.Create;
Obj.DoSomething;
{ triggers HookedFreeInstance }
Obj.Free;
UninstallHooks(TSomeClass);
writeln(vmtNewInstance);
writeln(vmtfreeinstance);
readln;
end.