using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; using Dalamud.Configuration.Internal; using Dalamud.Hooking.Internal; using Dalamud.Hooking.Internal.Verification; using TerraFX.Interop.Windows; 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 abstract class Hook : 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; // ReSharper disable once InconsistentNaming private const int IMAGE_DIRECTORY_ENTRY_IMPORT = 1; #pragma warning restore SA1310 private readonly IntPtr address; /// /// Initializes a new instance of the class. /// /// A memory address to install a hook. internal Hook(IntPtr address) { this.address = address; } /// /// 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 => 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 => this.IsDisposed ? Marshal.GetDelegateForFunctionPointer(this.address) : this.Original; /// /// Gets a value indicating whether the hook is enabled. /// public virtual bool IsEnabled => throw new NotImplementedException(); /// /// Gets a value indicating whether the hook has been disposed. /// public bool IsDisposed { get; private set; } /// public virtual string BackendName => throw new NotImplementedException(); /// /// Gets the unique GUID for this hook. /// protected Guid HookId { get; } = Guid.NewGuid(); /// /// Remove a hook from the current process. /// public virtual void Dispose() { if (this.IsDisposed) return; this.IsDisposed = true; } /// /// Starts intercepting a call to the function. /// /// Hook is already disposed. public virtual void Enable() => throw new NotImplementedException(); /// /// Stops intercepting a call to the function. /// public virtual void Disable() => 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. internal static 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. internal 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 = (IMAGE_DOS_HEADER*)module.BaseAddress; var pNt = (IMAGE_FILE_HEADER*)(module.BaseAddress + pDos->e_lfanew + 4); var isPe64 = pNt->SizeOfOptionalHeader == Marshal.SizeOf(); IMAGE_DATA_DIRECTORY* pDataDirectory; if (isPe64) { var pOpt = (IMAGE_OPTIONAL_HEADER64*)(module.BaseAddress + pDos->e_lfanew + 4 + Marshal.SizeOf()); pDataDirectory = &pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; } else { var pOpt = (IMAGE_OPTIONAL_HEADER32*)(module.BaseAddress + pDos->e_lfanew + 4 + Marshal.SizeOf()); pDataDirectory = &pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; } var moduleNameLowerWithNullTerminator = (moduleName + "\0").ToLowerInvariant(); foreach (ref var importDescriptor in new Span( (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.Equals(moduleNameLowerWithNullTerminator, StringComparison.InvariantCultureIgnoreCase)) 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. internal 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. internal static Hook FromSymbol(string moduleName, string exportName, T detour, bool useMinHook) { if (EnvironmentConfiguration.DalamudForceMinHook) useMinHook = true; var moduleHandle = Windows.Win32.PInvoke.GetModuleHandle(moduleName); if (moduleHandle.IsNull) throw new Exception($"Could not get a handle to module {moduleName}"); var procAddress = Windows.Win32.PInvoke.GetProcAddress(moduleHandle, exportName); if (procAddress.IsNull) throw new Exception($"Could not get the address of {moduleName}::{exportName}"); var address = HookManager.FollowJmp(procAddress.Value); if (useMinHook) return new MinHookHook(address, detour, Assembly.GetCallingAssembly()); else return new ReloadedHook(address, 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. internal static Hook FromAddress(IntPtr procAddress, T detour, bool useMinHook = false) { if (EnvironmentConfiguration.DalamudForceMinHook) useMinHook = true; HookVerifier.Verify(procAddress); procAddress = HookManager.FollowJmp(procAddress); if (useMinHook) return new MinHookHook(procAddress, detour, Assembly.GetCallingAssembly()); else return new ReloadedHook(procAddress, detour, Assembly.GetCallingAssembly()); } /// /// Check if this object has been disposed already. /// protected void CheckDisposed() { ObjectDisposedException.ThrowIf(this.IsDisposed, this); } private static unsafe IntPtr FromImportHelper32(IntPtr baseAddress, ref IMAGE_IMPORT_DESCRIPTOR desc, ref 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 IMAGE_IMPORT_DESCRIPTOR desc, ref 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"); } }