From a1237f87d522311f81764a495592cb85bdbcaa46 Mon Sep 17 00:00:00 2001 From: olivato Date: Tue, 1 Sep 2026 16:13:56 +0100 Subject: [PATCH] Add symbolObfusSettings.enableMemberReordering to shuffle type and member declaration order --- .../ObfusPasses/SymbolObfus/MemberReorder.cs | 146 ++++++++++++++++++ .../SymbolObfus/MemberReorder.cs.meta | 11 ++ .../ObfusPasses/SymbolObfus/SymbolRename.cs | 8 + Editor/Settings/SymbolObfuscationSettings.cs | 5 + 4 files changed, 170 insertions(+) create mode 100644 Editor/ObfusPasses/SymbolObfus/MemberReorder.cs create mode 100644 Editor/ObfusPasses/SymbolObfus/MemberReorder.cs.meta diff --git a/Editor/ObfusPasses/SymbolObfus/MemberReorder.cs b/Editor/ObfusPasses/SymbolObfus/MemberReorder.cs new file mode 100644 index 0000000..e24bf8c --- /dev/null +++ b/Editor/ObfusPasses/SymbolObfus/MemberReorder.cs @@ -0,0 +1,146 @@ +// 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 Obfuz.Utils; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Obfuz.ObfusPasses.SymbolObfus +{ + public class MemberReorder + { + private readonly Random _random; + private readonly IObfuscationPolicy _renamePolicy; + + public MemberReorder(int seed, IObfuscationPolicy renamePolicy) + { + if (seed == 0) + { + throw new Exception("SymbolObfuscationSettings.enableMemberReordering requires a non-zero nameRandomSeed, otherwise the member layout is identical in every build."); + } + _random = new Random(seed); + _renamePolicy = renamePolicy; + } + + public void Process(List modules) + { + foreach (ModuleDef mod in modules) + { + Reorder(mod.Types, t => t.IsGlobalModuleType || IsPositionPinnedType(t)); + foreach (TypeDef type in mod.GetTypes().ToList()) + { + Reorder(type.NestedTypes, IsPositionPinnedType); + if (MayReorderFields(type)) + { + Reorder(type.Fields, f => !_renamePolicy.NeedRename(f)); + } + Reorder(type.Methods, IsPositionPinnedMethod); + Reorder(type.Properties, p => !_renamePolicy.NeedRename(p)); + Reorder(type.Events, e => !_renamePolicy.NeedRename(e)); + } + } + } + + private bool IsPositionPinnedType(TypeDef type) + { + if (!_renamePolicy.NeedRename(type)) + { + return true; + } + return type.Methods.Any(MetaUtil.HasRuntimeInitializeOnLoadMethodAttribute); + } + + private bool IsPositionPinnedMethod(MethodDef method) + { + if (method.IsVirtual || method.IsAbstract || method.HasOverrides) + { + return true; + } + if (MetaUtil.HasRuntimeInitializeOnLoadMethodAttribute(method)) + { + return true; + } + return !_renamePolicy.NeedRename(method); + } + + private static bool MayReorderFields(TypeDef type) + { + if (type.IsEnum || type.IsValueType) + { + return false; + } + if (type.IsSequentialLayout || type.IsExplicitLayout) + { + return false; + } + if (MetaUtil.IsScriptOrSerializableType(type)) + { + return false; + } + return !type.Fields.Any(f => f.FieldOffset != null); + } + + private void Reorder(IList members, Func isPinned) + { + int count = members.Count; + if (count < 2) + { + return; + } + + var order = new List(count); + var movableSlots = new List(); + for (int i = 0; i < count; i++) + { + T member = members[i]; + order.Add(member); + if (!isPinned(member)) + { + movableSlots.Add(i); + } + } + if (movableSlots.Count < 2) + { + return; + } + + var movable = movableSlots.Select(i => order[i]).ToList(); + for (int i = movable.Count - 1; i > 0; i--) + { + int j = _random.Next(i + 1); + T tmp = movable[i]; + movable[i] = movable[j]; + movable[j] = tmp; + } + for (int i = 0; i < movableSlots.Count; i++) + { + order[movableSlots[i]] = movable[i]; + } + + members.Clear(); + foreach (T member in order) + { + members.Add(member); + } + } + } +} diff --git a/Editor/ObfusPasses/SymbolObfus/MemberReorder.cs.meta b/Editor/ObfusPasses/SymbolObfus/MemberReorder.cs.meta new file mode 100644 index 0000000..7260556 --- /dev/null +++ b/Editor/ObfusPasses/SymbolObfus/MemberReorder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7a8d01b1f9a245ad830455326004cf10 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ObfusPasses/SymbolObfus/SymbolRename.cs b/Editor/ObfusPasses/SymbolObfus/SymbolRename.cs index 6802659..8038a45 100644 --- a/Editor/ObfusPasses/SymbolObfus/SymbolRename.cs +++ b/Editor/ObfusPasses/SymbolObfus/SymbolRename.cs @@ -51,6 +51,8 @@ namespace Obfuz.ObfusPasses.SymbolObfus private readonly VirtualMethodGroupCalculator _virtualMethodGroupCalculator; private readonly List _virtualMethods = new List(); private readonly List _customPolicyTypes; + private readonly bool _enableMemberReordering; + private readonly int _nameRandomSeed; class CustomAttributeInfo { @@ -69,6 +71,8 @@ namespace Obfuz.ObfusPasses.SymbolObfus _virtualMethodGroupCalculator = new VirtualMethodGroupCalculator(); _nameMaker = settings.debug ? NameMakerFactory.CreateDebugNameMaker() : NameMakerFactory.CreateNameMakerBaseASCIICharSet(settings.obfuscatedNamePrefix, settings.nameRandomSeed); _customPolicyTypes = settings.customRenamePolicyTypes; + _enableMemberReordering = settings.enableMemberReordering; + _nameRandomSeed = settings.nameRandomSeed; } public void Init() @@ -205,6 +209,10 @@ namespace Obfuz.ObfusPasses.SymbolObfus public void Process() { + if (_enableMemberReordering) + { + new MemberReorder(_nameRandomSeed, _renamePolicy).Process(_toObfuscatedModules); + } _renameRecordMap.Init(_toObfuscatedModules, _nameMaker); BuildVirtualMethodGroup(); RenameTypes(); diff --git a/Editor/Settings/SymbolObfuscationSettings.cs b/Editor/Settings/SymbolObfuscationSettings.cs index 59d0e45..08f1366 100644 --- a/Editor/Settings/SymbolObfuscationSettings.cs +++ b/Editor/Settings/SymbolObfuscationSettings.cs @@ -35,6 +35,7 @@ namespace Obfuz.Settings public bool keepUnknownSymbolInSymbolMappingFile; public string symbolMappingFile; public int nameRandomSeed; + public bool enableMemberReordering; public List ruleFiles; public List customRenamePolicyTypes; } @@ -62,6 +63,9 @@ namespace Obfuz.Settings [Tooltip("random seed for generated names. 0 keeps the original deterministic sequence; any other value randomises which name each symbol gets, so the mapping differs between builds")] public int nameRandomSeed = 0; + [Tooltip("shuffle the declaration order of types and members, seeded from nameRandomSeed, so a positional diff of two builds cannot align them. Requires a non-zero nameRandomSeed")] + public bool enableMemberReordering = false; + [Tooltip("debug symbol mapping file path, used for debugging purposes")] public string debugSymbolMappingFile = "Assets/Obfuz/SymbolObfus/symbol-mapping-debug.xml"; @@ -87,6 +91,7 @@ namespace Obfuz.Settings keepUnknownSymbolInSymbolMappingFile = keepUnknownSymbolInSymbolMappingFile, symbolMappingFile = GetSymbolMappingFile(), nameRandomSeed = nameRandomSeed, + enableMemberReordering = enableMemberReordering, ruleFiles = ruleFiles?.ToList() ?? new List(), customRenamePolicyTypes = customRenamePolicyTypes?.Select(typeName => ReflectionUtil.FindUniqueTypeInCurrentAppDomain(typeName)).ToList() ?? new List(), };