From f574f73f9226d10999d0214cf9eca4ee495af96b Mon Sep 17 00:00:00 2001 From: olivato Date: Tue, 1 Sep 2026 16:13:56 +0100 Subject: [PATCH] Add a self-check for the member reordering exclusions --- Tests~/MemberReorder/.gitignore | 4 + Tests~/MemberReorder/Main.cs | 182 ++++++++++++++++++ Tests~/MemberReorder/fixture/Fixture.cs | 69 +++++++ Tests~/MemberReorder/fixture/fixture.csproj | 7 + Tests~/MemberReorder/probe.csproj | 27 +++ Tests~/MemberReorder/run.sh | 8 + Tests~/MemberReorder/stubs.cs | 14 ++ Tests~/MemberReorder/unitystub/Unity.cs | 10 + .../MemberReorder/unitystub/unitystub.csproj | 6 + 9 files changed, 327 insertions(+) create mode 100644 Tests~/MemberReorder/.gitignore create mode 100644 Tests~/MemberReorder/Main.cs create mode 100644 Tests~/MemberReorder/fixture/Fixture.cs create mode 100644 Tests~/MemberReorder/fixture/fixture.csproj create mode 100644 Tests~/MemberReorder/probe.csproj create mode 100755 Tests~/MemberReorder/run.sh create mode 100644 Tests~/MemberReorder/stubs.cs create mode 100644 Tests~/MemberReorder/unitystub/Unity.cs create mode 100644 Tests~/MemberReorder/unitystub/unitystub.csproj diff --git a/Tests~/MemberReorder/.gitignore b/Tests~/MemberReorder/.gitignore new file mode 100644 index 0000000..5388f21 --- /dev/null +++ b/Tests~/MemberReorder/.gitignore @@ -0,0 +1,4 @@ +bin/ +obj/ +reordered.dll +UnityEngine.CoreModule.dll diff --git a/Tests~/MemberReorder/Main.cs b/Tests~/MemberReorder/Main.cs new file mode 100644 index 0000000..3ce2943 --- /dev/null +++ b/Tests~/MemberReorder/Main.cs @@ -0,0 +1,182 @@ +using dnlib.DotNet; +using Obfuz.ObfusPasses.SymbolObfus; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +class AllowAll : IObfuscationPolicy +{ + public bool NeedRename(TypeDef t) => t.Name != "Plain08"; + public bool NeedRename(MethodDef m) => m.Name != "C"; + public bool NeedRename(FieldDef f) => f.Name != "f3"; + public bool NeedRename(PropertyDef p) => p.Name != "Prop2"; + public bool NeedRename(EventDef e) => e.Name != "E2"; +} + +static class Program +{ + static int failures; + static void Check(bool ok, string what) + { + Console.WriteLine((ok ? "PASS " : "FAIL ") + what); + if (!ok) failures++; + } + + static ModuleDefMD Load(string path) + { + var ctx = ModuleDef.CreateModuleContext(); + ((AssemblyResolver)ctx.AssemblyResolver).EnableTypeDefCache = true; + ((AssemblyResolver)ctx.AssemblyResolver).DefaultModuleContext = ctx; + var res = (AssemblyResolver)ctx.AssemblyResolver; + res.PreSearchPaths.Add(Path.GetDirectoryName(Path.GetFullPath(path))); + res.PreSearchPaths.Add(Path.GetDirectoryName(typeof(object).Assembly.Location)); + res.PostSearchPaths.Add(Path.GetDirectoryName(typeof(object).Assembly.Location)); + return ModuleDefMD.Load(path, ctx); + } + + static Dictionary> Snapshot(ModuleDefMD mod) + { + var d = new Dictionary>(); + d["#types"] = mod.Types.Select(t => t.FullName).ToList(); + foreach (var t in mod.GetTypes()) + { + d[t.FullName + "#f"] = t.Fields.Select(f => f.Name.String).ToList(); + d[t.FullName + "#m"] = t.Methods.Select(m => m.Name.String).ToList(); + d[t.FullName + "#p"] = t.Properties.Select(p => p.Name.String).ToList(); + d[t.FullName + "#e"] = t.Events.Select(e => e.Name.String).ToList(); + } + return d; + } + + static bool Same(List a, List b) => a.SequenceEqual(b); + + static readonly HashSet Pinned = new HashSet { "Plain08", "C", "f3", "Prop2", "E2", "V1", "V2", "V3", "", "Boot" }; + + static bool IsRenamable(string key, string name) + { + if (Pinned.Contains(name) || name.StartsWith(".")) return false; + if (key.StartsWith("Fx.Colour") || key.StartsWith("Fx.SeqLayout#f") || key.StartsWith("Fx.ExpLayout#f") + || key.StartsWith("Fx.PlainStruct#f") || key.StartsWith("Fx.SerialisableData#f") + || key.StartsWith("Fx.Script#f") || key.StartsWith("Fx.ScriptChild#f") || key.StartsWith("Fx.SoData#f")) return false; + return true; + } + + static int Main() + { + string dll = "fixture/bin/Debug/netstandard2.0/fixture.dll"; + var before = Snapshot(Load(dll)); + + var mod1 = Load(dll); + new MemberReorder(1234, new AllowAll()).Process(new List { mod1 }); + var after1 = Snapshot(mod1); + + var mod2 = Load(dll); + new MemberReorder(9999, new AllowAll()).Process(new List { mod2 }); + var after2 = Snapshot(mod2); + + // exclusions: order must be byte-identical to the original + foreach (var key in new[] { + "Fx.Colour#f", // enum members + "Fx.SeqLayout#f", // [StructLayout(Sequential)] + "Fx.ExpLayout#f", // [StructLayout(Explicit)] + [FieldOffset] + "Fx.PlainStruct#f", // implicitly sequential struct + "Fx.SerialisableData#f", // [Serializable] + "Fx.Script#f", // MonoBehaviour serialised fields + "Fx.ScriptChild#f", // MonoBehaviour subclass + "Fx.SoData#f", // ScriptableObject + }) + { + Check(Same(before[key], after1[key]), "field order pinned: " + key); + } + + // virtual methods keep their slot index + foreach (var t in new[] { "Fx.BaseVirt", "Fx.DerivedVirt" }) + { + var b = before[t + "#m"]; + var a = after1[t + "#m"]; + bool ok = true; + for (int i = 0; i < b.Count; i++) + { + if (b[i].StartsWith("V") && a[i] != b[i]) ok = false; + } + Check(ok, "virtual slots pinned: " + t); + } + + // pinned-by-policy members keep their index + Check(before["Fx.Plain01#f"].IndexOf("f3") == after1["Fx.Plain01#f"].IndexOf("f3"), "policy-pinned field index held"); + Check(before["Fx.Plain01#m"].IndexOf("C") == after1["Fx.Plain01#m"].IndexOf("C"), "policy-pinned method index held"); + Check(before["Fx.Plain09#p"].IndexOf("Prop2") == after1["Fx.Plain09#p"].IndexOf("Prop2"), "policy-pinned property index held"); + Check(before["Fx.Plain10#e"].IndexOf("E2") == after1["Fx.Plain10#e"].IndexOf("E2"), "policy-pinned event index held"); + Check(before["#types"].IndexOf("Fx.Plain08") == after1["#types"].IndexOf("Fx.Plain08"), "policy-pinned type index held"); + Check(before["#types"].IndexOf("Fx.BootA") == after1["#types"].IndexOf("Fx.BootA"), "RuntimeInitializeOnLoadMethod type index held"); + Check(after1["#types"][0] == "", " stays at index 0"); + + // membership is preserved everywhere + bool memberOk = before.Keys.All(k => after1.ContainsKey(k) + && before[k].OrderBy(x => x, StringComparer.Ordinal).SequenceEqual(after1[k].OrderBy(x => x, StringComparer.Ordinal))); + Check(memberOk, "no member lost or duplicated"); + + // the point of the plan: positional alignment must break, and rotate per seed + Check(!Same(before["#types"], after1["#types"]), "type order changed vs original"); + Check(!Same(after1["#types"], after2["#types"]), "type order differs between seeds"); + int movedFields = before["Fx.Plain01#f"].Where((n, i) => after1["Fx.Plain01#f"][i] != n).Count(); + Check(movedFields > 0, "plain class fields reordered"); + int movedMethods = before["Fx.Plain02#m"].Where((n, i) => after1["Fx.Plain02#m"][i] != n).Count(); + Check(movedMethods > 0, "plain class methods reordered"); + + // the reordered module still writes and reloads + var outPath = "reordered.dll"; + mod1.Write(outPath); + var reloaded = Load(outPath); + Check(Same(after1["#types"], reloaded.Types.Select(t => t.FullName).ToList()), "written module preserves new type order"); + Check(reloaded.GetTypes().Count() == mod1.GetTypes().Count(), "written module keeps every type"); + var rd = Snapshot(reloaded); + Check(rd.Keys.All(k => Same(after1[k], rd[k])), "written module preserves every member order"); + var owned = reloaded.GetTypes().All(t => t.Module != null + && t.Fields.All(f => f.DeclaringType == t) && t.Methods.All(m => m.DeclaringType == t)); + Check(owned, "declaring-type back-pointers intact after reorder"); + + var asm = System.Reflection.Assembly.LoadFrom(Path.GetFullPath(outPath)); + var derived = asm.GetType("Fx.DerivedVirt"); + var instance = Activator.CreateInstance(derived); + var baseType = asm.GetType("Fx.BaseVirt"); + Check((int)baseType.GetMethod("V1").Invoke(instance, null) == 11 + && (int)baseType.GetMethod("V2").Invoke(instance, null) == 22 + && (int)baseType.GetMethod("V3").Invoke(instance, null) == 33, "virtual dispatch still resolves to the override"); + var p09 = asm.GetType("Fx.Plain09"); + var p09i = Activator.CreateInstance(p09); + p09.GetProperty("Prop3").SetValue(p09i, 7); + Check((int)p09.GetProperty("Prop3").GetValue(p09i) == 7, "property accessors still bound after reorder"); + var exp = asm.GetType("Fx.ExpLayout"); + var expi = Activator.CreateInstance(exp); + exp.GetField("b").SetValue(expi, 5); + Check((int)exp.GetField("b").GetValue(expi) == 5 && (int)exp.GetField("a").GetValue(expi) == 0, "explicit-layout offsets still honoured"); + var seqFields = asm.GetType("Fx.SeqLayout").GetFields().Select(f => f.Name).ToList(); + Check(seqFields.SequenceEqual(new[] { "a", "b", "c", "d" }), "sequential-layout field order unchanged at runtime"); + var scriptFields = asm.GetType("Fx.Script").GetFields().Select(f => f.Name).ToList(); + Check(scriptFields.SequenceEqual(new[] { "hp", "title", "speed", "armed" }), "MonoBehaviour serialised field order unchanged at runtime"); + var enumNames = Enum.GetNames(asm.GetType("Fx.Colour")).ToList(); + Check(enumNames.SequenceEqual(new[] { "Red", "Green", "Blue", "Alpha", "Cyan", "Magenta" }), "enum member order unchanged at runtime"); + + var policy = new AllowAll(); + int renamed = 0, aligned = 0; + foreach (var key in before.Keys) + { + var a = after1[key]; + var b = after2[key]; + for (int i = 0; i < a.Count; i++) + { + if (!IsRenamable(key, a[i])) continue; + renamed++; + if (a[i] == b[i]) aligned++; + } + } + double rate = renamed == 0 ? 0 : (double)aligned / renamed; + Console.WriteLine($"positional recovery of renamed members across two seeds: {aligned}/{renamed} = {rate:P1}"); + Check(rate < 0.35, "positional differ cannot align renamed members between two builds"); + + Console.WriteLine(failures == 0 ? "ALL PASS" : failures + " FAILURE(S)"); + return failures == 0 ? 0 : 1; + } +} diff --git a/Tests~/MemberReorder/fixture/Fixture.cs b/Tests~/MemberReorder/fixture/Fixture.cs new file mode 100644 index 0000000..cf27002 --- /dev/null +++ b/Tests~/MemberReorder/fixture/Fixture.cs @@ -0,0 +1,69 @@ +using System; +using System.Runtime.InteropServices; +using UnityEngine; + +namespace Fx +{ + public enum Colour { Red, Green, Blue, Alpha, Cyan, Magenta } + + [StructLayout(LayoutKind.Sequential)] + public class SeqLayout { public int a; public int b; public int c; public int d; } + + [StructLayout(LayoutKind.Explicit)] + public class ExpLayout + { + [FieldOffset(0)] public int a; + [FieldOffset(4)] public int b; + [FieldOffset(8)] public int c; + } + + public struct PlainStruct { public int a; public int b; public int c; public int d; } + + [Serializable] + public class SerialisableData { public int a; public string b; public float c; public bool d; } + + public class Script : MonoBehaviour + { + public int hp; public string title; public float speed; public bool armed; + public void M1() { } public void M2() { } public void M3() { } public void M4() { } + } + + public class ScriptChild : Script { public int extra; public string more; public double yet; } + + public class SoData : ScriptableObject { public int x; public string y; public float z; } + + public class BaseVirt + { + public virtual int V1() => 1; + public virtual int V2() => 2; + public virtual int V3() => 3; + public int P1() => 1; + public int P2() => 2; + public int P3() => 3; + public int P4() => 4; + } + + public class DerivedVirt : BaseVirt + { + public override int V1() => 11; + public override int V2() => 22; + public override int V3() => 33; + public int Q1() => 1; + public int Q2() => 2; + public int Q3() => 3; + public int Q4() => 4; + } + + public class BootA { [RuntimeInitializeOnLoadMethod] public static void Boot() { } } + + public class Plain01 { public int f1, f2, f3, f4, f5, f6; public void A(){} public void B(){} public void C(){} public void D(){} public void E(){} public void F(){} } + public class Plain02 { public int f1, f2, f3, f4, f5, f6; public void A(){} public void B(){} public void C(){} public void D(){} public void E(){} public void F(){} } + public class Plain03 { public int f1, f2, f3, f4, f5, f6; public void A(){} public void B(){} public void C(){} public void D(){} public void E(){} public void F(){} } + public class Plain04 { public int f1, f2, f3, f4, f5, f6; public void A(){} public void B(){} public void C(){} public void D(){} public void E(){} public void F(){} } + public class Plain05 { public int f1, f2, f3, f4, f5, f6; public void A(){} public void B(){} public void C(){} public void D(){} public void E(){} public void F(){} } + public class Plain06 { public int f1, f2, f3, f4, f5, f6; public void A(){} public void B(){} public void C(){} public void D(){} public void E(){} public void F(){} } + public class Plain07 { public int f1, f2, f3, f4, f5, f6; public void A(){} public void B(){} public void C(){} public void D(){} public void E(){} public void F(){} } + public class Plain08 { public int f1, f2, f3, f4, f5, f6; public void A(){} public void B(){} public void C(){} public void D(){} public void E(){} public void F(){} } + public class Plain09 { public int Prop1 {get;set;} public int Prop2 {get;set;} public int Prop3 {get;set;} public int Prop4 {get;set;} } + public class Plain10 { public event Action E1; public event Action E2; public event Action E3; public event Action E4; public void Fire(){E1();E2();E3();E4();} } +} diff --git a/Tests~/MemberReorder/fixture/fixture.csproj b/Tests~/MemberReorder/fixture/fixture.csproj new file mode 100644 index 0000000..c62aee5 --- /dev/null +++ b/Tests~/MemberReorder/fixture/fixture.csproj @@ -0,0 +1,7 @@ + + + netstandard2.0 + fixture + + + diff --git a/Tests~/MemberReorder/probe.csproj b/Tests~/MemberReorder/probe.csproj new file mode 100644 index 0000000..1c67066 --- /dev/null +++ b/Tests~/MemberReorder/probe.csproj @@ -0,0 +1,27 @@ + + + Exe + net7.0 + disable + probe + probe + false + CS0168;CS0219;CS0414;CS1998;CS0162 + latest + + + ../../Plugins/dnlib.dll + + + + + + + + + + + + + + diff --git a/Tests~/MemberReorder/run.sh b/Tests~/MemberReorder/run.sh new file mode 100755 index 0000000..69ce112 --- /dev/null +++ b/Tests~/MemberReorder/run.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +dotnet build fixture/fixture.csproj -v q --nologo +dotnet build probe.csproj -v q --nologo +cp unitystub/bin/Debug/netstandard2.0/UnityEngine.CoreModule.dll bin/Debug/net7.0/ +cp unitystub/bin/Debug/netstandard2.0/UnityEngine.CoreModule.dll . +dotnet bin/Debug/net7.0/probe.dll diff --git a/Tests~/MemberReorder/stubs.cs b/Tests~/MemberReorder/stubs.cs new file mode 100644 index 0000000..bc5e17b --- /dev/null +++ b/Tests~/MemberReorder/stubs.cs @@ -0,0 +1,14 @@ +namespace UnityEngine.Assertions { + public static class Assert { + public static void IsTrue(bool c) { if(!c) throw new System.Exception("assert"); } + public static void IsTrue(bool c, string m) { if(!c) throw new System.Exception(m); } + public static void IsNotNull(object o) { if(o==null) throw new System.Exception("null"); } + } +} +namespace UnityEngine { + public static class Debug { + public static void Log(object o) { System.Console.WriteLine(o); } + public static void LogWarning(object o) { System.Console.WriteLine(o); } + public static void LogError(object o) { System.Console.WriteLine(o); } + } +} diff --git a/Tests~/MemberReorder/unitystub/Unity.cs b/Tests~/MemberReorder/unitystub/Unity.cs new file mode 100644 index 0000000..487c40b --- /dev/null +++ b/Tests~/MemberReorder/unitystub/Unity.cs @@ -0,0 +1,10 @@ +namespace UnityEngine +{ + public class Object { } + public class Component : Object { } + public class Behaviour : Component { } + public class MonoBehaviour : Behaviour { } + public class ScriptableObject : Object { } + [System.AttributeUsage(System.AttributeTargets.Method)] + public class RuntimeInitializeOnLoadMethodAttribute : System.Attribute { } +} diff --git a/Tests~/MemberReorder/unitystub/unitystub.csproj b/Tests~/MemberReorder/unitystub/unitystub.csproj new file mode 100644 index 0000000..cda1142 --- /dev/null +++ b/Tests~/MemberReorder/unitystub/unitystub.csproj @@ -0,0 +1,6 @@ + + + netstandard2.0 + UnityEngine.CoreModule + +