diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index a411883d5..2d32b8e8a 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Dalamud.Common; using Dalamud.Configuration.Internal; using Dalamud.Game; +using Dalamud.Hooking.Internal.Verification; using Dalamud.Plugin.Internal; using Dalamud.Storage; using Dalamud.Utility; @@ -73,6 +74,11 @@ internal sealed unsafe class Dalamud : IServiceType scanner, Localization.FromAssets(info.AssetDirectory!, configuration.LanguageOverride)); + using (Timings.Start("HookVerifier Init")) + { + HookVerifier.Initialize(scanner); + } + // Set up FFXIVClientStructs this.SetupClientStructsResolver(cacheDir); diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index 1cd3ef91d..b8fd78b4f 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using Dalamud.Configuration.Internal; using Dalamud.Hooking.Internal; +using Dalamud.Hooking.Internal.Verification; namespace Dalamud.Hooking; @@ -230,6 +231,8 @@ public abstract class Hook : IDalamudHook where T : Delegate if (EnvironmentConfiguration.DalamudForceMinHook) useMinHook = true; + HookVerifier.Verify(procAddress); + procAddress = HookManager.FollowJmp(procAddress); if (useMinHook) return new MinHookHook(procAddress, detour, Assembly.GetCallingAssembly()); diff --git a/Dalamud/Hooking/Internal/Verification/HookVerificationException.cs b/Dalamud/Hooking/Internal/Verification/HookVerificationException.cs new file mode 100644 index 000000000..c43b5d540 --- /dev/null +++ b/Dalamud/Hooking/Internal/Verification/HookVerificationException.cs @@ -0,0 +1,41 @@ +using System.Linq; + +namespace Dalamud.Hooking.Internal.Verification; + +/// +/// Exception thrown when a provided delegate for a hook does not match a known delegate. +/// +public class HookVerificationException : Exception +{ + private HookVerificationException(string message) + : base(message) + { + } + + /// + /// Create a new exception. + /// + /// The address of the function that is being hooked. + /// The delegate passed by the user. + /// The delegate we think is correct. + /// Additional context to show to the user. + /// The created exception. + internal static HookVerificationException Create(IntPtr address, Type passed, Type enforced, string message) + { + return new HookVerificationException( + $"Hook verification failed for address 0x{address.ToInt64():X}\n\n" + + $"Why: {message}\n" + + $"Passed Delegate: {GetSignature(passed)}\n" + + $"Correct Delegate: {GetSignature(enforced)}\n\n" + + "The hook delegate must exactly match the provided signature to prevent memory corruption and wrong data passed to originals."); + } + + private static string GetSignature(Type delegateType) + { + var method = delegateType.GetMethod("Invoke"); + if (method == null) return delegateType.Name; + + var parameters = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.Name)); + return $"{method.ReturnType.Name} ({parameters})"; + } +} diff --git a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs new file mode 100644 index 000000000..ad68ae38e --- /dev/null +++ b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs @@ -0,0 +1,107 @@ +using System.Linq; + +using Dalamud.Game; +using Dalamud.Logging.Internal; + +namespace Dalamud.Hooking.Internal.Verification; + +/// +/// Global utility that can verify whether hook delegates are correctly declared. +/// Initialized out-of-band, since Hook is instantiated all over the place without a service, so this cannot be +/// a service either. +/// +internal static class HookVerifier +{ + private static readonly ModuleLog Log = new("HookVerifier"); + + private static readonly VerificationEntry[] ToVerify = + [ + new( + "ActorControlSelf", + "E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64", + typeof(ActorControlSelfDelegate), + "Signature changed in Patch 7.4") // 7.4 (new parameters) + ]; + + private delegate void ActorControlSelfDelegate(uint category, uint eventId, uint param1, uint param2, uint param3, uint param4, uint param5, uint param6, uint param7, uint param8, ulong targetId, byte param9); + + /// + /// Initializes a new instance of the class. + /// + /// Process to scan in. + public static void Initialize(TargetSigScanner scanner) + { + foreach (var entry in ToVerify) + { + if (!scanner.TryScanText(entry.Signature, out var address)) + { + Log.Error("Could not resolve signature for hook {Name} ({Sig})", entry.Name, entry.Signature); + continue; + } + + entry.Address = address; + } + } + + /// + /// Verify the hook with the provided address and exception. + /// + /// The address of the function we are hooking. + /// The delegate type passed by the creator of the hook. + /// Exception thrown when we think the hook is not correctly declared. + public static void Verify(IntPtr address) where T : Delegate + { + var entry = ToVerify.FirstOrDefault(x => x.Address == address); + + // Nothing to verify for this hook? + if (entry == null) + { + return; + } + + var passedType = typeof(T); + + // Directly compare delegates + if (passedType == entry.TargetDelegateType) + { + return; + } + + var passedInvoke = passedType.GetMethod("Invoke")!; + var enforcedInvoke = entry.TargetDelegateType.GetMethod("Invoke")!; + + // Compare Return Type + var mismatch = passedInvoke.ReturnType != enforcedInvoke.ReturnType; + + // Compare Parameter Count + var passedParams = passedInvoke.GetParameters(); + var enforcedParams = enforcedInvoke.GetParameters(); + + if (passedParams.Length != enforcedParams.Length) + { + mismatch = true; + } + else + { + // Compare Parameter Types + for (var i = 0; i < passedParams.Length; i++) + { + if (passedParams[i].ParameterType != enforcedParams[i].ParameterType) + { + mismatch = true; + break; + } + } + } + + if (mismatch) + { + throw HookVerificationException.Create(address, passedType, entry.TargetDelegateType, entry.Message); + } + } + + private record VerificationEntry(string Name, string Signature, Type TargetDelegateType, string Message) + { + public nint Address { get; set; } + } +}