From 7a7ef727280f82d3a20e79db67e4dda1cd89c8d6 Mon Sep 17 00:00:00 2001 From: walon Date: Sun, 22 Jun 2025 19:33:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=8E=A7=E5=88=B6=E6=B5=81?= =?UTF-8?q?=E6=B7=B7=E6=B7=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Editor/Emit/BasicBlockCollection.cs | 10 + Editor/Emit/EvalStackCalculator.cs | 73 +- .../ConfigurableObfuscationPolicy.cs | 2 +- .../ConfigurableObfuscationPolicy.cs | 2 +- .../ControlFlowObfus/ControlFlowObfusPass.cs | 10 +- .../ControlFlowObfus/DefaultObfuscator.cs | 14 +- .../ControlFlowObfus/IObfuscator.cs | 7 +- .../MethodControlFlowCalculator.cs | 895 ++++++++++++++++++ .../MethodControlFlowCalculator.cs.meta | 11 + .../ConfigurableObfuscationPolicy.cs | 2 +- .../ConfigurableObfuscationPolicy.cs | 2 +- .../ControlFlowObfuscationSettings.cs | 4 + Editor/Utils/MathUtil.cs | 4 +- Editor/Utils/RandomUtil.cs | 19 + Editor/Utils/RandomUtil.cs.meta | 11 + 15 files changed, 1031 insertions(+), 35 deletions(-) create mode 100644 Editor/ObfusPasses/ControlFlowObfus/MethodControlFlowCalculator.cs create mode 100644 Editor/ObfusPasses/ControlFlowObfus/MethodControlFlowCalculator.cs.meta create mode 100644 Editor/Utils/RandomUtil.cs create mode 100644 Editor/Utils/RandomUtil.cs.meta diff --git a/Editor/Emit/BasicBlockCollection.cs b/Editor/Emit/BasicBlockCollection.cs index af10c0b..bab7632 100644 --- a/Editor/Emit/BasicBlockCollection.cs +++ b/Editor/Emit/BasicBlockCollection.cs @@ -212,11 +212,21 @@ namespace Obfuz.Emit } break; } + case FlowControl.Call: + case FlowControl.Next: + { + if (nextBlock != null) + { + curBlock.AddTargetBasicBlock(nextBlock); + } + break; + } case FlowControl.Return: case FlowControl.Throw: { break; } + default: throw new NotSupportedException($"Unsupported flow control: {lastInst.OpCode.FlowControl} in method {method.FullName}"); } } } diff --git a/Editor/Emit/EvalStackCalculator.cs b/Editor/Emit/EvalStackCalculator.cs index f8b11e6..3958a15 100644 --- a/Editor/Emit/EvalStackCalculator.cs +++ b/Editor/Emit/EvalStackCalculator.cs @@ -3,6 +3,7 @@ using dnlib.DotNet.Emit; using Obfuz.Utils; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using UnityEngine.Assertions; @@ -50,21 +51,34 @@ namespace Obfuz.Emit } } + + class EvalStackState + { + public bool visited; + + public readonly List inputStackDatas = new List(); + public readonly List runStackDatas = new List(); + } + class EvalStackCalculator { private readonly MethodDef _method; private readonly BasicBlockCollection _basicBlocks; private readonly Dictionary _instructionParameterInfos = new Dictionary(); private readonly Dictionary _evalStackTopDataTypeAfterInstructions = new Dictionary(); + private readonly Dictionary _blockEvalStackStates; public EvalStackCalculator(MethodDef method) { _method = method; _basicBlocks = new BasicBlockCollection(method, false); + _blockEvalStackStates = _basicBlocks.Blocks.ToDictionary(b => b, b => new EvalStackState()); SimulateRunAllBlocks(); } + public BasicBlockCollection BasicBlockCollection => _basicBlocks; + public bool TryGetParameterInfo(Instruction inst, out InstructionParameterInfo info) { return _instructionParameterInfos.TryGetValue(inst, out info); @@ -75,12 +89,9 @@ namespace Obfuz.Emit return _evalStackTopDataTypeAfterInstructions.TryGetValue(inst, out result); } - class EvalStackState + public EvalStackState GetEvalStackState(BasicBlock basicBlock) { - public bool visited; - - public readonly List inputStackDatas = new List(); - public readonly List runStackDatas = new List(); + return _blockEvalStackStates[basicBlock]; } private void PushStack(List datas, TypeSig type) @@ -163,6 +174,8 @@ namespace Obfuz.Emit } case ElementType.Var: case ElementType.MVar: + PushStack(datas, new EvalDataTypeWithSig(EvalDataType.ValueType, type)); + break; case ElementType.ValueArray: case ElementType.R: case ElementType.CModOpt: @@ -193,6 +206,11 @@ namespace Obfuz.Emit datas.Add(type); } + private void PushStackObject(List datas) + { + datas.Add(new EvalDataTypeWithSig(EvalDataType.Ref, _method.Module.CorLibTypes.Object)); + } + private EvalDataType CalcBasicBinOpRetType(EvalDataType op1, EvalDataType op2) { switch (op1) @@ -252,7 +270,6 @@ namespace Obfuz.Emit private void SimulateRunAllBlocks() { - Dictionary blockEvalStackStates = _basicBlocks.Blocks.ToDictionary(b => b, b => new EvalStackState()); bool methodHasReturnValue = _method.ReturnType.RemovePinnedAndModifiers().ElementType != ElementType.Void; CilBody body = _method.Body; @@ -260,15 +277,23 @@ namespace Obfuz.Emit { foreach (ExceptionHandler handler in body.ExceptionHandlers) { - if (handler.IsCatch) - { - BasicBlock bb = _basicBlocks.GetBasicBlockByInstruction(handler.HandlerStart); - blockEvalStackStates[bb].runStackDatas.Add(new EvalDataTypeWithSig(EvalDataType.Ref, null)); // Exception object is pushed onto the stack. - } - else if (handler.IsFilter) + if (handler.IsFilter) { BasicBlock bb = _basicBlocks.GetBasicBlockByInstruction(handler.FilterStart); - blockEvalStackStates[bb].runStackDatas.Add(new EvalDataTypeWithSig(EvalDataType.Ref, null)); // Exception object is pushed onto the stack. + var inputStackDatas = _blockEvalStackStates[bb].inputStackDatas; + if (inputStackDatas.Count == 0) + { + inputStackDatas.Add(new EvalDataTypeWithSig(EvalDataType.Ref, handler.CatchType.ToTypeSig())); + } + } + if (handler.IsCatch || handler.IsFilter) + { + BasicBlock bb = _basicBlocks.GetBasicBlockByInstruction(handler.HandlerStart); + var inputStackDatas = _blockEvalStackStates[bb].inputStackDatas; + if (inputStackDatas.Count == 0) + { + inputStackDatas.Add(new EvalDataTypeWithSig(EvalDataType.Ref, handler.CatchType.ToTypeSig())); + } } } } @@ -281,12 +306,13 @@ namespace Obfuz.Emit ? (IList)_method.GenericParameters.Select(p => (TypeSig)new GenericMVar(p.Number)).ToList() : null; var gac = new GenericArgumentContext(methodTypeGenericArgument, methodMethodGenericArgument); + var corLibTypes = _method.Module.CorLibTypes; var blockWalkStack = new Stack(_basicBlocks.Blocks.Reverse()); while (blockWalkStack.Count > 0) { BasicBlock block = blockWalkStack.Pop(); - EvalStackState state = blockEvalStackStates[block]; + EvalStackState state = _blockEvalStackStates[block]; if (state.visited) continue; state.visited = true; @@ -350,7 +376,7 @@ namespace Obfuz.Emit } case Code.Ldnull: { - PushStack(newPushedDatas, EvalDataType.I); + PushStackObject(newPushedDatas); break; } case Code.Ldc_I4_M1: @@ -493,7 +519,7 @@ namespace Obfuz.Emit case Code.Ldind_Ref: { Assert.IsTrue(stackSize > 0); - PushStack(newPushedDatas, EvalDataType.Ref); + PushStackObject(newPushedDatas); break; } case Code.Ldind_R4: @@ -676,7 +702,7 @@ namespace Obfuz.Emit } case Code.Ldstr: { - PushStack(newPushedDatas, EvalDataType.Ref); + PushStack(newPushedDatas, new EvalDataTypeWithSig(EvalDataType.Ref, corLibTypes.String)); break; } case Code.Newobj: @@ -692,7 +718,10 @@ namespace Obfuz.Emit } case Code.Isinst: { - PushStack(newPushedDatas, EvalDataType.Ref); + Assert.IsTrue(stackSize > 0); + var obj = stackDatas[stackSize - 1]; + Assert.IsTrue(obj.type == EvalDataType.Ref); + PushStack(newPushedDatas, obj); break; } case Code.Unbox: @@ -710,7 +739,7 @@ namespace Obfuz.Emit case Code.Box: { Assert.IsTrue(stackSize > 0); - PushStack(newPushedDatas, EvalDataType.Ref); + PushStackObject(newPushedDatas); break; } case Code.Throw: @@ -745,7 +774,7 @@ namespace Obfuz.Emit case Code.Newarr: { Assert.IsTrue(stackSize > 0); - PushStack(newPushedDatas, EvalDataType.Ref); + PushStack(newPushedDatas, new SZArraySig(((ITypeDefOrRef)inst.Operand).ToTypeSig())); break; } case Code.Ldlen: @@ -798,7 +827,7 @@ namespace Obfuz.Emit case Code.Ldelem_Ref: { Assert.IsTrue(stackSize >= 2); - PushStack(newPushedDatas, EvalDataType.Ref); + PushStackObject(newPushedDatas); break; } case Code.Ldelem: @@ -914,7 +943,7 @@ namespace Obfuz.Emit } foreach (BasicBlock outBb in block.outBlocks) { - EvalStackState outState = blockEvalStackStates[outBb]; + EvalStackState outState = _blockEvalStackStates[outBb]; if (outState.visited) { if (stackDatas.Count != outState.inputStackDatas.Count) diff --git a/Editor/ObfusPasses/CallObfus/ConfigurableObfuscationPolicy.cs b/Editor/ObfusPasses/CallObfus/ConfigurableObfuscationPolicy.cs index 2c27e77..aeb4723 100644 --- a/Editor/ObfusPasses/CallObfus/ConfigurableObfuscationPolicy.cs +++ b/Editor/ObfusPasses/CallObfus/ConfigurableObfuscationPolicy.cs @@ -253,7 +253,7 @@ namespace Obfuz.ObfusPasses.CallObfus { if (!_methodRuleCache.TryGetValue(method, out var rule)) { - rule = _configParser.GetMethodRule(method, s_default); + rule = _configParser.GetMethodRule(method, _global); _methodRuleCache[method] = rule; } return rule; diff --git a/Editor/ObfusPasses/ControlFlowObfus/ConfigurableObfuscationPolicy.cs b/Editor/ObfusPasses/ControlFlowObfus/ConfigurableObfuscationPolicy.cs index 6914fcf..75083ea 100644 --- a/Editor/ObfusPasses/ControlFlowObfus/ConfigurableObfuscationPolicy.cs +++ b/Editor/ObfusPasses/ControlFlowObfus/ConfigurableObfuscationPolicy.cs @@ -116,7 +116,7 @@ namespace Obfuz.ObfusPasses.ControlFlowObfus { if (!_methodRuleCache.TryGetValue(method, out var rule)) { - rule = _xmlParser.GetMethodRule(method, s_default); + rule = _xmlParser.GetMethodRule(method, _global); _methodRuleCache[method] = rule; } return rule; diff --git a/Editor/ObfusPasses/ControlFlowObfus/ControlFlowObfusPass.cs b/Editor/ObfusPasses/ControlFlowObfus/ControlFlowObfusPass.cs index b32ed37..9e9a5a8 100644 --- a/Editor/ObfusPasses/ControlFlowObfus/ControlFlowObfusPass.cs +++ b/Editor/ObfusPasses/ControlFlowObfus/ControlFlowObfusPass.cs @@ -14,6 +14,12 @@ namespace Obfuz.ObfusPasses.ControlFlowObfus public EncryptionScopeInfo encryptionScope; public DefaultMetadataImporter importer; public ModuleConstFieldAllocator constFieldAllocator; + public int minInstructionCountOfBasicBlockToObfuscate; + + public IRandom CreateRandom() + { + return encryptionScope.localRandomCreator(MethodEqualityComparer.CompareDeclaringTypes.GetHashCode(method)); + } } internal class ControlFlowObfusPass : ObfuscationMethodPassBase @@ -54,7 +60,6 @@ namespace Obfuz.ObfusPasses.ControlFlowObfus //Debug.Log($"Obfuscating method: {method.FullName} with EvalStackObfusPass"); ObfuscationPassContext ctx = ObfuscationPassContext.Current; - var calc = new BasicBlockCollection(method, false); var encryptionScope = ctx.encryptionScopeProvider.GetScope(method.Module); var ruleData = _obfuscationPolicy.GetObfuscationRuleData(method); var localRandom = encryptionScope.localRandomCreator(MethodEqualityComparer.CompareDeclaringTypes.GetHashCode(method)); @@ -66,8 +71,9 @@ namespace Obfuz.ObfusPasses.ControlFlowObfus constFieldAllocator = ctx.constFieldAllocator.GetModuleAllocator(method.Module), localRandom = localRandom, importer = ctx.moduleEntityManager.GetDefaultModuleMetadataImporter(method.Module, ctx.encryptionScopeProvider), + minInstructionCountOfBasicBlockToObfuscate = _settings.minInstructionCountOfBasicBlockToObfuscate, }; - _obfuscator.Obfuscate(calc, obfusMethodCtx); + _obfuscator.Obfuscate(method, obfusMethodCtx); } } } diff --git a/Editor/ObfusPasses/ControlFlowObfus/DefaultObfuscator.cs b/Editor/ObfusPasses/ControlFlowObfus/DefaultObfuscator.cs index 70d998a..59aca82 100644 --- a/Editor/ObfusPasses/ControlFlowObfus/DefaultObfuscator.cs +++ b/Editor/ObfusPasses/ControlFlowObfus/DefaultObfuscator.cs @@ -1,12 +1,20 @@ -using Obfuz.Emit; +using dnlib.DotNet; +using UnityEngine; namespace Obfuz.ObfusPasses.ControlFlowObfus { class DefaultObfuscator : ObfuscatorBase { - public override bool Obfuscate(BasicBlockCollection basicBlocks, ObfusMethodContext ctx) + public override bool Obfuscate(MethodDef method, ObfusMethodContext ctx) { - return false; + //Debug.Log($"Obfuscating method: {method.FullName} with ControlFlowObfusPass"); + var mcfc = new MethodControlFlowCalculator(method, ctx.CreateRandom(), ctx.constFieldAllocator, ctx.minInstructionCountOfBasicBlockToObfuscate); + if (!mcfc.TryObfus()) + { + Debug.LogWarning($"not obfuscate method: {method.FullName}"); + return false; + } + return true; } } } diff --git a/Editor/ObfusPasses/ControlFlowObfus/IObfuscator.cs b/Editor/ObfusPasses/ControlFlowObfus/IObfuscator.cs index b8dfc07..7909bf2 100644 --- a/Editor/ObfusPasses/ControlFlowObfus/IObfuscator.cs +++ b/Editor/ObfusPasses/ControlFlowObfus/IObfuscator.cs @@ -1,14 +1,15 @@ -using Obfuz.Emit; +using dnlib.DotNet; +using Obfuz.Emit; namespace Obfuz.ObfusPasses.ControlFlowObfus { interface IObfuscator { - bool Obfuscate(BasicBlockCollection basicBlocks, ObfusMethodContext ctx); + bool Obfuscate(MethodDef method, ObfusMethodContext ctx); } abstract class ObfuscatorBase : IObfuscator { - public abstract bool Obfuscate(BasicBlockCollection basicBlocks, ObfusMethodContext ctx); + public abstract bool Obfuscate(MethodDef method, ObfusMethodContext ctx); } } diff --git a/Editor/ObfusPasses/ControlFlowObfus/MethodControlFlowCalculator.cs b/Editor/ObfusPasses/ControlFlowObfus/MethodControlFlowCalculator.cs new file mode 100644 index 0000000..e509f4d --- /dev/null +++ b/Editor/ObfusPasses/ControlFlowObfus/MethodControlFlowCalculator.cs @@ -0,0 +1,895 @@ +using dnlib.DotNet; +using dnlib.DotNet.Emit; +using Obfuz.Data; +using Obfuz.Emit; +using Obfuz.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.ConstrainedExecution; +using UnityEngine; +using UnityEngine.Assertions; + +namespace Obfuz.ObfusPasses.ControlFlowObfus +{ + class MethodControlFlowCalculator + { + class BasicBlockInputOutputArguments + { + public readonly List locals = new List(); + + public BasicBlockInputOutputArguments() + { + } + + public BasicBlockInputOutputArguments(MethodDef method, List inputStackDatas) + { + ICorLibTypes corLibTypes = method.Module.CorLibTypes; + foreach (var data in inputStackDatas) + { + Local local = new Local(GetLocalTypeSig(corLibTypes, data)); + locals.Add(local); + method.Body.Variables.Add(local); + } + } + + private TypeSig GetLocalTypeSig(ICorLibTypes corLibTypes, EvalDataTypeWithSig type) + { + switch (type.type) + { + case EvalDataType.Int32: return corLibTypes.Int32; + case EvalDataType.Int64: return corLibTypes.Int64; + case EvalDataType.Float: return corLibTypes.Single; + case EvalDataType.Double: return corLibTypes.Double; + case EvalDataType.I: return corLibTypes.IntPtr; + case EvalDataType.Ref: Assert.IsNotNull(type.typeSig); return type.typeSig; + case EvalDataType.ValueType: Assert.IsNotNull(type.typeSig); return type.typeSig; + case EvalDataType.Token: throw new System.NotSupportedException("Token type is not supported in BasicBlockInputOutputArguments"); + default: throw new System.NotSupportedException("not supported EvalDataType"); + } + } + } + + class BasicBlockInfo + { + public BlockGroup group; + + //public int order; + public bool isSaveStackBlock; + public BasicBlockInfo prev; + public BasicBlockInfo next; + + public List instructions; + public List inputStackDatas; + public List outputStackDatas; + + public List inBasicBlocks = new List(); + public List outBasicBlocks = new List(); + + public BasicBlockInputOutputArguments inputArgs; + public BasicBlockInputOutputArguments outputArgs; + + public Instruction FirstInstruction => instructions[0]; + + public Instruction LastInstruction => instructions[instructions.Count - 1]; + + public Instruction GroupFirstInstruction => group.basicBlocks[0].FirstInstruction; + + + //public void InsertNext(BasicBlockInfo nextBb) + //{ + // if (next != null) + // { + // next.prev = nextBb; + // nextBb.next = next; + // } + // nextBb.prev = this; + // next = nextBb; + //} + + public void InsertBefore(BasicBlockInfo prevBb) + { + prev.next = prevBb; + prevBb.prev = prev; + prevBb.next = this; + this.prev = prevBb; + } + + public void AddOutBasicBlock(BasicBlockInfo outBb) + { + if (!outBasicBlocks.Contains(outBb)) + { + outBasicBlocks.Add(outBb); + outBb.inBasicBlocks.Add(this); + } + } + + public void ClearInBasicBlocks() + { + foreach (var inBb in inBasicBlocks) + { + inBb.outBasicBlocks.Remove(this); + } + inBasicBlocks.Clear(); + } + + public void RetargetInBasicBlocksTo(BasicBlockInfo prevBb, Dictionary inst2bb) + { + var oldInBlocks = new List(inBasicBlocks); + ClearInBasicBlocks(); + foreach (var oldInBb in oldInBlocks) + { + oldInBb.AddOutBasicBlock(prevBb); + } + // inBB => saveBb => cur + foreach (BasicBlockInfo inBb in prevBb.inBasicBlocks) + { + if (inBb.instructions.Count == 0) + { + // empty block, no need to retarget + continue; + } + Instruction lastInst = inBb.instructions.Last(); + if (lastInst.Operand is Instruction targetInst) + { + if (inst2bb.TryGetValue(targetInst, out BasicBlockInfo targetBb) && targetBb == this) + { + // retarget to prevBb + lastInst.Operand = prevBb.FirstInstruction; + } + } + else if (lastInst.Operand is Instruction[] targetInsts) + { + for (int i = 0; i < targetInsts.Length; i++) + { + targetInst = targetInsts[i]; + if (inst2bb.TryGetValue(targetInst, out BasicBlockInfo targetBb) && targetBb == this) + { + targetInsts[i] = prevBb.FirstInstruction; + } + } + } + } + } + } + + private readonly MethodDef _method; + private readonly IRandom _random; + private readonly ModuleConstFieldAllocator _constFieldAllocator; + private readonly int _minInstructionCountOfBasicBlockToObfuscate; + private readonly BasicBlockInfo _bbHead; + + public MethodControlFlowCalculator(MethodDef method, IRandom random, ModuleConstFieldAllocator constFieldAllocator, int minInstructionCountOfBasicBlockToObfuscate) + { + _method = method; + _random = random; + _constFieldAllocator = constFieldAllocator; + _minInstructionCountOfBasicBlockToObfuscate = minInstructionCountOfBasicBlockToObfuscate; + + _bbHead = new BasicBlockInfo() + { + instructions = new List(), + inputStackDatas = new List(), + outputStackDatas = new List(), + }; + } + + private void BuildBasicBlockLink(EvalStackCalculator evc) + { + BasicBlockInfo prev = _bbHead; + var bb2bb = new Dictionary(); + foreach (BasicBlock bb in evc.BasicBlockCollection.Blocks) + { + EvalStackState ess = evc.GetEvalStackState(bb); + var newBB = new BasicBlockInfo + { + prev = prev, + next = null, + instructions = bb.instructions, + inputStackDatas = ess.inputStackDatas, + outputStackDatas = ess.runStackDatas, + }; + prev.next = newBB; + prev = newBB; + bb2bb.Add(bb, newBB); + } + foreach (BasicBlock bb in evc.BasicBlockCollection.Blocks) + { + BasicBlockInfo bbi = bb2bb[bb]; + foreach (var inBb in bb.inBlocks) + { + bbi.inBasicBlocks.Add(bb2bb[inBb]); + } + foreach (var outBb in bb.outBlocks) + { + bbi.outBasicBlocks.Add(bb2bb[outBb]); + } + } + + // let _bbHead point to the first basic block + //_bbHead.instructions.Add(Instruction.Create(OpCodes.Br, _bbHead.next.FirstInstruction)); + _bbHead.next.inBasicBlocks.Add(_bbHead); + _bbHead.outBasicBlocks.Add(_bbHead.next); + } + + private bool CheckNotContainsNotSupportedEvalStackData() + { + for (BasicBlockInfo cur = _bbHead; cur != null; cur = cur.next) + { + foreach (var data in cur.inputStackDatas) + { + if (data.type == EvalDataType.Unknown || data.type == EvalDataType.Token) + { + Debug.LogError($"NotSupported EvalStackData found in method: {_method.FullName}, type: {data.type}"); + return false; + } + } + } + return true; + } + + + private void WalkInputArgumentGroup(BasicBlockInfo cur, BasicBlockInputOutputArguments inputArgs) + { + if (cur.inputArgs != null) + { + Assert.AreEqual(cur.inputArgs, inputArgs, "input arguments not match"); + return; + } + cur.inputArgs = inputArgs; + foreach (BasicBlockInfo inputBB in cur.inBasicBlocks) + { + if (inputBB.outputArgs != null) + { + Assert.AreEqual(inputBB.outputArgs, inputArgs, $"Input BB {inputBB} outputArgs does not match in method: {_method.FullName}"); + continue; + } + inputBB.outputArgs = cur.inputArgs; + foreach (var outBB in inputBB.outBasicBlocks) + { + WalkInputArgumentGroup(outBB, inputArgs); + } + } + } + + private readonly BasicBlockInputOutputArguments emptyEvalStackArgs = new BasicBlockInputOutputArguments(); + + private void ComputeInputOutputArguments() + { + for (BasicBlockInfo cur = _bbHead; cur != null; cur = cur.next) + { + if (cur.inputArgs == null) + { + if (cur.inputStackDatas.Count == 0) + { + cur.inputArgs = emptyEvalStackArgs; + } + else + { + var inputArgs = new BasicBlockInputOutputArguments(_method, cur.inputStackDatas); + WalkInputArgumentGroup(cur, inputArgs); + } + } + if (cur.outputArgs == null && cur.outputStackDatas.Count == 0) + { + cur.outputArgs = emptyEvalStackArgs; + } + } + for (BasicBlockInfo cur = _bbHead; cur != null; cur = cur.next) + { + if (cur.inputArgs == null) + { + throw new System.Exception($"Input arguments for BasicBlock {cur} in method {_method.FullName} is null"); + } + if (cur.outputArgs == null) + { + if (cur.instructions.Count > 0) + { + Code lastInstCode = cur.LastInstruction.OpCode.Code; + Assert.IsTrue(lastInstCode == Code.Throw || lastInstCode == Code.Rethrow); + cur.outputStackDatas = new List(); + } + cur.outputArgs = emptyEvalStackArgs; + } + } + } + + + private BasicBlockInfo CreateSaveStackBasicBlock(BasicBlockInfo to) + { + if (to.group == null) + { + throw new Exception($"BasicBlock {to} in method {_method.FullName} does not belong to any group. This should not happen."); + } + + var saveLocalBasicBlock = new BasicBlockInfo + { + group = to.group, + isSaveStackBlock = true, + inputStackDatas = to.inputStackDatas, + inputArgs = to.inputArgs, + outputStackDatas = new List(), + outputArgs = emptyEvalStackArgs, + instructions = new List(), + }; + + var locals = to.inputArgs.locals; + if (locals.Count > 0) + { + to.instructions.InsertRange(0, locals.Select(l => Instruction.Create(OpCodes.Ldloc, l))); + + } + for (int i = locals.Count - 1; i >= 0; i--) + { + saveLocalBasicBlock.instructions.Add(Instruction.Create(OpCodes.Stloc, locals[i])); + } + + to.inputArgs = emptyEvalStackArgs; + to.inputStackDatas = new List(); + + BlockGroup group = to.group; + group.basicBlocks.Insert(group.basicBlocks.IndexOf(to), saveLocalBasicBlock); + group.switchMachineCases.Add(new SwitchMachineCase { index = -1, prepareBlock = saveLocalBasicBlock, targetBlock = to}); + saveLocalBasicBlock.instructions.Add(Instruction.Create(OpCodes.Ldsfld, (FieldDef)null)); + saveLocalBasicBlock.instructions.Add(Instruction.Create(OpCodes.Br, group.switchMachineInst)); + + + return saveLocalBasicBlock; + } + + private void AdjustInputOutputEvalStack() + { + Dictionary inst2bb = BuildInstructionToBasicBlockInfoDic(); + for (BasicBlockInfo cur = _bbHead.next; cur != null; cur = cur.next) + { + if (cur.inputArgs.locals.Count == 0 && cur.instructions.Count < _minInstructionCountOfBasicBlockToObfuscate) + { + // small block, no need to save stack + continue; + } + + BasicBlockInfo saveBb = CreateSaveStackBasicBlock(cur); + cur.InsertBefore(saveBb); + cur.RetargetInBasicBlocksTo(saveBb, inst2bb); + //saveBb.AddOutBasicBlock(cur); + } + } + + private void InsertSwitchMachineBasicBlockForGroups(BlockGroup rootGroup) + { + Dictionary inst2bb = BuildInstructionToBasicBlockInfoDic(); + + InsertSwitchMachineBasicBlockForGroup(rootGroup, inst2bb); + } + + //private void ShuffleBasicBlocks0(List bbs) + //{ + // int n = bbs.Count; + // if (n <= 1) + // { + // return; + // } + + // var firstSection = new List() { bbs[0] }; + // var sectionsExcludeFirstLast = new List>(); + // List currentSection = firstSection; + // for (int i = 1; i < n; i++) + // { + // BasicBlockInfo cur = bbs[i]; + // if (cur.inputArgs.locals.Count == 0) + // { + // currentSection = new List() { cur }; + // sectionsExcludeFirstLast.Add(currentSection); + // } + // else + // { + // currentSection.Add(cur); + // } + // } + // if (sectionsExcludeFirstLast.Count <= 1) + // { + // return; + // } + // var lastSection = sectionsExcludeFirstLast.Last(); + // sectionsExcludeFirstLast.RemoveAt(sectionsExcludeFirstLast.Count - 1); + + + // RandomUtil.ShuffleList(sectionsExcludeFirstLast, _random); + + // bbs.Clear(); + // bbs.AddRange(firstSection); + // bbs.AddRange(sectionsExcludeFirstLast.SelectMany(section => section)); + // bbs.AddRange(lastSection); + // Assert.AreEqual(n, bbs.Count, "Shuffled basic blocks count should be the same as original count"); + //} + + private void ShuffleBasicBlocks(List bbs) + { + // TODO + + //int n = bbs.Count; + //BasicBlockInfo groupPrev = bbs[0].prev; + //BasicBlockInfo groupNext = bbs[n - 1].next; + ////RandomUtil.ShuffleList(bbs, _random); + //ShuffleBasicBlocks0(bbs); + //BasicBlockInfo prev = groupPrev; + //for (int i = 0; i < n; i++) + //{ + // BasicBlockInfo cur = bbs[i]; + // cur.prev = prev; + // prev.next = cur; + // prev = cur; + //} + //prev.next = groupNext; + //if (groupNext != null) + //{ + // groupNext.prev = prev; + //} + } + + private void InsertSwitchMachineBasicBlockForGroup(BlockGroup group, Dictionary inst2bb) + { + if (group.subGroups != null && group.subGroups.Count > 0) + { + foreach (var subGroup in group.subGroups) + { + InsertSwitchMachineBasicBlockForGroup(subGroup, inst2bb); + } + } + else if (group.switchMachineCases.Count > 0) + { + Assert.IsTrue(group.basicBlocks.Count > 0, "Group should contain at least one basic block"); + + BasicBlockInfo firstBlock = group.basicBlocks[0]; + var firstCase = group.switchMachineCases[0]; + //Assert.AreEqual(firstCase.prepareBlock, firstBlock, "First case prepare block should be the first basic block in group"); + + Assert.IsTrue(firstCase.targetBlock.inputArgs.locals.Count == 0); + Assert.IsTrue(firstCase.targetBlock.inputStackDatas.Count == 0); + + var instructions = new List() + { + Instruction.Create(OpCodes.Ldsfld, (FieldDef)null), + group.switchMachineInst, + Instruction.Create(OpCodes.Br, firstCase.targetBlock.FirstInstruction), + }; + if (firstCase.prepareBlock != firstBlock || firstBlock.inputStackDatas.Count != 0) + { + instructions.Insert(0, Instruction.Create(OpCodes.Br, firstBlock.FirstInstruction)); + } + + var switchMachineBb = new BasicBlockInfo() + { + group = group, + inputArgs = firstBlock.inputArgs, + outputArgs = emptyEvalStackArgs, + inputStackDatas = firstBlock.inputStackDatas, + outputStackDatas = new List(), + instructions = instructions, + }; + firstBlock.InsertBefore(switchMachineBb); + group.basicBlocks.Insert(0, switchMachineBb); + ShuffleBasicBlocks(group.basicBlocks); + + List switchTargets = (List)group.switchMachineInst.Operand; + + RandomUtil.ShuffleList(group.switchMachineCases, _random); + + for (int i = 0, n = group.switchMachineCases.Count; i < n; i++) + { + SwitchMachineCase switchMachineCase = group.switchMachineCases[i]; + switchMachineCase.index = i; + List prepareBlockInstructions = switchMachineCase.prepareBlock.instructions; + + Instruction setBranchIndexInst = prepareBlockInstructions[prepareBlockInstructions.Count - 2]; + Assert.AreEqual(setBranchIndexInst.OpCode, OpCodes.Ldsfld, "first instruction of prepareBlock should be Ldsfld"); + //setBranchIndexInst.Operand = i; + var indexField = _constFieldAllocator.Allocate(i); + setBranchIndexInst.Operand = indexField; + switchTargets.Add(switchMachineCase.targetBlock.FirstInstruction); + } + + // after shuffle + Assert.IsTrue(instructions.Count == 3 || instructions.Count == 4, "Switch machine basic block should contain 3 or 4 instructions"); + Assert.AreEqual(Code.Ldsfld, instructions[instructions.Count - 3].OpCode.Code, "First instruction should be Ldsfld"); + instructions[instructions.Count - 3].Operand = _constFieldAllocator.Allocate(firstCase.index); + } + } + + private bool IsPrevBasicBlockControlFlowNextToThis(BasicBlockInfo cur) + { + Instruction lastInst = cur.prev.LastInstruction; + switch (lastInst.OpCode.FlowControl) + { + case FlowControl.Cond_Branch: + case FlowControl.Call: + case FlowControl.Next: + case FlowControl.Break: + { + return true; + } + default: return false; + } + } + + private void InsertBrInstructionForConjoinedBasicBlocks() + { + for (BasicBlockInfo cur = _bbHead.next.next; cur != null; cur = cur.next) + { + if (cur.group == cur.prev.group && IsPrevBasicBlockControlFlowNextToThis(cur)) + { + cur.prev.instructions.Add(Instruction.Create(OpCodes.Br, cur.FirstInstruction)); + } + } + } + + private Dictionary BuildInstructionToBasicBlockInfoDic() + { + var inst2bb = new Dictionary(); + for (BasicBlockInfo cur = _bbHead.next; cur != null; cur = cur.next) + { + foreach (var inst in cur.instructions) + { + inst2bb[inst] = cur; + } + } + return inst2bb; + } + + + private class SwitchMachineCase + { + public int index; + public BasicBlockInfo prepareBlock; + public BasicBlockInfo targetBlock; + } + + private class BlockGroup + { + public BlockGroup parent; + + public List instructions; + + public List subGroups; + + public List basicBlocks; + + public Instruction switchMachineInst; + public List switchMachineCases; + + public BlockGroup(List instructions, Dictionary inst2group) + { + this.instructions = instructions; + UpdateInstructionGroup(inst2group); + } + + public BlockGroup(BlockGroup parent, List instructions, Dictionary inst2group) + { + this.instructions = instructions; + UpdateInstructionGroup(parent, inst2group); + } + + public BlockGroup RootParent => parent == null ? this : parent.RootParent; + + public void SetParent(BlockGroup newParent) + { + if (parent != null) + { + Assert.IsTrue(parent != newParent, "Parent group should not be the same as new parent"); + Assert.IsTrue(parent.subGroups.Contains(this), "Parent group should already contain this group"); + parent.subGroups.Remove(this); + } + parent = newParent; + if (newParent.subGroups == null) + { + newParent.subGroups = new List(); + } + Assert.IsFalse(newParent.subGroups.Contains(this), "New parent group should not already contain this group"); + newParent.subGroups.Add(this); + } + + private void UpdateInstructionGroup(Dictionary inst2group) + { + foreach (var inst in instructions) + { + if (inst2group.TryGetValue(inst, out BlockGroup existGroup)) + { + if (this != existGroup) + { + BlockGroup rootParent = existGroup.RootParent; + if (rootParent != this) + { + rootParent.SetParent(this); + } + } + } + else + { + inst2group[inst] = this; + } + } + } + + private void UpdateInstructionGroup(BlockGroup parentGroup, Dictionary inst2group) + { + foreach (var inst in instructions) + { + BlockGroup existGroup = inst2group[inst]; + Assert.AreEqual(parentGroup, existGroup, "Instruction group parent should be the same as parent group"); + inst2group[inst] = this; + } + SetParent(parentGroup); + } + + public void SplitInstructionsNotInAnySubGroupsToIndividualGroups(Dictionary inst2group) + { + if (subGroups == null || subGroups.Count == 0 || instructions.Count == 0) + { + return; + } + + foreach (var subGroup in subGroups) + { + subGroup.SplitInstructionsNotInAnySubGroupsToIndividualGroups(inst2group); + } + + var finalGroupList = new List(); + var curGroupInstructions = new List(); + + var firstInst2SubGroup = subGroups.ToDictionary(g => g.instructions[0]); + foreach (var inst in instructions) + { + BlockGroup group = inst2group[inst]; + if (group == this) + { + curGroupInstructions.Add(inst); + } + else + { + if (curGroupInstructions.Count > 0) + { + finalGroupList.Add(new BlockGroup(this, curGroupInstructions, inst2group)); + curGroupInstructions = new List(); + } + if (firstInst2SubGroup.TryGetValue(inst, out var subGroup)) + { + finalGroupList.Add(subGroup); + } + } + } + if (curGroupInstructions.Count > 0) + { + finalGroupList.Add(new BlockGroup(this, curGroupInstructions, inst2group)); + } + this.subGroups = finalGroupList; + } + + public void ComputeBasicBlocks(Dictionary inst2bb) + { + if (subGroups == null || subGroups.Count == 0) + { + basicBlocks = new List(); + foreach (var inst in instructions) + { + BasicBlockInfo block = inst2bb[inst]; + if (block.group != null) + { + if (block.group != this) + { + throw new Exception("BasicBlockInfo group should be the same as this BlockGroup"); + } + } + else + { + block.group = this; + basicBlocks.Add(block); + } + } + switchMachineInst = Instruction.Create(OpCodes.Switch, new List()); + switchMachineCases = new List(); + return; + } + foreach (var subGroup in subGroups) + { + subGroup.ComputeBasicBlocks(inst2bb); + } + } + } + + private class TryBlockGroup : BlockGroup + { + public TryBlockGroup(List instructions, Dictionary inst2group) : base(instructions, inst2group) + { + } + } + + private class ExceptionHandlerGroup : BlockGroup + { + public readonly ExceptionHandler exceptionHandler; + + public ExceptionHandlerGroup(ExceptionHandler exceptionHandler, List instructions, Dictionary inst2group) : base(instructions, inst2group) + { + this.exceptionHandler = exceptionHandler; + } + } + + private class ExceptionFilterGroup : BlockGroup + { + public readonly ExceptionHandler exceptionHandler; + + public ExceptionFilterGroup(ExceptionHandler exceptionHandler, List instructions, Dictionary inst2group) : base(instructions, inst2group) + { + this.exceptionHandler = exceptionHandler; + } + } + + private class ExceptionHandlerWithFilterGroup : BlockGroup + { + public readonly ExceptionHandler exceptionHandler; + //public readonly ExceptionFilterGroup filterGroup; + //public readonly ExceptionHandlerGroup handlerGroup; + public ExceptionHandlerWithFilterGroup(ExceptionHandler exceptionHandler, List filterInstructions, List handlerInstructions, List allInstructions, Dictionary inst2group) : base(allInstructions, inst2group) + { + this.exceptionHandler = exceptionHandler; + var filterGroup = new ExceptionFilterGroup(exceptionHandler, filterInstructions, inst2group); + var handlerGroup = new ExceptionHandlerGroup(exceptionHandler, handlerInstructions, inst2group); + } + } + + class TryBlockInfo + { + public Instruction tryStart; + public Instruction tryEnd; + public TryBlockGroup blockGroup; + } + + private Dictionary BuildInstruction2Index() + { + IList instructions = _method.Body.Instructions; + var inst2Index = new Dictionary(instructions.Count); + for (int i = 0; i < instructions.Count; i++) + { + Instruction inst = instructions[i]; + inst2Index.Add(inst, i); + } + return inst2Index; + } + + private BlockGroup SplitBasicBlockGroup() + { + Dictionary inst2Index = BuildInstruction2Index(); + var inst2blockGroup = new Dictionary(); + + List instructions = (List)_method.Body.Instructions; + + var tryBlocks = new List(); + foreach (var ex in _method.Body.ExceptionHandlers) + { + TryBlockInfo tryBlock = tryBlocks.Find(tryBlocks => tryBlocks.tryStart == ex.TryStart && tryBlocks.tryEnd == ex.TryEnd); + if (tryBlock == null) + { + int startIndex = inst2Index[ex.TryStart]; + int endIndex = ex.TryEnd != null ? inst2Index[ex.TryEnd] : inst2Index.Count; + TryBlockGroup blockGroup = new TryBlockGroup(instructions.GetRange(startIndex, endIndex - startIndex), inst2blockGroup); + tryBlock = new TryBlockInfo + { + tryStart = ex.TryStart, + tryEnd = ex.TryEnd, + blockGroup = blockGroup, + }; + tryBlocks.Add(tryBlock); + } + if (ex.FilterStart != null) + { + int filterStartIndex = inst2Index[ex.FilterStart]; + int filterEndIndex = ex.HandlerStart != null ? inst2Index[ex.HandlerStart] : inst2Index.Count; + int handlerStartIndex = filterEndIndex; + int handlerEndIndex = ex.HandlerEnd != null ? inst2Index[ex.HandlerEnd] : inst2Index.Count; + var filterHandlerGroup = new ExceptionHandlerWithFilterGroup(ex, + instructions.GetRange(filterStartIndex, filterEndIndex - filterStartIndex), + instructions.GetRange(handlerStartIndex, handlerEndIndex - handlerStartIndex), + instructions.GetRange(filterStartIndex, handlerEndIndex - filterStartIndex), inst2blockGroup); + } + else + { + int handlerStartIndex = inst2Index[ex.HandlerStart]; + int handlerEndIndex = ex.HandlerEnd != null ? inst2Index[ex.HandlerEnd] : inst2Index.Count; + ExceptionHandlerGroup handlerGroup = new ExceptionHandlerGroup(ex, instructions.GetRange(handlerStartIndex, handlerEndIndex - handlerStartIndex), inst2blockGroup); + } + } + var rootGroup = new BlockGroup(new List(instructions), inst2blockGroup); + rootGroup.SplitInstructionsNotInAnySubGroupsToIndividualGroups(inst2blockGroup); + + rootGroup.ComputeBasicBlocks(BuildInstructionToBasicBlockInfoDic()); + return rootGroup; + } + + private void FixInstructionTargets() + { + var inst2bb = BuildInstructionToBasicBlockInfoDic(); + foreach (var ex in _method.Body.ExceptionHandlers) + { + if (ex.TryStart != null) + { + ex.TryStart = inst2bb[ex.TryStart].GroupFirstInstruction; + } + if (ex.TryEnd != null) + { + ex.TryEnd = inst2bb[ex.TryEnd].GroupFirstInstruction; + } + if (ex.HandlerStart != null) + { + ex.HandlerStart = inst2bb[ex.HandlerStart].GroupFirstInstruction; + } + if (ex.HandlerEnd != null) + { + ex.HandlerEnd = inst2bb[ex.HandlerEnd].GroupFirstInstruction; + } + if (ex.FilterStart != null) + { + ex.FilterStart = inst2bb[ex.FilterStart].GroupFirstInstruction; + } + } + //foreach (var inst in inst2bb.Keys) + //{ + // if (inst.Operand is Instruction targetInst) + // { + // inst.Operand = inst2bb[targetInst].FirstInstruction; + // } + // else if (inst.Operand is Instruction[] targetInsts) + // { + // for (int i = 0; i < targetInsts.Length; i++) + // { + // targetInsts[i] = inst2bb[targetInsts[i]].FirstInstruction; + // } + // } + //} + } + + private void BuildInstructions() + { + IList methodInstructions = _method.Body.Instructions; + methodInstructions.Clear(); + for (BasicBlockInfo cur = _bbHead.next; cur != null; cur = cur.next) + { + foreach (Instruction inst in cur.instructions) + { + methodInstructions.Add(inst); + } + } + _method.Body.InitLocals = true; + //_method.Body.MaxStack = Math.Max(_method.Body.MaxStack , (ushort)1); // TODO: set to a reasonable value + //_method.Body.KeepOldMaxStack = true; + //_method.Body.UpdateInstructionOffsets(); + } + + public bool TryObfus() + { + // TODO: TEMP + //if (_method.Body.HasExceptionHandlers) + //{ + // return false; + //} + var evc = new EvalStackCalculator(_method); + BuildBasicBlockLink(evc); + if (!CheckNotContainsNotSupportedEvalStackData()) + { + Debug.LogError($"Method {_method.FullName} contains unsupported EvalStackData, obfuscation skipped."); + return false; + } + BlockGroup rootGroup = SplitBasicBlockGroup(); + if (rootGroup.basicBlocks != null && rootGroup.basicBlocks.Count == 1) + { + return false; + } + ComputeInputOutputArguments(); + AdjustInputOutputEvalStack(); + InsertBrInstructionForConjoinedBasicBlocks(); + InsertSwitchMachineBasicBlockForGroups(rootGroup); + + FixInstructionTargets(); + BuildInstructions(); + return true; + } + } +} diff --git a/Editor/ObfusPasses/ControlFlowObfus/MethodControlFlowCalculator.cs.meta b/Editor/ObfusPasses/ControlFlowObfus/MethodControlFlowCalculator.cs.meta new file mode 100644 index 0000000..059febf --- /dev/null +++ b/Editor/ObfusPasses/ControlFlowObfus/MethodControlFlowCalculator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 144b6474de40382498899f8b1c7f92a3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/ObfusPasses/EvalStackObfus/ConfigurableObfuscationPolicy.cs b/Editor/ObfusPasses/EvalStackObfus/ConfigurableObfuscationPolicy.cs index e2fbfc7..221b074 100644 --- a/Editor/ObfusPasses/EvalStackObfus/ConfigurableObfuscationPolicy.cs +++ b/Editor/ObfusPasses/EvalStackObfus/ConfigurableObfuscationPolicy.cs @@ -126,7 +126,7 @@ namespace Obfuz.ObfusPasses.EvalStackObfus { if (!_methodRuleCache.TryGetValue(method, out var rule)) { - rule = _xmlParser.GetMethodRule(method, s_default); + rule = _xmlParser.GetMethodRule(method, _global); _methodRuleCache[method] = rule; } return rule; diff --git a/Editor/ObfusPasses/ExprObfus/ConfigurableObfuscationPolicy.cs b/Editor/ObfusPasses/ExprObfus/ConfigurableObfuscationPolicy.cs index 6628341..f7045dc 100644 --- a/Editor/ObfusPasses/ExprObfus/ConfigurableObfuscationPolicy.cs +++ b/Editor/ObfusPasses/ExprObfus/ConfigurableObfuscationPolicy.cs @@ -126,7 +126,7 @@ namespace Obfuz.ObfusPasses.ExprObfus { if (!_methodRuleCache.TryGetValue(method, out var rule)) { - rule = _xmlParser.GetMethodRule(method, s_default); + rule = _xmlParser.GetMethodRule(method, _global); _methodRuleCache[method] = rule; } return rule; diff --git a/Editor/Settings/ControlFlowObfuscationSettings.cs b/Editor/Settings/ControlFlowObfuscationSettings.cs index 9417e01..67474b1 100644 --- a/Editor/Settings/ControlFlowObfuscationSettings.cs +++ b/Editor/Settings/ControlFlowObfuscationSettings.cs @@ -7,12 +7,15 @@ namespace Obfuz.Settings public class ControlFlowObfuscationSettingsFacade { + public int minInstructionCountOfBasicBlockToObfuscate; public List ruleFiles; } [Serializable] public class ControlFlowObfuscationSettings { + public int minInstructionCountOfBasicBlockToObfuscate = 3; + [Tooltip("rule config xml files")] public string[] ruleFiles; @@ -20,6 +23,7 @@ namespace Obfuz.Settings { return new ControlFlowObfuscationSettingsFacade { + minInstructionCountOfBasicBlockToObfuscate = minInstructionCountOfBasicBlockToObfuscate, ruleFiles = new List(ruleFiles ?? Array.Empty()), }; } diff --git a/Editor/Utils/MathUtil.cs b/Editor/Utils/MathUtil.cs index 1a12808..d2d2e3f 100644 --- a/Editor/Utils/MathUtil.cs +++ b/Editor/Utils/MathUtil.cs @@ -1,7 +1,9 @@ -using System; +using NUnit.Framework; +using System; namespace Obfuz.Utils { + internal static class MathUtil { //public static int ModInverseOdd32(int sa) diff --git a/Editor/Utils/RandomUtil.cs b/Editor/Utils/RandomUtil.cs new file mode 100644 index 0000000..0f54299 --- /dev/null +++ b/Editor/Utils/RandomUtil.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Obfuz.Utils +{ + static class RandomUtil + { + public static void ShuffleList(List list, IRandom random) + { + int n = list.Count; + for (int i = n - 1; i > 0; i--) + { + int j = random.NextInt(i + 1); + T temp = list[i]; + list[i] = list[j]; + list[j] = temp; + } + } + } +} diff --git a/Editor/Utils/RandomUtil.cs.meta b/Editor/Utils/RandomUtil.cs.meta new file mode 100644 index 0000000..961f1be --- /dev/null +++ b/Editor/Utils/RandomUtil.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d482c078394711d428e627843d2481d7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: