// Copyright 2025 Code Philosophy // // 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 dnlib.DotNet; using System; using System.Collections.Generic; using System.Linq; using System.Xml; static class Align { static ModuleDefMD Load(string p) { var ctx = ModuleDef.CreateModuleContext(); ((AssemblyResolver)ctx.AssemblyResolver).PreSearchPaths.Add( System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(p))); return ModuleDefMD.Load(p, ctx); } static string FieldNameOf(string sig) { int i = sig.LastIndexOf(' '); return i < 0 ? sig : sig.Substring(i + 1); } static string MethodNameOf(string sig) { int paren = sig.IndexOf('('); string head = paren < 0 ? sig : sig.Substring(0, paren); int sep = head.IndexOf("::", StringComparison.Ordinal); if (sep < 0) return head; string n = head.Substring(sep + 2); return n.StartsWith("@") ? n.Substring(1) : n; } static int Main(string[] args) { string origPath = args[0], obfPath = args[1], mapPath = args[2]; var typeBack = new Dictionary(); var memberBack = new Dictionary(); var renamedKey = new HashSet(); var doc = new XmlDocument(); doc.Load(mapPath); foreach (XmlNode aN in doc.DocumentElement.ChildNodes) { if (!(aN is XmlElement a) || a.Name != "assembly") continue; foreach (XmlNode tN in a.ChildNodes) { if (!(tN is XmlElement t) || t.Name != "type") continue; string origType = t.Attributes["fullName"].Value; string newType = t.Attributes["newFullName"]?.Value; string obfType = string.IsNullOrEmpty(newType) ? origType : newType; typeBack[obfType] = origType; if (!string.IsNullOrEmpty(newType) && newType != origType) renamedKey.Add("T|" + obfType); foreach (XmlNode mN in t.ChildNodes) { if (!(mN is XmlElement m)) continue; string kind = m.Name; string sig = m.Attributes["signature"]?.Value; if (sig == null) continue; string origName = kind == "method" ? MethodNameOf(sig) : FieldNameOf(sig); string newName = m.Attributes["newName"]?.Value; string obfName = string.IsNullOrEmpty(newName) ? origName : newName; string key = kind + "|" + obfType + "::" + obfName; memberBack[key] = origType + "::" + origName; if (!string.IsNullOrEmpty(newName) && newName != origName) renamedKey.Add(key); } } } var mo = Load(origPath); var mb = Load(obfPath); DoTypes(mo, mb, typeBack, renamedKey); DoMembers(mo, mb, typeBack, memberBack, renamedKey, "method"); DoMembers(mo, mb, typeBack, memberBack, renamedKey, "field"); WithinType(mo, mb, typeBack, memberBack, renamedKey, "method"); WithinType(mo, mb, typeBack, memberBack, renamedKey, "field"); return 0; } static List TypeRows(ModuleDefMD m) { var n = (int)m.Metadata.TablesStream.TypeDefTable.Rows; var r = new List(n); for (uint rid = 1; rid <= n; rid++) { var t = m.ResolveTypeDef(rid); if (t != null) r.Add(t); } return r; } static void DoTypes(ModuleDefMD mo, ModuleDefMD mb, Dictionary typeBack, HashSet renamedKey) { var orig = TypeRows(mo).Select(t => t.FullName).ToList(); var origSet = new HashSet(orig); var obf = new List(); var obfRenamed = new List(); foreach (var t in TypeRows(mb)) { if (!typeBack.TryGetValue(t.FullName, out var o) || !origSet.Contains(o)) continue; obf.Add(o); obfRenamed.Add(renamedKey.Contains("T|" + t.FullName)); } Report("types", orig, obf, obfRenamed); } static void DoMembers(ModuleDefMD mo, ModuleDefMD mb, Dictionary typeBack, Dictionary memberBack, HashSet renamedKey, string kind) { var orig = new List(); foreach (var t in TypeRows(mo)) foreach (var n in kind == "method" ? t.Methods.Select(x => x.Name.String) : t.Fields.Select(x => x.Name.String)) orig.Add(t.FullName + "::" + n); var origSet = new HashSet(orig); var obf = new List(); var obfRenamed = new List(); foreach (var t in TypeRows(mb)) { foreach (var n in kind == "method" ? t.Methods.Select(x => x.Name.String) : t.Fields.Select(x => x.Name.String)) { string key = kind + "|" + t.FullName + "::" + n; if (!memberBack.TryGetValue(key, out var o) || !origSet.Contains(o)) continue; obf.Add(o); obfRenamed.Add(renamedKey.Contains(key)); } } Report(kind + "s", orig, obf, obfRenamed); } static void WithinType(ModuleDefMD mo, ModuleDefMD mb, Dictionary typeBack, Dictionary memberBack, HashSet renamedKey, string kind) { var origByType = new Dictionary>(); foreach (var t in TypeRows(mo)) origByType[t.FullName] = (kind == "method" ? t.Methods.Select(x => x.Name.String) : t.Fields.Select(x => x.Name.String)).ToList(); int ren = 0, renHit = 0; foreach (var t in TypeRows(mb)) { if (!typeBack.TryGetValue(t.FullName, out var ot) || !origByType.TryGetValue(ot, out var origList)) continue; var mapped = new List(); var isRen = new List(); foreach (var n in kind == "method" ? t.Methods.Select(x => x.Name.String) : t.Fields.Select(x => x.Name.String)) { string key = kind + "|" + t.FullName + "::" + n; if (!memberBack.TryGetValue(key, out var full)) continue; string bare = full.Substring(full.LastIndexOf("::", StringComparison.Ordinal) + 2); if (!origList.Contains(bare)) continue; mapped.Add(bare); isRen.Add(renamedKey.Contains(key)); } var keep = new HashSet(mapped); var filteredOrig = origList.Where(keep.Contains).ToList(); int n2 = Math.Min(filteredOrig.Count, mapped.Count); for (int i = 0; i < n2; i++) { if (!isRen[i]) continue; ren++; if (filteredOrig[i] == mapped[i]) renHit++; } } Console.WriteLine($"{kind + "s",-8} WITHIN-TYPE renamed holding their slot {renHit,6}/{ren,-6} {(ren == 0 ? 0 : 100.0 * renHit / ren),5:F2}%"); } static void Report(string label, List origAll, List obf, List renamed) { var keep = new HashSet(obf); var orig = origAll.Where(keep.Contains).ToList(); if (orig.Count != obf.Count) { Console.WriteLine($"{label,-8} SKEW: filtered original {orig.Count} vs obfuscated {obf.Count} - not comparable"); } int n = Math.Min(orig.Count, obf.Count); int all = 0, allHit = 0, ren = 0, renHit = 0; for (int i = 0; i < n; i++) { bool hit = orig[i] == obf[i]; all++; if (hit) allHit++; if (renamed[i]) { ren++; if (hit) renHit++; } } Console.WriteLine($"{label,-8} compared {orig.Count,6} of {origAll.Count,6} | aligned {allHit,6}/{all,-6} {(all == 0 ? 0 : 100.0 * allHit / all),5:F2}% | renamed aligned {renHit,5}/{ren,-6} {(ren == 0 ? 0 : 100.0 * renHit / ren),5:F2}%"); } }