Follow PE export jmp thunks before applying a native detour (#1387)

* Follow PE export jmp thunks before applying a native detour.

GetProcAddress of il2cpp_runtime_invoke can be a jmp rel32 stub. Dobby aborting on that stub is BepInEx#1188-class. Resolve E9 and FF 25 in INativeDetour.Create so Default/Dobby and Funchook all patch the prologue. No-op when the pointer is already a function body.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Use IntPtr.Size when following FF 25 thunks.

nint.Size does not exist on the net6.0 BepInEx IL2CPP target.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Joshua TraverandCursor authored and GitHub committed 2026-09-01 07:03:47 +02:00
1 parent 7cb1246fcc
commit 5b766a3b7f
1 file changed
+45
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;
using BepInEx.Configuration;
using BepInEx.Logging;
using BepInEx.Unity.IL2CPP.Hook.Dobby;
using BepInEx.Unity.IL2CPP.Hook.Funchook;
using MonoMod.RuntimeDetour;
@@ -17,6 +19,8 @@ public interface INativeDetour : IDetour
"The native provider to use for managed detours"
);
private static readonly ManualLogSource ThunkLog = Logger.CreateLogSource("NativeDetour");
public nint OriginalMethodPtr { get; }
public nint DetourMethodPtr { get; }
public nint TrampolinePtr { get; }
@@ -27,6 +31,11 @@ public interface INativeDetour : IDetour
public static INativeDetour Create<T>(nint original, T target) where T : Delegate
{
var resolved = FollowExportThunks(original);
if (resolved != original)
ThunkLog.LogDebug($"Resolved export thunk 0x{original:X} -> 0x{resolved:X}");
original = resolved;
var detour = DetourProviderType.Value switch
{
DetourProvider.Dobby => new DobbyDetour(original, target),
@@ -41,6 +50,42 @@ public interface INativeDetour : IDetour
return detour;
}
/// <summary>
/// PE exports are often a <c>jmp rel32</c> (or <c>jmp [rip+disp]</c>) stub with int3 padding.
/// Dobby aborts if asked to patch that stub. Follow to the real prologue first.
/// No-op when the pointer is already a function body.
/// </summary>
private static nint FollowExportThunks(nint func)
{
if (func == 0) return func;
var fn = func;
for (var hops = 0; hops < 8; hops++)
{
byte op;
try { op = Marshal.ReadByte(fn); }
catch { return fn; }
if (op == 0xE9)
{
fn += 5 + Marshal.ReadInt32(fn + 1);
continue;
}
if (op == 0xFF && Marshal.ReadByte(fn + 1) == 0x25)
{
if (IntPtr.Size == 8)
fn = Marshal.ReadIntPtr(fn + 6 + Marshal.ReadInt32(fn + 2));
else
fn = Marshal.ReadIntPtr((nint)(uint)Marshal.ReadInt32(fn + 2));
continue;
}
break;
}
return fn;
}
public static INativeDetour CreateAndApply<T>(nint from, T to, out T original)
where T : Delegate
{