Browse Source

Restructure trampoline and detour code

Bepis 3 years ago
parent
commit
2d8800f54b

+ 4 - 2
BepInEx.IL2CPP/BepInEx.IL2CPP.csproj

@@ -83,7 +83,7 @@
     <Compile Include="IL2CPPChainloader.cs" />
     <Compile Include="Preloader.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
-    <Compile Include="Hook\TrampolineGenerator.cs" />
+    <Compile Include="Hook\DetourGenerator.cs" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\BepInEx.Core\BepInEx.Core.csproj">
@@ -103,6 +103,8 @@
     <None Include="app.config" />
     <None Include="packages.config" />
   </ItemGroup>
-  <ItemGroup />
+  <ItemGroup>
+    <Folder Include="UnityEngine\" />
+  </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
 </Project>

+ 55 - 81
BepInEx.IL2CPP/Hook/TrampolineGenerator.cs

@@ -3,27 +3,12 @@ using System.Collections.Generic;
 using System.Runtime.InteropServices;
 using BepInEx.Logging;
 using Iced.Intel;
-using MonoMod.RuntimeDetour;
 
 namespace BepInEx.IL2CPP
 {
-	public static class TrampolineGenerator
+	public static class DetourGenerator
 	{
-		public static IntPtr Generate(IntPtr originalFunctionPtr, IntPtr patchedFunctionPtr, out int trampolineLength)
-		{
-			var trampolineAlloc = DetourHelper.Native.MemAlloc(80);
-			DetourHelper.Native.MakeWritable(originalFunctionPtr, 32);
-			DetourHelper.Native.MakeWritable(trampolineAlloc, 80);
-
-			trampolineLength = Generate(originalFunctionPtr, patchedFunctionPtr, trampolineAlloc, IntPtr.Size == 8 ? 64 : 32);
-
-			DetourHelper.Native.MakeExecutable(trampolineAlloc, (uint)trampolineLength);
-			DetourHelper.Native.MakeExecutable(originalFunctionPtr, 32);
-
-			return trampolineAlloc;
-		}
-
-		private static void Disassemble(ManualLogSource logSource, IntPtr memoryPtr, int size)
+		public static void Disassemble(ManualLogSource logSource, IntPtr memoryPtr, int size)
 		{
 			byte[] data = new byte[size];
 			Marshal.Copy(memoryPtr, data, 0, size);
@@ -52,56 +37,44 @@ namespace BepInEx.IL2CPP
 			}
 		}
 
-		public static IntPtr Generate(ManualLogSource logSource, IntPtr originalFunctionPtr, IntPtr patchedFunctionPtr, out int trampolineLength)
+		public static int GetDetourLength(Architecture arch)
+			=> arch == Architecture.X64 ? 14 : 5;
+
+		/// <summary>
+		/// Writes a detour on <see cref="functionPtr"/> to redirect to <see cref="detourPtr"/>.
+		/// </summary>
+		/// <param name="functionPtr">The pointer to the function to apply the detour to.</param>
+		/// <param name="detourPtr">The pointer to the function to redirect to.</param>
+		/// <param name="architecture">The architecture of the current platform.</param>
+		/// <param name="minimumLength">The minimum amount of length that the detour should consume. If the generated redirect is smaller than this, the remaining space is padded with NOPs.</param>
+		public static void ApplyDetour(IntPtr functionPtr, IntPtr detourPtr, Architecture architecture, int minimumLength = 0)
 		{
-			logSource.LogDebug($"DoHook 0x{originalFunctionPtr.ToString("X")} -> 0x{patchedFunctionPtr.ToString("X")}");
-
-			var trampolineAlloc = DetourHelper.Native.MemAlloc(80);
-			DetourHelper.Native.MakeWritable(originalFunctionPtr, 32);
-			DetourHelper.Native.MakeWritable(trampolineAlloc, 80);
-
-			logSource.LogDebug($"Trampoline allocation: 0x{trampolineAlloc.ToString("X")}");
-
-
-			logSource.LogDebug("Original (32) asm");
-
-
-			Disassemble(logSource, originalFunctionPtr, 32);
-
-
-			trampolineLength = Generate(originalFunctionPtr, patchedFunctionPtr, trampolineAlloc, IntPtr.Size == 8 ? 64 : 32);
-
-
-			logSource.LogDebug("Modified (32) asm");
-
-			Disassemble(logSource, originalFunctionPtr, 32);
-
-			logSource.LogDebug($"Trampoline ({trampolineLength}) asm");
+			byte[] jmp = GenerateAbsoluteJump(detourPtr, functionPtr, architecture);
 
-			Disassemble(logSource, trampolineAlloc, trampolineLength);
+			Marshal.Copy(jmp, 0, functionPtr, jmp.Length);
 
-			DetourHelper.Native.MakeExecutable(trampolineAlloc, 80);
-			DetourHelper.Native.MakeExecutable(originalFunctionPtr, 32);
-
-			return trampolineAlloc;
+			// Fill remaining space with NOP instructions
+			for (int i = jmp.Length; i < minimumLength; i++)
+				Marshal.WriteByte(functionPtr + i, 0x90);
 		}
 
-		public static int Generate(IntPtr originalFunctionPtr, IntPtr patchedFunctionPtr, IntPtr trampolineFunctionPtr, int bitness)
+		/// <summary>
+		/// Reads assembly from <see cref="functionPtr"/> (at least <see cref="minimumTrampolineLength"/> bytes), and writes it to <see cref="trampolinePtr"/> plus a jmp to continue execution.
+		/// </summary>
+		/// <param name="instructionBuffer">The buffer to copy assembly from.</param>
+		/// <param name="functionPtr">The pointer to the function to copy assembly from.</param>
+		/// <param name="trampolinePtr">The pointer to write the trampoline assembly to.</param>
+		/// <param name="arch">The architecture of the current platform.</param>
+		/// <param name="minimumTrampolineLength">Copies at least this many bytes of assembly from <see cref="functionPtr"/>.</param>
+		/// <param name="trampolineLength">Returns the total length of the trampoline, in bytes.</param>
+		/// <param name="jmpLength">Returns the length of the jmp at the end of the trampoline, in bytes.</param>
+		public static void CreateTrampolineFromFunction(byte[] instructionBuffer, IntPtr functionPtr, IntPtr trampolinePtr, int minimumTrampolineLength, Architecture arch, out int trampolineLength, out int jmpLength)
 		{
-			byte[] instructionBuffer = new byte[80];
-
-			Marshal.Copy(originalFunctionPtr, instructionBuffer, 0, 80);
-
 			// Decode original function up until we go past the needed bytes to write the jump to patchedFunctionPtr
 
-			var generatedJmp = GenerateAbsoluteJump(patchedFunctionPtr, originalFunctionPtr, bitness == 64);
-
-
-			uint requiredBytes = (uint)generatedJmp.Length;
-
 			var codeReader = new ByteArrayCodeReader(instructionBuffer);
-			var decoder = Decoder.Create(bitness, codeReader);
-			decoder.IP = (ulong)originalFunctionPtr.ToInt64();
+			var decoder = Decoder.Create(arch == Architecture.X64 ? 64 : 32, codeReader);
+			decoder.IP = (ulong)functionPtr.ToInt64();
 
 			uint totalBytes = 0;
 			var origInstructions = new InstructionList();
@@ -112,7 +85,7 @@ namespace BepInEx.IL2CPP
 				totalBytes += (uint)instr.Length;
 				if (instr.Code == Code.INVALID)
 					throw new Exception("Found garbage");
-				if (totalBytes >= requiredBytes)
+				if (totalBytes >= minimumTrampolineLength)
 					break;
 
 				switch (instr.FlowControl)
@@ -125,21 +98,24 @@ namespace BepInEx.IL2CPP
 						{
 							var target = instr.NearBranchTarget;
 						}
-						goto default;
+
+						break;
+					//goto default;
+					case FlowControl.Interrupt:// eg. int n
+						break;
 
 					case FlowControl.IndirectBranch:// eg. jmp reg/mem
 					case FlowControl.ConditionalBranch:// eg. je, jno, etc
 					case FlowControl.Return:// eg. ret
 					case FlowControl.Call:// eg. call method
 					case FlowControl.IndirectCall:// eg. call reg/mem
-					case FlowControl.Interrupt:// eg. int n
 					case FlowControl.XbeginXabortXend:
 					case FlowControl.Exception:// eg. ud0
 					default:
 						throw new Exception("Not supported by this simple example - " + instr.FlowControl);
 				}
 			}
-			if (totalBytes < requiredBytes)
+			if (totalBytes < minimumTrampolineLength)
 				throw new Exception("Not enough bytes!");
 
 			if (origInstructions.Count == 0)
@@ -150,51 +126,49 @@ namespace BepInEx.IL2CPP
 
 			if (lastInstr.FlowControl != FlowControl.Return)
 			{
-				if (bitness == 64)
+				Instruction detourInstruction;
+
+				if (arch == Architecture.X64)
 				{
-					origInstructions.Add(Instruction.CreateBranch(Code.Jmp_rel32_64, lastInstr.NextIP));
+					detourInstruction = Instruction.CreateBranch(Code.Jmp_rel32_64, lastInstr.NextIP);
 				}
 				else
 				{
-					origInstructions.Add(Instruction.CreateBranch(Code.Jmp_rel32_32, lastInstr.NextIP));
+					detourInstruction = Instruction.CreateBranch(Code.Jmp_rel32_32, lastInstr.NextIP);
 				}
+
+				origInstructions.Add(detourInstruction);
 			}
 
 
 			// Generate trampoline from instruction list
 
 			var codeWriter = new CodeWriterImpl();
-			ulong relocatedBaseAddress = (ulong)trampolineFunctionPtr;
+			ulong relocatedBaseAddress = (ulong)trampolinePtr;
 			var block = new InstructionBlock(codeWriter, origInstructions, relocatedBaseAddress);
 
 			bool success = BlockEncoder.TryEncode(decoder.Bitness, block, out var errorMessage, out var result);
+
 			if (!success)
 			{
 				throw new Exception(errorMessage);
 			}
 
 			// Write generated trampoline
-			
-			var newCode = codeWriter.ToArray();
-			Marshal.Copy(newCode, 0, trampolineFunctionPtr, newCode.Length);
-
 
-			// Overwrite the start of trampolineFunctionPtr with a jump to patchedFunctionPtr
-
-			Marshal.Copy(generatedJmp, 0, originalFunctionPtr, (int)requiredBytes);
+			var newCode = codeWriter.ToArray();
+			Marshal.Copy(newCode, 0, trampolinePtr, newCode.Length);
 
-			// Fill overwritten instructions with NOP
-			for (int i = (int)requiredBytes; i < totalBytes; i++)
-				Marshal.WriteByte(originalFunctionPtr + i, 0x90);
 
-			return newCode.Length;
+			jmpLength = newCode.Length - (int)totalBytes;
+			trampolineLength = newCode.Length;
 		}
 
-		private static byte[] GenerateAbsoluteJump(IntPtr address, IntPtr currentAddress, bool x64)
+		public static byte[] GenerateAbsoluteJump(IntPtr targetAddress, IntPtr currentAddress, Architecture arch)
 		{
 			byte[] jmpBytes;
 
-			if (x64)
+			if (arch == Architecture.X64)
 			{
 				jmpBytes = new byte[]
 				{
@@ -202,7 +176,7 @@ namespace BepInEx.IL2CPP
 					0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00	// Absolute destination address
 				};
 
-				Array.Copy(BitConverter.GetBytes(address.ToInt64()), 0, jmpBytes, 6, 8);
+				Array.Copy(BitConverter.GetBytes(targetAddress.ToInt64()), 0, jmpBytes, 6, 8);
 			}
 			else
 			{
@@ -212,7 +186,7 @@ namespace BepInEx.IL2CPP
 					0x00, 0x00, 0x00, 0x00	// Relative destination address
 				};
 
-				Array.Copy(BitConverter.GetBytes(address.ToInt32() - (currentAddress.ToInt32() + 5)), 0, jmpBytes, 1, 4);
+				Array.Copy(BitConverter.GetBytes(targetAddress.ToInt32() - (currentAddress.ToInt32() + 5)), 0, jmpBytes, 1, 4);
 			}
 
 			return jmpBytes;

+ 82 - 13
BepInEx.IL2CPP/Hook/FastNativeDetour.cs

@@ -15,24 +15,25 @@ namespace BepInEx.IL2CPP.Hook
 		public bool IsApplied { get; protected set; }
 
 
-		public IntPtr OriginalFuncPtr { get; protected set; }
-		public IntPtr DetourFuncPtr { get; protected set; }
+		public IntPtr OriginalFunctionPtr { get; protected set; }
+		public IntPtr DetourFunctionPtr { get; protected set; }
 
 
 		public IntPtr TrampolinePtr { get; protected set; } = IntPtr.Zero;
 		public int TrampolineSize { get; protected set; } = 0;
+		protected int TrampolineJmpSize { get; set; } = 0;
 
 		protected MethodInfo TrampolineMethod { get; set; }
 
 
-		public FastNativeDetour(IntPtr originalFuncPtr, IntPtr detourFuncPtr)
+		public FastNativeDetour(IntPtr originalFunctionPtr, IntPtr detourFunctionPtr)
 		{
-			OriginalFuncPtr = originalFuncPtr;
-			DetourFuncPtr = detourFuncPtr;
+			OriginalFunctionPtr = originalFunctionPtr;
+			DetourFunctionPtr = detourFunctionPtr;
 
 			// TODO: This may not be safe during undo if the method is smaller than 20 bytes
 			BackupBytes = new byte[20];
-			Marshal.Copy(originalFuncPtr, BackupBytes, 0, 20);
+			Marshal.Copy(originalFunctionPtr, BackupBytes, 0, 20);
 		}
 
 
@@ -47,21 +48,83 @@ namespace BepInEx.IL2CPP.Hook
 			if (IsApplied)
 				return;
 
-			int trampolineLength;
 
-			if (debuggerLogSource == null)
-				TrampolinePtr = TrampolineGenerator.Generate(OriginalFuncPtr, DetourFuncPtr, out trampolineLength);
-			else
-				TrampolinePtr = TrampolineGenerator.Generate(debuggerLogSource, OriginalFuncPtr, DetourFuncPtr, out trampolineLength);
+			DetourHelper.Native.MakeWritable(OriginalFunctionPtr, 32);
 
-			TrampolineSize = trampolineLength;
+
+			if (debuggerLogSource != null)
+			{
+				debuggerLogSource.LogDebug($"Detouring 0x{OriginalFunctionPtr.ToString("X")} -> 0x{OriginalFunctionPtr.ToString("X")}");
+
+
+				debuggerLogSource.LogDebug("Original (32) asm");
+
+				DetourGenerator.Disassemble(debuggerLogSource, OriginalFunctionPtr, 32);
+			}
+
+			var arch = IntPtr.Size == 8 ? Architecture.X64 : Architecture.X86;
+
+			GenerateTrampolineInner(out int trampolineLength, out int jmpLength);
+
+			debuggerLogSource?.LogDebug($"Cleared function length: {trampolineLength} - {jmpLength} = {trampolineLength - jmpLength}");
+
+			debuggerLogSource?.LogDebug($"Trampoline allocation: 0x{TrampolinePtr.ToString("X")}");
+
+			DetourGenerator.ApplyDetour(OriginalFunctionPtr, DetourFunctionPtr, arch, trampolineLength - jmpLength);
+
+
+			if (debuggerLogSource != null)
+			{
+				debuggerLogSource.LogDebug("Modified (32) asm");
+
+				DetourGenerator.Disassemble(debuggerLogSource, OriginalFunctionPtr, 32);
+
+				debuggerLogSource.LogDebug($"Trampoline ({trampolineLength}) asm");
+
+				DetourGenerator.Disassemble(debuggerLogSource, TrampolinePtr, trampolineLength);
+			}
+
+			DetourHelper.Native.MakeExecutable(OriginalFunctionPtr, 32);
 
 			IsApplied = true;
 		}
 
+
+		private void GenerateTrampolineInner(out int trampolineLength, out int jmpLength)
+		{
+			if (TrampolinePtr != IntPtr.Zero)
+			{
+				trampolineLength = TrampolineSize;
+				jmpLength = TrampolineJmpSize;
+				return;
+			}
+
+			byte[] instructionBuffer = new byte[32];
+			Marshal.Copy(OriginalFunctionPtr, instructionBuffer, 0, 32);
+
+			var trampolineAlloc = DetourHelper.Native.MemAlloc(80);
+
+			DetourHelper.Native.MakeWritable(trampolineAlloc, 80);
+
+			var arch = IntPtr.Size == 8 ? Architecture.X64 : Architecture.X86;
+
+			DetourGenerator.CreateTrampolineFromFunction(instructionBuffer, OriginalFunctionPtr, trampolineAlloc,
+				DetourGenerator.GetDetourLength(arch), arch, out trampolineLength, out jmpLength);
+
+			DetourHelper.Native.MakeExecutable(trampolineAlloc, 80);
+
+			TrampolinePtr = trampolineAlloc;
+			TrampolineSize = trampolineLength;
+			TrampolineJmpSize = jmpLength;
+		}
+
+
 		public void Undo()
 		{
-			Marshal.Copy(BackupBytes, 0, OriginalFuncPtr, BackupBytes.Length);
+			if (!IsApplied)
+				return;
+
+			Marshal.Copy(BackupBytes, 0, OriginalFunctionPtr, BackupBytes.Length);
 
 			DetourHelper.Native.MemFree(TrampolinePtr);
 
@@ -80,6 +143,12 @@ namespace BepInEx.IL2CPP.Hook
 		{
 			if (TrampolineMethod == null)
 			{
+				// Generate trampoline without applying the detour
+				GenerateTrampolineInner(out _, out _);
+
+				if (TrampolinePtr == IntPtr.Zero)
+					throw new InvalidOperationException("Trampoline pointer is not available");
+
 				TrampolineMethod = DetourHelper.GenerateNativeProxy(TrampolinePtr, signature);
 			}
 

+ 12 - 24
BepInEx.IL2CPP/IL2CPPChainloader.cs

@@ -5,10 +5,10 @@ using System.Linq;
 using System.Reflection;
 using System.Runtime.InteropServices;
 using BepInEx.Bootstrap;
+using BepInEx.IL2CPP.Hook;
 using BepInEx.Logging;
 using BepInEx.Preloader.Core;
 using BepInEx.Preloader.Core.Logging;
-using MonoMod.RuntimeDetour;
 using UnhollowerBaseLib.Runtime;
 using UnhollowerRuntimeLib;
 using UnityEngine;
@@ -41,21 +41,7 @@ namespace BepInEx.IL2CPP
 
 		private static RuntimeInvokeDetour originalInvoke;
 
-		private static ManualLogSource unhollowerLogSource = Logger.CreateLogSource("Unhollower");
-
-		private class DetourHandler : UnhollowerRuntimeLib.ManagedDetour
-		{
-			public T Detour<T>(IntPtr from, T to) where T : Delegate
-			{
-				var detour = new NativeDetour(from, to, new NativeDetourConfig { ManualApply = true });
-
-				var trampoline = detour.GenerateTrampoline<T>();
-
-				detour.Apply();
-
-				return trampoline;
-			}
-		}
+		private static readonly ManualLogSource unhollowerLogSource = Logger.CreateLogSource("Unhollower");
 
 
 		public override unsafe void Initialize(string gameExePath = null)
@@ -72,9 +58,11 @@ namespace BepInEx.IL2CPP
 			{
 				IntPtr originalFunc = new IntPtr(*(void**)ptr);
 
-				var trampolinePtr = TrampolineGenerator.Generate(unhollowerLogSource, originalFunc, patchedFunctionPtr, out _);
+				var detour = new FastNativeDetour(originalFunc, patchedFunctionPtr);
+				
+				detour.Apply();
 
-				*(void**)ptr = (void*)trampolinePtr;
+				*(void**)ptr = (void*)detour.TrampolinePtr;
 			};
 
 			var gameAssemblyModule = Process.GetCurrentProcess().Modules.Cast<ProcessModule>().First(x => x.ModuleName.Contains("GameAssembly"));
@@ -84,14 +72,14 @@ namespace BepInEx.IL2CPP
 
 			PreloaderLogger.Log.LogDebug($"Runtime invoke pointer: 0x{functionPtr.ToInt64():X}");
 
-			var invokeDetour = new NativeDetour(functionPtr, Marshal.GetFunctionPointerForDelegate(new RuntimeInvokeDetour(OnInvokeMethod)));
+			var invokeDetour = new FastNativeDetour(functionPtr, Marshal.GetFunctionPointerForDelegate(new RuntimeInvokeDetour(OnInvokeMethod)));
 
-			originalInvoke = invokeDetour.GenerateTrampoline<RuntimeInvokeDetour>();
+			invokeDetour.Apply(unhollowerLogSource);
 
-			//invokeDetour.Apply(unhollowerLogSource);
-			invokeDetour.Apply();
+			originalInvoke = invokeDetour.GenerateTrampoline<RuntimeInvokeDetour>();
 		}
 
+
 		private static bool HasSet = false;
 
 		private static HashSet<string> recordedNames = new HashSet<string>();
@@ -108,11 +96,11 @@ namespace BepInEx.IL2CPP
 			{
 				try
 				{
-					UnityEngine.Application.s_LogCallbackHandler = new Action<string, string, LogType>(UnityLogCallback);
+					Application.s_LogCallbackHandler = new Action<string, string, LogType>(UnityLogCallback);
 
 					UnityLogSource.LogMessage($"callback set - {methodName}");
 
-					UnityEngine.Application.CallLogCallback("test from OnInvokeMethod", "", LogType.Log, true);
+					Application.CallLogCallback("test from OnInvokeMethod", "", LogType.Log, true);
 				}
 				catch (Exception ex)
 				{