diff --git a/README.md b/README.md index 6fbc895..cc094e6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,28 @@ -# SPT-AssemblyTool +SPT-AssemblyTool +----- +Can deobfuscate Assembly-CSharp.dll files, and apply deobfuscation name remapping (provided you provide a remapping file) + +### Usage + +#### Deobfuscation + +``` +SPT-AssemblyTool.exe -d -m "...\EscapeFromTarkov_Data\Managed" "...\Assembly-CSharp.dll" +``` + +Generates a `Assembly-CSharp-cleaned.dll` in the directory that Assembly-CSharp.dll resides in. + +#### Remapping + +``` +-r -o "...\Assembly-CSharp-old.dll" -m "...\EscapeFromTarkov_Data\Managed" --mapping-file "...\mappings.json" "...\Assembly-CSharp-cleaned.dll" +``` + +Requires an old deobfuscated Assembly-CSharp.dll, and the mapping file created for it. An example mapping file is located under `example-mapping.json`. + + + +### Credits + +Uses LGPL licensed code from [IL2CPPAssemblyUnhollower](https://github.com/knah/Il2CppAssemblyUnhollower) for the remapper \ No newline at end of file diff --git a/SPT-AssemblyTool.sln b/SPT-AssemblyTool.sln new file mode 100644 index 0000000..d04f323 --- /dev/null +++ b/SPT-AssemblyTool.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31424.327 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SPT-AssemblyTool", "SPT-AssemblyTool\SPT-AssemblyTool.csproj", "{30471FBA-3D20-47AD-A035-46A3604BEF40}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {30471FBA-3D20-47AD-A035-46A3604BEF40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30471FBA-3D20-47AD-A035-46A3604BEF40}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30471FBA-3D20-47AD-A035-46A3604BEF40}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30471FBA-3D20-47AD-A035-46A3604BEF40}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E939CA1A-B078-4091-8A93-78A303569BDA} + EndGlobalSection +EndGlobal diff --git a/SPT-AssemblyTool/Assets/de4dot/AssemblyData.dll b/SPT-AssemblyTool/Assets/de4dot/AssemblyData.dll new file mode 100644 index 0000000..6b8a250 Binary files /dev/null and b/SPT-AssemblyTool/Assets/de4dot/AssemblyData.dll differ diff --git a/SPT-AssemblyTool/Assets/de4dot/de4dot.blocks.dll b/SPT-AssemblyTool/Assets/de4dot/de4dot.blocks.dll new file mode 100644 index 0000000..6983abd Binary files /dev/null and b/SPT-AssemblyTool/Assets/de4dot/de4dot.blocks.dll differ diff --git a/SPT-AssemblyTool/Assets/de4dot/de4dot.code.dll b/SPT-AssemblyTool/Assets/de4dot/de4dot.code.dll new file mode 100644 index 0000000..c43643e Binary files /dev/null and b/SPT-AssemblyTool/Assets/de4dot/de4dot.code.dll differ diff --git a/SPT-AssemblyTool/Assets/de4dot/de4dot.cui.dll b/SPT-AssemblyTool/Assets/de4dot/de4dot.cui.dll new file mode 100644 index 0000000..3e32a85 Binary files /dev/null and b/SPT-AssemblyTool/Assets/de4dot/de4dot.cui.dll differ diff --git a/SPT-AssemblyTool/Assets/de4dot/de4dot.exe b/SPT-AssemblyTool/Assets/de4dot/de4dot.exe new file mode 100644 index 0000000..d8ae798 Binary files /dev/null and b/SPT-AssemblyTool/Assets/de4dot/de4dot.exe differ diff --git a/SPT-AssemblyTool/Assets/de4dot/de4dot.mdecrypt.dll b/SPT-AssemblyTool/Assets/de4dot/de4dot.mdecrypt.dll new file mode 100644 index 0000000..1b48852 Binary files /dev/null and b/SPT-AssemblyTool/Assets/de4dot/de4dot.mdecrypt.dll differ diff --git a/SPT-AssemblyTool/Assets/de4dot/dnlib.dll b/SPT-AssemblyTool/Assets/de4dot/dnlib.dll new file mode 100644 index 0000000..c43c9b6 Binary files /dev/null and b/SPT-AssemblyTool/Assets/de4dot/dnlib.dll differ diff --git a/SPT-AssemblyTool/Deobfuscator.cs b/SPT-AssemblyTool/Deobfuscator.cs new file mode 100644 index 0000000..8e20c20 --- /dev/null +++ b/SPT-AssemblyTool/Deobfuscator.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace SPT_AssemblyTool +{ + internal static class Deobfuscator + { + internal static void Deobfuscate(Program.Arguments args) + { + var executablePath = typeof(Program).Assembly.Location; + var de4dotLocation = Path.Combine(Path.GetDirectoryName(executablePath), "Assets", "de4dot", "de4dot.exe"); + + var assemblyPath = args.Values[0]; + + string token; + + using (var assemblyDefinition = AssemblyDefinition.ReadAssembly(assemblyPath)) + { + var potentialStringDelegates = new List(); + + foreach (var type in assemblyDefinition.MainModule.Types) + { + foreach (var method in type.Methods) + { + if (method.ReturnType.FullName != "System.String" + || method.Parameters.Count != 1 + || method.Parameters[0].ParameterType.FullName != "System.Int32" + || method.Body == null + || !method.IsStatic) + { + continue; + } + + if (!method.Body.Instructions.Any(x => + x.OpCode.Code == Code.Callvirt && + ((MethodReference)x.Operand).FullName == "System.Object System.AppDomain::GetData(System.String)")) + { + continue; + } + + potentialStringDelegates.Add(method); + } + } + + if (potentialStringDelegates.Count != 1) + { + Program.WriteError($"Expected to find 1 potential string delegate method; found {potentialStringDelegates.Count}. Candidates: {string.Join("\r\n", potentialStringDelegates.Select(x => x.FullName))}"); + } + + token = potentialStringDelegates[0].MetadataToken.ToString(); + } + + var process = Process.Start(de4dotLocation, + $"--un-name \"!^<>[a-z0-9]$&!^<>[a-z0-9]__.*$&![A-Z][A-Z]\\$<>.*$&^[a-zA-Z_<{{$][a-zA-Z_0-9<>{{}}$.`-]*$\" \"{assemblyPath}\" --strtyp delegate --strtok \"{token}\""); + + process.WaitForExit(); + + + // Fixes "ResolutionScope is null" by rewriting the assembly + var cleanedDllPath = Path.Combine(Path.GetDirectoryName(assemblyPath), Path.GetFileNameWithoutExtension(assemblyPath) + "-cleaned.dll"); + + var resolver = new DefaultAssemblyResolver(); + resolver.AddSearchDirectory(args.ManagedPath); + + using (var memoryStream = new MemoryStream(File.ReadAllBytes(cleanedDllPath))) + using (var assemblyDefinition = AssemblyDefinition.ReadAssembly(memoryStream, new ReaderParameters() + { + AssemblyResolver = resolver + })) + { + assemblyDefinition.Write(cleanedDllPath); + } + } + } +} diff --git a/SPT-AssemblyTool/NArgs.cs b/SPT-AssemblyTool/NArgs.cs new file mode 100644 index 0000000..ce4bd44 --- /dev/null +++ b/SPT-AssemblyTool/NArgs.cs @@ -0,0 +1,405 @@ +/* + NArgs + The MIT License (MIT) + + Copyright(c) 2021 Bepis + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace NArgs +{ + /// + /// Command-line argument parser. + /// + public static class Arguments + { + /// + /// Parses arguments and constructs an object. + /// + /// The type of the object to construct. Must inherit from + /// The command-line arguments to parse. + /// + public static T Parse(string[] args) where T : IArgumentCollection, new() + { + Dictionary> valueSwitches = new Dictionary>(); + Dictionary> boolSwitches = new Dictionary>(); + + var config = new T { Values = new List() }; + + var commandProps = GetCommandProperties(); + + foreach (var kv in commandProps) + { + if (kv.Value.PropertyType == typeof(bool)) + { + boolSwitches.Add(kv.Key, x => kv.Value.SetValue(config, x)); + } + else if (kv.Value.PropertyType == typeof(string)) + { + valueSwitches.Add(kv.Key, x => kv.Value.SetValue(config, x)); + } + else if (typeof(IList).IsAssignableFrom(kv.Value.PropertyType)) + { + if (kv.Value.GetValue(config) == null) + { + kv.Value.SetValue(config, new List()); + } + + valueSwitches.Add(kv.Key, x => + { + var list = (IList)kv.Value.GetValue(config); + list.Add(x); + }); + } + else if (typeof(Enum).IsAssignableFrom(kv.Value.PropertyType)) + { + valueSwitches.Add(kv.Key, x => + { + if (!TryParseEnum(kv.Value.PropertyType, x, true, out var value)) + throw new ArgumentException("Invalid value for argument: " + x); + + kv.Value.SetValue(config, value); + }); + } + } + + CommandDefinitionAttribute previousSwitchDefinition = null; + bool valuesOnly = false; + + foreach (string arg in args) + { + if (arg == "--") + { + // no more switches, only values + valuesOnly = true; + + continue; + } + + if (valuesOnly) + { + config.Values.Add(arg); + continue; + } + + if (arg.StartsWith("-") + || arg.StartsWith("--")) + { + string previousSwitch; + + if (arg.StartsWith("--")) + previousSwitch = arg.Substring(2); + else + previousSwitch = arg.Substring(1); + + if (boolSwitches.Keys.TryFirst(x + => x.LongArg.Equals(previousSwitch, StringComparison.InvariantCultureIgnoreCase) + || x.ShortArg?.Equals(previousSwitch, StringComparison.InvariantCultureIgnoreCase) == true, + out var definition)) + { + boolSwitches[definition](true); + previousSwitch = null; + + continue; + } + + if (valueSwitches.Keys.TryFirst(x + => x.LongArg.Equals(previousSwitch, StringComparison.InvariantCultureIgnoreCase) + || x.ShortArg?.Equals(previousSwitch, StringComparison.InvariantCultureIgnoreCase) == true, + out definition)) + { + previousSwitchDefinition = definition; + + continue; + } + + Console.WriteLine("Unrecognized command line option: " + arg); + throw new Exception(); + } + + if (previousSwitchDefinition != null) + { + valueSwitches[previousSwitchDefinition](arg); + previousSwitchDefinition = null; + } + else + { + config.Values.Add(arg); + } + } + + foreach (var kv in commandProps) + { + if (!kv.Key.Required) + continue; + + if (kv.Value.PropertyType == typeof(string)) + if (kv.Value.GetValue(config) == null) + throw new ArgumentException($"Required argument not provided: {kv.Key.LongArg}"); + + if (kv.Value.PropertyType == typeof(IList)) + if (((IList)kv.Value.GetValue(config)).Count == 0) + throw new ArgumentException($"Required argument not provided: {kv.Key.LongArg}"); + } + + return config; + } + + /// + /// Generates a string to be printed as console help text. + /// + /// The type of the arguments object to create help instructions for. Must inherit from + /// The copyright text to add at the top, if any. + /// The usage text to add at the top, if any. + public static string PrintLongHelp(string copyrightText = null, string usageText = null) where T : IArgumentCollection + { + var commands = GetCommandProperties(); + + var builder = new StringBuilder(); + + if (copyrightText != null) + builder.AppendLine(copyrightText); + + if (usageText != null) + builder.AppendLine(usageText); + + builder.AppendLine(); + builder.AppendLine(); + + var orderedCommands = commands + .OrderByDescending(x => x.Key.Order) + .ThenBy(x => x.Key.ShortArg ?? "zzzz") + .ThenBy(x => x.Key.LongArg); + + foreach (var command in orderedCommands) + { + var valueString = string.Empty; + + if (command.Value.PropertyType == typeof(IList) + || command.Value.PropertyType == typeof(string)) + { + valueString = " "; + } + else if (typeof(Enum).IsAssignableFrom(command.Value.PropertyType)) + { + valueString = $" ({string.Join(" | ", Enum.GetNames(command.Value.PropertyType))})"; + } + + string listing = command.Key.ShortArg != null + ? $" -{command.Key.ShortArg}, --{command.Key.LongArg}{valueString}" + : $" --{command.Key.LongArg}{valueString}"; + + const int listingWidth = 45; + const int descriptionWidth = 65; + + string listingWidthString = "".PadLeft(listingWidth); + + builder.Append(listing.PadRight(listingWidth)); + + if (listing.Length > listingWidth - 3) + { + builder.AppendLine(); + builder.Append(listingWidthString); + } + + if (!string.IsNullOrEmpty(command.Key.Description)) + { + BuildArgumentDescription(builder, command.Key.Description, listingWidth, descriptionWidth); + } + + builder.AppendLine(); + } + + builder.AppendLine(); + + return builder.ToString(); + } + + private static void BuildArgumentDescription(StringBuilder builder, string description, int listingWidth, int descriptionWidth) + { + int lineLength = 0; + int lineStartIndex = 0; + int lastValidLength = 0; + + for (var index = 0; index < description.Length; index++) + { + char c = description[index]; + + void PrintLine() + { + var descriptionSubstring = description.Substring(lineStartIndex, lastValidLength); + builder.AppendLine(descriptionSubstring); + builder.Append(' ', listingWidth); + + lineStartIndex = 1 + index - (lineLength - lastValidLength); + lineLength = 1 + index - lineStartIndex; + lastValidLength = lineLength; + } + + if ((c == ' ' && lineLength >= descriptionWidth) | c == '\n') + { + bool printAgain = false; + + if (c == '\n' && lineLength < descriptionWidth) + lastValidLength = lineLength; + else if (c == '\n') + printAgain = true; + + PrintLine(); + + if (printAgain) + { + // This works and I'm not sure how. + + lastValidLength--; + lineLength--; + PrintLine(); + lastValidLength++; + lineLength++; + } + + continue; + } + + if (c == ' ') + lastValidLength = lineLength; + + lineLength++; + } + + if (lineLength > 0) + { + var remainingSubstring = description.Substring(lineStartIndex); + builder.AppendLine(remainingSubstring); + } + } + + private static Dictionary GetCommandProperties() + { + var commands = new Dictionary(); + + foreach (var prop in typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + var commandDef = prop.GetCustomAttribute(); + + if (commandDef == null) + continue; + + commands.Add(commandDef, prop); + } + + return commands; + } + + private static bool TryFirst(this IEnumerable enumerable, Func predicate, out T value) + { + foreach (var item in enumerable) + { + if (predicate(item)) + { + value = item; + return true; + } + } + + value = default; + return false; + } + + private static MethodInfo GenericTryParseMethodInfo = null; + private static bool TryParseEnum(Type enumType, string value, bool caseSensitive, out object val) + { + // Workaround for non-generic Enum.TryParse not being present below .NET 5 + + if (GenericTryParseMethodInfo == null) + { + GenericTryParseMethodInfo = typeof(Enum).GetMethods(BindingFlags.Public | BindingFlags.Static) + .First(x => x.Name == "TryParse" && x.GetGenericArguments().Length == 1 && + x.GetParameters().Length == 3); + } + + var objectArray = new object[] { value, caseSensitive, null }; + + var result = GenericTryParseMethodInfo.MakeGenericMethod(enumType) + .Invoke(null, objectArray); + + val = objectArray[2]; + return (bool)result; + } + } + + /// + /// Specifies an object is an argument collection. + /// + public interface IArgumentCollection + { + /// + /// A list of values that were passed in as arguments, but not associated with an option. + /// + IList Values { get; set; } + } + + public class CommandDefinitionAttribute : Attribute + { + /// + /// The short version of an option, e.g. "-a". Optional. + /// + public string ShortArg { get; set; } + + /// + /// The long version of an option, e.g. "--append". Required. + /// + public string LongArg { get; set; } + + /// + /// Whether or not to fail parsing if this argument has not been provided. + /// + public bool Required { get; set; } = false; + + /// + /// The description of the option, to be used in the help text. + /// + public string Description { get; set; } = null; + + /// + /// Used in ordering this command in the help list. + /// + public int Order { get; set; } = 0; + + /// The long version of an option, e.g. "--append". + public CommandDefinitionAttribute(string longArg) + { + LongArg = longArg; + } + + /// The short version of an option, e.g. "-a". + /// The long version of an option, e.g. "--append". + public CommandDefinitionAttribute(string shortArg, string longArg) + { + ShortArg = shortArg; + LongArg = longArg; + } + } +} \ No newline at end of file diff --git a/SPT-AssemblyTool/Program.cs b/SPT-AssemblyTool/Program.cs new file mode 100644 index 0000000..4afd7c6 --- /dev/null +++ b/SPT-AssemblyTool/Program.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.IO; +using NArgs; + +namespace SPT_AssemblyTool +{ + class Program + { + internal static void WriteError(string error) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(error); + Console.ResetColor(); + + Environment.Exit(1); + } + + static void Main(string[] args) + { + Arguments arguments = NArgs.Arguments.Parse(args); + + if (arguments.Values.Count == 0 || arguments.Help) + { + Console.WriteLine(NArgs.Arguments.PrintLongHelp( + usageText: "Usage: SPT-AssemblyTool [options] ")); + return; + } + + var assemblyPath = arguments.Values[0]; + + if (!File.Exists(assemblyPath)) + WriteError("Target assembly file does not exist"); + + if (!Directory.Exists(arguments.ManagedPath)) + WriteError("--managed-path option is not correct"); + + if (arguments.DeobfuscateMode) + { + Deobfuscator.Deobfuscate(arguments); + } + + if (arguments.RemapMode) + { + if (!File.Exists(arguments.OldAssemblyPath)) + WriteError("Old assembly path option is not correct"); + + if (!File.Exists(arguments.MappingFilePath)) + WriteError("Mapping file path option is not correct"); + + Remapper.Remap(arguments); + } + } + + internal class Arguments : IArgumentCollection + { + public IList Values { get; set; } + + [CommandDefinition("d", "deobfuscate", Description = "Deobfuscation mode", Order = 1)] + public bool DeobfuscateMode { get; set; } + + [CommandDefinition("m", "managed-path", Description = "Path to EFT managed folder. Required", Required = true)] + public string ManagedPath { get; set; } + + [CommandDefinition("r", "remap", Description = "Remapping mode", Order = 1)] + public bool RemapMode { get; set; } + + [CommandDefinition("o", "old-assembly", Description = "Path to previously decompiled Assembly-CSharp.dll file. Only used in remapping mode, required")] + public string OldAssemblyPath { get; set; } + + [CommandDefinition("mapping-file", Description = "Path to mapping path .json file. Only used in remapping mode, required")] + public string MappingFilePath { get; set; } + + [CommandDefinition("h", "help", Description = "Prints help text", Order = -1)] + public bool Help { get; set; } + } + } +} diff --git a/SPT-AssemblyTool/Properties/launchSettings.json b/SPT-AssemblyTool/Properties/launchSettings.json new file mode 100644 index 0000000..2cadeb9 --- /dev/null +++ b/SPT-AssemblyTool/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "SPT-AssemblyTool": { + "commandName": "Project", + "commandLineArgs": "-r -o \"B:\\Assembly-CSharp-old.dll\" -m \"N:\\EFT\\EscapeFromTarkov_Data\\Managed\" -mapping-file \"D:\\Sourcecode\\GitGud-Bepsi\\SPT-AssemblyTool\\mappings.json\" \"B:\\Assembly-CSharp-mid.dll\"" + } + } +} \ No newline at end of file diff --git a/SPT-AssemblyTool/Remapper.cs b/SPT-AssemblyTool/Remapper.cs new file mode 100644 index 0000000..4270d88 --- /dev/null +++ b/SPT-AssemblyTool/Remapper.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Mono.Cecil; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace SPT_AssemblyTool +{ + internal static class Remapper + { + public static void Remap(Program.Arguments args) + { + var mappingDictionary = JsonConvert.DeserializeObject>(File.ReadAllText(args.MappingFilePath)); + + var newMappingDictionary = new Dictionary(); + var reportDictionary = new Dictionary(); + + var resolver = new DefaultAssemblyResolver(); + resolver.AddSearchDirectory(args.ManagedPath); + + var readerParameters = new ReaderParameters { AssemblyResolver = resolver }; + var assemblyPath = args.Values[0]; + + using var oldAssembly = AssemblyDefinition.ReadAssembly(args.OldAssemblyPath, readerParameters); + using var newAssembly = AssemblyDefinition.ReadAssembly(assemblyPath, readerParameters); + + foreach (var (obfuscatedName, deobfuscatedName) in mappingDictionary) + { + TypeDefinition oldTypeDefinition = oldAssembly.MainModule.GetType(deobfuscatedName) ?? oldAssembly.MainModule.GetType(obfuscatedName); + + if (oldTypeDefinition == null) + Program.WriteError($"Could not find mapping definition in old assembly: {obfuscatedName}={deobfuscatedName}"); + + var result = FindBestMatchType(oldTypeDefinition, newAssembly, null); + + string @namespace; + string type; + + if (!deobfuscatedName.Contains(".")) + { + @namespace = ""; + type = deobfuscatedName; + } + else + { + @namespace = deobfuscatedName.Substring(0, deobfuscatedName.LastIndexOf(".")); + type = deobfuscatedName.Remove(0, deobfuscatedName.LastIndexOf(".")); + } + + var newType = result[0].Item2; + + newMappingDictionary[newType.FullName] = deobfuscatedName; + reportDictionary[deobfuscatedName] = result.Export(); + + newType.Namespace = @namespace; + newType.Name = type; + } + + var assemblyDirectory = Path.GetDirectoryName(assemblyPath); + + var newAssemblyPath = Path.Combine(assemblyDirectory, Path.GetFileNameWithoutExtension(assemblyPath) + "-remapped.dll"); + var newMappingPath = Path.Combine(assemblyDirectory, "new-mapping.json"); + var newMappingReportPath = Path.Combine(assemblyDirectory, "new-mapping-report.json"); + + newAssembly.Write(newAssemblyPath); + + var serializer = new JsonSerializer + { + Formatting = Formatting.Indented + }; + + void writeToJsonFile(string filename, object data) + { + using var textWriter = new StreamWriter(filename); + serializer.Serialize(textWriter, data); + } + + writeToJsonFile(newMappingPath, newMappingDictionary); + writeToJsonFile(newMappingReportPath, reportDictionary); + } + + private static WeightDictionary FindBestMatchType(TypeDefinition obfType, AssemblyDefinition newAssembly, TypeDefinition enclosingCleanType) + { + // This method uses code from https://github.com/knah/Il2CppAssemblyUnhollower/blob/master/AssemblyUnhollower/DeobfuscationMapGenerator.cs + // licensed under LGPL + + var inheritanceDepthOfOriginal = 0; + var currentBase = obfType.BaseType; + while (true) + { + var newBase = currentBase?.Resolve().BaseType; + if (newBase == null) break; + + inheritanceDepthOfOriginal++; + currentBase = newBase; + } + + + var bestPenalty = int.MinValue; + var weightDictionary = new WeightDictionary(5); + + var source = enclosingCleanType?.NestedTypes ?? newAssembly.MainModule.Types; + + foreach (var candidateCleanType in source) + { + if (obfType.HasMethods != candidateCleanType.HasMethods) + continue; + + if (obfType.HasFields != candidateCleanType.HasFields) + continue; + + if (obfType.IsEnum) + if (obfType.Fields.Count != candidateCleanType.Fields.Count) + continue; + + int currentPenalty = 0; + + var tryBase = candidateCleanType.BaseType; + var actualBaseDepth = 0; + while (tryBase != null) + { + if (tryBase?.Name == currentBase?.Name && tryBase?.Namespace == currentBase?.Namespace) + break; + + tryBase = tryBase?.Resolve().BaseType; + actualBaseDepth++; + } + + if (tryBase == null && currentBase != null) + continue; + + var baseDepthDifference = Math.Abs(actualBaseDepth - inheritanceDepthOfOriginal); + + if (baseDepthDifference > 1) + continue; // heuristic optimization + + currentPenalty -= baseDepthDifference * 50; + + currentPenalty -= Math.Abs(candidateCleanType.Fields.Count - obfType.Fields.Count) * 5; + currentPenalty -= Math.Abs(obfType.NestedTypes.Count - candidateCleanType.NestedTypes.Count) * 10; + currentPenalty -= Math.Abs(obfType.Properties.Count - candidateCleanType.Properties.Count) * 5; + currentPenalty -= Math.Abs(obfType.Interfaces.Count - candidateCleanType.Interfaces.Count) * 35; + + foreach (var obfuscatedField in obfType.Fields) + { + if (candidateCleanType.Fields.Any(it => it.Name == obfuscatedField.Name)) + currentPenalty += 10; + } + + foreach (var obfuscatedMethod in obfType.Methods) + { + if (obfuscatedMethod.Name.Contains(".ctor")) + continue; + + if (candidateCleanType.Methods.Any(it => it.Name == obfuscatedMethod.Name)) + currentPenalty += obfuscatedMethod.Name.Length / 10 * 5 + 1; + } + + if (currentPenalty == bestPenalty) + { + weightDictionary.Add(currentPenalty, candidateCleanType); + } + else if (currentPenalty > bestPenalty) + { + bestPenalty = currentPenalty; + weightDictionary.Add(currentPenalty, candidateCleanType); + } + } + + // if (bestPenalty < -100) + // bestMatch = null; + + return weightDictionary; + } + + public class WeightDictionary : List<(int, TypeDefinition)> + { + private int maxValues { get; } + + public WeightDictionary(int maxValues) + { + this.maxValues = maxValues; + } + + public void Add(int key, TypeDefinition value) + { + if (Count < maxValues) + Add((key, value)); + + int lowestWeight = this.Min(x => x.Item1); + + if (key <= lowestWeight) + return; + + Remove(this.Last(x => x.Item1 == lowestWeight)); + + Add((key, value)); + + Sort(new Comparer()); + } + + public JObject Export() + { + var jobj = new JObject(); + + foreach (var (weight, type) in this) + { + jobj[type.FullName] = weight; + } + + return jobj; + } + + private class Comparer : IComparer<(int, TypeDefinition)> + { + public int Compare((int, TypeDefinition) x, (int, TypeDefinition) y) + { + // invert the direction, so the biggest is at the top + return -x.Item1.CompareTo(y.Item1); + } + } + } + } +} \ No newline at end of file diff --git a/SPT-AssemblyTool/SPT-AssemblyTool.csproj b/SPT-AssemblyTool/SPT-AssemblyTool.csproj new file mode 100644 index 0000000..1a9536d --- /dev/null +++ b/SPT-AssemblyTool/SPT-AssemblyTool.csproj @@ -0,0 +1,183 @@ + + + + Exe + net5.0 + SPT_AssemblyTool + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/example-mappings.json b/example-mappings.json new file mode 100644 index 0000000..6398171 --- /dev/null +++ b/example-mappings.json @@ -0,0 +1,4 @@ +{ + "GClass2206`1": "Remapped.BindableState", + "GInterface254": "Remapped.IBundleLock" +} \ No newline at end of file