using System; using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; using Dalamud.Configuration.Internal; using Dalamud.Hooking.Internal; 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. /// /// Delegate type to represents a function prototype. This must be the same prototype as original function do. public class Hook : IDisposable, IDalamudHook where T : Delegate { #pragma warning disable SA1310 // ReSharper disable once InconsistentNaming private const ulong IMAGE_ORDINAL_FLAG64 = 0x8000000000000000; // ReSharper disable once InconsistentNaming private const uint IMAGE_ORDINAL_FLAG32 = 0x80000000; #pragma warning restore SA1310 private readonly IntPtr address; private readonly Hook? compatHookImpl; /// /// Initializes a new instance of the class. /// Hook is not activated until Enable() method is called. /// /// A memory address to install a hook. /// Callback function. Delegate must have a same original function prototype. [Obsolete("Use Hook.FromAddress instead.")] public Hook(IntPtr address, T detour) : this(address, detour, false, Assembly.GetCallingAssembly()) { } /// /// Initializes a new instance of the class. /// Hook is not activated until Enable() method is called. /// Please do not use MinHook unless you have thoroughly troubleshot why Reloaded does not work. /// /// A memory address to install a hook. /// Callback function. Delegate must have a same original function prototype. /// Use the MinHook hooking library instead of Reloaded. [Obsolete("Use Hook.FromAddress instead.")] public Hook(IntPtr address, T detour, bool useMinHook) : this(address, detour, useMinHook, Assembly.GetCallingAssembly()) { } /// /// Initializes a new instance of the class. /// /// A memory address to install a hook. internal Hook(IntPtr address) { this.address = address; } [Obsolete("Use Hook.FromAddress instead.")] private Hook(IntPtr address, T detour, bool useMinHook, Assembly callingAssembly) { if (EnvironmentConfiguration.DalamudForceMinHook) useMinHook = true; this.address = address = HookManager.FollowJmp(address); if (useMinHook) this.compatHookImpl = new MinHookHook(address, detour, callingAssembly); else this.compatHookImpl = new ReloadedHook(address, detour, callingAssembly); } /// /// Gets a memory address of the target function. /// /// Hook is already disposed. public IntPtr Address { get { this.CheckDisposed(); return this.address; } } /// /// Gets a delegate function that can be used to call the actual function as if function is not hooked yet. /// /// Hook is already disposed. public virtual T Original => this.compatHookImpl != null ? this.compatHookImpl!.Original : throw new NotImplementedException(); /// /// Gets a delegate function that can be used to call the actual function as if function is not hooked yet. /// This can be called even after Dispose. /// public T OriginalDisposeSafe { get { if (this.compatHookImpl != null) return this.compatHookImpl!.OriginalDisposeSafe; if (this.IsDisposed) return Marshal.GetDelegateForFunctionPointer(this.address); return this.Original; } } /// /// Gets a value indicating whether or not the hook is enabled. /// public virtual bool IsEnabled => this.compatHookImpl != null ? this.compatHookImpl!.IsEnabled : throw new NotImplementedException(); /// /// Gets a value indicating whether or not the hook has been disposed. /// public bool IsDisposed { get; private set; } /// public virtual string BackendName => this.compatHookImpl != null ? this.compatHookImpl!.BackendName : throw new NotImplementedException(); /// /// Creates a hook by rewriting import table address. /// /// A memory address to install a hook. /// Callback function. Delegate must have a same original function prototype. /// The hook with the supplied parameters. public static unsafe Hook FromFunctionPointerVariable(IntPtr address, T detour) { return new FunctionPointerVariableHook(address, detour, Assembly.GetCallingAssembly()); } /// /// Creates a hook by rewriting import table address. /// /// Module to check for. Current process' main module if null. /// Name of the DLL, including the extension. /// Decorated name of the function. /// Hint or ordinal. 0 to unspecify. /// Callback function. Delegate must have a same original function prototype. /// The hook with the supplied parameters. public static unsafe Hook FromImport(ProcessModule? module, string moduleName, string functionName, uint hintOrOrdinal, T detour) { module ??= Process.GetCurrentProcess().MainModule; if (module == null) throw new InvalidOperationException("Current module is null?"); var pDos = (PeHeader.IMAGE_DOS_HEADER*)module.BaseAddress; var pNt = (PeHeader.IMAGE_FILE_HEADER*)(module.BaseAddress + (int)pDos->e_lfanew + 4); var isPe64 = pNt->SizeOfOptionalHeader == Marshal.SizeOf(); PeHeader.IMAGE_DATA_DIRECTORY* pDataDirectory; if (isPe64) { var pOpt = (PeHeader.IMAGE_OPTIONAL_HEADER64*)(module.BaseAddress + (int)pDos->e_lfanew + 4 + Marshal.SizeOf()); pDataDirectory = &pOpt->ImportTable; } else { var pOpt = (PeHeader.IMAGE_OPTIONAL_HEADER32*)(module.BaseAddress + (int)pDos->e_lfanew + 4 + Marshal.SizeOf()); pDataDirectory = &pOpt->ImportTable; } var moduleNameLowerWithNullTerminator = (moduleName + "\0").ToLowerInvariant(); foreach (ref var importDescriptor in new Span( (PeHeader.IMAGE_IMPORT_DESCRIPTOR*)(module.BaseAddress + (int)pDataDirectory->VirtualAddress), (int)(pDataDirectory->Size / Marshal.SizeOf()))) { // Having all zero values signals the end of the table. We didn't find anything. if (importDescriptor.Characteristics == 0) throw new MissingMethodException("Specified dll not found"); // Skip invalid entries, just in case. if (importDescriptor.Name == 0) continue; // Name must be contained in this directory. if (importDescriptor.Name < pDataDirectory->VirtualAddress) continue; var currentDllNameWithNullTerminator = Marshal.PtrToStringUTF8( module.BaseAddress + (int)importDescriptor.Name, (int)Math.Min(pDataDirectory->Size + pDataDirectory->VirtualAddress - importDescriptor.Name, moduleNameLowerWithNullTerminator.Length)); // Is this entry about the DLL that we're looking for? (Case insensitive) if (currentDllNameWithNullTerminator.ToLowerInvariant() != moduleNameLowerWithNullTerminator) continue; if (isPe64) { return new FunctionPointerVariableHook(FromImportHelper64(module.BaseAddress, ref importDescriptor, ref *pDataDirectory, functionName, hintOrOrdinal), detour, Assembly.GetCallingAssembly()); } else { return new FunctionPointerVariableHook(FromImportHelper32(module.BaseAddress, ref importDescriptor, ref *pDataDirectory, functionName, hintOrOrdinal), detour, Assembly.GetCallingAssembly()); } } throw new MissingMethodException("Specified dll not found"); } /// /// Creates a hook. Hooking address is inferred by calling to GetProcAddress() function. /// The hook is not activated until Enable() method is called. /// /// A name of the module currently loaded in the memory. (e.g. ws2_32.dll). /// A name of the exported function name (e.g. send). /// Callback function. Delegate must have a same original function prototype. /// The hook with the supplied parameters. public static Hook FromSymbol(string moduleName, string exportName, T detour) => FromSymbol(moduleName, exportName, detour, false); /// /// Creates a hook. Hooking address is inferred by calling to GetProcAddress() function. /// The hook is not activated until Enable() method is called. /// Please do not use MinHook unless you have thoroughly troubleshot why Reloaded does not work. /// /// A name of the module currently loaded in the memory. (e.g. ws2_32.dll). /// A name of the exported function name (e.g. send). /// Callback function. Delegate must have a same original function prototype. /// Use the MinHook hooking library instead of Reloaded. /// The hook with the supplied parameters. public static Hook FromSymbol(string moduleName, string exportName, T detour, bool useMinHook) { if (EnvironmentConfiguration.DalamudForceMinHook) useMinHook = true; var moduleHandle = NativeFunctions.GetModuleHandleW(moduleName); if (moduleHandle == IntPtr.Zero) throw new Exception($"Could not get a handle to module {moduleName}"); var procAddress = NativeFunctions.GetProcAddress(moduleHandle, exportName); if (procAddress == IntPtr.Zero) throw new Exception($"Could not get the address of {moduleName}::{exportName}"); procAddress = HookManager.FollowJmp(procAddress); if (useMinHook) return new MinHookHook(procAddress, detour, Assembly.GetCallingAssembly()); else return new ReloadedHook(procAddress, detour, Assembly.GetCallingAssembly()); } /// /// Creates a hook. Hooking address is inferred by calling to GetProcAddress() function. /// The hook is not activated until Enable() method is called. /// Please do not use MinHook unless you have thoroughly troubleshot why Reloaded does not work. /// /// A memory address to install a hook. /// Callback function. Delegate must have a same original function prototype. /// Use the MinHook hooking library instead of Reloaded. /// The hook with the supplied parameters. public static Hook FromAddress(IntPtr procAddress, T detour, bool useMinHook = false) { if (EnvironmentConfiguration.DalamudForceMinHook) useMinHook = true; procAddress = HookManager.FollowJmp(procAddress); if (useMinHook) return new MinHookHook(procAddress, detour, Assembly.GetCallingAssembly()); else return new ReloadedHook(procAddress, detour, Assembly.GetCallingAssembly()); } /// /// Remove a hook from the current process. /// public virtual void Dispose() { if (this.IsDisposed) return; this.compatHookImpl?.Dispose(); this.IsDisposed = true; } /// /// Starts intercepting a call to the function. /// public virtual void Enable() { if (this.compatHookImpl != null) this.compatHookImpl.Enable(); else throw new NotImplementedException(); } /// /// Stops intercepting a call to the function. /// public virtual void Disable() { if (this.compatHookImpl != null) this.compatHookImpl.Disable(); else throw new NotImplementedException(); } /// /// Check if this object has been disposed already. /// protected void CheckDisposed() { if (this.IsDisposed) { throw new ObjectDisposedException(message: "Hook is already disposed", null); } } private static unsafe IntPtr FromImportHelper32(IntPtr baseAddress, ref PeHeader.IMAGE_IMPORT_DESCRIPTOR desc, ref PeHeader.IMAGE_DATA_DIRECTORY dir, string functionName, uint hintOrOrdinal) { var importLookupsOversizedSpan = new Span((uint*)(baseAddress + (int)desc.OriginalFirstThunk), (int)((dir.Size - desc.OriginalFirstThunk) / Marshal.SizeOf())); var importAddressesOversizedSpan = new Span((uint*)(baseAddress + (int)desc.FirstThunk), (int)((dir.Size - desc.FirstThunk) / Marshal.SizeOf())); var functionNameWithNullTerminator = functionName + "\0"; for (int i = 0, i_ = Math.Min(importLookupsOversizedSpan.Length, importAddressesOversizedSpan.Length); i < i_ && importLookupsOversizedSpan[i] != 0 && importAddressesOversizedSpan[i] != 0; i++) { var importLookup = importLookupsOversizedSpan[i]; // Is this entry importing by ordinals? A lot of socket functions are the case. if ((importLookup & IMAGE_ORDINAL_FLAG32) != 0) { var ordinal = importLookup & ~IMAGE_ORDINAL_FLAG32; // Is this the entry? if (hintOrOrdinal == 0 || ordinal != hintOrOrdinal) continue; // Is this entry not importing by ordinals, and are we using hint exclusively to find the entry? } else { var hint = Marshal.ReadInt16(baseAddress + (int)importLookup); if (functionName.Length > 0) { // Is this the entry? if (hint != hintOrOrdinal) continue; } else { // Name must be contained in this directory. var currentFunctionNameWithNullTerminator = Marshal.PtrToStringUTF8( baseAddress + (int)importLookup + 2, (int)Math.Min(dir.VirtualAddress + dir.Size - (uint)baseAddress - importLookup - 2, (uint)functionNameWithNullTerminator.Length)); // Is this entry about the function that we're looking for? if (currentFunctionNameWithNullTerminator != functionNameWithNullTerminator) continue; } } return baseAddress + (int)desc.FirstThunk + (i * Marshal.SizeOf()); } throw new MissingMethodException("Specified method not found"); } private static unsafe IntPtr FromImportHelper64(IntPtr baseAddress, ref PeHeader.IMAGE_IMPORT_DESCRIPTOR desc, ref PeHeader.IMAGE_DATA_DIRECTORY dir, string functionName, uint hintOrOrdinal) { var importLookupsOversizedSpan = new Span((ulong*)(baseAddress + (int)desc.OriginalFirstThunk), (int)((dir.Size - desc.OriginalFirstThunk) / Marshal.SizeOf())); var importAddressesOversizedSpan = new Span((ulong*)(baseAddress + (int)desc.FirstThunk), (int)((dir.Size - desc.FirstThunk) / Marshal.SizeOf())); var functionNameWithNullTerminator = functionName + "\0"; for (int i = 0, i_ = Math.Min(importLookupsOversizedSpan.Length, importAddressesOversizedSpan.Length); i < i_ && importLookupsOversizedSpan[i] != 0 && importAddressesOversizedSpan[i] != 0; i++) { var importLookup = importLookupsOversizedSpan[i]; // Is this entry importing by ordinals? A lot of socket functions are the case. if ((importLookup & IMAGE_ORDINAL_FLAG64) != 0) { var ordinal = importLookup & ~IMAGE_ORDINAL_FLAG64; // Is this the entry? if (hintOrOrdinal == 0 || ordinal != hintOrOrdinal) continue; // Is this entry not importing by ordinals, and are we using hint exclusively to find the entry? } else { var hint = Marshal.ReadInt16(baseAddress + (int)importLookup); if (functionName.Length == 0) { // Is this the entry? if (hint != hintOrOrdinal) continue; } else { // Name must be contained in this directory. var currentFunctionNameWithNullTerminator = Marshal.PtrToStringUTF8( baseAddress + (int)importLookup + 2, (int)Math.Min((ulong)dir.VirtualAddress + dir.Size - (ulong)baseAddress - importLookup - 2, (ulong)functionNameWithNullTerminator.Length)); // Is this entry about the function that we're looking for? if (currentFunctionNameWithNullTerminator != functionNameWithNullTerminator) continue; } } return baseAddress + (int)desc.FirstThunk + (i * Marshal.SizeOf()); } throw new MissingMethodException("Specified method not found"); } }