From 5b766a3b7f6c164d4798924a93f3acf4db769d06 Mon Sep 17 00:00:00 2001 From: Joshua Traver Date: Tue, 1 Sep 2026 01:03:47 -0400 Subject: [PATCH] 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 * Use IntPtr.Size when following FF 25 thunks. nint.Size does not exist on the net6.0 BepInEx IL2CPP target. Co-authored-by: Cursor --- .../Hook/INativeDetour.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Hook/INativeDetour.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Hook/INativeDetour.cs index 7f27660..61ec72c 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Hook/INativeDetour.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Hook/INativeDetour.cs @@ -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(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; } + /// + /// PE exports are often a jmp rel32 (or jmp [rip+disp]) 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. + /// + 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(nint from, T to, out T original) where T : Delegate {