From 369d7af8a00008796a60e00c2c9ef9dbbe9db2de Mon Sep 17 00:00:00 2001 From: Raymond Date: Mon, 30 Aug 2021 16:33:57 -0400 Subject: [PATCH] feat: AsmHook --- Dalamud/Hooking/AsmHook.cs | 180 ++++++++++++++++++++++++ Dalamud/Hooking/AsmHookBehaviour.cs | 24 ++++ Dalamud/Hooking/Hook.cs | 76 +--------- Dalamud/Hooking/Internal/HookManager.cs | 73 ++++++++++ 4 files changed, 278 insertions(+), 75 deletions(-) create mode 100644 Dalamud/Hooking/AsmHook.cs create mode 100644 Dalamud/Hooking/AsmHookBehaviour.cs diff --git a/Dalamud/Hooking/AsmHook.cs b/Dalamud/Hooking/AsmHook.cs new file mode 100644 index 000000000..49c951867 --- /dev/null +++ b/Dalamud/Hooking/AsmHook.cs @@ -0,0 +1,180 @@ +using System; +using System.Reflection; +using System.Reflection.Emit; + +using Dalamud.Hooking.Internal; +using Dalamud.Memory; +using Reloaded.Hooks; + +namespace Dalamud.Hooking +{ + /// + /// Manages a hook which can be used to intercept a call to native function. + /// This class is basically a thin wrapper around the LocalHook type to provide helper functions. + /// + public sealed class AsmHook : IDisposable, IDalamudHook + { + private readonly IntPtr address; + private readonly Reloaded.Hooks.Definitions.IAsmHook hookImpl; + + private bool isActivated = false; + private bool isEnabled = false; + + private DynamicMethod statsMethod; + + /// + /// Initializes a new instance of the class. + /// This is an assembly hook and should not be used for except under unique circumstances. + /// Hook is not activated until Enable() method is called. + /// + /// A memory address to install a hook. + /// Assembly code representing your hook. + /// The name of what you are hooking, since a delegate is not required. + /// How the hook is inserted into the execution flow. + public AsmHook(IntPtr address, byte[] assembly, string name, AsmHookBehaviour asmHookBehaviour = AsmHookBehaviour.ExecuteFirst) + { + address = HookManager.FollowJmp(address); + + var hasOtherHooks = HookManager.Originals.ContainsKey(address); + if (!hasOtherHooks) + { + MemoryHelper.ReadRaw(address, 0x32, out var original); + HookManager.Originals[address] = original; + } + + this.address = address; + this.hookImpl = ReloadedHooks.Instance.CreateAsmHook(assembly, address.ToInt64(), (Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour)asmHookBehaviour); + + this.statsMethod = new DynamicMethod(name, null, null); + this.statsMethod.GetILGenerator().Emit(OpCodes.Ret); + var dele = this.statsMethod.CreateDelegate(typeof(Action)); + + HookManager.TrackedHooks.Add(new HookInfo(this, dele, Assembly.GetCallingAssembly())); + } + + /// + /// Initializes a new instance of the class. + /// This is an assembly hook and should not be used for except under unique circumstances. + /// Hook is not activated until Enable() method is called. + /// + /// A memory address to install a hook. + /// FASM syntax assembly code representing your hook. The first line should be use64. + /// The name of what you are hooking, since a delegate is not required. + /// How the hook is inserted into the execution flow. + public AsmHook(IntPtr address, string[] assembly, string name, AsmHookBehaviour asmHookBehaviour = AsmHookBehaviour.ExecuteFirst) + { + address = HookManager.FollowJmp(address); + + var hasOtherHooks = HookManager.Originals.ContainsKey(address); + if (!hasOtherHooks) + { + MemoryHelper.ReadRaw(address, 0x32, out var original); + HookManager.Originals[address] = original; + } + + this.address = address; + this.hookImpl = ReloadedHooks.Instance.CreateAsmHook(assembly, address.ToInt64(), (Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour)asmHookBehaviour); + + this.statsMethod = new DynamicMethod(name, null, null); + this.statsMethod.GetILGenerator().Emit(OpCodes.Ret); + var dele = this.statsMethod.CreateDelegate(typeof(Action)); + + HookManager.TrackedHooks.Add(new HookInfo(this, dele, Assembly.GetCallingAssembly())); + } + + /// + /// Gets a memory address of the target function. + /// + /// Hook is already disposed. + public IntPtr Address + { + get + { + this.CheckDisposed(); + return this.address; + } + } + + /// + /// Gets a value indicating whether or not the hook is enabled. + /// + public bool IsEnabled + { + get + { + this.CheckDisposed(); + return this.isEnabled; + } + } + + /// + /// Gets a value indicating whether or not the hook has been disposed. + /// + public bool IsDisposed { get; private set; } + + /// + /// Remove a hook from the current process. + /// + public void Dispose() + { + if (this.IsDisposed) + return; + + this.IsDisposed = true; + + if (this.isEnabled) + { + this.isEnabled = false; + this.hookImpl.Disable(); + } + } + + /// + /// Starts intercepting a call to the function. + /// + public void Enable() + { + this.CheckDisposed(); + + if (!this.isActivated) + { + this.isActivated = true; + this.hookImpl.Activate(); + } + + if (!this.isEnabled) + { + this.isEnabled = true; + this.hookImpl.Enable(); + } + } + + /// + /// Stops intercepting a call to the function. + /// + public void Disable() + { + this.CheckDisposed(); + + if (!this.isEnabled) + return; + + if (this.isEnabled) + { + this.isEnabled = false; + this.hookImpl.Disable(); + } + } + + /// + /// Check if this object has been disposed already. + /// + private void CheckDisposed() + { + if (this.IsDisposed) + { + throw new ObjectDisposedException(message: "Hook is already disposed", null); + } + } + } +} diff --git a/Dalamud/Hooking/AsmHookBehaviour.cs b/Dalamud/Hooking/AsmHookBehaviour.cs new file mode 100644 index 000000000..9f856ae67 --- /dev/null +++ b/Dalamud/Hooking/AsmHookBehaviour.cs @@ -0,0 +1,24 @@ +namespace Dalamud.Hooking +{ + /// + /// Defines the behaviour used by the Dalamud.Hooking.AsmHook. + /// This is equivalent to the same enumeration in Reloaded and is included so you do not have to reference the assembly. + /// + public enum AsmHookBehaviour + { + /// + /// Executes your assembly code before the original. + /// + ExecuteFirst = 0, + + /// + /// Executes your assembly code after the original. + /// + ExecuteAfter = 1, + + /// + /// Do not execute original replaced code (Dangerous!). + /// + DoNotExecuteOriginal = 2, + } +} diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index 422fa8130..feea082d7 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -1,13 +1,9 @@ using System; -using System.Linq; using System.Reflection; -using System.Runtime.InteropServices; using Dalamud.Hooking.Internal; using Dalamud.Memory; -using Iced.Intel; using Reloaded.Hooks; -using Serilog; namespace Dalamud.Hooking { @@ -29,7 +25,7 @@ namespace Dalamud.Hooking /// Callback function. Delegate must have a same original function prototype. public Hook(IntPtr address, T detour) { - address = FollowJmp(address); + address = HookManager.FollowJmp(address); var hasOtherHooks = HookManager.Originals.ContainsKey(address); if (!hasOtherHooks) @@ -150,76 +146,6 @@ namespace Dalamud.Hooking this.hookImpl.Disable(); } - /// - /// Follow a JMP or Jcc instruction to the next logical location. - /// - /// Address of the instruction. - /// The address referenced by the jmp. - private static IntPtr FollowJmp(IntPtr address) - { - while (true) - { - var hasOtherHooks = HookManager.Originals.ContainsKey(address); - if (hasOtherHooks) - { - // This address has been hooked already. Do not follow a jmp into a trampoline of our own making. - Log.Verbose($"Detected hook trampoline at {address.ToInt64():X}, stopping jump resolution."); - return address; - } - - var bytes = MemoryHelper.ReadRaw(address, 8); - - var codeReader = new ByteArrayCodeReader(bytes); - var decoder = Decoder.Create(64, codeReader); - decoder.IP = (ulong)address.ToInt64(); - decoder.Decode(out var inst); - - if (inst.Mnemonic == Mnemonic.Jmp) - { - var kind = inst.Op0Kind; - - IntPtr newAddress; - switch (inst.Op0Kind) - { - case OpKind.NearBranch64: - case OpKind.NearBranch32: - case OpKind.NearBranch16: - newAddress = (IntPtr)inst.NearBranchTarget; - break; - case OpKind.Immediate16: - case OpKind.Immediate8to16: - case OpKind.Immediate8to32: - case OpKind.Immediate8to64: - case OpKind.Immediate32to64: - case OpKind.Immediate32 when IntPtr.Size == 4: - case OpKind.Immediate64: - newAddress = (IntPtr)inst.GetImmediate(0); - break; - case OpKind.Memory when inst.IsIPRelativeMemoryOperand: - newAddress = (IntPtr)inst.IPRelativeMemoryAddress; - newAddress = Marshal.ReadIntPtr(newAddress); - break; - case OpKind.Memory: - newAddress = (IntPtr)inst.MemoryDisplacement64; - newAddress = Marshal.ReadIntPtr(newAddress); - break; - default: - var debugBytes = string.Join(" ", bytes.Take(inst.Length).Select(b => $"{b:X2}")); - throw new Exception($"Unknown OpKind {inst.Op0Kind} from {debugBytes}"); - } - - Log.Verbose($"Resolving assembly jump ({kind}) from {address.ToInt64():X} to {newAddress.ToInt64():X}"); - address = newAddress; - } - else - { - break; - } - } - - return address; - } - /// /// Check if this object has been disposed already. /// diff --git a/Dalamud/Hooking/Internal/HookManager.cs b/Dalamud/Hooking/Internal/HookManager.cs index 60ea3ef3a..e955b43a1 100644 --- a/Dalamud/Hooking/Internal/HookManager.cs +++ b/Dalamud/Hooking/Internal/HookManager.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; using Dalamud.Logging.Internal; using Dalamud.Memory; +using Iced.Intel; using Microsoft.Win32; namespace Dalamud.Hooking.Internal @@ -83,6 +86,76 @@ namespace Dalamud.Hooking.Internal Originals.Clear(); } + /// + /// Follow a JMP or Jcc instruction to the next logical location. + /// + /// Address of the instruction. + /// The address referenced by the jmp. + internal static IntPtr FollowJmp(IntPtr address) + { + while (true) + { + var hasOtherHooks = HookManager.Originals.ContainsKey(address); + if (hasOtherHooks) + { + // This address has been hooked already. Do not follow a jmp into a trampoline of our own making. + Log.Verbose($"Detected hook trampoline at {address.ToInt64():X}, stopping jump resolution."); + return address; + } + + var bytes = MemoryHelper.ReadRaw(address, 8); + + var codeReader = new ByteArrayCodeReader(bytes); + var decoder = Decoder.Create(64, codeReader); + decoder.IP = (ulong)address.ToInt64(); + decoder.Decode(out var inst); + + if (inst.Mnemonic == Mnemonic.Jmp) + { + var kind = inst.Op0Kind; + + IntPtr newAddress; + switch (inst.Op0Kind) + { + case OpKind.NearBranch64: + case OpKind.NearBranch32: + case OpKind.NearBranch16: + newAddress = (IntPtr)inst.NearBranchTarget; + break; + case OpKind.Immediate16: + case OpKind.Immediate8to16: + case OpKind.Immediate8to32: + case OpKind.Immediate8to64: + case OpKind.Immediate32to64: + case OpKind.Immediate32 when IntPtr.Size == 4: + case OpKind.Immediate64: + newAddress = (IntPtr)inst.GetImmediate(0); + break; + case OpKind.Memory when inst.IsIPRelativeMemoryOperand: + newAddress = (IntPtr)inst.IPRelativeMemoryAddress; + newAddress = Marshal.ReadIntPtr(newAddress); + break; + case OpKind.Memory: + newAddress = (IntPtr)inst.MemoryDisplacement64; + newAddress = Marshal.ReadIntPtr(newAddress); + break; + default: + var debugBytes = string.Join(" ", bytes.Take(inst.Length).Select(b => $"{b:X2}")); + throw new Exception($"Unknown OpKind {inst.Op0Kind} from {debugBytes}"); + } + + Log.Verbose($"Resolving assembly jump ({kind}) from {address.ToInt64():X} to {newAddress.ToInt64():X}"); + address = newAddress; + } + else + { + break; + } + } + + return address; + } + private static unsafe void RevertHooks() { foreach (var (address, originalBytes) in Originals)