0
0
mirror of https://github.com/sp-tarkov/assembly-tool.git synced 2025-02-12 16:50:44 -05:00

Initial work and basic functionality

This commit is contained in:
Cj 2025-01-01 19:17:56 -05:00
parent 664ff994cf
commit 02a6c417ab
6 changed files with 389 additions and 40 deletions

View File

@ -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<RemapModel>();
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;
}
}

View File

@ -6,6 +6,7 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RunPostBuildEvent>Always</RunPostBuildEvent>
</PropertyGroup>
<ItemGroup>

View File

@ -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;
}
}

View File

@ -0,0 +1,271 @@
using dnlib.DotNet;
using ReCodeItLib.Models;
using ReCodeItLib.Utils;
namespace ReCodeItLib.ReMapper;
public class AutoMatcher(List<RemapModel> mappings)
{
private ModuleDefMD? Module { get; set; }
private List<TypeDef>? CandidateTypes { get; set; }
private static List<string> _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>()
{
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();
}
}

View File

@ -34,14 +34,19 @@ public class ReMapper
public void InitializeRemap(
List<RemapModel> 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<TypeDef> 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<TypeDef> types)
private void FindBestMatches(IEnumerable<TypeDef> 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());

View File

@ -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++;
}