Improve unhooking of hooked functions

This commit is contained in:
nebel 2023-06-03 22:44:23 +09:00
parent 5a8fff515f
commit 64fddf10bb
No known key found for this signature in database
6 changed files with 127 additions and 59 deletions

View file

@ -35,12 +35,8 @@ public sealed class AsmHook : IDisposable, IDalamudHook
{ {
address = HookManager.FollowJmp(address); address = HookManager.FollowJmp(address);
var hasOtherHooks = HookManager.Originals.ContainsKey(address); // We cannot call TrimAfterHook here because the hook is activated by the caller.
if (!hasOtherHooks) HookManager.RegisterUnhooker(address);
{
MemoryHelper.ReadRaw(address, 0x32, out var original);
HookManager.Originals[address] = original;
}
this.address = address; this.address = address;
this.hookImpl = ReloadedHooks.Instance.CreateAsmHook(assembly, address.ToInt64(), (Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour)asmHookBehaviour); this.hookImpl = ReloadedHooks.Instance.CreateAsmHook(assembly, address.ToInt64(), (Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour)asmHookBehaviour);
@ -65,12 +61,8 @@ public sealed class AsmHook : IDisposable, IDalamudHook
{ {
address = HookManager.FollowJmp(address); address = HookManager.FollowJmp(address);
var hasOtherHooks = HookManager.Originals.ContainsKey(address); // We cannot call TrimAfterHook here because the hook is activated by the caller.
if (!hasOtherHooks) HookManager.RegisterUnhooker(address);
{
MemoryHelper.ReadRaw(address, 0x32, out var original);
HookManager.Originals[address] = original;
}
this.address = address; this.address = address;
this.hookImpl = ReloadedHooks.Instance.CreateAsmHook(assembly, address.ToInt64(), (Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour)asmHookBehaviour); this.hookImpl = ReloadedHooks.Instance.CreateAsmHook(assembly, address.ToInt64(), (Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour)asmHookBehaviour);

View file

@ -41,12 +41,7 @@ internal class FunctionPointerVariableHook<T> : Hook<T>
{ {
lock (HookManager.HookEnableSyncRoot) lock (HookManager.HookEnableSyncRoot)
{ {
var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address); var unhooker = HookManager.RegisterUnhooker(this.Address);
if (!hasOtherHooks)
{
MemoryHelper.ReadRaw(this.Address, 0x32, out var original);
HookManager.Originals[this.Address] = original;
}
if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList)) if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList))
{ {
@ -104,6 +99,8 @@ internal class FunctionPointerVariableHook<T> : Hook<T>
// Add afterwards, so the hookIdent starts at 0. // Add afterwards, so the hookIdent starts at 0.
indexList.Add(this); indexList.Add(this);
unhooker.TrimAfterHook();
HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly)); HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly));
} }
} }

View file

@ -16,7 +16,10 @@ namespace Dalamud.Hooking.Internal;
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal class HookManager : IDisposable, IServiceType internal class HookManager : IDisposable, IServiceType
{ {
private static readonly ModuleLog Log = new("HM"); /// <summary>
/// Logger shared with <see cref="Unhooker"/>
/// </summary>
internal static readonly ModuleLog Log = new("HM");
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private HookManager() private HookManager()
@ -34,9 +37,21 @@ internal class HookManager : IDisposable, IServiceType
internal static ConcurrentDictionary<Guid, HookInfo> TrackedHooks { get; } = new(); internal static ConcurrentDictionary<Guid, HookInfo> TrackedHooks { get; } = new();
/// <summary> /// <summary>
/// Gets a static dictionary of original code for a hooked address. /// Gets a static dictionary of unhookers for a hooked address.
/// </summary> /// </summary>
internal static ConcurrentDictionary<IntPtr, byte[]> Originals { get; } = new(); internal static ConcurrentDictionary<IntPtr, Unhooker> Unhookers { get; } = new();
/// <summary>
/// Creates a new Unhooker instance for the provided address if no such unhooker was already registered, or returns
/// an existing instance if the address registered previously.
/// </summary>
/// <param name="address">The address of the instruction.</param>
/// <returns>A new Unhooker instance.</returns>
public static Unhooker RegisterUnhooker(IntPtr address)
{
Log.Verbose($"Registering hook at 0x{address.ToInt64():X}");
return Unhookers.GetOrAdd(address, adr => new Unhooker(adr));
}
/// <summary> /// <summary>
/// Gets a static dictionary of the number of hooks on a given address. /// Gets a static dictionary of the number of hooks on a given address.
@ -48,7 +63,7 @@ internal class HookManager : IDisposable, IServiceType
{ {
RevertHooks(); RevertHooks();
TrackedHooks.Clear(); TrackedHooks.Clear();
Originals.Clear(); Unhookers.Clear();
} }
/// <summary> /// <summary>
@ -60,7 +75,7 @@ internal class HookManager : IDisposable, IServiceType
{ {
while (true) while (true)
{ {
var hasOtherHooks = HookManager.Originals.ContainsKey(address); var hasOtherHooks = HookManager.Unhookers.ContainsKey(address);
if (hasOtherHooks) if (hasOtherHooks)
{ {
// This address has been hooked already. Do not follow a jmp into a trampoline of our own making. // This address has been hooked already. Do not follow a jmp into a trampoline of our own making.
@ -124,28 +139,11 @@ internal class HookManager : IDisposable, IServiceType
return address; return address;
} }
private static unsafe void RevertHooks() private static void RevertHooks()
{ {
foreach (var (address, originalBytes) in Originals) foreach (var unhooker in Unhookers.Values)
{ {
var i = 0; unhooker.Unhook();
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;
}
var snippet = originalBytes[0..i];
if (i > 0)
{
Log.Verbose($"Reverting hook at 0x{address.ToInt64():X} ({snippet.Length} bytes)");
MemoryHelper.ChangePermission(address, i, MemoryProtection.ExecuteReadWrite, out var oldPermissions);
MemoryHelper.WriteRaw(address, snippet);
MemoryHelper.ChangePermission(address, i, oldPermissions);
}
} }
} }
} }

View file

@ -1,8 +1,6 @@
using System; using System;
using System.Reflection; using System.Reflection;
using Dalamud.Memory;
namespace Dalamud.Hooking.Internal; namespace Dalamud.Hooking.Internal;
/// <summary> /// <summary>
@ -24,12 +22,7 @@ internal class MinHookHook<T> : Hook<T> where T : Delegate
{ {
lock (HookManager.HookEnableSyncRoot) lock (HookManager.HookEnableSyncRoot)
{ {
var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address); var unhooker = HookManager.RegisterUnhooker(this.Address);
if (!hasOtherHooks)
{
MemoryHelper.ReadRaw(this.Address, 0x32, out var original);
HookManager.Originals[this.Address] = original;
}
if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList)) if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList))
indexList = HookManager.MultiHookTracker[this.Address] = new(); indexList = HookManager.MultiHookTracker[this.Address] = new();
@ -41,6 +34,8 @@ internal class MinHookHook<T> : Hook<T> where T : Delegate
// Add afterwards, so the hookIdent starts at 0. // Add afterwards, so the hookIdent starts at 0.
indexList.Add(this); indexList.Add(this);
unhooker.TrimAfterHook();
HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly)); HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly));
} }
} }

View file

@ -1,7 +1,6 @@
using System; using System;
using System.Reflection; using System.Reflection;
using Dalamud.Memory;
using Reloaded.Hooks; using Reloaded.Hooks;
namespace Dalamud.Hooking.Internal; namespace Dalamud.Hooking.Internal;
@ -25,17 +24,13 @@ internal class ReloadedHook<T> : Hook<T> where T : Delegate
{ {
lock (HookManager.HookEnableSyncRoot) lock (HookManager.HookEnableSyncRoot)
{ {
var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address); var unhooker = HookManager.RegisterUnhooker(address);
if (!hasOtherHooks)
{
MemoryHelper.ReadRaw(this.Address, 0x32, out var original);
HookManager.Originals[this.Address] = original;
}
this.hookImpl = ReloadedHooks.Instance.CreateHook<T>(detour, address.ToInt64()); this.hookImpl = ReloadedHooks.Instance.CreateHook<T>(detour, address.ToInt64());
this.hookImpl.Activate(); this.hookImpl.Activate();
this.hookImpl.Disable(); this.hookImpl.Disable();
unhooker.TrimAfterHook();
HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly)); HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly));
} }
} }

View file

@ -0,0 +1,91 @@
using System;
using Dalamud.Memory;
namespace Dalamud.Hooking.Internal;
/// <summary>
/// A class which stores a copy of the bytes at a location which will be hooked in the future, such that those bytes can
/// be restored later to "unhook" the function.
/// </summary>
public class Unhooker
{
private readonly IntPtr address;
private byte[] originalBytes;
private bool trimmed;
/// <summary>
/// Initializes a new instance of the <see cref="Unhooker"/> class. Upon creation, the Unhooker stores a copy of
/// the bytes stored at the provided address, and can be used to restore these bytes when the hook should be
/// removed. As such this class should be instantiated before the function is actually hooked.
/// </summary>
/// <param name="address">The address which will be hooked.</param>
public Unhooker(IntPtr address)
{
this.address = address;
MemoryHelper.ReadRaw(address, 0x32, out this.originalBytes);
}
/// <summary>
/// When called after a hook is created, checks the pre-hook original bytes and post-hook modified bytes, trimming
/// the original bytes stored and removing unmodified bytes from the end of the byte sequence. Assuming no
/// concurrent actions modified the same address space, this should result in storing only the minimum bytes
/// required to unhook the function.
/// </summary>
public void TrimAfterHook()
{
if (this.trimmed)
{
return;
}
this.originalBytes = this.originalBytes[..this.GetFullHookLength()];
this.trimmed = true;
}
/// <summary>
/// Attempts to unhook the function by replacing the hooked bytes with the original bytes. If
/// <see cref="TrimAfterHook"/> was called, the trimmed original bytes stored at that time will be used for
/// unhooking. Otherwise, a naive algorithm which only restores bytes until the first unchanged byte will be used in
/// order to avoid overwriting adjacent data.
/// </summary>
public void Unhook()
{
var len = this.trimmed ? this.originalBytes.Length : this.GetNaiveHookLength();
if (len > 0)
{
HookManager.Log.Verbose($"Reverting hook at 0x{this.address.ToInt64():X} ({len} bytes, trimmed={this.trimmed})");
MemoryHelper.ChangePermission(this.address, len, MemoryProtection.ExecuteReadWrite, out var oldPermissions);
MemoryHelper.WriteRaw(this.address, this.originalBytes[..len]);
MemoryHelper.ChangePermission(this.address, len, oldPermissions);
}
}
private unsafe int GetNaiveHookLength()
{
var current = (byte*)this.address;
for (var i = 0; i < this.originalBytes.Length; i++)
{
if (current[i] == this.originalBytes[i])
{
return i;
}
}
return 0;
}
private unsafe int GetFullHookLength()
{
var current = (byte*)this.address;
for (var i = this.originalBytes.Length - 1; i >= 0; i--)
{
if (current[i] != this.originalBytes[i])
{
return i + 1;
}
}
return 0;
}
}