MosswartMassacre/MosswartMassacre/ManyHook.cs
2025-05-24 21:10:45 +02:00

392 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Decal.Adapter;
namespace MosswartMassacre
{
public class MultiHook
{
internal IntPtr Entrypoint;
internal Delegate Del;
internal List<int> CallLocations = new List<int>();
internal List<Hook> Hooks = new List<Hook>();
public MultiHook(int entrypoint, params int[] callLocations)
{
Entrypoint = (IntPtr)entrypoint;
CallLocations.AddRange(callLocations);
Hooks.AddRange(callLocations.Select(c => new Hook((int)Entrypoint, c)));
}
public bool Setup(Delegate del)
{
Del = del;
return !Hooks.Any(h => !h.Setup(del));
}
public bool Remove()
{
return !Hooks.Any(h => !h.Remove());
}
}
/// <summary>
/// New improved Hooker
/// </summary>
public class Hook
{
internal IntPtr Entrypoint;
internal Delegate Del;
internal int call;
public Hook(int entrypoint, int call_location)
{
Entrypoint = (IntPtr)entrypoint;
call = call_location;
}
public bool Setup(Delegate del)
{
if (!hookers.Contains(this))
{
Del = del;
if (ReadCall(call) != (int)Entrypoint)
{
PluginCore.WriteToChat(
$"[Hook] Failed to detour 0x{call:X8}. " +
$"Expected 0x{((int)Entrypoint):X8}, " +
$"got 0x{ReadCall(call):X8}"
);
return false;
}
if (!PatchCall(call, Marshal.GetFunctionPointerForDelegate(Del)))
return false;
hookers.Add(this);
PluginCore.WriteToChat($"[Hook] Hooked 0x{(int)Entrypoint:X8}");
return true;
}
return false;
}
public bool Remove()
{
if (hookers.Contains(this))
{
hookers.Remove(this);
if (PatchCall(call, Entrypoint))
{
PluginCore.WriteToChat($"[Hook] Un-hooked 0x{(int)Entrypoint:X8}");
return true;
}
}
return false;
}
// static half
internal static List<Hook> hookers = new List<Hook>();
[DllImport("kernel32.dll")]
internal static extern bool VirtualProtectEx(
IntPtr hProcess, IntPtr lpAddress,
UIntPtr dwSize, int flNewProtect,
out int lpflOldProtect
);
internal static void Write(IntPtr address, int newValue)
{
unsafe
{
VirtualProtectEx(
Process.GetCurrentProcess().Handle,
address, (UIntPtr)4, 0x40, out int old
);
*(int*)address = newValue;
VirtualProtectEx(
Process.GetCurrentProcess().Handle,
address, (UIntPtr)4, old, out old
);
}
}
internal static bool PatchCall(int callLocation, IntPtr newPointer)
{
unsafe
{
byte* p = (byte*)callLocation;
if ((p[0] & 0xFE) != 0xE8) return false;
int newOffset = (int)newPointer - (callLocation + 5);
Write((IntPtr)(callLocation + 1), newOffset);
return true;
}
}
internal static int ReadCall(int callLocation)
{
unsafe
{
byte* p = (byte*)callLocation;
if ((p[0] & 0xFE) != 0xE8) return 0;
int prevOffset = *(int*)(callLocation + 1);
return prevOffset + (callLocation + 5);
}
}
internal static void Cleanup()
{
for (int i = hookers.Count - 1; i >= 0; i--)
hookers[i].Remove();
}
}
/// <summary>
/// New improved Hooker (Virtualtable edition)
/// </summary>
public class VHook
{
internal int Entrypoint;
internal Delegate Del;
internal int call;
public VHook(int entrypoint, int vtbl_address)
{
Entrypoint = entrypoint;
call = vtbl_address;
}
public bool Setup(Delegate del)
{
if (!hookers.Contains(this))
{
Del = del;
if (ReadVCall(call) != Entrypoint) return false;
if (PatchVCall(call, (int)Marshal.GetFunctionPointerForDelegate(Del)))
{
hookers.Add(this);
PluginCore.WriteToChat($"[VHook] Hooked vtbl slot 0x{call:X8}");
return true;
}
}
return false;
}
public bool Remove()
{
if (hookers.Contains(this))
{
hookers.Remove(this);
if (PatchVCall(call, Entrypoint))
{
PluginCore.WriteToChat($"[VHook] Un-hooked vtbl slot 0x{call:X8}");
return true;
}
}
return false;
}
// static half
internal static List<VHook> hookers = new List<VHook>();
internal static bool PatchVCall(int callLocation, int newPointer)
{
unsafe
{
Hook.Write((IntPtr)callLocation, newPointer);
return *(int*)callLocation == newPointer;
}
}
internal unsafe static int ReadVCall(int callLocation) => *(int*)callLocation;
internal static void Cleanup()
{
for (int i = hookers.Count - 1; i >= 0; i--)
hookers[i].Remove();
}
}
internal static class ChatHooks
{
// For loc_463D60 UIElement, probably SetText(this, string)
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void SetTextDelegate(IntPtr @this, IntPtr strPtr);
// For loc_469440 Same signature; adjust if you find its different in IDA!
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void SetTextDelegate2(IntPtr @this, IntPtr strPtr);
private static MultiHook _hook1, _hook2;
private static SetTextDelegate _orig1;
private static SetTextDelegate2 _orig2;
// All relevant direct code addresses (calls/jmps) to loc_463D60:
private static readonly int[] Sites1 = {
//0x00464A22,
//0x0046B8FF,
//0x0047037F,
//0x00474AA1,
0x004D22A3,
0x004E5F20,
0x004F4663,
// (skip 0x004BD370, it's a jmp)
};
// All relevant direct code addresses (calls/jmps) to loc_469440:
private static readonly int[] Sites2 = {
//0x00468EFA,
//0x00469F23,
//0x0046A4C5,
//0x004B80EC,
0x004CDD49,
0x004F4679,
//0x004F5392,
//0x004F623B,
//0x004F6253,
// (skip 0x004F46E6, it's a jmp)
};
private static bool IsSanePtr(IntPtr ptr)
{
long v = ptr.ToInt64();
// Only allow user-mode addresses (protects from 0, 0x10, or other garbage)
return v > 0x10000 && v < 0x00007FFFFFFFFFFF;
}
private static void MyHook1(IntPtr @this, IntPtr strPtr)
{
string msg;
long val = strPtr.ToInt64();
// Always log, even if pointer looks bad!
if (val == 0 || val == 0x10 || val < 0x10000)
{
msg = $"[SetText1] strPtr=0x{val:X} [skipped-invalid]";
}
else
{
try
{
// Try direct pointer to string (UNICODE)
msg = Marshal.PtrToStringUni(strPtr);
if (string.IsNullOrEmpty(msg))
{
msg = $"[SetText1] strPtr=0x{val:X} [direct:empty]";
}
else
{
msg = $"[SetText1] strPtr=0x{val:X} \"{msg}\"";
}
}
catch (Exception ex)
{
msg = $"[SetText1] strPtr=0x{val:X} [EX:{ex.GetType().Name}]";
}
}
// Actually write to log
LogToFile("SetText1", msg);
// Always call the original
_orig1(@this, strPtr);
}
// Repeat for MyHook2, just change the tag/log if you want to differentiate
private static void MyHook2(IntPtr @this, IntPtr strPtr)
{
try
{
string msg;
long val = strPtr.ToInt64();
if (val == 0 || val == 0x10 || val < 0x10000)
{
msg = $"[SetText2] strPtr=0x{val:X} [skipped-invalid]";
}
else
{
try
{
msg = Marshal.PtrToStringUni(strPtr);
if (string.IsNullOrEmpty(msg))
msg = $"[SetText2] strPtr=0x{val:X} [direct:empty]";
else
msg = $"[SetText2] \"{msg}\"";
}
catch (Exception ex)
{
msg = $"[SetText2] strPtr=0x{val:X} [EX:{ex.GetType().Name}]";
}
}
LogToFile("SetText2", msg);
}
catch { }
finally
{
_orig2(@this, strPtr);
}
}
public static void Init()
{
unsafe
{
foreach (int addr in Sites2)
{
byte* p = (byte*)addr;
int callTarget = Hook.ReadCall(addr);
// Print the bytes and resolved call target
PluginCore.WriteToChat(
$"[DEBUG] Addr: 0x{addr:X8} Bytes: {p[0]:X2} {p[1]:X2} {p[2]:X2} {p[3]:X2} {p[4]:X2} -> 0x{callTarget:X8}"
);
}
}
// Patch for loc_463D60 (ENTRY1)
_hook1 = new MultiHook(0x00463D60, Sites1);
_orig1 = Marshal.GetDelegateForFunctionPointer<SetTextDelegate>(_hook1.Entrypoint);
if (_hook1.Setup((SetTextDelegate)MyHook1))
PluginCore.WriteToChat("[ChatHooks] SetText1 hook installed.");
else
PluginCore.WriteToChat("[ChatHooks] SetText1 hook FAILED.");
// Patch for loc_469440 (ENTRY2)
_hook2 = new MultiHook(0x00469440, Sites2);
_orig2 = Marshal.GetDelegateForFunctionPointer<SetTextDelegate2>(_hook2.Entrypoint);
if (_hook2.Setup((SetTextDelegate2)MyHook2))
PluginCore.WriteToChat("[ChatHooks] SetText2 hook installed.");
else
PluginCore.WriteToChat("[ChatHooks] SetText2 hook FAILED.");
}
public static void Dispose()
{
_hook1?.Remove();
_hook2?.Remove();
_hook1 = null;
_hook2 = null;
Hook.Cleanup();
VHook.Cleanup();
PluginCore.WriteToChat("[ChatHooks] All hooks removed.");
}
private static void LogToFile(string prefix, string msg)
{
try
{
string characterName = CoreManager.Current.CharacterFilter.Name;
string pluginFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
string characterFolder = Path.Combine(pluginFolder, characterName);
Directory.CreateDirectory(characterFolder);
string logFile = Path.Combine(characterFolder, "hooklog.txt");
File.AppendAllText(logFile, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{prefix}] {msg}\r\n");
}
catch { }
}
}
}