From 3f00d5ca91411eca39a6e79b765bce7e52664d57 Mon Sep 17 00:00:00 2001 From: walon Date: Fri, 13 Jun 2025 21:00:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20ReflectionCompatibilityDet?= =?UTF-8?q?ector?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReflectionCompatibilityDetector.cs | 285 ++++++++++++++++++ .../ReflectionCompatibilityDetector.cs.meta | 11 + .../ObfusPasses/SymbolObfus/SymbolRename.cs | 7 + Editor/Settings/SymbolObfuscationSettings.cs | 5 + 4 files changed, 308 insertions(+) create mode 100644 Editor/ObfusPasses/SymbolObfus/ReflectionCompatibilityDetector.cs create mode 100644 Editor/ObfusPasses/SymbolObfus/ReflectionCompatibilityDetector.cs.meta diff --git a/Editor/ObfusPasses/SymbolObfus/ReflectionCompatibilityDetector.cs b/Editor/ObfusPasses/SymbolObfus/ReflectionCompatibilityDetector.cs new file mode 100644 index 0000000..240a846 --- /dev/null +++ b/Editor/ObfusPasses/SymbolObfus/ReflectionCompatibilityDetector.cs @@ -0,0 +1,285 @@ +using dnlib.DotNet; +using dnlib.DotNet.Emit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace Obfuz.ObfusPasses.SymbolObfus +{ + public class ReflectionCompatibilityDetector + { + private readonly List _obfuscatedAndNotObfuscatedModules; + private readonly IObfuscationPolicy _renamePolicy; + + public ReflectionCompatibilityDetector(List obfuscatedAndNotObfuscatedModules, IObfuscationPolicy renamePolicy) + { + _obfuscatedAndNotObfuscatedModules = obfuscatedAndNotObfuscatedModules; + _renamePolicy = renamePolicy; + } + + public void Analyze() + { + foreach (ModuleDef mod in _obfuscatedAndNotObfuscatedModules) + { + foreach (TypeDef type in mod.GetTypes()) + { + foreach (MethodDef method in type.Methods) + { + AnalyzeMethod(method); + } + } + } + } + + private MethodDef _curCallingMethod; + private IList _curInstructions; + private int _curInstIndex; + + private void AnalyzeMethod(MethodDef method) + { + if (!method.HasBody) + { + return; + } + _curCallingMethod = method; + _curInstructions = method.Body.Instructions; + _curInstIndex = 0; + for (int n = _curInstructions.Count; _curInstIndex < n; _curInstIndex++) + { + var inst = _curInstructions[_curInstIndex]; + switch (inst.OpCode.Code) + { + case Code.Call: + { + AnalyzeCall(inst.Operand as IMethod); + break; + } + case Code.Callvirt: + { + ITypeDefOrRef constrainedType = null; + if (_curInstIndex > 0) + { + var prevInst = _curInstructions[_curInstIndex - 1]; + if (prevInst.OpCode.Code == Code.Constrained) + { + constrainedType = prevInst.Operand as ITypeDefOrRef; + } + } + AnalyzeCallvir(inst.Operand as IMethod, constrainedType); + break; + } + } + } + } + + private ITypeDefOrRef FindLatestTypeOf(int backwardFindInstructionCount) + { + // find sequence ldtoken ; + for (int i = 2; i <= backwardFindInstructionCount; i++) + { + int index = _curInstIndex - i; + if (index < 0) + { + return null; + } + Instruction inst1 = _curInstructions[index]; + Instruction inst2 = _curInstructions[index + 1]; + if (inst1.OpCode.Code == Code.Ldtoken && inst2.OpCode.Code == Code.Call) + { + if (!(inst1.Operand is ITypeDefOrRef typeDefOrRef)) + { + continue; + } + IMethod method = inst2.Operand as IMethod; + if (method.Name == "GetTypeFromHandle" && method.DeclaringType.FullName == "System.Type") + { + // Ldtoken ; Call System.Type.GetTypeFromHandle(System.RuntimeTypeHandle handle) + return typeDefOrRef; + } + } + } + return null; + } + + private void AnalyzeCall(IMethod calledMethod) + { + TypeDef callType = calledMethod.DeclaringType.ResolveTypeDef(); + if (callType == null) + { + return; + } + switch (callType.FullName) + { + case "System.Enum": + { + AnalyzeEnum(calledMethod, callType); + break; + } + case "System.Type": + { + AnalyzeGetType(calledMethod, callType); + break; + } + case "System.Reflection.Assembly": + { + if (calledMethod.Name == "GetType") + { + AnalyzeGetType(calledMethod, callType); + } + break; + } + } + } + + private void AnalyzeCallvir(IMethod calledMethod, ITypeDefOrRef constrainedType) + { + TypeDef callType = calledMethod.DeclaringType.ResolveTypeDef(); + if (callType == null) + { + return; + } + string calledMethodName = calledMethod.Name; + switch (callType.FullName) + { + case "System.Object": + { + if (calledMethodName == "ToString") + { + if (constrainedType != null) + { + TypeDef enumTypeDef = constrainedType.ResolveTypeDef(); + if (enumTypeDef != null && enumTypeDef.IsEnum && _renamePolicy.NeedRename(enumTypeDef)) + { + Debug.LogError($"[ReflectionCompatibilityDetector] Reflection compatibility issue in {_curCallingMethod}: Enum.ToString() T:{enumTypeDef.FullName} is renamed."); + } + } + } + break; + } + case "System.Type": + { + AnalyzeGetType(calledMethod, callType); + break; + } + } + } + + private TypeSig GetMethodGenericParameter(IMethod method) + { + if (method is MethodSpec ms) + { + return ms.GenericInstMethodSig.GenericArguments.FirstOrDefault(); + } + else + { + return null; + } + } + + private void AnalyzeEnum(IMethod method, TypeDef typeDef) + { + const int extraSearchInstructionCount = 3; + TypeDef parseType = GetMethodGenericParameter(method)?.ToTypeDefOrRef()?.ResolveTypeDef(); + switch (method.Name) + { + case "Parse": + { + if (parseType != null) + { + // Enum.Parse(string name) or Enum.Parse(string name, bool caseInsensitive) + if (_renamePolicy.NeedRename(parseType)) + { + Debug.LogError($"[ReflectionCompatibilityDetector] Reflection compatibility issue in {_curCallingMethod}: Enum.Parse T:{parseType.FullName} is renamed."); + } + } + else + { + // Enum.Parse(Type type, string name) or Enum.Parse(Type type, string name, bool ignoreCase) + TypeDef enumType = FindLatestTypeOf(method.GetParamCount() + extraSearchInstructionCount)?.ResolveTypeDef(); + if (enumType != null && enumType.IsEnum && _renamePolicy.NeedRename(enumType)) + { + Debug.LogError($"[ReflectionCompatibilityDetector] Reflection compatibility issue in {_curCallingMethod}: Enum.Parse argument type:{enumType.FullName} is renamed."); + } + else + { + Debug.LogWarning($"[ReflectionCompatibilityDetector] Reflection compatibility issue in {_curCallingMethod}: Enum.Parse argument `type` should not be renamed."); + } + } + break; + } + case "TryParse": + { + if (parseType != null) + { + // Enum.TryParse(string name, out T result) or Enum.TryParse(string name, bool ignoreCase, out T result) + if (_renamePolicy.NeedRename(parseType)) + { + Debug.LogError($"[ReflectionCompatibilityDetector] Reflection compatibility issue in {_curCallingMethod}: Enum.TryParse T:{parseType.FullName} is renamed."); + } + } + else + { + throw new Exception("impossible"); + } + break; + } + case "GetName": + { + // Enum.GetName(Type type, object value) + TypeDef enumType = FindLatestTypeOf(method.GetParamCount() + extraSearchInstructionCount)?.ResolveTypeDef(); + if (enumType != null && enumType.IsEnum && enumType.Fields.Any(f => _renamePolicy.NeedRename(f))) + { + Debug.LogError($"[ReflectionCompatibilityDetector] Reflection compatibility issue in {_curCallingMethod}: Enum.GetName field of type:{enumType.FullName} is renamed."); + } + else + { + Debug.LogWarning($"[ReflectionCompatibilityDetector] Reflection compatibility issue in {_curCallingMethod}: Enum.GetName field of argument `type` should not be renamed."); + } + break; + } + case "GetNames": + { + // Enum.GetNames(Type type) + TypeDef enumType = FindLatestTypeOf(method.GetParamCount() + extraSearchInstructionCount)?.ResolveTypeDef(); + if (enumType != null && enumType.IsEnum && enumType.Fields.Any(f => _renamePolicy.NeedRename(f))) + { + Debug.LogError($"[ReflectionCompatibilityDetector] Reflection compatibility issue in {_curCallingMethod}: Enum.GetNams field of type:{enumType.FullName} is renamed."); + } + else + { + Debug.LogWarning($"[ReflectionCompatibilityDetector] Reflection compatibility issue in {_curCallingMethod}: Enum.GetNames field of argument `type` should not be renamed."); + } + break; + } + } + } + + private void AnalyzeGetType(IMethod method, TypeDef declaringType) + { + switch (method.Name) + { + case "GetType": + { + Debug.LogWarning($"[ReflectionCompatibilityDetector] Reflection compatibility issue in {_curCallingMethod}: Type.GetType argument `typeName` should not be renamed."); + break; + } + case "GetField": + case "GetFields": + case "GetMethod": + case "GetMethods": + case "GetProperty": + case "GetProperties": + case "GetEvent": + case "GetEvents": + case "GetMembers": + { + Debug.LogWarning($"[ReflectionCompatibilityDetector] Reflection compatibility issue in {_curCallingMethod}: called method:{method} the members of type should not be renamed."); + break; + } + } + } + } +} diff --git a/Editor/ObfusPasses/SymbolObfus/ReflectionCompatibilityDetector.cs.meta b/Editor/ObfusPasses/SymbolObfus/ReflectionCompatibilityDetector.cs.meta new file mode 100644 index 0000000..f5212e1 --- /dev/null +++ b/Editor/ObfusPasses/SymbolObfus/ReflectionCompatibilityDetector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b98c97a4d1db5d945bce0fdd2bd09202 +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 537b0bc..3a978b9 100644 --- a/Editor/ObfusPasses/SymbolObfus/SymbolRename.cs +++ b/Editor/ObfusPasses/SymbolObfus/SymbolRename.cs @@ -15,6 +15,7 @@ namespace Obfuz.ObfusPasses.SymbolObfus public class SymbolRename { private readonly bool _useConsistentNamespaceObfuscation; + private readonly bool _detectReflectionCompatibility; private readonly List _obfuscationRuleFiles; private readonly string _mappingXmlPath; @@ -42,6 +43,7 @@ namespace Obfuz.ObfusPasses.SymbolObfus public SymbolRename(SymbolObfuscationSettingsFacade settings) { _useConsistentNamespaceObfuscation = settings.useConsistentNamespaceObfuscation; + _detectReflectionCompatibility = settings.detectReflectionCompatibility; _mappingXmlPath = settings.symbolMappingFile; _obfuscationRuleFiles = settings.ruleFiles.ToList(); _renameRecordMap = new RenameRecordMap(settings.symbolMappingFile, settings.debug, settings.keepUnknownSymbolInSymbolMappingFile); @@ -178,6 +180,11 @@ namespace Obfuz.ObfusPasses.SymbolObfus { _renameRecordMap.Init(_toObfuscatedModules, _nameMaker); PrecomputeNeedRename(); + if (_detectReflectionCompatibility) + { + var reflectionCompatibilityDetector = new ReflectionCompatibilityDetector(_obfuscatedAndNotObfuscatedModules, _renamePolicy); + reflectionCompatibilityDetector.Analyze(); + } RenameTypes(); RenameFields(); RenameMethods(); diff --git a/Editor/Settings/SymbolObfuscationSettings.cs b/Editor/Settings/SymbolObfuscationSettings.cs index a2725fc..f661c4d 100644 --- a/Editor/Settings/SymbolObfuscationSettings.cs +++ b/Editor/Settings/SymbolObfuscationSettings.cs @@ -11,6 +11,7 @@ namespace Obfuz.Settings public bool debug; public string obfuscatedNamePrefix; public bool useConsistentNamespaceObfuscation; + public bool detectReflectionCompatibility; public bool keepUnknownSymbolInSymbolMappingFile; public string symbolMappingFile; public List ruleFiles; @@ -28,6 +29,9 @@ namespace Obfuz.Settings [Tooltip("obfuscate same namespace to one name")] public bool useConsistentNamespaceObfuscation = true; + [Tooltip("detect reflection compatibility, if true, will detect if the obfuscated name is compatibility with reflection, such as Type.GetType(), Enum.Parse(), etc.")] + public bool detectReflectionCompatibility = true; + [Tooltip("keep unknown symbol in symbol mapping file, if false, unknown symbol will be removed from mapping file")] public bool keepUnknownSymbolInSymbolMappingFile = true; @@ -55,6 +59,7 @@ namespace Obfuz.Settings debug = debug, obfuscatedNamePrefix = obfuscatedNamePrefix, useConsistentNamespaceObfuscation = useConsistentNamespaceObfuscation, + detectReflectionCompatibility = detectReflectionCompatibility, keepUnknownSymbolInSymbolMappingFile = keepUnknownSymbolInSymbolMappingFile, symbolMappingFile = GetSymbolMappingFile(), ruleFiles = ruleFiles?.ToList() ?? new List(),