using dnlib.DotNet; using Obfuz.Utils; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Xml; using UnityEngine; namespace Obfuz.ObfusPasses.ConstEncrypt { public class ConfigurableEncryptPolicy : EncryptPolicyBase { private readonly List _toObfuscatedAssemblyNames; class NumberRange where T : struct { public readonly T? min; public readonly T? max; public NumberRange(T? min, T? max) { this.min = min; this.max = max; } } class ObfuscationRule { public bool? disableEncrypt; public bool? encryptInt; public bool? encryptLong; public bool? encryptFloat; public bool? encryptDouble; public bool? encryptArray; public bool? encryptString; public bool? encryptConstInLoop; public bool? encryptStringInLoop; public bool? cacheConstInLoop; public bool? cacheConstNotInLoop; public bool? cacheStringInLoop; public bool? cacheStringNotInLoop; public HashSet notEncryptInts = new HashSet(); public HashSet notEncryptLongs = new HashSet(); public HashSet notEncryptStrings = new HashSet(); public List> notEncryptIntRanges = new List>(); public List> notEncryptLongRanges = new List>(); public List> notEncryptFloatRanges = new List>(); public List> notEncryptDoubleRanges = new List>(); public List> notEncryptArrayLengthRanges = new List>(); public List> notEncryptStringLengthRanges = new List>(); public void InheritParent(ObfuscationRule parentRule) { if (disableEncrypt == null) disableEncrypt = parentRule.disableEncrypt; if (encryptInt == null) encryptInt = parentRule.encryptInt; if (encryptLong == null) encryptLong = parentRule.encryptLong; if (encryptFloat == null) encryptFloat = parentRule.encryptFloat; if (encryptDouble == null) encryptDouble = parentRule.encryptDouble; if (encryptArray == null) encryptArray = parentRule.encryptArray; if (encryptString == null) encryptString = parentRule.encryptString; if (encryptConstInLoop == null) encryptConstInLoop = parentRule.encryptConstInLoop; if (encryptStringInLoop == null) encryptStringInLoop = parentRule.encryptStringInLoop; if (cacheConstInLoop == null) cacheConstInLoop = parentRule.cacheConstInLoop; if (cacheConstNotInLoop == null) cacheConstNotInLoop = parentRule.cacheConstNotInLoop; if (cacheStringInLoop == null) cacheStringInLoop = parentRule.cacheStringInLoop; if (cacheStringNotInLoop == null) cacheStringNotInLoop = parentRule.cacheStringNotInLoop; notEncryptInts.AddRange(parentRule.notEncryptInts); notEncryptLongs.AddRange(parentRule.notEncryptLongs); notEncryptStrings.AddRange(parentRule.notEncryptStrings); notEncryptIntRanges.AddRange(parentRule.notEncryptIntRanges); notEncryptLongRanges.AddRange(parentRule.notEncryptLongRanges); notEncryptFloatRanges.AddRange(parentRule.notEncryptFloatRanges); notEncryptDoubleRanges.AddRange(parentRule.notEncryptDoubleRanges); notEncryptArrayLengthRanges.AddRange(parentRule.notEncryptArrayLengthRanges); notEncryptStringLengthRanges.AddRange(parentRule.notEncryptStringLengthRanges); } } class MethodSpec { public string name; public NameMatcher nameMatcher; public ObfuscationRule rule; } class TypeSpec { public string name; public NameMatcher nameMatcher; public ObfuscationRule rule; public List methods = new List(); } class AssemblySpec { public string name; public ObfuscationRule rule; public List types = new List(); } private static readonly ObfuscationRule s_default = new ObfuscationRule() { disableEncrypt = false, encryptInt = true, encryptLong = true, encryptFloat = true, encryptDouble = true, encryptArray = true, encryptString = true, encryptConstInLoop = true, encryptStringInLoop = true, cacheConstInLoop = true, cacheConstNotInLoop = false, cacheStringInLoop = true, cacheStringNotInLoop = true, }; private ObfuscationRule _global; private readonly Dictionary _assemblySpecs = new Dictionary(); private readonly Dictionary _methodRuleCache = new Dictionary(); public ConfigurableEncryptPolicy(List toObfuscatedAssemblyNames, List xmlConfigFiles) { _toObfuscatedAssemblyNames = toObfuscatedAssemblyNames; LoadConfigs(xmlConfigFiles); InheritParentRules(); } private void LoadConfigs(List configFiles) { if (configFiles == null || configFiles.Count == 0) { Debug.LogWarning($"ConfigurableObfuscationPolicy::LoadConfigs configFiles is empty, using default policy"); return; } foreach (var configFile in configFiles) { if (string.IsNullOrEmpty(configFile)) { throw new Exception($"ObfuzSettings.constEncryptSettings.configFiles contains empty file name"); } LoadConfig(configFile); } } private void LoadConfig(string configFile) { if (string.IsNullOrEmpty(configFile)) { Debug.LogWarning($"ConfigurableObfuscationPolicy::LoadConfig configFile is empty, using default policy"); return; } Debug.Log($"ConfigurableObfuscationPolicy::LoadConfig {configFile}"); var doc = new XmlDocument(); doc.Load(configFile); var root = doc.DocumentElement; if (root.Name != "obfuz") { throw new Exception($"Invalid xml file {configFile}, root name should be 'obfuz'"); } foreach (XmlNode node in root.ChildNodes) { if (!(node is XmlElement ele)) { continue; } switch (ele.Name) { case "global": _global = ParseObfuscationRule(ele, true); break; case "assembly": { AssemblySpec assSpec = ParseAssembly(ele); string name = assSpec.name; if (!_toObfuscatedAssemblyNames.Contains(name)) { throw new Exception($"Invalid xml file {configFile}, assembly name {name} isn't in toObfuscatedAssemblyNames"); } if (_assemblySpecs.ContainsKey(name)) { throw new Exception($"Invalid xml file {configFile}, assembly name {name} is duplicated"); } _assemblySpecs.Add(name, assSpec); break; } default: throw new Exception($"Invalid xml file {configFile}, unknown node {ele.Name}"); } } } private void InheritParentRules() { if (_global == null) { _global = s_default; } else { _global.InheritParent(s_default); } foreach (AssemblySpec assSpec in _assemblySpecs.Values) { assSpec.rule.InheritParent(_global); foreach (TypeSpec typeSpec in assSpec.types) { typeSpec.rule.InheritParent(assSpec.rule); foreach (MethodSpec methodSpec in typeSpec.methods) { methodSpec.rule.InheritParent(typeSpec.rule); } } } } private ObfuscationRule ParseObfuscationRule(XmlElement ele, bool parseWhitelist) { var rule = new ObfuscationRule(); if (ele.HasAttribute("disableEncrypt")) { rule.disableEncrypt = ConfigUtil.ParseBool(ele.GetAttribute("disableEncrypt")); } if (ele.HasAttribute("encryptInt")) { rule.encryptInt = ConfigUtil.ParseBool(ele.GetAttribute("encryptInt")); } if (ele.HasAttribute("encryptLong")) { rule.encryptLong = ConfigUtil.ParseBool(ele.GetAttribute("encryptLong")); } if (ele.HasAttribute("encryptFloat")) { rule.encryptFloat = ConfigUtil.ParseBool(ele.GetAttribute("encryptFloat")); } if (ele.HasAttribute("encryptDouble")) { rule.encryptDouble = ConfigUtil.ParseBool(ele.GetAttribute("encryptDouble")); } if (ele.HasAttribute("encryptBytes")) { rule.encryptArray = ConfigUtil.ParseBool(ele.GetAttribute("encryptArray")); } if (ele.HasAttribute("encryptString")) { rule.encryptString = ConfigUtil.ParseBool(ele.GetAttribute("encryptString")); } if (ele.HasAttribute("encryptConstInLoop")) { rule.encryptConstInLoop = ConfigUtil.ParseBool(ele.GetAttribute("encryptConstInLoop")); } if (ele.HasAttribute("encryptStringInLoop")) { rule.encryptStringInLoop = ConfigUtil.ParseBool(ele.GetAttribute("encryptStringInLoop")); } if (ele.HasAttribute("cacheConstInLoop")) { rule.cacheConstInLoop = ConfigUtil.ParseBool(ele.GetAttribute("cacheConstInLoop")); } if (ele.HasAttribute("cacheConstNotInLoop")) { rule.cacheConstNotInLoop = ConfigUtil.ParseBool(ele.GetAttribute("cacheConstNotInLoop")); } if (ele.HasAttribute("cacheStringInLoop")) { rule.cacheStringInLoop = ConfigUtil.ParseBool(ele.GetAttribute("cacheStringInLoop")); } if (ele.HasAttribute("cacheStringNotInLoop")) { rule.cacheStringNotInLoop = ConfigUtil.ParseBool(ele.GetAttribute("cacheStringNotInLoop")); } if (parseWhitelist) { ParseWhitelist(ele, rule); } return rule; } private void ParseWhitelist(XmlElement ruleEle, ObfuscationRule rule) { foreach (XmlNode xmlNode in ruleEle.ChildNodes) { if (!(xmlNode is XmlElement childEle)) { continue; } switch (childEle.Name) { case "whitelist": { string type = childEle.GetAttribute("type"); if (string.IsNullOrEmpty(type)) { throw new Exception($"Invalid xml file, whitelist type is empty"); } string value = childEle.InnerText; switch (type) { case "int": { rule.notEncryptInts.AddRange(value.Split(",").Select(s => int.Parse(s.Trim()))); break; } case "long": { rule.notEncryptLongs.AddRange(value.Split(",").Select(s => long.Parse(s.Trim()))); break; } case "string": { rule.notEncryptStrings.AddRange(value.Split(",").Select(s => s.Trim())); break; } case "int-range": { var parts = value.Split(","); if (parts.Length != 2) { throw new Exception($"Invalid xml file, int-range {value} is invalid"); } rule.notEncryptIntRanges.Add(new NumberRange(ConfigUtil.ParseNullableInt(parts[0]), ConfigUtil.ParseNullableInt(parts[1]))); break; } case "long-range": { var parts = value.Split(","); if (parts.Length != 2) { throw new Exception($"Invalid xml file, long-range {value} is invalid"); } rule.notEncryptLongRanges.Add(new NumberRange(ConfigUtil.ParseNullableLong(parts[0]), ConfigUtil.ParseNullableLong(parts[1]))); break; } case "float-range": { var parts = value.Split(","); if (parts.Length != 2) { throw new Exception($"Invalid xml file, float-range {value} is invalid"); } rule.notEncryptFloatRanges.Add(new NumberRange(ConfigUtil.ParseNullableFloat(parts[0]), ConfigUtil.ParseNullableFloat(parts[1]))); break; } case "double-range": { var parts = value.Split(","); if (parts.Length != 2) { throw new Exception($"Invalid xml file, double-range {value} is invalid"); } rule.notEncryptDoubleRanges.Add(new NumberRange(ConfigUtil.ParseNullableDouble(parts[0]), ConfigUtil.ParseNullableDouble(parts[1]))); break; } case "string-length-range": { var parts = value.Split(","); if (parts.Length != 2) { throw new Exception($"Invalid xml file, string-length-range {value} is invalid"); } rule.notEncryptStringLengthRanges.Add(new NumberRange(ConfigUtil.ParseNullableInt(parts[0]), ConfigUtil.ParseNullableInt(parts[1]))); break; } case "array-length-range": { var parts = value.Split(","); if (parts.Length != 2) { throw new Exception($"Invalid xml file, array-length-range {value} is invalid"); } rule.notEncryptArrayLengthRanges.Add(new NumberRange(ConfigUtil.ParseNullableInt(parts[0]), ConfigUtil.ParseNullableInt(parts[1]))); break; } default: throw new Exception($"Invalid xml file, unknown whitelist type {type} in {childEle.Name} node"); } break; } default: throw new Exception($"Invalid xml file, unknown node {childEle.Name}"); } } } private AssemblySpec ParseAssembly(XmlElement element) { var assemblySpec = new AssemblySpec(); assemblySpec.name = element.GetAttribute("name"); if (string.IsNullOrEmpty(assemblySpec.name)) { throw new Exception($"Invalid xml file, assembly name is empty"); } assemblySpec.rule = ParseObfuscationRule(element, false); foreach (XmlNode node in element.ChildNodes) { if (!(node is XmlElement ele)) { continue; } switch (ele.Name) { case "type": assemblySpec.types.Add(ParseType(ele)); break; default: throw new Exception($"Invalid xml file, unknown node {ele.Name}"); } } return assemblySpec; } private TypeSpec ParseType(XmlElement element) { var typeSpec = new TypeSpec(); typeSpec.name = element.GetAttribute("name"); typeSpec.nameMatcher = new NameMatcher(typeSpec.name); if (string.IsNullOrEmpty(typeSpec.name)) { throw new Exception($"Invalid xml file, type name is empty"); } typeSpec.rule = ParseObfuscationRule(element, false); foreach (XmlNode node in element.ChildNodes) { if (!(node is XmlElement ele)) { continue; } switch (ele.Name) { case "method": typeSpec.methods.Add(ParseMethod(ele)); break; default: throw new Exception($"Invalid xml file, unknown node {ele.Name}"); } } return typeSpec; } private MethodSpec ParseMethod(XmlElement element) { var methodSpec = new MethodSpec(); methodSpec.name = element.GetAttribute("name"); methodSpec.nameMatcher = new NameMatcher(methodSpec.name); if (string.IsNullOrEmpty(methodSpec.name)) { throw new Exception($"Invalid xml file, method name is empty"); } methodSpec.rule = ParseObfuscationRule(element, false); return methodSpec; } private ObfuscationRule ComputeMethodObfuscationRule(MethodDef method) { var assemblyName = method.DeclaringType.Module.Assembly.Name; if (!_assemblySpecs.TryGetValue(assemblyName, out var assSpec)) { return _global; } string declaringTypeName = method.DeclaringType.FullName; foreach (var typeSpec in assSpec.types) { if (typeSpec.nameMatcher.IsMatch(declaringTypeName)) { foreach (var methodSpec in typeSpec.methods) { if (methodSpec.nameMatcher.IsMatch(method.Name)) { return methodSpec.rule; } } return typeSpec.rule; } } return assSpec.rule; } private ObfuscationRule GetMethodObfuscationRule(MethodDef method) { if (!_methodRuleCache.TryGetValue(method, out var rule)) { rule = ComputeMethodObfuscationRule(method); _methodRuleCache[method] = rule; } return rule; } public override bool NeedObfuscateMethod(MethodDef method) { ObfuscationRule rule = GetMethodObfuscationRule(method); return rule.disableEncrypt != true; } public override ConstCachePolicy GetMethodConstCachePolicy(MethodDef method) { ObfuscationRule rule = GetMethodObfuscationRule(method); return new ConstCachePolicy { cacheConstInLoop = rule.cacheConstInLoop.Value, cacheConstNotInLoop = rule.cacheConstNotInLoop.Value, cacheStringInLoop = rule.cacheStringInLoop.Value, cacheStringNotInLoop = rule.cacheStringNotInLoop.Value, }; } public override bool NeedObfuscateInt(MethodDef method, bool currentInLoop, int value) { ObfuscationRule rule = GetMethodObfuscationRule(method); if (rule.encryptInt == false) { return false; } if (currentInLoop && rule.encryptConstInLoop == false) { return false; } if (rule.notEncryptInts.Contains(value)) { return false; } foreach (var range in rule.notEncryptIntRanges) { if (range.min != null && value < range.min) { continue; } if (range.max != null && value > range.max) { continue; } return false; } return true; } public override bool NeedObfuscateLong(MethodDef method, bool currentInLoop, long value) { ObfuscationRule rule = GetMethodObfuscationRule(method); if (rule.encryptLong == false) { return false; } if (currentInLoop && rule.encryptConstInLoop == false) { return false; } if (rule.notEncryptLongs.Contains(value)) { return false; } foreach (var range in rule.notEncryptLongRanges) { if (range.min != null && value < range.min) { continue; } if (range.max != null && value > range.max) { continue; } return false; } return true; } public override bool NeedObfuscateFloat(MethodDef method, bool currentInLoop, float value) { ObfuscationRule rule = GetMethodObfuscationRule(method); if (rule.encryptFloat == false) { return false; } if (currentInLoop && rule.encryptConstInLoop == false) { return false; } foreach (var range in rule.notEncryptFloatRanges) { if (range.min != null && value < range.min) { continue; } if (range.max != null && value > range.max) { continue; } return false; } return true; } public override bool NeedObfuscateDouble(MethodDef method, bool currentInLoop, double value) { ObfuscationRule rule = GetMethodObfuscationRule(method); if (rule.encryptDouble == false) { return false; } if (currentInLoop && rule.encryptConstInLoop == false) { return false; } foreach (var range in rule.notEncryptDoubleRanges) { if (range.min != null && value < range.min) { continue; } if (range.max != null && value > range.max) { continue; } return false; } return true; } public override bool NeedObfuscateString(MethodDef method, bool currentInLoop, string value) { ObfuscationRule rule = GetMethodObfuscationRule(method); if (rule.encryptString == false) { return false; } if (currentInLoop && rule.encryptConstInLoop == false) { return false; } if (rule.notEncryptStrings.Contains(value)) { return false; } foreach (var range in rule.notEncryptStringLengthRanges) { if (range.min != null && value.Length < range.min) { continue; } if (range.max != null && value.Length > range.max) { continue; } return false; } return true; } public override bool NeedObfuscateArray(MethodDef method, bool currentInLoop, byte[] array) { ObfuscationRule rule = GetMethodObfuscationRule(method); if (rule.encryptArray == false) { return false; } if (currentInLoop && rule.encryptConstInLoop == false) { return false; } foreach (var range in rule.notEncryptArrayLengthRanges) { if (range.min != null && array.Length < range.min) { continue; } if (range.max != null && array.Length > range.max) { continue; } return false; } return true; } } }