xtool/contrib/Delphi_MemoryModule/FuncHook.pas

169 lines
5.2 KiB
ObjectPascal

(*==============================================================================
====== Function hooking ======
Functions exported from DLL contain single instruction "JMP [fn_addr]" that
points to actual code. So there could be 2 ways of hooking:
1) Patch the JMP not touching the code
+ Original proc could be called via HookInfo.OrigProc
- (or feature) Only the one imported function is hooked. If there's another
export definition or dynamic load, the hook won't change them.
2) Patch the very code as usual
+ All function imports are impacted
- No way to use original proc
2nd way is called "strict address mode" here and is controlled by HookProcedure()'s
StrictMode parameter.
Compatibility: RAD Studio 2009+ (uses advanced records and Exit(param)), x32/x64.
==============================================================================*)
unit FuncHook;
interface
uses
Windows;
type
TInjectInstr = packed record
Opcode: UInt8;
Offset: Int32;
end;
PInjectInstr = ^TInjectInstr;
THookInfo = record
private
OrigCode: TInjectInstr; // Original instruction
HookCode: TInjectInstr; // Hook instruction (saved only for check on uninstall)
ProcAddr: Pointer; // Address of a routine to hook
TrampAddr: Pointer; // Address of a trampoline (original routine; DLL exports only)
HookAddr: Pointer; // Address of a hook routine
StrictMode: Boolean; // Hook was installed in strict address mode
public
procedure Init(ProcAddr, HookAddr: Pointer);
property OrigProc: Pointer read TrampAddr;
end;
function HookProcedure(StrictMode: Boolean; var HookInfo: THookInfo): Boolean;
function UnhookProcedure(var HookInfo: THookInfo): Boolean;
implementation
const
INDIRECT_JMP = $25FF;
RELATIVE_JMP = $E9;
EmptyInstr: TInjectInstr = (Opcode: 0; Offset: Int32($DEADBEEF));
function IsEqual(const Instr1, Instr2: TInjectInstr): Boolean;
begin
Result := (Instr1.Opcode = Instr2.Opcode) and (Instr1.Offset = Instr2.Offset);
end;
{ THookInfo }
procedure THookInfo.Init(ProcAddr, HookAddr: Pointer);
begin
ZeroMemory(@Self, SizeOf(Self));
OrigCode := EmptyInstr;
Self.ProcAddr := ProcAddr;
Self.HookAddr := HookAddr;
end;
// Utility function to (un)install hook by patching function code.
// Install: (un)install the hook
// HookInfo: all required data
// Returns: True = success, False = fail
function PatchCode(Install: Boolean; var HookInfo: THookInfo): Boolean;
// Get the real address of a function (for functions exported from DLL)
function GetStrictAddr: Pointer;
type
TAbsIndirectJmp = packed record
OpCode: UInt16; // $FF25 (x32: Jmp, FF /4; x64: Jmp, Rel /4)
Addr : Int32;
end;
PAbsIndirectJmp = ^TAbsIndirectJmp;
var
jmp: PAbsIndirectJmp;
begin
Result := nil;
jmp := PAbsIndirectJmp(HookInfo.ProcAddr);
if jmp.OpCode = INDIRECT_JMP then
{$IFDEF CPUX86}
Result := PPointer(jmp.Addr)^;
{$ENDIF}
{$IFDEF CPUX64}
Result := PPointer(PByte(HookInfo.ProcAddr) + jmp.Addr + SizeOf(TAbsIndirectJmp))^;
{$ENDIF}
end;
var
OldProtect: DWORD;
DestAddr: PInjectInstr;
begin
// Check strict address mode
if HookInfo.StrictMode
then DestAddr := GetStrictAddr
else DestAddr := HookInfo.ProcAddr;
Result := VirtualProtect(DestAddr, SizeOf(TInjectInstr), PAGE_EXECUTE_READWRITE, OldProtect);
if not Result then Exit;
if Install then
begin
// For functions exported from DLL, the only instruction they contain is
// "JMP [fn_addr]" so we can save the address as a trampoline
if not HookInfo.StrictMode
then HookInfo.TrampAddr := GetStrictAddr
else HookInfo.TrampAddr := nil;
HookInfo.OrigCode := DestAddr^;
DestAddr^ := HookInfo.HookCode;
end
else
begin
// Check that patch wasn't overwritten
if IsEqual(HookInfo.HookCode, DestAddr^) then
begin
DestAddr^ := HookInfo.OrigCode;
// Clear OrigCode field thus indicating that hook is not installed
HookInfo.OrigCode := EmptyInstr;
HookInfo.StrictMode := False;
end
else
Result := False;
end;
FlushInstructionCache(GetCurrentProcess, DestAddr, SizeOf(TInjectInstr));
VirtualProtect(DestAddr, SizeOf(TInjectInstr), OldProtect, OldProtect);
end;
// Install the hook
// StrictMode: "strict address mode" flag
// HookInfo: all required data
// Returns: True = success, False = fail
function HookProcedure(StrictMode: Boolean; var HookInfo: THookInfo): Boolean;
begin
// Required data is missing?
if (HookInfo.HookAddr = nil) or (HookInfo.ProcAddr = nil) or
// Hook is installed already?
not IsEqual(HookInfo.OrigCode, EmptyInstr) then
Exit(False);
HookInfo.HookCode.Opcode := RELATIVE_JMP;
HookInfo.HookCode.Offset := PByte(HookInfo.HookAddr) - PByte(HookInfo.ProcAddr) - SizeOf(TInjectInstr);
HookInfo.StrictMode := StrictMode;
Result := PatchCode(True, HookInfo);
end;
// Uninstall the hook
// HookInfo: all required data
// Returns: True = success, False = fail
function UnhookProcedure(var HookInfo: THookInfo): Boolean;
begin
// Required data is missing?
if (HookInfo.HookAddr = nil) or (HookInfo.ProcAddr = nil) or
// Hook is not installed yet?
IsEqual(HookInfo.OrigCode, EmptyInstr) then
Exit(False);
Result := PatchCode(False, HookInfo);
end;
end.