diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cfae9ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ +work +.idea \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7a52b92 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "Reactor.Greenhouse/Mappings"] + path = Reactor.Greenhouse/Mappings + url = https://github.com/NuclearPowered/Mappings diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0a04128 --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3a90407 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +## Reactor.Greenhouse + +Standalone program which generates mappings for OxygenFilter + +## Reactor.OxygenFilter + +Library for mapping stuff + +## Reactor.OxygenFilter.MSBuild + +Library for using mappings in a project \ No newline at end of file diff --git a/Reactor.Greenhouse/Compiler.cs b/Reactor.Greenhouse/Compiler.cs new file mode 100644 index 0000000..9d31da4 --- /dev/null +++ b/Reactor.Greenhouse/Compiler.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Mono.Cecil; +using Reactor.OxygenFilter; + +namespace Reactor.Greenhouse +{ + public static class Compiler + { + public static void Apply(this Mappings current, Mappings mappings) + { + static void ApplyType(List types, MappedType newType) + { + var type = types.SingleOrDefault(x => newType.Equals(x)); + if (type == null) + { + types.Add(newType); + } + else + { + type.Mapped = newType.Mapped; + + void ApplyMembers(List members, List newMembers) + { + foreach (var newMember in newMembers) + { + var member = members.SingleOrDefault(x => newMember.Equals(x, false)); + + if (member == null) + { + members.Add(newMember); + } + else + { + member.Mapped = newMember.Mapped; + member.Original = newMember.Original; + } + } + } + + ApplyMembers(type.Fields, newType.Fields); + ApplyMembers(type.Properties, newType.Properties); + + foreach (var newMember in newType.Methods) + { + var member = type.Methods.SingleOrDefault(x => newMember.Equals(x, false)); + + if (member == null) + { + type.Methods.Add(newMember); + } + else + { + member.Mapped = newMember.Mapped; + member.Original = newMember.Original; + member.Parameters = newMember.Parameters; + } + } + + foreach (var nestedType in newType.Nested) + { + ApplyType(type.Nested, nestedType); + } + } + } + + foreach (var newType in mappings.Types) + { + ApplyType(current.Types, newType); + } + } + + private static Regex Regex { get; } = new Regex(@"{{(?[\w\.]+)}}", RegexOptions.Compiled); + + public static string MapSignature(this OriginalDescriptor original, Mappings mappings) + { + var signature = original.Signature; + + var matches = Regex.Matches(signature); + foreach (Match match in matches) + { + var group = match.Groups["expression"]; + if (group.Success) + { + signature = signature.Replace(match.Value, mappings.FindByMapped(group.Value).Original.Name); + } + } + + return signature; + } + + private static bool TestField(MappedMember field, TypeDefinition typeDef, FieldDefinition fieldDef, Mappings mappings) + { + return (field.Original.Name == null || field.Original.Name == fieldDef.Name) && + (field.Original.Index == null || field.Original.Index == typeDef.Fields.IndexOf(fieldDef)) && + (field.Original.Signature == null || field.Original.MapSignature(mappings) == fieldDef.GetSignature()) && + (field.Original.Const == null || !typeDef.IsEnum && field.Original.Const.Value.Equals(fieldDef.Constant)); + } + + private static bool TestMethod(MappedMember method, TypeDefinition typeDef, MethodDefinition methodDef, Mappings mappings) + { + return (method.Original.Index == null || method.Original.Index == typeDef.Methods.IndexOf(methodDef)) && + (method.Original.Signature == null || method.Original.MapSignature(mappings) == methodDef.GetSignature()) && + (method.Original.Name == null || method.Original.Name == methodDef.Name); + } + + private static bool TestProperty(MappedMember property, TypeDefinition typeDef, PropertyDefinition propertyDef, Mappings mappings) + { + return (property.Original.Name == null || property.Original.Name == propertyDef.Name) && + (property.Original.Index == null || property.Original.Index == typeDef.Properties.IndexOf(propertyDef)) && + (property.Original.Signature == null || property.Original.MapSignature(mappings) == propertyDef.GetSignature()); + } + + private static bool TestType(MappedType type, TypeDefinition typeDef, Mappings mappings) + { + return (type.Original.Name == null || type.Original.Name == typeDef.Name) && + type.Methods.All(method => typeDef.Methods.Any(m => TestMethod(method, typeDef, m, mappings))) && + type.Fields.All(field => typeDef.Fields.Any(f => TestField(field, typeDef, f, mappings))) && + type.Properties.All(property => typeDef.Properties.Any(p => TestProperty(property, typeDef, p, mappings))); + } + + private static void Compile(this MappedType type, TypeDefinition typeDef, Mappings mappings) + { + type.Original = new OriginalDescriptor { Name = typeDef.Name }; + + foreach (var nested in type.Nested) + { + var nestedDef = typeDef.NestedTypes.Single(t => + TestType(type, t, mappings) && + nested.Original.Index == null || typeDef.NestedTypes.IndexOf(t) == nested.Original.Index + ); + + nested.Compile(nestedDef, mappings); + } + + foreach (var property in type.Properties) + { + try + { + var propertyDef = typeDef.Properties.Single(p => TestProperty(property, typeDef, p, mappings)); + + property.Original = new OriginalDescriptor { Name = propertyDef.Name }; + } + catch (Exception e) + { + throw new Exception($"Compilation of {property} failed", e); + } + } + + foreach (var field in type.Fields) + { + try + { + var fieldDef = typeDef.Fields.Single(f => TestField(field, typeDef, f, mappings)); + + field.Original = new OriginalDescriptor { Name = fieldDef.Name }; + } + catch (Exception e) + { + throw new Exception($"Compilation of {field} failed", e); + } + } + + foreach (var method in type.Methods) + { + try + { + var methodDef = typeDef.Methods.Single(m => TestMethod(method, typeDef, m, mappings)); + + method.Original = new OriginalDescriptor { Name = methodDef.Name, Signature = methodDef.GetSignature() }; + } + catch (Exception e) + { + throw new Exception($"Compilation of {method} failed", e); + } + } + } + + public static void Compile(this Mappings mappings, ModuleDefinition moduleDef) + { + foreach (var type in mappings.Types) + { + try + { + var typeDef = moduleDef.Types.Single(t => TestType(type, t, mappings)); + + type.Compile(typeDef, mappings); + } + catch (Exception e) + { + throw new Exception($"Compilation of {type} failed", e); + } + } + } + } +} diff --git a/Reactor.Greenhouse/Extensions.cs b/Reactor.Greenhouse/Extensions.cs new file mode 100644 index 0000000..0b75195 --- /dev/null +++ b/Reactor.Greenhouse/Extensions.cs @@ -0,0 +1,28 @@ +using System.Linq; +using Mono.Cecil; + +namespace Reactor.Greenhouse +{ + public static class Extensions + { + public static CustomAttribute GetCustomAttribute(this ICustomAttributeProvider cap, string attribute) + { + if (!cap.HasCustomAttributes) + { + return null; + } + + return cap.CustomAttributes.FirstOrDefault(attrib => attrib.AttributeType.FullName == attribute); + } + + public static uint? GetOffset(this MethodDefinition methodDef) + { + var attribute = methodDef.GetCustomAttribute("Il2CppDummyDll.AddressAttribute"); + if (attribute == null) + return null; + + var offset = attribute.Fields.Single(x => x.Name == "Offset"); + return new System.ComponentModel.UInt32Converter().ConvertFrom(offset) as uint?; + } + } +} diff --git a/Reactor.Greenhouse/Generator.cs b/Reactor.Greenhouse/Generator.cs new file mode 100644 index 0000000..e3ffde1 --- /dev/null +++ b/Reactor.Greenhouse/Generator.cs @@ -0,0 +1,138 @@ +using System.Linq; +using Mono.Cecil; +using Reactor.OxygenFilter; + +namespace Reactor.Greenhouse +{ + public static class Generator + { + public static Mappings Generate(ModuleDefinition old, ModuleDefinition latest) + { + var result = new Mappings(); + + foreach (var oldType in old.Types) + { + if (oldType.Name.StartsWith("<") || oldType.Namespace.StartsWith("GoogleMobileAds")) + continue; + + void AddType(TypeDefinition type) + { + var mapped = new MappedType(new OriginalDescriptor { Name = type.Name }, oldType.Name); + + var i = 0; + foreach (var field in type.Fields) + { + var j = 0; + var i1 = i; + var oldFields = oldType.Fields.Where(x => j++ == i1 && x.GetSignature().ToString() == field.GetSignature().ToString()).ToArray(); + if (oldFields.Length == 1) + { + var oldField = oldFields.First(); + if (oldField.Name == field.Name || !field.Name.IsObfuscated()) + continue; + + mapped.Fields.Add(new MappedMember(new OriginalDescriptor { Index = i1 }, oldField.Name)); + } + + i++; + } + + foreach (var method in type.Methods) + { + if (!method.HasParameters || method.IsSetter || method.IsGetter) + { + continue; + } + + var oldMethods = oldType.Methods.Where(x => x.Name == method.Name && x.Parameters.Count == method.Parameters.Count).ToArray(); + if (oldMethods.Length != 1) + { + continue; + } + + var oldMethod = oldMethods.Single(); + + var oldParameters = oldMethod.Parameters.Select(x => x.Name).ToList(); + + if (method.Parameters.Select(x => x.Name).SequenceEqual(oldParameters)) + { + continue; + } + + mapped.Methods.Add(new MappedMethod(new OriginalDescriptor { Name = method.Name, Signature = method.GetSignature() }, null) + { + Parameters = oldParameters + }); + } + + if (type.Name == oldType.Name || (!type.Name.IsObfuscated() && !mapped.Fields.Any() && !mapped.Methods.Any())) + { + return; + } + + result.Types.Add(mapped); + } + + var exact = latest.Types.FirstOrDefault(x => x.FullName == oldType.FullName); + if (exact != null) + { + AddType(exact); + continue; + } + + if (oldType.IsEnum) + { + var first = oldType.Fields.Select(x => x.Name).ToArray(); + var type = latest.Types.SingleOrDefault(x => x.IsEnum && x.Fields.Select(f => f.Name).SequenceEqual(first)); + if (type != null) + { + AddType(type); + } + + continue; + } + + static bool Test(TypeReference typeReference) + { + return typeReference.IsGenericParameter || typeReference.Namespace != string.Empty || !typeReference.Name.IsObfuscated(); + } + + var methods = oldType.Methods.Where(x => Test(x.ReturnType) && x.Parameters.All(p => Test(p.ParameterType))).Select(x => x.GetSignature()).ToArray(); + var fields = oldType.Fields.Where(x => Test(x.FieldType) && (!x.FieldType.HasGenericParameters || x.FieldType.GenericParameters.All(Test))).Select(x => x.GetSignature()).ToArray(); + var properties = oldType.Properties.Select(x => x.Name).ToArray(); + + var types = latest.Types + .Where(t => t.Attributes == oldType.Attributes) + .ToArray(); + + TypeDefinition winner = null; + var winnerPoints = -1; + + foreach (var t in types) + { + var points = 0; + points += t.Properties.Count(p => properties.Contains((p.GetMethod?.Name ?? p.SetMethod.Name).Substring(4))); + points += fields.Count(s => t.Fields.Any(f => f.GetSignature().ToString() == s.ToString())); + points += methods.Count(s => t.Methods.Any(m => m.GetSignature().ToString() == s.ToString())); + + if (points > winnerPoints) + { + winnerPoints = points; + winner = t; + } + else if (points == winnerPoints) + { + winner = null; + } + } + + if (winner != null && winnerPoints > 0) + { + AddType(winner); + } + } + + return result; + } + } +} diff --git a/Reactor.Greenhouse/Mappings b/Reactor.Greenhouse/Mappings new file mode 160000 index 0000000..216cbf4 --- /dev/null +++ b/Reactor.Greenhouse/Mappings @@ -0,0 +1 @@ +Subproject commit 216cbf4c946b5ab10ef12b9cca39af72d2394496 diff --git a/Reactor.Greenhouse/Program.cs b/Reactor.Greenhouse/Program.cs new file mode 100644 index 0000000..744d047 --- /dev/null +++ b/Reactor.Greenhouse/Program.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Mono.Cecil; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using Reactor.Greenhouse.Setup; +using Reactor.OxygenFilter; + +namespace Reactor.Greenhouse +{ + internal static class Program + { + private static Task Main(string[] args) + { + var rootCommand = new RootCommand + { + new Option("steam"), + new Option("itch"), + }; + + rootCommand.Handler = CommandHandler.Create(GenerateAsync); + + return rootCommand.InvokeAsync(args); + } + + public static async Task GenerateAsync(bool steam, bool itch) + { + var gameManager = new GameManager(); + await gameManager.SetupAsync(steam, itch); + + JsonConvert.DefaultSettings = () => new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + DefaultValueHandling = DefaultValueHandling.Ignore, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + ContractResolver = ShouldSerializeContractResolver.Instance, + }; + + Console.WriteLine($"Generating mappings from {gameManager.PreObfuscation.Name} ({gameManager.PreObfuscation.Version})"); + using var old = ModuleDefinition.ReadModule(File.OpenRead(gameManager.PreObfuscation.Dll)); + + if (steam) + { + await GenerateAsync(gameManager.Steam, old); + } + + if (itch) + { + await GenerateAsync(gameManager.Itch, old); + } + } + + private static async Task GenerateAsync(Game game, ModuleDefinition old) + { + Console.WriteLine($"Compiling mappings for {game.Name} ({game.Version})"); + + using var moduleDef = ModuleDefinition.ReadModule(File.OpenRead(game.Dll)); + var version = game.Version; + var postfix = game.Postfix; + + var generated = Generator.Generate(old, moduleDef); + + await File.WriteAllTextAsync(Path.Combine("work", version + postfix + ".generated.json"), JsonConvert.SerializeObject(generated, Formatting.Indented)); + + Apply(generated, Path.Combine("Mappings", "universal.json")); + Apply(generated, Path.Combine("Mappings", version + postfix + ".json")); + + generated.Compile(moduleDef); + + Directory.CreateDirectory(Path.Combine("Mappings", "bin")); + await File.WriteAllTextAsync(Path.Combine("Mappings", "bin", game.Name.ToLower() + ".json"), JsonConvert.SerializeObject(generated)); + } + + private static void Apply(Mappings generated, string file) + { + if (File.Exists(file)) + { + var mappings = JsonConvert.DeserializeObject(File.ReadAllText(file)); + generated.Apply(mappings); + } + } + + public class ShouldSerializeContractResolver : CamelCasePropertyNamesContractResolver + { + public static ShouldSerializeContractResolver Instance { get; } = new ShouldSerializeContractResolver(); + + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + var property = base.CreateProperty(member, memberSerialization); + + if (property.PropertyType != null && property.PropertyType != typeof(string)) + { + if (property.PropertyType.GetInterface(nameof(IEnumerable)) != null) + { + property.ShouldSerialize = instance => (instance?.GetType().GetProperty(property.UnderlyingName!)!.GetValue(instance) as IEnumerable)?.Count() > 0; + } + } + + return property; + } + } + } +} diff --git a/Reactor.Greenhouse/Reactor.Greenhouse.csproj b/Reactor.Greenhouse/Reactor.Greenhouse.csproj new file mode 100644 index 0000000..ebd873f --- /dev/null +++ b/Reactor.Greenhouse/Reactor.Greenhouse.csproj @@ -0,0 +1,18 @@ + + + Exe + net5.0 + latest + true + + + + + + + + + + + + diff --git a/Reactor.Greenhouse/Setup/Game.cs b/Reactor.Greenhouse/Setup/Game.cs new file mode 100644 index 0000000..6ce2ef4 --- /dev/null +++ b/Reactor.Greenhouse/Setup/Game.cs @@ -0,0 +1,88 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Il2CppDumper; +using Reactor.Greenhouse.Setup.Provider; +using IOPath = System.IO.Path; + +namespace Reactor.Greenhouse.Setup +{ + public class Game + { + public BaseProvider Provider { get; } + public string Name { get; } + public char Postfix { get; } + public string Path { get; } + public string Dll { get; } + public string Version { get; private set; } + + public Game(BaseProvider provider, string name, string path) + { + Provider = provider; + provider.Game = this; + Name = name; + Postfix = char.ToLowerInvariant(name[0]); + Path = path; + Dll = IOPath.Combine(Path, "DummyDll", "Assembly-CSharp.dll"); + } + + public async Task DownloadAsync() + { + Provider.Setup(); + await Provider.DownloadAsync(); + UpdateVersion(); + } + + public void UpdateVersion() + { + if (Version != null) + return; + + Version = GameVersionParser.Parse(System.IO.Path.Combine(Path, "Among Us_Data", "globalgamemanagers")); + } + + public void Dump() + { + Console.WriteLine($"Dumping {Name}"); + + var hash = ComputeHash(IOPath.Combine(Path, "GameAssembly.dll")); + var hashFile = IOPath.Combine(Path, "Assembly-CSharp.dll.md5"); + + if (File.Exists(hashFile) && File.ReadAllText(hashFile) == hash) + { + return; + } + + if (!Il2CppDumper.Il2CppDumper.PerformDump( + IOPath.Combine(Path, "GameAssembly.dll"), + IOPath.Combine(Path, "Among Us_Data", "il2cpp_data", "Metadata", "global-metadata.dat"), + Path, + new Config + { + RequireAnyKey = false, + GenerateScript = false, + DumpProperty = true, + DumpAttribute = true + }, + Console.WriteLine + )) + { + throw new Exception("Il2CppDumper failed"); + } + + File.WriteAllText(hashFile, hash); + } + + private static string ComputeHash(string file) + { + using var md5 = MD5.Create(); + using var assemblyStream = File.OpenRead(file); + + var hash = md5.ComputeHash(assemblyStream); + + return Encoding.UTF8.GetString(hash); + } + } +} diff --git a/Reactor.Greenhouse/Setup/GameManager.cs b/Reactor.Greenhouse/Setup/GameManager.cs new file mode 100644 index 0000000..f899e6e --- /dev/null +++ b/Reactor.Greenhouse/Setup/GameManager.cs @@ -0,0 +1,86 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using DepotDownloader; +using Reactor.Greenhouse.Setup.Provider; + +namespace Reactor.Greenhouse.Setup +{ + public class GameManager + { + public string WorkPath { get; } + + public Game PreObfuscation { get; } + public Game Steam { get; } + public Game Itch { get; } + + public GameManager() + { + WorkPath = Path.GetFullPath("work"); + PreObfuscation = new Game(new SteamProvider(true), "preobfuscation", Path.Combine(WorkPath, "preobfuscation")); + Steam = new Game(new SteamProvider(false), "steam", Path.Combine(WorkPath, "steam")); + Itch = new Game(new ItchProvider(), "itch", Path.Combine(WorkPath, "itch")); + } + + public async Task SetupAsync(bool setupSteam, bool setupItch) + { + var preObfuscation = PreObfuscation.Provider.IsUpdateNeeded(); + var steam = setupSteam && Steam.Provider.IsUpdateNeeded(); + var itch = setupItch && Itch.Provider.IsUpdateNeeded(); + + if (preObfuscation || steam || itch) + { + ContentDownloader.ShutdownSteam3(); + + if (preObfuscation) + { + await PreObfuscation.DownloadAsync(); + Console.WriteLine($"Downloaded {nameof(PreObfuscation)} ({PreObfuscation.Version})"); + } + + if (steam) + { + await Steam.DownloadAsync(); + Console.WriteLine($"Downloaded {nameof(Steam)} ({Steam.Version})"); + } + + if (itch) + { + await Itch.DownloadAsync(); + Console.WriteLine($"Downloaded {nameof(Itch)} ({Itch.Version})"); + } + } + + ContentDownloader.ShutdownSteam3(); + + PreObfuscation.UpdateVersion(); + + if (setupSteam) + { + Steam.UpdateVersion(); + } + + if (setupItch) + { + Itch.UpdateVersion(); + } + + if (PreObfuscation.Version != "2020.9.9") + { + throw new ArgumentException("Pre obfuscation version is invalid"); + } + + PreObfuscation.Dump(); + + if (setupSteam) + { + Steam.Dump(); + } + + if (setupItch) + { + Itch.Dump(); + } + } + } +} diff --git a/Reactor.Greenhouse/Setup/GameVersionParser.cs b/Reactor.Greenhouse/Setup/GameVersionParser.cs new file mode 100644 index 0000000..283446d --- /dev/null +++ b/Reactor.Greenhouse/Setup/GameVersionParser.cs @@ -0,0 +1,32 @@ +using System.IO; +using System.Linq; +using System.Text; + +namespace Reactor.Greenhouse.Setup +{ + public static class GameVersionParser + { + public static int IndexOf(this byte[] source, byte[] pattern) + { + for (var i = 0; i < source.Length; i++) + { + if (source.Skip(i).Take(pattern.Length).SequenceEqual(pattern)) + { + return i; + } + } + + return -1; + } + + public static string Parse(string file) + { + var bytes = File.ReadAllBytes(file); + + var pattern = Encoding.UTF8.GetBytes("public.app-category.games"); + var index = bytes.IndexOf(pattern) + pattern.Length + 127; + + return Encoding.UTF8.GetString(bytes.Skip(index).TakeWhile(x => x != 0).ToArray()); + } + } +} diff --git a/Reactor.Greenhouse/Setup/Provider/BaseProvider.cs b/Reactor.Greenhouse/Setup/Provider/BaseProvider.cs new file mode 100644 index 0000000..b552ab7 --- /dev/null +++ b/Reactor.Greenhouse/Setup/Provider/BaseProvider.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; + +namespace Reactor.Greenhouse.Setup.Provider +{ + public abstract class BaseProvider + { + public Game Game { get; internal set; } + + public abstract void Setup(); + public abstract Task DownloadAsync(); + public abstract bool IsUpdateNeeded(); + } +} diff --git a/Reactor.Greenhouse/Setup/Provider/ItchProvider.cs b/Reactor.Greenhouse/Setup/Provider/ItchProvider.cs new file mode 100644 index 0000000..4d3144c --- /dev/null +++ b/Reactor.Greenhouse/Setup/Provider/ItchProvider.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using AngleSharp.Html.Dom; +using AngleSharp.Html.Parser; +using DepotDownloader; +using Newtonsoft.Json; + +namespace Reactor.Greenhouse.Setup.Provider +{ + public class ItchProvider : BaseProvider + { + private HttpClient HttpClient { get; } = new HttpClient(new HttpClientHandler + { + CookieContainer = new CookieContainer() + }); + + public string Username { get; set; } + public string Password { get; set; } + + public override void Setup() + { + var environmentVariable = Environment.GetEnvironmentVariable("ITCH"); + + if (environmentVariable != null) + { + var split = environmentVariable.Split(":"); + Username = split[0]; + Password = split[1]; + } + else + { + Console.Write("itch.io username: "); + Username = Console.ReadLine(); + + Console.Write("itch.io password: "); + Password = Util.ReadPassword(); + + Console.WriteLine(); + } + } + + public override async Task DownloadAsync() + { + var htmlParser = new HtmlParser(); + + var csrfResponse = await HttpClient.GetAsync("https://itch.io/login"); + + var csrfDocument = htmlParser.ParseDocument(await csrfResponse.Content.ReadAsStreamAsync()); + + var loginResponse = await HttpClient.PostAsync("https://itch.io/login", new FormUrlEncodedContent(new[] + { + new KeyValuePair("csrf_token", ((IHtmlInputElement) csrfDocument.QuerySelector("input[name=csrf_token]")).Value), + new KeyValuePair("username", Username), + new KeyValuePair("password", Password) + })); + + if (!loginResponse.IsSuccessStatusCode || (await loginResponse.Content.ReadAsStringAsync()).Contains("Errors")) + { + throw new Exception("itch.io authentication failed"); + } + + Console.WriteLine("Logged into itch.io"); + + var pageDocument = htmlParser.ParseDocument(await HttpClient.GetStringAsync("https://innersloth.itch.io/among-us")); + var downloadPageUrl = ((IHtmlAnchorElement) pageDocument.QuerySelector("a[class=button]")).Href; + + var downloadDocument = htmlParser.ParseDocument(await HttpClient.GetStringAsync(downloadPageUrl)); + var uploadId = ((IHtmlAnchorElement) downloadDocument.QuerySelector("a[class='button download_btn']")).Dataset["upload_id"]; + + var keyRegex = new Regex("key\":\"(.+)\","); + var key = downloadDocument.QuerySelectorAll("script[type='text/javascript']") + .Cast() + .Select(x => keyRegex.Match(x.InnerHtml)) + .Single(x => x.Success) + .Groups[1].Value; + + var json = await HttpClient.PostAsync($"https://innersloth.itch.io/among-us/file/{uploadId}?key={key}", null); + var response = JsonConvert.DeserializeObject(await json.Content.ReadAsStringAsync()); + + Console.WriteLine($"Downloading {response.Url}"); + + var files = new[] { "GameAssembly.dll", "global-metadata.dat", "globalgamemanagers" }; + using var zipArchive = new ZipArchive(await HttpClient.GetStreamAsync(response.Url), ZipArchiveMode.Read); + foreach (var entry in zipArchive.Entries) + { + if (files.Contains(entry.Name)) + { + var path = Path.Combine("work", "itch", entry.FullName.Substring(entry.FullName.IndexOf("/", StringComparison.Ordinal) + 1)); + + Directory.GetParent(path)!.Create(); + entry.ExtractToFile(path, true); + } + } + } + + public override bool IsUpdateNeeded() + { + return !Directory.Exists(Game.Path); + } + + public class DownloadResponse + { + public string Url { get; set; } + public bool External { get; set; } + } + } +} diff --git a/Reactor.Greenhouse/Setup/Provider/SteamProvider.cs b/Reactor.Greenhouse/Setup/Provider/SteamProvider.cs new file mode 100644 index 0000000..543e323 --- /dev/null +++ b/Reactor.Greenhouse/Setup/Provider/SteamProvider.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using DepotDownloader; +using SteamKit2; + +namespace Reactor.Greenhouse.Setup.Provider +{ + public class SteamProvider : BaseProvider + { + private const uint AppId = 945360; + private const uint DepotId = 945361; + private const ulong PreObfuscationManifest = 3596575937380717449; + + public bool IsPreObfuscation { get; } + + public SteamProvider(bool isPreObfuscation) + { + IsPreObfuscation = isPreObfuscation; + } + + public override bool IsUpdateNeeded() + { + DepotConfigStore.LoadFromFile(Path.Combine(Game.Path, ".DepotDownloader", "depot.config")); + if (DepotConfigStore.Instance.InstalledManifestIDs.TryGetValue(DepotId, out var installedManifest)) + { + if (IsPreObfuscation) + { + if (installedManifest == PreObfuscationManifest) + { + return false; + } + } + else + { + if (ContentDownloader.steam3 == null) + { + ContentDownloader.InitializeSteam3(); + } + + ContentDownloader.steam3!.RequestAppInfo(AppId); + + var depots = ContentDownloader.GetSteam3AppSection(AppId, EAppInfoSection.Depots); + if (installedManifest == depots[DepotId.ToString()]["manifests"][ContentDownloader.DEFAULT_BRANCH].AsUnsignedLong()) + { + return false; + } + } + } + + return true; + } + + public override void Setup() + { + if (ContentDownloader.steam3 != null && ContentDownloader.steam3.bConnected) + { + return; + } + + AccountSettingsStore.LoadFromFile("account.config"); + + var environmentVariable = Environment.GetEnvironmentVariable("STEAM"); + + if (environmentVariable != null) + { + var split = environmentVariable.Split(":"); + ContentDownloader.InitializeSteam3(split[0], split[1]); + } + else + { + ContentDownloader.Config.RememberPassword = true; + + Console.Write("Steam username: "); + var username = Console.ReadLine(); + + string password = null; + + if (!AccountSettingsStore.Instance.LoginKeys.ContainsKey(username)) + { + Console.Write("Steam password: "); + password = ContentDownloader.Config.SuppliedPassword = Util.ReadPassword(); + Console.WriteLine(); + } + + ContentDownloader.InitializeSteam3(username, password); + } + + ContentDownloader.Config.UsingFileList = true; + ContentDownloader.Config.FilesToDownload = new List + { + "GameAssembly.dll" + }; + ContentDownloader.Config.FilesToDownloadRegex = new List + { + new Regex("^Among Us_Data/il2cpp_data/Metadata/global-metadata.dat$".Replace("/", "[\\\\|/]"), RegexOptions.Compiled | RegexOptions.IgnoreCase), + new Regex("^Among Us_Data/globalgamemanagers$".Replace("/", "[\\\\|/]"), RegexOptions.Compiled | RegexOptions.IgnoreCase) + }; + } + + public override Task DownloadAsync() + { + ContentDownloader.Config.InstallDirectory = Game.Path; + return ContentDownloader.DownloadAppAsync(AppId, DepotId, IsPreObfuscation ? PreObfuscationManifest : ContentDownloader.INVALID_MANIFEST_ID); + } + } +} diff --git a/Reactor.OxygenFilter.MSBuild/Context.cs b/Reactor.OxygenFilter.MSBuild/Context.cs new file mode 100644 index 0000000..b3bc47b --- /dev/null +++ b/Reactor.OxygenFilter.MSBuild/Context.cs @@ -0,0 +1,32 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace Reactor.OxygenFilter.MSBuild +{ + public static class Context + { + public static string DataPath { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ".reactor"); + public static string TempPath { get; } = Path.Combine(DataPath, "temp"); + + public static string ComputeHash(FileInfo file) + { + using var md5 = MD5.Create(); + using var assemblyStream = file.OpenRead(); + + var hash = md5.ComputeHash(assemblyStream); + + return Encoding.UTF8.GetString(hash); + } + + public static string ComputeHash(string text) + { + using var md5 = MD5.Create(); + + var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(text)); + + return Encoding.UTF8.GetString(hash); + } + } +} diff --git a/Reactor.OxygenFilter.MSBuild/Deobfuscate.cs b/Reactor.OxygenFilter.MSBuild/Deobfuscate.cs new file mode 100644 index 0000000..cd909a6 --- /dev/null +++ b/Reactor.OxygenFilter.MSBuild/Deobfuscate.cs @@ -0,0 +1,161 @@ +using System.IO; +using System.Linq; +using System.Collections.Generic; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Mono.Cecil; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Reactor.OxygenFilter.MSBuild +{ + public class Deobfuscate : Task + { + [Required] + public string AmongUs { get; set; } + + [Required] + public string[] Input { get; set; } + + [Required] + public string Mappings { get; set; } + + [Output] + public string[] Deobfuscated { get; set; } + + public override bool Execute() + { + var path = Path.Combine(Context.DataPath, "mapped"); + + Directory.CreateDirectory(path); + + JsonConvert.DefaultSettings = () => new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + + var mappings = JsonConvert.DeserializeObject(Mappings); + + var resolver = new DefaultAssemblyResolver(); + resolver.AddSearchDirectory(Path.Combine(Context.DataPath, "references")); + resolver.AddSearchDirectory(Path.Combine(AmongUs, "BepInEx", "core")); + resolver.AddSearchDirectory(Path.Combine(AmongUs, "BepInEx", "plugins")); + resolver.AddSearchDirectory(Path.Combine(AmongUs, "BepInEx", "unhollowed")); + + var deobfuscated = new List(); + + foreach (var input in Input) + { + var fileName = Path.Combine(path, Path.GetFileName(input)); + deobfuscated.Add(fileName); + + var hash = Context.ComputeHash(new FileInfo(input)); + var hashFile = fileName + ".md5"; + + if (File.Exists(hashFile) && File.ReadAllText(hashFile) == hash) + { + continue; + } + + using var stream = File.Open(input, FileMode.Open, FileAccess.Read, FileShare.Read); + + using var moduleDefinition = ModuleDefinition.ReadModule(stream, new ReaderParameters { AssemblyResolver = resolver }); + var toDeobfuscate = new Dictionary(); + + foreach (var type in moduleDefinition.GetTypeReferences()) + { + var typeDefinition = type.Resolve(); + + if (typeDefinition == null) + { + Log.LogWarning($"Unresolved type reference: {type.FullName}, {type.Scope}"); + continue; + } + + var mapped = mappings.FindByOriginal(typeDefinition.FullName); + + if (mapped?.Mapped != null) + { + toDeobfuscate[type] = mapped.Mapped; + } + } + + foreach (var member in moduleDefinition.GetMemberReferences()) + { + var memberDefinition = member.Resolve(); + + if (memberDefinition == null) + { + Log.LogWarning($"Unresolved member reference: {member.FullName}, {member.DeclaringType.Scope}"); + continue; + } + + var mappedType = mappings.FindByOriginal(memberDefinition.DeclaringType.FullName); + + if (mappedType != null) + { + var mappedMembers = mappedType.Fields.Concat(mappedType.Properties).Concat(mappedType.Methods); + + var mapped = mappedMembers.FirstOrDefault(x => x.Original.Name == member.Name); + + if (mapped?.Mapped != null) + { + toDeobfuscate[member] = mapped.Mapped; + } + else if (memberDefinition is MethodDefinition methodDefinition) + { + if (methodDefinition.IsGetter) + { + var property = memberDefinition.DeclaringType.Properties.FirstOrDefault(x => x.GetMethod?.Name == memberDefinition.Name); + if (property != null) + { + mapped = mappedType.Properties.FirstOrDefault(x => x.Original.Name == property.Name); + if (mapped?.Mapped != null) + { + toDeobfuscate[property] = mapped.Mapped; + toDeobfuscate[member] = "get_" + mapped.Mapped; + } + } + } + else if (methodDefinition.IsSetter) + { + var property = memberDefinition.DeclaringType.Properties.FirstOrDefault(x => x.SetMethod?.Name == memberDefinition.Name); + + if (property != null) + { + mapped = mappedType.Properties.FirstOrDefault(x => x.Original.Name == property.Name); + + if (mapped?.Mapped != null) + { + toDeobfuscate[property] = mapped.Mapped; + toDeobfuscate[member] = "set_" + mapped.Mapped; + } + } + } + } + } + } + + foreach (var pair in toDeobfuscate) + { + pair.Key.Name = pair.Value; + } + + foreach (var typeReference in moduleDefinition.GetTypeReferences()) + { + if (typeReference.Scope.Name == "Assembly-CSharp") + { + typeReference.Scope.Name += "-Deobfuscated"; + } + } + + moduleDefinition.Write(fileName); + File.WriteAllText(hashFile, hash); + } + + Deobfuscated = deobfuscated.ToArray(); + + return true; + } + } +} diff --git a/Reactor.OxygenFilter.MSBuild/GenerateReferences.cs b/Reactor.OxygenFilter.MSBuild/GenerateReferences.cs new file mode 100644 index 0000000..62e7981 --- /dev/null +++ b/Reactor.OxygenFilter.MSBuild/GenerateReferences.cs @@ -0,0 +1,111 @@ +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Mono.Cecil; + +namespace Reactor.OxygenFilter.MSBuild +{ + public class GenerateReferences : Task + { + [Required] + public string AmongUs { get; set; } + + [Required] + public string Mappings { get; set; } + + [Output] + public string ReferencesPath { get; set; } + + public override bool Execute() + { + ReferencesPath = Path.Combine(Context.DataPath, "references"); + + Directory.CreateDirectory(Context.DataPath); + + var skip = true; + + var gameAssemblyPath = Path.Combine(AmongUs, "GameAssembly.dll"); + var hash = Context.ComputeHash(new FileInfo(gameAssemblyPath)); + var hashPath = Path.Combine(Context.DataPath, "GameAssembly.dll.md5"); + + if (!File.Exists(hashPath) || hash != File.ReadAllText(hashPath)) + { + skip = false; + } + + var mappingsHash = Context.ComputeHash(Mappings); + var mappingsHashPath = Path.Combine(Context.DataPath, "mappings.md5"); + + if (!File.Exists(mappingsHashPath) || mappingsHash != File.ReadAllText(mappingsHashPath)) + { + skip = false; + } + + if (skip) + { + return true; + } + + if (Directory.Exists(Context.TempPath)) + { + Directory.Delete(Context.TempPath, true); + } + + var dumperConfig = new Il2CppDumper.Config + { + GenerateScript = false, + GenerateDummyDll = true + }; + + Log.LogMessage(MessageImportance.High, "Generating Il2CppDumper intermediate assemblies"); + + Il2CppDumper.Il2CppDumper.PerformDump( + gameAssemblyPath, + Path.Combine(AmongUs, "Among Us_Data", "il2cpp_data", "Metadata", "global-metadata.dat"), + Context.TempPath, dumperConfig, _ => + { + } + ); + + Log.LogMessage(MessageImportance.High, "Executing Reactor.OxygenFilter"); + + var oxygenFilter = new OxygenFilter(); + + var dumpedDll = new FileInfo(Path.Combine(Context.TempPath, "DummyDll", "Assembly-CSharp.dll")); + oxygenFilter.Start(Mappings, dumpedDll, dumpedDll); + + Log.LogMessage(MessageImportance.High, "Executing Il2CppUnhollower generator"); + + UnhollowerBaseLib.LogSupport.WarningHandler += s => Log.LogWarning(s); + UnhollowerBaseLib.LogSupport.ErrorHandler += s => Log.LogError(s); + + var unityBaseLibDir = Path.Combine(AmongUs, "BepInEx", "unhollowed", "base"); + + var unhollowerOptions = new AssemblyUnhollower.UnhollowerOptions + { + GameAssemblyPath = gameAssemblyPath, + MscorlibPath = Path.Combine(AmongUs, "mono", "Managed", "mscorlib.dll"), + SourceDir = Path.Combine(Context.TempPath, "DummyDll"), + OutputDir = Path.Combine(Context.TempPath, "unhollowed"), + UnityBaseLibsDir = unityBaseLibDir, + NoCopyUnhollowerLibs = true + }; + + AssemblyUnhollower.Program.Main(unhollowerOptions); + + Directory.CreateDirectory(ReferencesPath); + + var assemblyDefinition = AssemblyDefinition.ReadAssembly(Path.Combine(unhollowerOptions.OutputDir, "Assembly-CSharp.dll")); + + assemblyDefinition.Name = new AssemblyNameDefinition(assemblyDefinition.Name.Name + "-Deobfuscated", assemblyDefinition.Name.Version); + assemblyDefinition.MainModule.Name += "-Deobfuscated"; + + assemblyDefinition.Write(Path.Combine(ReferencesPath, "Assembly-CSharp-Deobfuscated.dll")); + + File.WriteAllText(hashPath, hash); + File.WriteAllText(mappingsHashPath, mappingsHash); + + return true; + } + } +} diff --git a/Reactor.OxygenFilter.MSBuild/LoadMappings.cs b/Reactor.OxygenFilter.MSBuild/LoadMappings.cs new file mode 100644 index 0000000..bc6e2ac --- /dev/null +++ b/Reactor.OxygenFilter.MSBuild/LoadMappings.cs @@ -0,0 +1,47 @@ +using System.IO; +using System.Net.Http; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Reactor.OxygenFilter.MSBuild +{ + public class LoadMappings : Task + { + public string TargetGamePlatform { get; set; } = "Steam"; + + [Required] + public string Mappings { get; set; } + + [Output] + public string MappingsJson { get; set; } + + public override bool Execute() + { + if (File.Exists(Mappings)) + { + MappingsJson = File.ReadAllText(Mappings); + return true; + } + + var file = Path.Combine(Context.TempPath, Mappings.Replace("/", "_") + $"-{TargetGamePlatform.ToLower()}" + ".json"); + + if (File.Exists(file)) + { + MappingsJson = File.ReadAllText(file); + return true; + } + + var split = Mappings.Split(':'); + var repo = split[0]; + var version = split[1]; + + var httpClient = new HttpClient(); + var json = httpClient.GetStringAsync($"https://github.com/{repo}/releases/download/{version}/{TargetGamePlatform.ToLower()}.json").GetAwaiter().GetResult(); + + MappingsJson = json; + File.WriteAllText(file, json); + + return true; + } + } +} diff --git a/Reactor.OxygenFilter.MSBuild/Reactor.OxygenFilter.MSBuild.TargetFramework.props b/Reactor.OxygenFilter.MSBuild/Reactor.OxygenFilter.MSBuild.TargetFramework.props new file mode 100644 index 0000000..5e4c58a --- /dev/null +++ b/Reactor.OxygenFilter.MSBuild/Reactor.OxygenFilter.MSBuild.TargetFramework.props @@ -0,0 +1,48 @@ + + + true + false + netstandard2.1 + net472 + $(MSBuildThisFileDirectory)..\lib\$(TaskFolder)\Reactor.OxygenFilter.MSBuild.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Reactor.OxygenFilter.MSBuild/Reactor.OxygenFilter.MSBuild.TargetFrameworks.props b/Reactor.OxygenFilter.MSBuild/Reactor.OxygenFilter.MSBuild.TargetFrameworks.props new file mode 100644 index 0000000..a1fb6eb --- /dev/null +++ b/Reactor.OxygenFilter.MSBuild/Reactor.OxygenFilter.MSBuild.TargetFrameworks.props @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Reactor.OxygenFilter.MSBuild/Reactor.OxygenFilter.MSBuild.csproj b/Reactor.OxygenFilter.MSBuild/Reactor.OxygenFilter.MSBuild.csproj new file mode 100644 index 0000000..804bc8d --- /dev/null +++ b/Reactor.OxygenFilter.MSBuild/Reactor.OxygenFilter.MSBuild.csproj @@ -0,0 +1,41 @@ + + + netstandard2.1;net472 + latest + true + + 0.1.0 + git + https://github.com/NuclearPowered/Reactor.OxygenFilter + LGPL-3.0-or-later + Library for using Reactor.OxygenFilter with msbuild + + + + + + + + + + + + + + + + + + + + + + + <_PackageFiles Include="bin\$(Configuration)\*\Reactor.OxygenFilter.dll;bin\$(Configuration)\*\Newtonsoft.Json.dll;bin\$(Configuration)\*\Il2CppDumper.dll;bin\$(Configuration)\*\AssemblyUnhollower.dll;bin\$(Configuration)\*\Unhollower*Lib.dll;bin\$(Configuration)\*\Iced.dll;bin\$(Configuration)\*\Mono.*.dll"> + lib%(RecursiveDir) + false + Content + + + + diff --git a/Reactor.OxygenFilter.MSBuild/Reobfuscate.cs b/Reactor.OxygenFilter.MSBuild/Reobfuscate.cs new file mode 100644 index 0000000..3341de5 --- /dev/null +++ b/Reactor.OxygenFilter.MSBuild/Reobfuscate.cs @@ -0,0 +1,245 @@ +using System.IO; +using System.Linq; +using System.Collections.Generic; +using AssemblyUnhollower; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Mono.Cecil; +using Mono.Cecil.Rocks; + +namespace Reactor.OxygenFilter.MSBuild +{ + public class Reobfuscate : Task + { + [Required] + public string Input { get; set; } + + [Required] + public string[] ReferencedAssemblies { get; set; } + + private static string GetObfuscated(ICustomAttributeProvider cap) + { + var attribute = cap.GetCustomAttribute("UnhollowerBaseLib.Attributes.ObfuscatedNameAttribute"); + return attribute?.ConstructorArguments.Single().Value as string; + } + + private const string postfix = "-Deobfuscated"; + + public override bool Execute() + { + using var stream = File.Open(Input, FileMode.Open, FileAccess.ReadWrite); + var resolver = new DefaultAssemblyResolver(); + + foreach (var directory in ReferencedAssemblies.Select(Path.GetDirectoryName).Distinct()) + { + resolver.AddSearchDirectory(directory); + } + + using var moduleDefinition = ModuleDefinition.ReadModule(stream, new ReaderParameters { AssemblyResolver = resolver }); + var toObfuscate = new Dictionary(); + + foreach (var type in moduleDefinition.GetTypeReferences()) + { + var typeDefinition = type.Resolve(); + + if (typeDefinition == null) + { + Log.LogWarning($"Unresolved type reference: {type.FullName}, {type.Scope}"); + continue; + } + + var obfuscated = GetObfuscated(typeDefinition); + + if (obfuscated != null) + { + toObfuscate[type] = obfuscated; + } + } + + foreach (var member in moduleDefinition.GetMemberReferences()) + { + var memberDefinition = member.Resolve(); + + if (memberDefinition == null) + { + Log.LogWarning($"Unresolved member reference: {member.FullName}, {member.DeclaringType.Scope}"); + continue; + } + + var obfuscated = GetObfuscated(memberDefinition); + + if (obfuscated != null) + { + toObfuscate[member] = obfuscated; + } + } + + foreach (var type in moduleDefinition.GetAllTypes()) + { + foreach (var customAttribute in type.CustomAttributes) + { + TypeReference lastType = null; + + foreach (var argument in customAttribute.ConstructorArguments.ToList()) + { + if (argument.Type.FullName == "System.Type") + { + lastType = (TypeReference) argument.Value; + + var t = lastType; + + while (t != null) + { + var obfuscated = GetObfuscated(t.Resolve()); + + if (obfuscated != null) + { + toObfuscate[t] = obfuscated; + } + + t = t.DeclaringType; + } + } + else + { + // obfuscate nameof + if (argument.Type.FullName == "System.String" && lastType != null) + { + var value = (string) argument.Value; + + var lastTypeDef = lastType.Resolve(); + + IMemberDefinition member = null; + + var field = lastTypeDef.Fields.SingleOrDefault(x => x.Name == value); + + if (field != null) + { + member = field; + } + + var method = lastTypeDef.Methods.FirstOrDefault(x => x.Name == value); + + if (method != null) + { + member = method; + } + + var property = lastTypeDef.Properties.SingleOrDefault(x => x.Name == value); + + if (property != null) + { + member = property; + } + + if (member != null) + { + var obfuscated = GetObfuscated(member); + + if (obfuscated != null) + { + customAttribute.ConstructorArguments[customAttribute.ConstructorArguments.IndexOf(argument)] + = new CustomAttributeArgument(argument.Type, obfuscated); + } + } + } + + lastType = null; + } + } + } + } + + // fix generic methods + foreach (var methodDefinition in moduleDefinition.GetAllTypes().SelectMany(x => x.Methods)) + { + if (!methodDefinition.HasBody) + continue; + + foreach (var instruction in methodDefinition.Body.Instructions) + { + if (instruction.Operand is MethodReference deobfuscatedCallReference) + { + var deobfuscatedCall = deobfuscatedCallReference.Resolve(); + if (deobfuscatedCall == null) + continue; + + var obfuscated = GetObfuscated(deobfuscatedCall); + if (obfuscated != null) + { + // get same type from obfuscated assembly + + var assemblyDefinition = resolver.Resolve(new AssemblyNameDefinition("Assembly-CSharp", null)); + + var hierarchy = new List(); + var type = deobfuscatedCall.DeclaringType; + + while (type != null) + { + hierarchy.Insert(0, type); + type = type.DeclaringType; + } + + var typeDefinition = assemblyDefinition.MainModule.GetType(GetObfuscated(hierarchy.First())); + + foreach (var element in hierarchy.Skip(1)) + { + typeDefinition = typeDefinition.NestedTypes.Single(x => x.Name == GetObfuscated(element)); + } + + // get same method from obfuscated assembly + MethodReference definition = typeDefinition.GetMethods().Single(x => x.Name == obfuscated); + + // obfuscate generics + if (deobfuscatedCallReference is GenericInstanceMethod generic) + { + var genericDefinition = new GenericInstanceMethod(definition); + definition = genericDefinition; + + foreach (var parameter in generic.GenericArguments) + { + var resolved = parameter.Resolve(); + if (resolved != null) + { + var s = GetObfuscated(resolved); + if (s != null) + { + parameter.Name = s; + } + } + + genericDefinition.GenericArguments.Add(parameter); + } + } + + instruction.Operand = deobfuscatedCallReference.Module.ImportReference(definition); + } + } + } + } + + foreach (var pair in toObfuscate) + { + pair.Key.Name = pair.Value; + } + + foreach (var typeReference in moduleDefinition.GetTypeReferences()) + { + typeReference.Module.Name = typeReference.Module.Name.Replace(postfix, string.Empty); + typeReference.Scope.Name = typeReference.Scope.Name.Replace(postfix, string.Empty); + } + + foreach (var memberReference in moduleDefinition.GetMemberReferences()) + { + memberReference.Module.Name = memberReference.Module.Name.Replace(postfix, string.Empty); + memberReference.DeclaringType.Scope.Name = memberReference.DeclaringType.Scope.Name.Replace(postfix, string.Empty); + } + + var outputDirectory = Path.Combine(Path.GetDirectoryName(Input), "reobfuscated"); + Directory.CreateDirectory(outputDirectory); + moduleDefinition.Write(Path.Combine(outputDirectory, Path.GetFileName(Input))); + + return true; + } + } +} diff --git a/Reactor.OxygenFilter.sln b/Reactor.OxygenFilter.sln new file mode 100644 index 0000000..48b20a5 --- /dev/null +++ b/Reactor.OxygenFilter.sln @@ -0,0 +1,30 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reactor.OxygenFilter", "Reactor.OxygenFilter\Reactor.OxygenFilter.csproj", "{D32CB975-7C2A-495F-8D6A-2955ADF684D4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reactor.Greenhouse", "Reactor.Greenhouse\Reactor.Greenhouse.csproj", "{AF5C3614-4FC9-4D28-BA58-1B9103B5BAB1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reactor.OxygenFilter.MSBuild", "Reactor.OxygenFilter.MSBuild\Reactor.OxygenFilter.MSBuild.csproj", "{C65EE22A-784F-4B6A-974A-BA27418235E5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D32CB975-7C2A-495F-8D6A-2955ADF684D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D32CB975-7C2A-495F-8D6A-2955ADF684D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D32CB975-7C2A-495F-8D6A-2955ADF684D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D32CB975-7C2A-495F-8D6A-2955ADF684D4}.Release|Any CPU.Build.0 = Release|Any CPU + {AF5C3614-4FC9-4D28-BA58-1B9103B5BAB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF5C3614-4FC9-4D28-BA58-1B9103B5BAB1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF5C3614-4FC9-4D28-BA58-1B9103B5BAB1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF5C3614-4FC9-4D28-BA58-1B9103B5BAB1}.Release|Any CPU.Build.0 = Release|Any CPU + {C65EE22A-784F-4B6A-974A-BA27418235E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C65EE22A-784F-4B6A-974A-BA27418235E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C65EE22A-784F-4B6A-974A-BA27418235E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C65EE22A-784F-4B6A-974A-BA27418235E5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + EndGlobalSection +EndGlobal diff --git a/Reactor.OxygenFilter/Extensions.cs b/Reactor.OxygenFilter/Extensions.cs new file mode 100644 index 0000000..33dfcca --- /dev/null +++ b/Reactor.OxygenFilter/Extensions.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Text; +using Mono.Cecil; + +namespace Reactor.OxygenFilter +{ + public static class Extensions + { + public static bool IsObfuscated(this string text) + { + return text.Length == 11 && text.All(char.IsUpper); + } + + public static string GetSignature(this MethodDefinition methodDefinition) + { + var sb = new StringBuilder(); + + sb.Append(methodDefinition.ReturnType.FullName); + sb.Append(" "); + + sb.Append("("); + if (methodDefinition.HasParameters) + { + for (var i = 0; i < methodDefinition.Parameters.Count; i++) + { + var parameterType = methodDefinition.Parameters[i].ParameterType; + + if (i > 0) + sb.Append(","); + + if (parameterType is SentinelType) + sb.Append("...,"); + + sb.Append(parameterType.FullName); + } + } + + sb.Append(")"); + + return sb.ToString(); + } + + public static string GetSignature(this FieldDefinition fieldDefinition) + { + return fieldDefinition.FieldType.FullName; + } + + public static string GetSignature(this PropertyDefinition propertyDefinition) + { + return propertyDefinition.PropertyType.FullName; + } + } +} diff --git a/Reactor.OxygenFilter/Mappings.cs b/Reactor.OxygenFilter/Mappings.cs new file mode 100644 index 0000000..5282e9d --- /dev/null +++ b/Reactor.OxygenFilter/Mappings.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace Reactor.OxygenFilter +{ + public class Mappings + { + public List Types { get; set; } = new List(); + + public MappedType Find(string name, Func predicate) + { + var split = name.Split('.').SelectMany(x => x.Split('+')).ToArray(); + + if (split.Length > 1) + { + MappedType type = null; + + foreach (var s in split) + { + if (type == null) + { + type = Find(s, predicate); + } + else + { + type = type.Nested.SingleOrDefault(x => predicate(x) == s); + } + } + + return type; + } + + return Types.SingleOrDefault(x => predicate(x) == name); + } + + public MappedType FindByMapped(string name) + { + return Find(name, x => x.Mapped); + } + + public MappedType FindByOriginal(string name) + { + return Find(name, x => x.Original.Name); + } + } + + public class OriginalDescriptor + { + public string Name { get; set; } + public int? Index { get; set; } + public string Signature { get; set; } + public Constant Const { get; set; } + + public bool Equals(OriginalDescriptor obj) + { + return Name == obj.Name && Index == obj.Index && Signature == obj.Signature && Const == obj.Const; + } + + public bool IsEmpty() + { + return Name == null && Index == null; + } + + public override string ToString() + { + return Name ?? Index?.ToString(); + } + + public class Constant + { + private object _value; + + public object Value + { + get => Convert.ChangeType(_value, Type); + set => _value = value; + } + + public Type Type { get; set; } + } + } + + public class OriginalDescriptorConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, OriginalDescriptor value, JsonSerializer serializer) + { + if (value.Name != null && value.Index == null && value.Signature == null) + { + writer.WriteValue(value.Name); + } + else if (value.Index != null && value.Name == null && value.Signature == null) + { + writer.WriteValue(value.Index); + } + else + { + serializer.Serialize(writer, value); + } + } + + public override OriginalDescriptor ReadJson(JsonReader reader, Type objectType, OriginalDescriptor existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.Value is string name) + { + return new OriginalDescriptor { Name = name }; + } + + if (reader.Value is long index) + { + return new OriginalDescriptor { Index = (int?) index }; + } + + if (reader.TokenType == JsonToken.StartObject) + { + return serializer.Deserialize(reader); + } + + throw new JsonSerializationException(); + } + } + + public class MappedMember + { + [JsonProperty(Order = -3)] + [JsonConverter(typeof(OriginalDescriptorConverter))] + public OriginalDescriptor Original { get; set; } + + [JsonProperty(Order = -2)] + public string Mapped { get; set; } + + public override string ToString() + { + var result = Original.ToString(); + + if (Mapped != null) + { + result += $" (name: {Mapped})"; + } + + return result; + } + + public MappedMember() + { + } + + public MappedMember(OriginalDescriptor original, string mapped) + { + Original = original; + + if (original.Name != mapped) + { + Mapped = mapped; + } + } + + public bool Equals(MappedMember obj, bool compareOriginal = true) + { + if (obj == null) + { + return false; + } + + return Mapped == obj.Mapped && (!compareOriginal || (Original.Name == "^" && obj.Mapped != null) || Original.Equals(obj.Original)); + } + } + + public class MappedMethod : MappedMember + { + public List Parameters { get; set; } = new List(); + + public MappedMethod() + { + } + + public MappedMethod(OriginalDescriptor original, string mapped) : base(original, mapped) + { + } + } + + public class MappedType : MappedMember + { + public List Properties { get; set; } = new List(); + public List Fields { get; set; } = new List(); + public List Methods { get; set; } = new List(); + + public List Nested { get; set; } = new List(); + + public MappedType() + { + } + + public MappedType(OriginalDescriptor original, string mapped) : base(original, mapped) + { + } + } +} diff --git a/Reactor.OxygenFilter/ObfuscationMapper.cs b/Reactor.OxygenFilter/ObfuscationMapper.cs new file mode 100644 index 0000000..dc9bc93 --- /dev/null +++ b/Reactor.OxygenFilter/ObfuscationMapper.cs @@ -0,0 +1,231 @@ +using System; +using System.Linq; +using Mono.Cecil; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Reactor.OxygenFilter +{ + public class ObfuscationMapper + { + public ModuleDefinition ModuleDef { get; } + public Mappings Mappings { get; private set; } + + public ObfuscationMapper(ModuleDefinition moduleDef) + { + ModuleDef = moduleDef; + } + + public void Map() + { + var mappedAttribute = new TypeDefinition("", "MappedAttribute", TypeAttributes.Public, ModuleDef.ImportReference(typeof(Attribute))) + { + Attributes = TypeAttributes.Class & TypeAttributes.Public + }; + + ModuleDef.Types.Add(mappedAttribute); + + var mappedAttributeCtor = new MethodReference(".ctor", ModuleDef.TypeSystem.Void, mappedAttribute) + { + HasThis = true, + Parameters = { new ParameterDefinition(ModuleDef.TypeSystem.String) } + }; + + void MapMember(ICustomAttributeProvider attributes, string mapped) + { + attributes.CustomAttributes.Add(new CustomAttribute(mappedAttributeCtor) + { + ConstructorArguments = { new CustomAttributeArgument(ModuleDef.TypeSystem.String, mapped) } + }); + } + + // beebyte 1 iq? + foreach (var propertyDef in ModuleDef.Types.SelectMany(x => x.NestedTypes.Prepend(x)).SelectMany(x => x.Properties)) + { + const string getPrefix = "get_"; + const string setPrefix = "set_"; + + var getName = propertyDef.GetMethod != null && propertyDef.GetMethod.Name.StartsWith(getPrefix) ? propertyDef.GetMethod.Name.Substring(getPrefix.Length) : null; + var setName = propertyDef.SetMethod != null && propertyDef.SetMethod.Name.StartsWith(setPrefix) ? propertyDef.SetMethod.Name.Substring(setPrefix.Length) : null; + + if (getName != null && setName != null && getName != setName) + { + throw new Exception($"{propertyDef.FullName} has 2 different accessor names"); + } + + if (getName != null || setName != null) + { + var name = getName ?? setName; + + MapMember(propertyDef, name); + + if (propertyDef.GetMethod != null) + MapMember(propertyDef.GetMethod, "get_" + name); + + if (propertyDef.SetMethod != null) + MapMember(propertyDef.SetMethod, "set_" + name); + } + } + + void MapType(MappedType type, TypeDefinition typeDef) + { + if (typeDef == null) + { + throw new NullReferenceException($"Type {type} was not found!"); + } + + if (type.Mapped != null) + { + MapMember(typeDef, type.Mapped); + } + + foreach (var property in type.Properties) + { + var propertyDef = typeDef.Properties.SingleOrDefault(x => x.Name == property.Original.Name); + + if (propertyDef == null) + { + throw new NullReferenceException($"Property {property} was not found in {type}!"); + } + + if (property.Mapped != null) + { + MapMember(propertyDef, property.Mapped); + + if (propertyDef.GetMethod != null) + MapMember(propertyDef.GetMethod, "get_" + property.Mapped); + + if (propertyDef.SetMethod != null) + MapMember(propertyDef.SetMethod, "set_" + property.Mapped); + } + } + + foreach (var field in type.Fields) + { + var fieldDef = typeDef.Fields.SingleOrDefault(x => x.Name == field.Original.Name); + + if (fieldDef == null) + { + throw new NullReferenceException($"Field {field} was not found in {type}!"); + } + + if (field.Mapped != null) + { + MapMember(fieldDef, field.Mapped); + } + } + + foreach (var method in type.Methods) + { + var methodDef = typeDef.Methods + .SingleOrDefault(x => x.Name == method.Original.Name && (method.Original.Signature == null || x.GetSignature().ToString() == method.Original.Signature)); + + if (methodDef == null) + { + throw new NullReferenceException($"Method {method} was not found in {type}!"); + } + + foreach (var methodDef2 in ModuleDef.Types + .Where(x => x.BaseType == typeDef) + .Select(x => x.Methods.SingleOrDefault(m => m.Name == method.Original.Name && (method.Original.Signature == null || m.GetSignature().ToString() == method.Original.Signature))) + .Where(x => x != null) + .Prepend(methodDef) + ) + { + if (method.Mapped != null) + { + MapMember(methodDef2, method.Mapped); + } + + for (var i = 0; i < method.Parameters.Count; i++) + { + MapMember(methodDef2.Parameters.ElementAt(i), method.Parameters[i]); + } + } + } + + foreach (var nested in type.Nested) + { + MapType(nested, typeDef.NestedTypes.SingleOrDefault(x => x.Name == nested.Original.Name)); + } + } + + foreach (var type in Mappings.Types) + { + MapType(type, ModuleDef.GetType(type.Original.Name)); + } + + foreach (var typeDef in ModuleDef.Types) + { + var i = 0; + + foreach (var member in typeDef.Properties) + { + if (member.Name.IsObfuscated() && member.CustomAttributes.All(x => x.AttributeType != mappedAttribute)) + { + MapMember(member, $"Property_{i}"); + } + + i++; + } + + i = 0; + + foreach (var member in typeDef.Fields) + { + if (member.Name.IsObfuscated() && member.CustomAttributes.All(x => x.AttributeType != mappedAttribute)) + { + MapMember(member, $"Field_{i}"); + } + + i++; + } + + i = 0; + + foreach (var member in typeDef.Methods) + { + if (member.Name.IsObfuscated() && member.CustomAttributes.All(x => x.AttributeType != mappedAttribute)) + { + MapMember(member, $"Method_{i}"); + } + + var j = 0; + foreach (var parameter in member.Parameters) + { + if (parameter.Name.IsObfuscated() && parameter.CustomAttributes.All(x => x.AttributeType != mappedAttribute)) + { + MapMember(parameter, $"Parameter_{j}"); + } + + j++; + } + + i++; + } + + i = 0; + + foreach (var member in typeDef.NestedTypes) + { + if (member.Name.IsObfuscated() && member.CustomAttributes.All(x => x.AttributeType != mappedAttribute)) + { + MapMember(member, $"Nested_{i}"); + } + + i++; + } + } + } + + public void LoadMappings(string mappings) + { + JsonConvert.DefaultSettings = () => new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + + Mappings = JsonConvert.DeserializeObject(mappings); + } + } +} diff --git a/Reactor.OxygenFilter/OxygenFilter.cs b/Reactor.OxygenFilter/OxygenFilter.cs new file mode 100644 index 0000000..a6dfd4f --- /dev/null +++ b/Reactor.OxygenFilter/OxygenFilter.cs @@ -0,0 +1,24 @@ +using System.IO; +using Mono.Cecil; + +namespace Reactor.OxygenFilter +{ + public class OxygenFilter + { + public void Start(string mappings, FileInfo dumpedDll, FileInfo outputDll) + { + using var inputStream = dumpedDll.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.Read); + using var outputStream = dumpedDll == outputDll ? inputStream : dumpedDll.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.Read); + + var resolver = new DefaultAssemblyResolver(); + resolver.AddSearchDirectory(dumpedDll.DirectoryName); + + using var module = ModuleDefinition.ReadModule(inputStream, new ReaderParameters(ReadingMode.Immediate) { AssemblyResolver = resolver }); + var mapper = new ObfuscationMapper(module); + mapper.LoadMappings(mappings); + mapper.Map(); + module.Dispose(); + module.Write(outputStream); + } + } +} diff --git a/Reactor.OxygenFilter/Reactor.OxygenFilter.csproj b/Reactor.OxygenFilter/Reactor.OxygenFilter.csproj new file mode 100644 index 0000000..b050d6f --- /dev/null +++ b/Reactor.OxygenFilter/Reactor.OxygenFilter.csproj @@ -0,0 +1,16 @@ + + + net472;netstandard2.0 + latest + + 0.1.0 + git + https://github.com/NuclearPowered/Reactor.OxygenFilter + LGPL-3.0-or-later + Library for deobfuscating Among Us + + + + + + \ No newline at end of file