diff --git a/ReCodeItCLI/Commands/AutoMatcher.cs b/ReCodeItCLI/Commands/AutoMatcher.cs new file mode 100644 index 0000000..3759ec5 --- /dev/null +++ b/ReCodeItCLI/Commands/AutoMatcher.cs @@ -0,0 +1,48 @@ +using CliFx; +using CliFx.Attributes; +using CliFx.Infrastructure; +using ReCodeItLib.Models; +using ReCodeItLib.ReMapper; +using ReCodeItLib.Utils; + +namespace ReCodeItCLI.Commands; + +[Command("AutoMatch", Description = "Automatically tries to generate a mapping object with the provided arguments.")] +public class AutoMatchCommand : ICommand +{ + [CommandParameter(0, IsRequired = true, Description = "The absolute path to your obfuscated assembly or exe file, folder must contain all references to be resolved.")] + public required string AssemblyPath { get; init; } + + [CommandParameter(1, IsRequired = true, Description = "Full old type name including namespace")] + public required string OldTypeName { get; init; } + + [CommandParameter(2, IsRequired = true, Description = "The name you want the type to be renamed to")] + public required string NewTypeName { get; init; } + + [CommandParameter(3, IsRequired = false, Description = "Path to your mapping file so it can be updated if a match is found")] + public string MappingsPath { get; init; } + + public ValueTask ExecuteAsync(IConsole console) + { + DataProvider.IsCli = true; + DataProvider.LoadAppSettings(); + + Logger.LogSync("Finding match..."); + + var remaps = new List(); + + if (!string.IsNullOrEmpty(MappingsPath)) + { + remaps.AddRange(DataProvider.LoadMappingFile(MappingsPath)); + } + + new AutoMatcher(remaps) + .AutoMatch(AssemblyPath, OldTypeName, NewTypeName); + + // Wait for log termination + Logger.Terminate(); + while(Logger.IsRunning()) {} + + return default; + } +} \ No newline at end of file diff --git a/ReCodeItCLI/ReCodeItCLI.csproj b/ReCodeItCLI/ReCodeItCLI.csproj index d3dadb7..3877a3e 100644 --- a/ReCodeItCLI/ReCodeItCLI.csproj +++ b/ReCodeItCLI/ReCodeItCLI.csproj @@ -6,6 +6,7 @@ net8.0 enable enable + Always diff --git a/RecodeItLib/Remapper/AssemblyUtils.cs b/RecodeItLib/Remapper/AssemblyUtils.cs new file mode 100644 index 0000000..2a325f5 --- /dev/null +++ b/RecodeItLib/Remapper/AssemblyUtils.cs @@ -0,0 +1,34 @@ +using dnlib.DotNet; +using ReCodeItLib.Utils; + +namespace ReCodeItLib.ReMapper; + +internal static class AssemblyUtils +{ + public static string TryDeObfuscate(ModuleDefMD module, string assemblyPath, out ModuleDefMD cleanedModule) + { + if (!module!.GetTypes().Any(t => t.Name.Contains("GClass"))) + { + Logger.LogSync("Assembly is obfuscated, running de-obfuscation...\n", ConsoleColor.Yellow); + + module.Dispose(); + module = null; + + Deobfuscator.Deobfuscate(assemblyPath); + + var cleanedName = Path.GetFileNameWithoutExtension(assemblyPath); + cleanedName = $"{cleanedName}-cleaned.dll"; + + var newPath = Path.GetDirectoryName(assemblyPath); + newPath = Path.Combine(newPath!, cleanedName); + + Logger.LogSync($"Cleaning assembly: {newPath}", ConsoleColor.Green); + + cleanedModule = DataProvider.LoadModule(newPath); + return newPath; + } + + cleanedModule = module; + return assemblyPath; + } +} \ No newline at end of file diff --git a/RecodeItLib/Remapper/AutoMatcher.cs b/RecodeItLib/Remapper/AutoMatcher.cs new file mode 100644 index 0000000..07fcc33 --- /dev/null +++ b/RecodeItLib/Remapper/AutoMatcher.cs @@ -0,0 +1,271 @@ +using dnlib.DotNet; +using ReCodeItLib.Models; +using ReCodeItLib.Utils; + +namespace ReCodeItLib.ReMapper; + +public class AutoMatcher(List mappings) +{ + private ModuleDefMD? Module { get; set; } + + private List? CandidateTypes { get; set; } + + private static List _tokens = DataProvider.Settings!.Remapper!.TokensToMatch; + + public void AutoMatch(string assemblyPath, string oldTypeName, string newTypeName) + { + assemblyPath = AssemblyUtils.TryDeObfuscate( + DataProvider.LoadModule(assemblyPath), + assemblyPath, + out var module); + + Module = module; + CandidateTypes = Module.GetTypes() + .Where(t => _tokens.Any(token => t.Name.StartsWith(token))) + // .Where(t => t.Name != oldTypeName) + .ToList(); + + var targetTypeDef = FindTargetType(oldTypeName); + + Logger.LogSync($"Found target type: {targetTypeDef!.FullName}", ConsoleColor.Green); + + var remapModel = new RemapModel(); + remapModel.NewTypeName = newTypeName; + + StartFilter(targetTypeDef, remapModel, assemblyPath); + } + + private TypeDef? FindTargetType(string oldTypeName) + { + return Module!.GetTypes().FirstOrDefault(t => t.FullName == oldTypeName); + } + + private void StartFilter(TypeDef target, RemapModel remapModel, string assemblyPath) + { + Logger.LogSync($"Starting Candidates: {CandidateTypes!.Count}", ConsoleColor.Yellow); + + // Purpose of this pass is to eliminate any types that have no matching parameters + foreach (var candidate in CandidateTypes!.ToList()) + { + if (!PassesGeneralChecks(target, candidate, remapModel.SearchParams.GenericParams)) + { + CandidateTypes!.Remove(candidate); + continue; + } + + if (!ContainsTargetMethods(target, candidate, remapModel.SearchParams.Methods)) + { + CandidateTypes!.Remove(candidate); + continue; + } + + if (!ContainsTargetFields(target, candidate, remapModel.SearchParams.Fields)) + { + CandidateTypes!.Remove(candidate); + continue; + } + + if (!ContainsTargetProperties(target, candidate, remapModel.SearchParams.Properties)) + { + CandidateTypes!.Remove(candidate); + } + } + + if (CandidateTypes!.Count == 1) + { + Logger.LogSync("Narrowed candidates down to one. Testing generated model...", ConsoleColor.Green); + + var tmpList = new List() + { + remapModel + }; + + new ReMapper().InitializeRemap(tmpList, assemblyPath, validate: true); + } + } + + private bool PassesGeneralChecks(TypeDef target, TypeDef candidate, GenericParams parms) + { + if (target.IsPublic != candidate.IsPublic) return false; + if (target.IsAbstract != candidate.IsAbstract) return false; + if (target.IsInterface != candidate.IsInterface) return false; + if (target.IsEnum != candidate.IsEnum) return false; + if (target.IsValueType != candidate.IsValueType) return false; + if (target.HasGenericParameters != candidate.HasGenericParameters) return false; + if (target.IsNested != candidate.IsNested) return false; + if (target.IsSealed != candidate.IsSealed) return false; + if (target.HasCustomAttributes != candidate.HasCustomAttributes) return false; + + parms.IsPublic = target.IsPublic; + parms.IsAbstract = target.IsAbstract; + parms.IsInterface = target.IsInterface; + parms.IsEnum = target.IsEnum; + parms.IsStruct = target.IsValueType && !target.IsEnum; + parms.HasGenericParameters = target.HasGenericParameters; + parms.IsNested = target.IsNested; + parms.IsSealed = target.IsSealed; + parms.HasAttribute = target.HasCustomAttributes; + + return true; + } + + private bool ContainsTargetMethods(TypeDef target, TypeDef candidate, MethodParams methods) + { + // Target has no methods and type has no methods + if (!target.Methods.Any() && !candidate.Methods.Any()) + { + methods.MethodCount = 0; + return true; + } + + // Target has no methods but type has methods + if (!target.Methods.Any() && candidate.Methods.Any()) return false; + + // Target has methods but type has no methods + if (target.Methods.Any() && !candidate.Methods.Any()) return false; + + // Target has a different number of methods + if (target.Methods.Count != candidate.Methods.Count) return false; + + var commonMethods = target.Methods + .Where(m => !m.IsConstructor && !m.IsGetter && !m.IsSetter) + .Select(s => s.Name) + .Intersect(candidate.Methods + .Where(m => !m.IsConstructor && !m.IsGetter && !m.IsSetter) + .Select(s => s.Name)); + + // Methods in target that are not in candidate + var includeMethods = target.Methods + .Where(m => !m.IsConstructor && !m.IsGetter && !m.IsSetter) + .Select(s => s.Name.ToString()) + .Except(candidate.Methods + .Where(m => !m.IsConstructor && !m.IsGetter && !m.IsSetter) + .Select(s => s.Name.ToString())); + + // Methods in candidate that are not in target + var excludeMethods = candidate.Methods + .Where(m => !m.IsConstructor && !m.IsGetter && !m.IsSetter) + .Select(s => s.Name.ToString()) + .Except(target.Methods + .Where(m => !m.IsConstructor && !m.IsGetter && !m.IsSetter) + .Select(s => s.Name.ToString())); + + methods.IncludeMethods.Clear(); + methods.IncludeMethods.AddRange(includeMethods); + + methods.ExcludeMethods.AddRange(excludeMethods); + + return commonMethods.Any(); + } + + private bool ContainsTargetFields(TypeDef target, TypeDef candidate, FieldParams fields) + { + // Target has no fields and type has no fields + if (!target.Fields.Any() && !candidate.Fields.Any()) + { + fields.FieldCount = 0; + return true; + } + + // Target has no fields but type has fields + if (!target.Fields.Any() && candidate.Fields.Any()) return false; + + // Target has fields but type has no fields + if (target.Fields.Any() && !candidate.Fields.Any()) return false; + + // Target has a different number of fields + if (target.Fields.Count != candidate.Fields.Count) return false; + + var commonFields = target.Fields + .Select(s => s.Name) + .Intersect(candidate.Fields.Select(s => s.Name)); + + // Methods in target that are not in candidate + var includeFields = target.Fields + .Select(s => s.Name.ToString()) + .Except(candidate.Fields.Select(s => s.Name.ToString())); + + // Methods in candidate that are not in target + var excludeFields = candidate.Fields + .Select(s => s.Name.ToString()) + .Except(target.Fields.Select(s => s.Name.ToString())); + + fields.IncludeFields.Clear(); + fields.IncludeFields.AddRange(includeFields); + + fields.ExcludeFields.AddRange(excludeFields); + + return commonFields.Any(); + } + + private bool ContainsTargetProperties(TypeDef target, TypeDef candidate, PropertyParams props) + { + // Both target and candidate don't have properties + if (!target.Properties.Any() && !candidate.Properties.Any()) + { + props.PropertyCount = 0; + return true; + } + + // Target has no props but type has props + if (!target.Properties.Any() && candidate.Properties.Any()) return false; + + // Target has props but type has no props + if (target.Properties.Any() && !candidate.Properties.Any()) return false; + + // Target has a different number of props + if (target.Properties.Count != candidate.Properties.Count) return false; + + var commonProps = target.Properties + .Select(s => s.Name) + .Intersect(candidate.Properties.Select(s => s.Name)); + + // Props in target that are not in candidate + var includeProps = target.Properties + .Select(s => s.Name.ToString()) + .Except(candidate.Properties.Select(s => s.Name.ToString())); + + // Props in candidate that are not in target + var excludeProps = candidate.Properties + .Select(s => s.Name.ToString()) + .Except(target.Properties.Select(s => s.Name.ToString())); + + props.IncludeProperties.Clear(); + props.IncludeProperties.AddRange(includeProps); + + props.ExcludeProperties.AddRange(excludeProps); + + return commonProps.Any(); + } + + private void CompareMethods(MappingDiff diff) + { + var diffsByTarget = diff.Target.Methods + .Select(m => m.Name) + .Except(diff.Candidate.Methods.Select(m => m.Name)) + .ToList(); + + if (diffsByTarget.Any()) + { + Logger.LogSync($"Methods in target not present in candidate:\n {string.Join(", ", diffsByTarget)}", ConsoleColor.Yellow); + } + + var diffsByCandidate = diff.Candidate.Methods + .Select(m => m.Name) + .Except(diff.Target.Methods.Select(m => m.Name)) + .ToList(); + + if (diffsByCandidate.Any()) + { + Logger.LogSync($"Methods in candidate not present in target:\n {string.Join(", ", diffsByCandidate)}", ConsoleColor.Yellow); + } + } + + private class MappingDiff + { + public required TypeDef Target; + public required TypeDef Candidate; + + public RemapModel RemapModel = new(); + } +} \ No newline at end of file diff --git a/RecodeItLib/Remapper/ReMapper.cs b/RecodeItLib/Remapper/ReMapper.cs index 589c1bb..4ccf524 100644 --- a/RecodeItLib/Remapper/ReMapper.cs +++ b/RecodeItLib/Remapper/ReMapper.cs @@ -34,14 +34,19 @@ public class ReMapper public void InitializeRemap( List remapModels, string assemblyPath, - string outPath, + string outPath = "", bool validate = false) { _remaps = []; _remaps = remapModels; _alreadyGivenNames = []; - Module = DataProvider.LoadModule(assemblyPath); + assemblyPath = AssemblyUtils.TryDeObfuscate( + DataProvider.LoadModule(assemblyPath), + assemblyPath, + out var module); + + Module = module; OutPath = outPath; @@ -51,9 +56,13 @@ public class ReMapper Stopwatch.Start(); var types = Module.GetTypes(); + + if (!validate) + { + GenerateDynamicRemaps(assemblyPath, types); + } - TryDeObfuscate(types, assemblyPath); - FindBestMatches(types); + FindBestMatches(types, validate); ChooseBestMatches(); // Don't go any further during a validation @@ -71,37 +80,8 @@ public class ReMapper // We are done, write the assembly WriteAssembly(); } - - private void TryDeObfuscate(IEnumerable types, string assemblyPath) - { - if (!Module!.GetTypes().Any(t => t.Name.Contains("GClass"))) - { - Logger.LogSync("Assembly is obfuscated, running de-obfuscation...\n", ConsoleColor.Yellow); - - Module.Dispose(); - Module = null; - - Deobfuscator.Deobfuscate(assemblyPath); - - var cleanedName = Path.GetFileNameWithoutExtension(assemblyPath); - cleanedName = $"{cleanedName}-cleaned.dll"; - - var newPath = Path.GetDirectoryName(assemblyPath); - newPath = Path.Combine(newPath!, cleanedName); - - Console.WriteLine($"Cleaning assembly: {newPath}"); - - Module = DataProvider.LoadModule(newPath); - types = Module.GetTypes(); - - GenerateDynamicRemaps(newPath, types); - return; - } - - GenerateDynamicRemaps(assemblyPath, types); - } - - private void FindBestMatches(IEnumerable types) + + private void FindBestMatches(IEnumerable types, bool validate) { Logger.LogSync("Finding Best Matches...", ConsoleColor.Green); @@ -115,10 +95,13 @@ public class ReMapper }) ); } - - while (!tasks.TrueForAll(t => t.Status == TaskStatus.RanToCompletion)) + + if (!validate) { - Logger.DrawProgressBar(tasks.Where(t => t.IsCompleted)!.Count(), tasks.Count, 50); + while (!tasks.TrueForAll(t => t.Status == TaskStatus.RanToCompletion)) + { + Logger.DrawProgressBar(tasks.Where(t => t.IsCompleted)!.Count(), tasks.Count, 50); + } } Task.WaitAll(tasks.ToArray()); diff --git a/RecodeItLib/Remapper/Statistics.cs b/RecodeItLib/Remapper/Statistics.cs index f337675..c6b49aa 100644 --- a/RecodeItLib/Remapper/Statistics.cs +++ b/RecodeItLib/Remapper/Statistics.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using Newtonsoft.Json; using ReCodeItLib.Enums; using ReCodeItLib.Models; using ReCodeItLib.Utils; @@ -14,7 +15,7 @@ public class Statistics( public void DisplayStatistics(bool validate = false) { DisplayAlternativeMatches(); - DisplayFailuresAndChanges(); + DisplayFailuresAndChanges(validate); if (!validate) { @@ -45,7 +46,7 @@ public class Statistics( } } - private void DisplayFailuresAndChanges() + private void DisplayFailuresAndChanges(bool validate) { var failures = 0; var changes = 0; @@ -75,6 +76,17 @@ public class Statistics( continue; } + if (validate) + { + var str = JsonConvert.SerializeObject(remap, Formatting.Indented); + + Logger.Log("Generated Model: ", ConsoleColor.Blue); + Logger.Log(str, ConsoleColor.Blue); + + Logger.Log("Passed validation", ConsoleColor.Green); + return; + } + changes++; }