From 75b3b52e54c1a890f639ae2435f6579f85e64c11 Mon Sep 17 00:00:00 2001 From: ghorsington Date: Sat, 1 Jun 2019 23:00:17 +0300 Subject: [PATCH] Use Cecil to find preloader patches --- BepInEx.Preloader/Patching/AssemblyPatcher.cs | 75 +++++++++-- BepInEx.Preloader/Patching/PatcherPlugin.cs | 5 +- BepInEx.Preloader/Preloader.cs | 118 +++++------------- 3 files changed, 97 insertions(+), 101 deletions(-) diff --git a/BepInEx.Preloader/Patching/AssemblyPatcher.cs b/BepInEx.Preloader/Patching/AssemblyPatcher.cs index 249b8db..bfb5a62 100644 --- a/BepInEx.Preloader/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader/Patching/AssemblyPatcher.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using BepInEx.Preloader.RuntimeFixes; @@ -23,6 +24,8 @@ namespace BepInEx.Preloader.Patching /// internal static class AssemblyPatcher { + private const BindingFlags ALL = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.IgnoreCase; + public static List PatcherPlugins { get; } = new List(); private static readonly string DumpedAssembliesPath = Path.Combine(Paths.BepInExRootPath, "DumpedAssemblies"); @@ -36,32 +39,83 @@ namespace BepInEx.Preloader.Patching PatcherPlugins.Add(patcher); } + private static T CreateDelegate(MethodInfo method) where T : class => method != null ? Delegate.CreateDelegate(typeof(T), method) as T : null; + /// /// Adds all patchers from all managed assemblies specified in a directory. /// /// Directory to search patcher DLLs from. /// A function that locates assembly patchers in a given managed assembly. public static void AddPatchersFromDirectory(string directory, - Func> patcherLocator) + Func patcherLocator) { if (!Directory.Exists(directory)) return; var sortedPatchers = new SortedDictionary(); - foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll", SearchOption.AllDirectories)) - try - { - var assembly = Assembly.LoadFrom(assemblyPath); + var patchers = TypeLoader.FindPluginTypes(directory, patcherLocator); - foreach (var patcher in patcherLocator(assembly)) - sortedPatchers.Add(patcher.Name, patcher); + foreach (var keyValuePair in patchers) + { + var assembly = keyValuePair.Key; + var patcherCollection = keyValuePair.Value; + + var ass = Assembly.LoadFile(assembly.MainModule.FileName); + + foreach (var patcherPlugin in patcherCollection) + { + try + { + var type = ass.GetType(patcherPlugin.Type.FullName); + + var methods = type.GetMethods(ALL); + + patcherPlugin.Initializer = CreateDelegate(methods.FirstOrDefault(m => m.Name.Equals("Initialize", StringComparison.InvariantCultureIgnoreCase) && + m.GetParameters().Length == 0 && + m.ReturnType == typeof(void))); + + patcherPlugin.Finalizer = CreateDelegate(methods.FirstOrDefault(m => m.Name.Equals("Finish", StringComparison.InvariantCultureIgnoreCase) && + m.GetParameters().Length == 0 && + m.ReturnType == typeof(void))); + + patcherPlugin.TargetDLLs = CreateDelegate>>(type.GetProperty("TargetDLLs", ALL).GetGetMethod()); + + var patcher = methods.FirstOrDefault(m => m.Name.Equals("Patch", StringComparison.CurrentCultureIgnoreCase) && + m.ReturnType == typeof(void) && + m.GetParameters().Length == 0 && + (m.GetParameters()[0].ParameterType == typeof(AssemblyDefinition) || + m.GetParameters()[0].ParameterType == typeof(AssemblyDefinition).MakeByRefType())); + + patcherPlugin.Patcher = (ref AssemblyDefinition pAss) => + { + //we do the array fuckery here to get the ref result out + object[] args = { pAss }; + + patcher.Invoke(null, args); + + pAss = (AssemblyDefinition)args[0]; + }; + + sortedPatchers.Add($"{ass.GetName().Name}/{type.FullName}", patcherPlugin); + patcherPlugin.Type = null; + } + catch (Exception e) + { + Logger.LogError($"Failed to load patcher [{patcherPlugin.Type.FullName}]: {e.Message}"); + if (e is ReflectionTypeLoadException re) + Logger.LogDebug(TypeLoader.TypeLoadExceptionToString(re)); + else + Logger.LogDebug(e.ToString()); + } } - catch (BadImageFormatException) { } //unmanaged DLL - catch (ReflectionTypeLoadException) { } //invalid references + } foreach (KeyValuePair patcher in sortedPatchers) AddPatcher(patcher.Value); + + foreach (var assemblyDefinition in patchers.Keys) + assemblyDefinition.Dispose(); } private static void InitializePatchers() @@ -123,7 +177,7 @@ namespace BepInEx.Preloader.Patching // Then, perform the actual patching var patchedAssemblies = new HashSet(); foreach (var assemblyPatcher in PatcherPlugins) - foreach (string targetDll in assemblyPatcher.TargetDLLs) + foreach (string targetDll in assemblyPatcher.TargetDLLs()) if (assemblies.TryGetValue(targetDll, out var assembly)) { Logger.LogInfo($"Patching [{assembly.Name.Name}] with [{assemblyPatcher.Name}]"); @@ -135,7 +189,6 @@ namespace BepInEx.Preloader.Patching // Finally, load patched assemblies into memory - if (ConfigDumpAssemblies.Value || ConfigLoadDumpedAssemblies.Value) { if (!Directory.Exists(DumpedAssembliesPath)) diff --git a/BepInEx.Preloader/Patching/PatcherPlugin.cs b/BepInEx.Preloader/Patching/PatcherPlugin.cs index b602d4b..7ae6301 100644 --- a/BepInEx.Preloader/Patching/PatcherPlugin.cs +++ b/BepInEx.Preloader/Patching/PatcherPlugin.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Mono.Cecil; namespace BepInEx.Preloader.Patching { @@ -8,10 +9,12 @@ namespace BepInEx.Preloader.Patching /// internal class PatcherPlugin { + public TypeDefinition Type { get; set; } + /// /// Target assemblies to patch. /// - public IEnumerable TargetDLLs { get; set; } = null; + public Func> TargetDLLs { get; set; } = null; /// /// Initializer method that is run before any patching occurs. diff --git a/BepInEx.Preloader/Preloader.cs b/BepInEx.Preloader/Preloader.cs index 1fe7064..69a50c5 100644 --- a/BepInEx.Preloader/Preloader.cs +++ b/BepInEx.Preloader/Preloader.cs @@ -41,7 +41,7 @@ namespace BepInEx.Preloader Logger.Listeners.Add(PreloaderLog); - + string consoleTile = $"BepInEx {typeof(Paths).Assembly.GetName().Version} - {Process.GetCurrentProcess().ProcessName}"; ConsoleWindow.Title = consoleTile; @@ -69,12 +69,13 @@ namespace BepInEx.Preloader AssemblyPatcher.AddPatcher(new PatcherPlugin - { TargetDLLs = new[] { ConfigEntrypointAssembly.Value }, - Patcher = PatchEntrypoint, - Name = "BepInEx.Chainloader" - }); + { + TargetDLLs = () => new[] { ConfigEntrypointAssembly.Value }, + Patcher = PatchEntrypoint, + Name = "BepInEx.Chainloader" + }); - AssemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath, GetPatcherMethods); + AssemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath, ToPatcherPlugin); Logger.LogInfo($"{AssemblyPatcher.PatcherPlugins.Count} patcher plugin(s) loaded"); @@ -122,95 +123,34 @@ namespace BepInEx.Preloader } } - /// - /// Scans the assembly for classes that use the patcher contract, and returns a list of valid patchers. - /// - /// The assembly to scan. - /// A list of assembly patchers that were found in the assembly. - public static List GetPatcherMethods(Assembly assembly) + public static PatcherPlugin ToPatcherPlugin(TypeDefinition type) { - var patcherMethods = new List(); - var flags = BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase; + if (type.IsInterface || type.IsAbstract) + return null; - foreach (var type in assembly.GetExportedTypes()) - try - { - if (type.IsInterface) - continue; + var targetDlls = type.Methods.FirstOrDefault(m => m.Name.Equals("get_TargetDLLs", StringComparison.InvariantCultureIgnoreCase) && + m.IsPublic && + m.IsStatic); - var targetsProperty = type.GetProperty("TargetDLLs", - flags, - null, - typeof(IEnumerable), - Type.EmptyTypes, - null); + if (targetDlls == null || + targetDlls.ReturnType.FullName != "System.Collections.Generic.IEnumerable`1") + return null; - //first try get the ref patcher method - var patcher = type.GetMethod("Patch", - flags, - null, - CallingConventions.Any, - new[] { typeof(AssemblyDefinition).MakeByRefType() }, - null); + var patch = type.Methods.FirstOrDefault(m => m.Name.Equals("Patch") && + m.IsPublic && + m.IsStatic && + m.ReturnType.FullName == "System.Void" && + m.Parameters.Count == 1 && + (m.Parameters[0].ParameterType.FullName == "Mono.Cecil.AssemblyDefinition&" || + m.Parameters[0].ParameterType.FullName == "Mono.Cecil.AssemblyDefinition")); - if (patcher == null) //otherwise try getting the non-ref patcher method - patcher = type.GetMethod("Patch", - flags, - null, - CallingConventions.Any, - new[] { typeof(AssemblyDefinition) }, - null); + if (patch == null) + return null; - if (targetsProperty == null || !targetsProperty.CanRead || patcher == null) - continue; - - var assemblyPatcher = new PatcherPlugin(); - - assemblyPatcher.Name = $"{assembly.GetName().Name}/{type.FullName}"; - assemblyPatcher.Patcher = (ref AssemblyDefinition ass) => - { - //we do the array fuckery here to get the ref result out - object[] args = { ass }; - - patcher.Invoke(null, args); - - ass = (AssemblyDefinition)args[0]; - }; - - assemblyPatcher.TargetDLLs = (IEnumerable)targetsProperty.GetValue(null, null); - - var initMethod = type.GetMethod("Initialize", - flags, - null, - CallingConventions.Any, - Type.EmptyTypes, - null); - - if (initMethod != null) - assemblyPatcher.Initializer = () => initMethod.Invoke(null, null); - - var finalizeMethod = type.GetMethod("Finish", - flags, - null, - CallingConventions.Any, - Type.EmptyTypes, - null); - - if (finalizeMethod != null) - assemblyPatcher.Finalizer = () => finalizeMethod.Invoke(null, null); - - patcherMethods.Add(assemblyPatcher); - } - catch (Exception ex) - { - Logger.LogWarning($"Could not load patcher methods from {assembly.GetName().Name}"); - Logger.LogWarning(ex); - } - - Logger.Log(patcherMethods.Count > 0 ? LogLevel.Info : LogLevel.Debug, - $"Loaded {patcherMethods.Count} patcher methods from {assembly.GetName().Name}"); - - return patcherMethods; + return new PatcherPlugin + { + Type = type + }; } ///