diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 28892907e..359a77692 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -74,6 +74,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Dalamud/Game/Network/WinSockHandlers.cs b/Dalamud/Game/Network/Internal/WinSockHandlers.cs similarity index 74% rename from Dalamud/Game/Network/WinSockHandlers.cs rename to Dalamud/Game/Network/Internal/WinSockHandlers.cs index 8a20bb98f..68be91fb1 100644 --- a/Dalamud/Game/Network/WinSockHandlers.cs +++ b/Dalamud/Game/Network/Internal/WinSockHandlers.cs @@ -3,8 +3,9 @@ using System.Net.Sockets; using System.Runtime.InteropServices; using Dalamud.Hooking; +using Dalamud.Hooking.Internal; -namespace Dalamud.Game +namespace Dalamud.Game.Network.Internal { /// /// This class enables TCP optimizations in the game socket for better performance. @@ -18,8 +19,9 @@ namespace Dalamud.Game /// public WinSockHandlers() { - this.ws2SocketHook = Hook.FromSymbol("ws2_32.dll", "socket", new SocketDelegate(this.OnSocket)); - this.ws2SocketHook.Enable(); + this.ws2SocketHook = HookManager.DirtyLinuxUser ? null + : Hook.FromSymbol("ws2_32.dll", "socket", this.OnSocket); + this.ws2SocketHook?.Enable(); } [UnmanagedFunctionPointer(CallingConvention.Winapi)] @@ -30,7 +32,7 @@ namespace Dalamud.Game /// public void Dispose() { - this.ws2SocketHook.Dispose(); + this.ws2SocketHook?.Dispose(); } private IntPtr OnSocket(int af, int type, int protocol) @@ -47,11 +49,11 @@ namespace Dalamud.Game // https://linux.die.net/man/7/tcp // https://assets.extrahop.com/whitepapers/TCP-Optimization-Guide-by-ExtraHop.pdf var value = new IntPtr(1); - NativeFunctions.SetSockOpt(socket, SocketOptionLevel.Tcp, SocketOptionName.NoDelay, ref value, 4); + _ = NativeFunctions.SetSockOpt(socket, SocketOptionLevel.Tcp, SocketOptionName.NoDelay, ref value, 4); // Enable tcp_quickack option. This option is undocumented in MSDN but it is supported in Windows 7 and onwards. value = new IntPtr(1); - NativeFunctions.SetSockOpt(socket, SocketOptionLevel.Tcp, SocketOptionName.AddMembership, ref value, 4); + _ = NativeFunctions.SetSockOpt(socket, SocketOptionLevel.Tcp, SocketOptionName.AddMembership, ref value, 4); } } diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index 90c611844..3e06f4726 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -1,10 +1,7 @@ using System; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using CoreHook; using Dalamud.Hooking.Internal; +using Dalamud.Hooking.Internal.Implementations; namespace Dalamud.Hooking { @@ -13,13 +10,9 @@ namespace Dalamud.Hooking /// 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 sealed class Hook : IDisposable, IDalamudHook where T : Delegate + public sealed class Hook : IDisposable where T : Delegate { - private readonly IntPtr address; - - private readonly T original; - - private readonly LocalHook hookInfo; + private readonly IDalamudHookImpl hookImpl; /// /// Initializes a new instance of the class. @@ -28,11 +21,8 @@ namespace Dalamud.Hooking /// A memory address to install a hook. /// Callback function. Delegate must have a same original function prototype. public Hook(IntPtr address, T detour) + : this(address, detour, false) { - this.hookInfo = LocalHook.Create(address, detour, null); // Installs a hook here - this.address = address; - this.original = Marshal.GetDelegateForFunctionPointer(this.hookInfo.OriginalAddress); - HookManager.TrackedHooks.Add(new HookInfo() { Delegate = detour, Hook = this, Assembly = Assembly.GetCallingAssembly() }); } /// @@ -41,61 +31,44 @@ namespace Dalamud.Hooking /// /// A memory address to install a hook. /// Callback function. Delegate must have a same original function prototype. - /// A callback object which can be accessed within the detour. - [Obsolete("There is no need to specify new YourDelegateType or callbackParam", true)] - public Hook(IntPtr address, Delegate detour, object callbackParam = null) - : this(address, detour as T) + /// Follow any JMPs to the actual method that needs hooking. + /// + /// The followJmp parameter is only used when ReloadedHooks are used, which currently is only for Linux users. + /// Generally, this is only necessary when hooking Win32 functions. + /// + public Hook(IntPtr address, T detour, bool followJmp) { + this.hookImpl = HookManager.DirtyLinuxUser + ? new ReloadedHookImpl(address, detour, followJmp) + : new CoreHookImpl(address, detour); } /// /// Gets a memory address of the target function. /// /// Hook is already disposed. - public IntPtr Address - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - this.CheckDisposed(); - return this.address; - } - } + public IntPtr Address => this.hookImpl.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 T Original - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - this.CheckDisposed(); - return this.original; - } - } + public T Original => this.hookImpl.Original; /// /// Gets a value indicating whether or not the hook is enabled. /// - public bool IsEnabled - { - get - { - this.CheckDisposed(); - return this.hookInfo.ThreadACL.IsExclusive; - } - } + public bool IsEnabled => this.hookImpl.IsEnabled; /// /// Gets a value indicating whether or not the hook has been disposed. /// - public bool IsDisposed { get; private set; } + public bool IsDisposed => this.hookImpl.IsDisposed; + /// /// Creates a hook. Hooking address is inferred by calling to GetProcAddress() function. - /// Hook is not activated until Enable() method is called. + /// 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). @@ -103,65 +76,39 @@ namespace Dalamud.Hooking /// The hook with the supplied parameters. public static Hook FromSymbol(string moduleName, string exportName, T detour) { - // Get a function address from the symbol name. - var address = LocalHook.GetProcAddress(moduleName, exportName); + if (HookManager.DirtyLinuxUser) + { + var moduleHandle = NativeFunctions.GetModuleHandleW(moduleName); + if (moduleHandle == IntPtr.Zero) + throw new Exception($"Could not get a handle to module {moduleName}"); - return new Hook(address, detour); + var procAddress = NativeFunctions.GetProcAddress(moduleHandle, exportName); + if (procAddress == IntPtr.Zero) + throw new Exception($"Could not get the address of {moduleName}::{exportName}"); + + return new Hook(procAddress, detour, true); + } + else + { + var address = CoreHook.LocalHook.GetProcAddress(moduleName, exportName); + return new Hook(address, detour); + } } - /// - /// Creates a hook. Hooking address is inferred by calling to GetProcAddress() function. - /// 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. - /// A callback object which can be accessed within the detour. - /// The hook with the supplied parameters. - [Obsolete("There is no need to specify new YourDelegateType or callbackParam", true)] - public static Hook FromSymbol(string moduleName, string exportName, Delegate detour, object callbackParam = null) => FromSymbol(moduleName, exportName, detour as T); - /// /// Remove a hook from the current process. /// - public void Dispose() - { - if (this.IsDisposed) - { - return; - } - - this.IsDisposed = true; - this.hookInfo.Dispose(); - } + public void Dispose() => this.hookImpl.Dispose(); /// /// Starts intercepting a call to the function. /// - public void Enable() - { - this.CheckDisposed(); - - this.hookInfo.ThreadACL.SetExclusiveACL(null); - } + public void Enable() => this.hookImpl.Enable(); /// /// Stops intercepting a call to the function. /// - public void Disable() - { - this.CheckDisposed(); + public void Disable() => this.hookImpl.Disable(); - this.hookInfo.ThreadACL.SetInclusiveACL(null); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void CheckDisposed() - { - if (this.IsDisposed) - { - throw new ObjectDisposedException("Hook is already disposed."); - } - } } } diff --git a/Dalamud/Hooking/Internal/HookManager.cs b/Dalamud/Hooking/Internal/HookManager.cs index 1651c696e..29d80be47 100644 --- a/Dalamud/Hooking/Internal/HookManager.cs +++ b/Dalamud/Hooking/Internal/HookManager.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using Dalamud.Memory; + namespace Dalamud.Hooking.Internal { /// @@ -8,16 +10,35 @@ namespace Dalamud.Hooking.Internal /// internal class HookManager : IDisposable { - // private readonly Dalamud dalamud; + private static readonly ModuleLog Log = new("HM"); + private static bool checkLinuxOnce = true; + private static bool isRunningLinux = false; /// /// Initializes a new instance of the class. /// /// Dalamud instance. - public HookManager(Dalamud dalamud) + internal HookManager(Dalamud dalamud) { _ = dalamud; - // this.dalamud = dalamud; + } + + /// + /// Gets a value indicating whether the client is running under Linux Wine. + /// + /// A value indicating whether the game is running under Wine. + internal static bool DirtyLinuxUser + { + get + { + if (checkLinuxOnce) + { + var value = Environment.GetEnvironmentVariable("XL_WINEONLINUX"); + isRunningLinux = value is not null; + } + + return isRunningLinux; + } } /// @@ -25,9 +46,43 @@ namespace Dalamud.Hooking.Internal /// internal static List TrackedHooks { get; } = new(); + /// + /// Gets a static list of original code for a hooked address. + /// + internal static List<(IntPtr Address, byte[] Original)> Originals { get; } = new(); + /// public void Dispose() { + RevertHooks(); + TrackedHooks.Clear(); + Originals.Clear(); + } + + private static unsafe void RevertHooks() + { + foreach (var (address, originalBytes) in Originals) + { + var i = 0; + var current = (byte*)address; + // Find how many bytes have been modified by comparing to the saved original + for (; i < originalBytes.Length; i++) + { + if (current[i] == originalBytes[i]) + break; + } + + if (i > 0) + { + Log.Debug($"Reverting hook at 0x{address.ToInt64():X}"); + fixed (byte* original = originalBytes) + { + MemoryHelper.ChangePermission(address, i, MemoryProtection.ExecuteReadWrite, out var oldPermissions); + MemoryHelper.WriteRaw(address, originalBytes); + MemoryHelper.ChangePermission(address, i, oldPermissions); + } + } + } } } } diff --git a/Dalamud/Hooking/Internal/Implementations/CoreHookImpl.cs b/Dalamud/Hooking/Internal/Implementations/CoreHookImpl.cs new file mode 100644 index 000000000..bfff3eae8 --- /dev/null +++ b/Dalamud/Hooking/Internal/Implementations/CoreHookImpl.cs @@ -0,0 +1,133 @@ +using System; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Dalamud.Hooking.Internal.Implementations +{ + /// + /// 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. + internal sealed class CoreHookImpl : IDisposable, IDalamudHookImpl where T : Delegate + { + private readonly IntPtr address; + + private readonly CoreHook.LocalHook hookImpl; + private readonly T original; + + /// + /// 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. + public CoreHookImpl(IntPtr address, T detour) + { + this.address = address; + + this.hookImpl = CoreHook.LocalHook.Create(address, detour, null); + this.original = Marshal.GetDelegateForFunctionPointer(this.hookImpl.OriginalAddress); + + HookManager.TrackedHooks.Add(new HookInfo(this, detour, 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 delegate function that can be used to call the actual function as if function is not hooked yet. + /// + /// Hook is already disposed. + public T Original + { + get + { + this.CheckDisposed(); + return this.original; + } + } + + /// + /// Gets a value indicating whether or not the hook is enabled. + /// + public bool IsEnabled + { + get + { + this.CheckDisposed(); + return this.hookImpl.ThreadACL.IsExclusive; + } + } + + /// + /// Gets a value indicating whether or not the hook has been disposed. + /// + public bool IsDisposed { get; private set; } + + /// + /// 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 IDalamudHookImpl FromSymbol(string moduleName, string exportName, T detour) + { + var address = CoreHook.LocalHook.GetProcAddress(moduleName, exportName); + return new CoreHookImpl(address, detour); + } + + /// + /// Remove a hook from the current process. + /// + public void Dispose() + { + if (this.IsDisposed) + return; + + this.IsDisposed = true; + this.hookImpl.Dispose(); + } + + /// + /// Starts intercepting a call to the function. + /// + public void Enable() + { + this.CheckDisposed(); + this.hookImpl.ThreadACL.SetExclusiveACL(null); + } + + /// + /// Stops intercepting a call to the function. + /// + public void Disable() + { + this.CheckDisposed(); + this.hookImpl.ThreadACL.SetExclusiveACL(null); + } + + /// + /// Check if this object has been disposed already. + /// + private void CheckDisposed() + { + if (this.IsDisposed) + { + throw new ObjectDisposedException("Hook is already disposed."); + } + } + } +} diff --git a/Dalamud/Hooking/Internal/Implementations/IDalamudHookImpl.cs b/Dalamud/Hooking/Internal/Implementations/IDalamudHookImpl.cs new file mode 100644 index 000000000..6290086d2 --- /dev/null +++ b/Dalamud/Hooking/Internal/Implementations/IDalamudHookImpl.cs @@ -0,0 +1,28 @@ +using System; + +namespace Dalamud.Hooking.Internal.Implementations +{ + /// + /// 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. + internal interface IDalamudHookImpl : IDisposable, IDalamudHook where T : Delegate + { + /// + /// 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 T Original { get; } + + /// + /// Starts intercepting a call to the function. + /// + public void Enable(); + + /// + /// Stops intercepting a call to the function. + /// + public void Disable(); + } +} diff --git a/Dalamud/Hooking/Internal/Implementations/ReloadedHookImpl.cs b/Dalamud/Hooking/Internal/Implementations/ReloadedHookImpl.cs new file mode 100644 index 000000000..b8b0bebcc --- /dev/null +++ b/Dalamud/Hooking/Internal/Implementations/ReloadedHookImpl.cs @@ -0,0 +1,196 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; + +using Dalamud.Memory; + +namespace Dalamud.Hooking.Internal.Implementations +{ + /// + /// 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. + internal sealed class ReloadedHookImpl : IDalamudHookImpl where T : Delegate + { + private readonly IntPtr address; + private readonly Reloaded.Hooks.Definitions.IHook hookImpl; + + /// + /// 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. + public ReloadedHookImpl(IntPtr address, T detour) + : this(address, detour, false) + { + } + + /// + /// 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. + /// Follow any JMPs to the actual method that needs hooking. + public ReloadedHookImpl(IntPtr address, T detour, bool followJmp) + { + if (followJmp) + { + // This is horrible hackery to follow various types of JMP. + // It likely needs to stop when entering a reloaded hook trampoline. + // I would much rather use Iced to check against a Instruction type. + while (true) + { + var b1 = Marshal.ReadByte(address); + if (b1 == 0xE9) + { + var jumpOffset = Marshal.ReadInt32(address + 1); + address += jumpOffset + 5; + continue; + } + + var b2 = Marshal.ReadByte(address, 1); + if (b1 == 0xFF && b2 == 0x25) + { + address = Marshal.ReadIntPtr(address + 6); + continue; + } + + break; + } + } + + var otherHook = HookManager.Originals.FirstOrDefault(o => o.Address == address); + if (otherHook == default) + { + MemoryHelper.ReadRaw(address, 50, out var original); + HookManager.Originals.Add((address, original)); + } + + this.address = address; + this.hookImpl = Reloaded.Hooks.ReloadedHooks.Instance.CreateHook(detour, address.ToInt64()); + + HookManager.TrackedHooks.Add(new HookInfo(this, detour, 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 delegate function that can be used to call the actual function as if function is not hooked yet. + /// + /// Hook is already disposed. + public T Original + { + get + { + this.CheckDisposed(); + return this.hookImpl.OriginalFunction; + } + } + + /// + /// Gets a value indicating whether or not the hook is enabled. + /// + public bool IsEnabled + { + get + { + this.CheckDisposed(); + return this.hookImpl.IsHookEnabled; + } + } + + /// + /// Gets a value indicating whether or not the hook has been disposed. + /// + public bool IsDisposed { get; private set; } + + /// + /// 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 IDalamudHookImpl FromSymbol(string moduleName, string exportName, T detour) + { + 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}"); + + return new ReloadedHookImpl(procAddress, detour, true); + } + + /// + /// Remove a hook from the current process. + /// + public void Dispose() + { + if (this.IsDisposed) + return; + + this.IsDisposed = true; + + if (this.hookImpl.IsHookEnabled) + this.hookImpl.Disable(); + } + + /// + /// Starts intercepting a call to the function. + /// + public void Enable() + { + this.CheckDisposed(); + + if (!this.hookImpl.IsHookActivated) + this.hookImpl.Activate(); + + if (!this.hookImpl.IsHookEnabled) + this.hookImpl.Enable(); + } + + /// + /// Stops intercepting a call to the function. + /// + public void Disable() + { + this.CheckDisposed(); + + if (!this.hookImpl.IsHookActivated) + return; + + if (this.hookImpl.IsHookEnabled) + this.hookImpl.Disable(); + } + + /// + /// Check if this object has been disposed already. + /// + private void CheckDisposed() + { + if (this.IsDisposed) + { + throw new ObjectDisposedException("Hook is already disposed."); + } + } + } +} diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index b4c51d275..9c5fe3f6f 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -10,6 +10,7 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.Internal.DXGI; using Dalamud.Hooking; +using Dalamud.Hooking.Internal; using ImGuiNET; using ImGuiScene; using Serilog; @@ -101,17 +102,17 @@ namespace Dalamud.Interface.Internal Log.Error(e, "RTSS Free failed"); } - var user32 = NativeFunctions.GetModuleHandleW("user32.dll"); - var setCursorAddr = NativeFunctions.GetProcAddress(user32, "SetCursor"); + this.setCursorHook = HookManager.DirtyLinuxUser ? null + : Hook.FromSymbol("user32.dll", "SetCursor", this.SetCursorDetour); + this.presentHook = new Hook(this.address.Present, this.PresentDetour, true); + this.resizeBuffersHook = new Hook(this.address.ResizeBuffers, this.ResizeBuffersDetour, true); + + var setCursorAddress = this.setCursorHook?.Address ?? IntPtr.Zero; Log.Verbose("===== S W A P C H A I N ====="); - Log.Verbose($"SetCursor address 0x{setCursorAddr.ToInt64():X}"); - Log.Verbose($"Present address 0x{this.address.Present.ToInt64():X}"); - Log.Verbose($"ResizeBuffers address 0x{this.address.ResizeBuffers.ToInt64():X}"); - - this.setCursorHook = new Hook(setCursorAddr, this.SetCursorDetour); - this.presentHook = new Hook(this.address.Present, this.PresentDetour); - this.resizeBuffersHook = new Hook(this.address.ResizeBuffers, this.ResizeBuffersDetour); + Log.Verbose($"SetCursor address 0x{setCursorAddress.ToInt64():X}"); + Log.Verbose($"Present address 0x{this.presentHook.Address.ToInt64():X}"); + Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook.Address.ToInt64():X}"); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)]