Restructure trampoline and detour code

This commit is contained in:
Bepis committed 2020-09-04 02:57:08 +10:00
1 parent 2c2b44588e
commit 2d8800f54b
4 files changed
+153 -120

No files matched your search

+4 -2
View File
@@ -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>
@@ -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")}");
byte[] jmp = GenerateAbsoluteJump(detourPtr, functionPtr, architecture);
var trampolineAlloc = DetourHelper.Native.MemAlloc(80);
DetourHelper.Native.MakeWritable(originalFunctionPtr, 32);
DetourHelper.Native.MakeWritable(trampolineAlloc, 80);
Marshal.Copy(jmp, 0, functionPtr, jmp.Length);
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");
Disassemble(logSource, trampolineAlloc, trampolineLength);
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);
Marshal.Copy(newCode, 0, trampolinePtr, newCode.Length);
// Overwrite the start of trampolineFunctionPtr with a jump to patchedFunctionPtr
Marshal.Copy(generatedJmp, 0, originalFunctionPtr, (int)requiredBytes);
// 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
View File
@@ -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
View File
@@ -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)));
invokeDetour.Apply(unhollowerLogSource);
originalInvoke = invokeDetour.GenerateTrampoline<RuntimeInvokeDetour>();
//invokeDetour.Apply(unhollowerLogSource);
invokeDetour.Apply();
}
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)
{