diff --git a/Tests~/AlignmentCheck/.gitignore b/Tests~/AlignmentCheck/.gitignore new file mode 100644 index 0000000..cd42ee3 --- /dev/null +++ b/Tests~/AlignmentCheck/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ diff --git a/Tests~/AlignmentCheck/Align.cs b/Tests~/AlignmentCheck/Align.cs new file mode 100644 index 0000000..f2b6219 --- /dev/null +++ b/Tests~/AlignmentCheck/Align.cs @@ -0,0 +1,201 @@ +// 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}%"); + } +} diff --git a/Tests~/AlignmentCheck/README.md b/Tests~/AlignmentCheck/README.md new file mode 100644 index 0000000..d3d95d2 --- /dev/null +++ b/Tests~/AlignmentCheck/README.md @@ -0,0 +1,11 @@ +Measures whether a positional differ can align an obfuscated assembly against its +pre-obfuscation original, which is the attack member reordering exists to defeat. + + dotnet run --project align.csproj -- \ + /Library/Obfuz//OriginalAssemblies/Assembly-CSharp.dll \ + /Library/Obfuz//ObfuscatedAssemblies/Assembly-CSharp.dll \ + /Assets/Obfuz/SymbolObfus/symbol-mapping.xml + +Reports two different things. The whole-dump figure is the acceptance criterion. The +within-type figure is what a structural attack that re-identifies the declaring type +first would get instead, and reordering does not address it. diff --git a/Tests~/AlignmentCheck/align.csproj b/Tests~/AlignmentCheck/align.csproj new file mode 100644 index 0000000..14c6883 --- /dev/null +++ b/Tests~/AlignmentCheck/align.csproj @@ -0,0 +1,13 @@ + + + Exe + net7.0 + align + latest + false + + + ../../Plugins/dnlib.dll + + +